commit 93c8ace8e76362861b4c98b71874dc8fe4c4c78a Author: lihanqi <13868246742@163.com> Date: Fri Aug 1 19:09:57 2025 +0800 彩票助手后端1.0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2f94e61 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip diff --git a/Excel导入使用说明.md b/Excel导入使用说明.md new file mode 100644 index 0000000..d0f52af --- /dev/null +++ b/Excel导入使用说明.md @@ -0,0 +1,484 @@ +# Excel数据导入功能使用说明 + +## 功能概述 + +本系统提供了Excel数据导入功能,可以将包含T1、T2、T3、T4、T5、T6、T7、T8、T10和T11工作表的Excel文件数据导入到十四个数据库表中: + +### 红球数据(T1 Sheet): +- `history_all` - 红球全部历史数据表 +- `history_100` - 红球最近100期数据表 +- `history_top` - 红球历史数据排行表 +- `history_top_100` - 红球100期数据排行表 + +### 蓝球数据(T2 Sheet): +- `blue_history_all` - 蓝球全部历史数据表 +- `blue_history_100` - 蓝球最近100期数据表 +- `blue_history_top` - 蓝球历史数据排行表 +- `blue_history_top_100` - 蓝球100期数据排行表 + +### 红球线系数数据(T3 Sheet): +- `t3` - 红球组红球的线系数表 + +### 蓝球组红球线系数数据(T4 Sheet): +- `t4` - 蓝球组红球的线系数表 + +### 蓝球组蓝球线系数数据(T5 Sheet): +- `t5` - 蓝球组蓝球的线系数表 + +### 红球组蓝球线系数数据(T6 Sheet): +- `t6` - 红球组蓝球的线系数表 + +### 红球组红球面系数数据(T7 Sheet): +- `t7` - 红球组红球的面系数表 + +### 红球组蓝球面系数数据(T8 Sheet): +- `t8` - 红球组蓝球的面系数表 + +### 彩票开奖信息数据(T10 Sheet): +- `lottery_draws` - 彩票开奖信息表 + +### 蓝球组红球面系数数据(T11 Sheet): +- `t11` - 蓝球组红球的面系数表 + +## Excel文件格式要求 + +### 文件要求 +- **文件格式**:必须是`.xlsx`格式 +- **工作表名称**:必须包含名为`T1`、`T2`、`T3`、`T4`、`T5`、`T6`、`T7`、`T8`、`T10`和`T11`的工作表 +- **编码**:支持中文 + +### 数据结构要求 + +**T1工作表(红球数据)**的数据结构如下: + +| 列位置 | 列名 | 对应表 | 字段说明 | +|--------|------|--------|----------| +| A-G | 全部历史数据 | history_all | 球号、出现频次、出现频率%、平均隐现期次、最长隐现期次、最多连出期次、点系数 | +| H-M | 最近100期数据 | history_100 | 出现频次、出现频率%、平均隐现期、当前隐现期、最多连出期次、点系数 | +| N-P | 历史数据排行 | history_top | 排行、球号、点系数 | +| Q-S | 100期数据排行 | history_top_100 | 排行、球号、点系数 | + +**T2工作表(蓝球数据)**的数据结构如下: + +| 列位置 | 列名 | 对应表 | 字段说明 | +|--------|------|--------|----------| +| A-G | 全部历史数据 | blue_history_all | 球号、出现频次、出现频率%、平均隐现期次、最长隐现期次、最多连出期次、点系数 | +| H-M | 最近100期数据 | blue_history_100 | 出现频次、出现频率%、平均隐现期、当前隐现期、最多连出期次、点系数 | +| N-P | 历史数据排行 | blue_history_top | 排行、球号、点系数 | +| Q-S | 100期数据排行 | blue_history_top_100 | 排行、球号、点系数 | + +**T3工作表(红球线系数数据)**的数据结构如下: + +| 列位置 | 数据组织 | 对应表 | 字段说明 | +|--------|----------|--------|----------| +| C | 1号组线系数 | t3 | 主球=1,从球=1-33,线系数=C列 | +| F | 2号组线系数 | t3 | 主球=2,从球=1-33,线系数=F列 | +| I | 3号组线系数 | t3 | 主球=3,从球=1-33,线系数=I列 | +| ... | 依此类推 | t3 | 每三列为一组,组号即主球号 | + +**说明**:T3工作表每三列为一组数据,每组有33行数据,从球号固定为1-33(行号),线系数在C、F、I、L...列。 + +**T4工作表(蓝球组红球线系数数据)**的数据结构如下: + +| 列位置 | 数据组织 | 对应表 | 字段说明 | +|--------|----------|--------|----------| +| C | 蓝球1号线系数 | t4 | 主球=1,从球=1-33,线系数=C列 | +| F | 蓝球2号线系数 | t4 | 主球=2,从球=1-33,线系数=F列 | +| I | 蓝球3号线系数 | t4 | 主球=3,从球=1-33,线系数=I列 | +| ... | 依此类推 | t4 | 每三列为一组,最多16组 | + +**说明**:T4工作表每三列为一组数据,每组有33行数据,蓝球号码1-16(主球),红球号码1-33(从球,行号),线系数在C、F、I、L...列。 + +**T5工作表(蓝球组蓝球线系数数据)**的数据结构如下: + +| 列位置 | 数据组织 | 对应表 | 字段说明 | +|--------|----------|--------|----------| +| C | 蓝球1号线系数 | t5 | 主球=1,从球=1-16,线系数=C列 | +| F | 蓝球2号线系数 | t5 | 主球=2,从球=1-16,线系数=F列 | +| I | 蓝球3号线系数 | t5 | 主球=3,从球=1-16,线系数=I列 | +| ... | 依此类推 | t5 | 每三列为一组,最多16组 | + +**说明**:T5工作表每三列为一组数据,每组有16行数据,蓝球号码1-16(主球和从球),线系数在C、F、I、L...列。 + +**T6工作表(红球组蓝球线系数数据)**的数据结构如下: + +| 列位置 | 数据组织 | 对应表 | 字段说明 | +|--------|----------|--------|----------| +| C | 红球1号线系数 | t6 | 主球=1,从球=1-16,线系数=C列 | +| F | 红球2号线系数 | t6 | 主球=2,从球=1-16,线系数=F列 | +| I | 红球3号线系数 | t6 | 主球=3,从球=1-16,线系数=I列 | +| ... | 依此类推 | t6 | 每三列为一组,最多33组 | + +**说明**:T6工作表每三列为一组数据,每组有16行数据,红球号码1-33(主球),蓝球号码1-16(从球,行号),线系数在C、F、I、L...列。 + +**T7工作表(红球组红球面系数数据)**的数据结构如下: + +| 列位置 | 数据组织 | 对应表 | 字段说明 | +|--------|----------|--------|----------| +| C | 红球1号面系数 | t7 | 主球=1,从球=2-33,面系数=C列 | +| F | 红球2号面系数 | t7 | 主球=2,从球=1,3-33,面系数=F列 | +| I | 红球3号面系数 | t7 | 主球=3,从球=1-2,4-33,面系数=I列 | +| ... | 依此类推 | t7 | 每三列为一组,最多33组 | + +**说明**:T7工作表每三列为一组数据,每组有33行数据,红球号码1-33(主球和从球),面系数在C、F、I、L...列。**特殊处理**:排除自己和自己组合的情况。 + +**Excel数据结构**: +- **第1行**:标题行(1号组、面系数、2号组、面系数...) +- **第2行**:从球1号的数据(对应所有主球的面系数) +- **第3行**:从球2号的数据(对应所有主球的面系数) +- **...** +- **第34行**:从球33号的数据(对应所有主球的面系数) + +**处理逻辑**: +- **1号组(主球1)**: + - 球号列:B列,面系数列:C列 + - 读取所有行,从B列获取从球号,从C列获取面系数 + - 排除对角线(主球1=从球1的情况) +- **2号组(主球2)**: + - 球号列:E列,面系数列:F列 + - 读取所有行,从E列获取从球号,从F列获取面系数 + - 排除对角线(主球2=从球2的情况) +- **依此类推...** + +**关键改进**: +- **动态读取球号**:从Excel的球号列(B、E、H...)读取实际球号,不依赖行号 +- **完整数据覆盖**:读取到Excel的最后一行,确保包含33号球数据 +- **不排除对角线**:读取到什么数据就插入什么数据,包括主球=从球的情况 +- **最终生成**:33×33=1089条记录 + +### 具体列映射 + +#### 红球表映射(T1 Sheet) + +**history_all 表 (列A-G)** +- A列:球号 (ballNumber) +- B列:出现频次 (frequencyCount) +- C列:出现频率% (frequencyPercentage) +- D列:平均隐现期次 (averageInterval) +- E列:最长隐现期次 (maxHiddenInterval) +- F列:最多连出期次 (maxConsecutiveCount) +- G列:点系数 (pointCoefficient) + +**history_100 表 (列H-M,球号使用A列)** +- A列:球号 (ballNumber) +- H列:出现频次 (frequencyCount) +- J列:平均隐现期 (averageInterval) +- K列:当前隐现期 (nowInterval) +- L列:最多连出期次 (maxConsecutiveCount) +- M列:点系数 (pointCoefficient) + +**history_top 表 (列N-P)** +- N列:排行 (no) +- O列:球号 (ballNumber) +- P列:点系数 (pointCoefficient) + +**history_top_100 表 (列Q-S)** +- Q列:排行 (no) +- R列:球号 (ballNumber) +- S列:点系数 (pointCoefficient) + +#### 蓝球表映射(T2 Sheet) + +**blue_history_all 表 (列A-G)** +- A列:球号 (ballNumber) +- B列:出现频次 (frequencyCount) +- C列:出现频率% (frequencyPercentage) +- D列:平均隐现期次 (averageInterval) +- E列:最长隐现期次 (maxHiddenInterval) +- F列:最多连出期次 (maxConsecutiveCount) +- G列:点系数 (pointCoefficient) + +**blue_history_100 表 (列H-M,球号使用A列)** +- A列:球号 (ballNumber) +- H列:出现频次 (frequencyCount) +- J列:平均隐现期 (averageInterval) +- K列:当前隐现期 (nowInterval) +- L列:最多连出期次 (maxConsecutiveCount) +- M列:点系数 (pointCoefficient) + +**blue_history_top 表 (列N-P)** +- N列:排行 (no) +- O列:球号 (ballNumber) +- P列:点系数 (pointCoefficient) + +**blue_history_top_100 表 (列Q-S)** +- Q列:排行 (no) +- R列:球号 (ballNumber) +- S列:点系数 (pointCoefficient) + +#### T3表映射(T3 Sheet) + +**t3 表(红球线系数数据)** +- 数据组织:每三列为一组,每组33行数据 +- 红球号码范围:1-33(主球和从球都是) +- 线系数位置:C、F、I、L...列 + +**数据映射**: +- C列:红球1号线系数(主球=1,从球=1-33,线系数=C列) +- F列:红球2号线系数(主球=2,从球=1-33,线系数=F列) +- I列:红球3号线系数(主球=3,从球=1-33,线系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:主红球号码(1-33) +- slaveBallNumber:从红球号码(固定1-33,对应行号) +- lineCoefficient:线系数(每组第三列,保留两位小数) + +#### T4表映射(T4 Sheet) + +**t4 表(蓝球组红球的线系数)** +- 数据组织:每三列为一组,每组33行数据 +- 蓝球号码范围:1-16(主球) +- 红球号码范围:1-33(从球,对应行号) +- 线系数位置:C、F、I、L...列 + +**数据映射**: +- C列:蓝球1号线系数(主球=1,从球=1-33,线系数=C列) +- F列:蓝球2号线系数(主球=2,从球=1-33,线系数=F列) +- I列:蓝球3号线系数(主球=3,从球=1-33,线系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:蓝球号码(1-16) +- slaveBallNumber:红球号码(固定1-33,对应行号) +- lineCoefficient:线系数(每组第三列,保留两位小数) + +#### T5表映射(T5 Sheet) + +**t5 表(蓝球组蓝球的线系数)** +- 数据组织:每三列为一组,每组16行数据 +- 蓝球号码范围:1-16(主球和从球都是) +- 线系数位置:C、F、I、L...列 + +**数据映射**: +- C列:蓝球1号线系数(主球=1,从球=1-16,线系数=C列) +- F列:蓝球2号线系数(主球=2,从球=1-16,线系数=F列) +- I列:蓝球3号线系数(主球=3,从球=1-16,线系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:主蓝球号码(1-16) +- slaveBallNumber:从蓝球号码(固定1-16,对应行号) +- lineCoefficient:线系数(每组第三列,保留两位小数) + +#### T6表映射(T6 Sheet) + +**t6 表(红球组蓝球的线系数)** +- 数据组织:每三列为一组,每组16行数据 +- 红球号码范围:1-33(主球) +- 蓝球号码范围:1-16(从球) +- 线系数位置:C、F、I、L...列 + +**数据映射**: +- C列:红球1号线系数(主球=1,从球=1-16,线系数=C列) +- F列:红球2号线系数(主球=2,从球=1-16,线系数=F列) +- I列:红球3号线系数(主球=3,从球=1-16,线系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:主红球号码(1-33) +- slaveBallNumber:从蓝球号码(固定1-16,对应行号) +- lineCoefficient:线系数(每组第三列,保留两位小数) + +#### T7表映射(T7 Sheet) + +**t7 表(红球组红球的面系数)** +- 数据组织:每三列为一组,每组33行数据 +- 红球号码范围:1-33(主球和从球都是) +- 面系数位置:C、F、I、L...列 +- 特殊处理:读取到什么数据就插入什么数据,包括对角线 + +**数据映射**: +- C列:红球1号面系数(主球=1,从球=1-33,面系数=C列) +- F列:红球2号面系数(主球=2,从球=1-33,面系数=F列) +- I列:红球3号面系数(主球=3,从球=1-33,面系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:主红球号码(1-33) +- slaveBallNumber:从红球号码(1-33,包括与主球相同的号码) +- faceCoefficient:面系数(每组第三列,保留两位小数) + +#### T8表映射(T8 Sheet) + +**t8 表(红球组蓝球的面系数)** +- 数据组织:每三列为一组,每组16行数据 +- 红球号码范围:1-33(主球) +- 蓝球号码范围:1-16(从球) +- 面系数位置:C、F、I、L...列 + +**数据映射**: +- C列:红球1号面系数(主球=1,从球=1-16,面系数=C列) +- F列:红球2号面系数(主球=2,从球=1-16,面系数=F列) +- I列:红球3号面系数(主球=3,从球=1-16,面系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:主红球号码(1-33) +- slaveBallNumber:从蓝球号码(固定1-16,对应行号) +- faceCoefficient:面系数(每组第三列,保留两位小数) + +#### T10表映射(T10 Sheet) + +**lottery_draws 表(彩票开奖信息)** +- 数据组织:标准表格结构,每行一条开奖记录 +- 开奖期号:Long类型主键 +- 开奖日期:Date类型,支持多种格式 +- 红球1-6:Integer类型 +- 蓝球:Integer类型 + +**数据映射**: +- A列:开奖期号(drawId) +- B列:开奖日期(drawDate) +- C列:红球1(redBall1) +- D列:红球2(redBall2) +- E列:红球3(redBall3) +- F列:红球4(redBall4) +- G列:红球5(redBall5) +- H列:红球6(redBall6) +- I列:蓝球(blueBall) + +**字段映射**: +- drawId:开奖期号(Long类型,主键) +- drawDate:开奖日期(Date类型,支持yyyy-MM-dd、yyyy/MM/dd等格式) +- redBall1-redBall6:红球1-6(Integer类型) +- blueBall:蓝球(Integer类型) + +**数据特性**: +- 所有字段均为必填项 +- 开奖期号为主键,不能重复 +- 日期格式自动识别和转换 +- 数据完整性验证 + +#### T11表映射(T11 Sheet) + +**t11 表(蓝球组红球的面系数)** +- 数据组织:每三列为一组,每组33行数据 +- 蓝球号码范围:1-16(主球) +- 红球号码范围:1-33(从球) +- 面系数位置:C、F、I、L...列 + +**数据映射**: +- C列:蓝球1号面系数(主球=1,从球=1-33,面系数=C列) +- F列:蓝球2号面系数(主球=2,从球=1-33,面系数=F列) +- I列:蓝球3号面系数(主球=3,从球=1-33,面系数=I列) +- 依此类推... + +**字段映射**: +- masterBallNumber:主蓝球号码(1-16) +- slaveBallNumber:从红球号码(固定1-33,对应行号) +- faceCoefficient:面系数(每组第三列,保留两位小数) + +## 使用方法 + +### 1. API接口方式 + +#### 1.1 文件上传导入 +```http +POST /api/excel/upload +Content-Type: multipart/form-data + +参数: +- file: Excel文件 (.xlsx格式) +``` + +#### 1.2 文件路径导入 +```http +POST /api/excel/import-by-path +Content-Type: application/x-www-form-urlencoded + +参数: +- filePath: Excel文件的完整路径 (例如: D:/data/kaifa1.xlsx) +``` + +#### 1.3 获取导入说明 +```http +GET /api/excel/import-info +``` + +### 2. 程序调用方式 + +```java +@Autowired +private ExcelImportService excelImportService; + +// 方式1:通过文件路径导入 +String result = excelImportService.importExcelFileByPath("D:/data/kaifa1.xlsx"); + +// 方式2:通过MultipartFile导入 +String result = excelImportService.importExcelFile(multipartFile); +``` + +### 3. 测试方式 + +运行测试类: +```java +// 运行 ExcelImportTest 类中的测试方法 +@Test +public void testImportExcelByPath() { + // 修改文件路径为实际路径 + String filePath = "D:/code/xy-ai-cpzs/kaifa1.xlsx"; + String result = excelImportService.importExcelFileByPath(filePath); + System.out.println("导入结果:" + result); +} +``` + +## 注意事项 + +1. **数据清空**:每次导入前会清空现有数据,请谨慎操作 +2. **数据验证**:系统会验证数据的完整性,球号为空的记录会被跳过 +3. **错误处理**:导入过程中如有错误会回滚操作并返回错误信息 +4. **日志记录**:导入过程会记录详细日志,便于问题排查 +5. **文件大小**:建议文件大小不超过10MB +6. **并发限制**:避免同时进行多个导入操作 + +## 常见问题 + +### Q1: 提示"未找到T1/T2/T3/T4/T5/T6/T7/T8/T10/T11工作表" +**A**: 请检查Excel文件是否包含名为"T1"(红球数据)、"T2"(蓝球数据)、"T3"(红球线系数)、"T4"(蓝球组红球线系数)、"T5"(蓝球组蓝球线系数)、"T6"(红球组蓝球线系数)、"T7"(红球组红球面系数)、"T8"(红球组蓝球面系数)、"T10"(彩票开奖信息)和"T11"(蓝球组红球面系数)的工作表,注意区分大小写。如果缺少某个工作表,系统会跳过该部分数据并显示警告。 + +### Q2: 导入后数据不完整 +**A**: 请检查Excel数据格式是否正确,确保数值类型的列包含有效数字。 + +### Q3: 导入失败提示文件格式错误 +**A**: 请确保文件是.xlsx格式,不支持.xls格式。 + +### Q4: 如何查看导入日志 +**A**: 导入过程中的日志会输出到控制台,可以通过查看应用日志了解详细信息。 + +### Q5: 报错"Cannot get a NUMERIC value from a STRING cell" +**A**: 这个错误已经修复。系统现在能够自动处理字符串类型的数值单元格,会尝试将字符串转换为数值。 + +### Q6: Excel中有公式单元格怎么办 +**A**: 系统支持公式单元格,会自动读取公式计算后的结果值。 + +### Q7: 单元格为空怎么处理 +**A**: 空白单元格会被自动跳过,对应的字段值会设为null。 + +## 数据类型支持 + +系统支持以下类型的Excel单元格: +- **数值类型** - 直接读取数值 +- **字符串类型** - 尝试转换为数值(如果包含数字) +- **公式类型** - 读取公式计算结果 +- **空白类型** - 设为null +- **其他类型** - 会记录警告日志并设为null + +## 数据精度处理 + +- **浮点数字段**:自动保留两位小数(使用四舍五入) +- **整数字段**:直接转换为整数(去除小数部分) +- **特殊值处理**:NaN和无穷大值会被设为null + +示例: +- `123.456789` → `123.46` +- `12.1` → `12.10` +- `5` → `5.00` + +## Swagger文档 + +启动应用后,可以通过以下地址访问API文档: +- Swagger UI: http://localhost:8123/api/swagger-ui.html +- Knife4j UI: http://localhost:8123/api/doc.html + +在文档中可以直接测试Excel导入接口。 \ No newline at end of file diff --git a/VIP兑换记录查询API使用说明.md b/VIP兑换记录查询API使用说明.md new file mode 100644 index 0000000..2d03d6c --- /dev/null +++ b/VIP兑换记录查询API使用说明.md @@ -0,0 +1,182 @@ +# VIP兑换记录查询API使用说明 + +## 接口概述 + +本文档描述了VIP兑换记录查询相关的API接口,主要用于查询用户的VIP兑换记录信息。 + +## 接口列表 + +### 1. 获取用户所有兑换记录 + +**接口地址:** `GET /vip-exchange-record/user/{userId}` + +**接口描述:** 根据用户ID获取该用户的所有VIP兑换记录 + +**请求参数:** +- `userId` (必填): 用户ID,路径参数,必须大于0 + +**请求示例:** +```http +GET /vip-exchange-record/user/123 +``` + +**响应示例:** +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "id": 1, + "userId": 123, + "type": "月度会员", + "exchangeMode": 1, + "orderNo": 1234567890123456, + "orderAmount": 0, + "isUse": 1, + "exchangeTime": "2024-01-15T10:30:00", + "updateTime": "2024-01-15T10:30:00" + }, + { + "id": 2, + "userId": 123, + "type": "年度会员", + "exchangeMode": 1, + "orderNo": 1234567890123457, + "orderAmount": 0, + "isUse": 1, + "exchangeTime": "2024-02-15T14:20:00", + "updateTime": "2024-02-15T14:20:00" + } + ] +} +``` + +### 2. 分页获取用户兑换记录 + +**接口地址:** `GET /vip-exchange-record/user/{userId}/page` + +**接口描述:** 根据用户ID分页获取该用户的VIP兑换记录 + +**请求参数:** +- `userId` (必填): 用户ID,路径参数,必须大于0 +- `page` (可选): 页码,从1开始,默认为1 +- `size` (可选): 每页大小,默认为10,最大100 + +**请求示例:** +```http +GET /vip-exchange-record/user/123/page?page=1&size=5 +``` + +**响应示例:** +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "id": 1, + "userId": 123, + "type": "月度会员", + "exchangeMode": 1, + "orderNo": 1234567890123456, + "orderAmount": 0, + "isUse": 1, + "exchangeTime": "2024-01-15T10:30:00", + "updateTime": "2024-01-15T10:30:00" + } + ] +} +``` + +### 3. 获取兑换记录详情 + +**接口地址:** `GET /vip-exchange-record/{recordId}` + +**接口描述:** 根据兑换记录ID获取详细信息 + +**请求参数:** +- `recordId` (必填): 兑换记录ID,路径参数,必须大于0 + +**请求示例:** +```http +GET /vip-exchange-record/1 +``` + +**响应示例:** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "userId": 123, + "type": "月度会员", + "exchangeMode": 1, + "orderNo": 1234567890123456, + "orderAmount": 0, + "isUse": 1, + "exchangeTime": "2024-01-15T10:30:00", + "updateTime": "2024-01-15T10:30:00" + } +} +``` + +## 数据字段说明 + +### VipExchangeRecord 字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | Long | 兑换记录唯一标识符 | +| userId | Long | 用户ID | +| type | String | 会员类型(月度会员/年度会员) | +| exchangeMode | Integer | 兑换方式(1-VIP码兑换) | +| orderNo | Long | 订单编号(16位随机数字) | +| orderAmount | Integer | 订单金额(单位:分) | +| isUse | Integer | 是否已兑换(0-未兑换,1-已兑换) | +| exchangeTime | Date | 兑换时间 | +| updateTime | Date | 更新时间 | + +## 响应状态码 + +| 状态码 | 说明 | +|--------|------| +| 0 | 成功 | +| 40000 | 请求参数错误 | +| 40400 | 请求数据不存在 | +| 50000 | 系统内部异常 | + +## 错误响应示例 + +```json +{ + "code": 40000, + "message": "用户ID不能为空且必须大于0", + "data": null +} +``` + +## 使用说明 + +1. **数据排序**: 所有查询结果都按照兑换时间倒序排列(最新的记录在前) + +2. **分页查询**: + - 页码从1开始 + - 每页大小限制在1-100之间 + - 超出范围会使用默认值 + +3. **参数校验**: + - 用户ID和记录ID必须为正整数 + - 参数错误会返回40000状态码 + +4. **异常处理**: + - 系统异常会返回50000状态码 + - 详细错误信息会在message字段中说明 + +## 注意事项 + +1. 接口支持Swagger文档,可通过 `/swagger-ui/index.html` 查看详细文档 +2. 所有接口都有完整的日志记录,便于问题排查 +3. 建议在生产环境中添加认证和权限控制 +4. 分页查询采用内存分页,大数据量时建议使用数据库分页优化 \ No newline at end of file diff --git a/kaifa1.xlsx b/kaifa1.xlsx new file mode 100644 index 0000000..aaf1711 Binary files /dev/null and b/kaifa1.xlsx differ diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/nls-sample-16k.wav b/nls-sample-16k.wav new file mode 100644 index 0000000..b6ffc37 Binary files /dev/null and b/nls-sample-16k.wav differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4bf350a --- /dev/null +++ b/pom.xml @@ -0,0 +1,198 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.6 + + + com.xy + xy-ai-cpzs + 0.0.1-SNAPSHOT + xy-ai-cpzs + xy-ai-cpzs + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-web + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + 3.1.1281 + + + + + com.alibaba.nls + nls-sdk-recognizer + 2.2.1 + + + + + + + + + + + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + org.glassfish.jaxb + jaxb-runtime + 2.3.1 + + + com.alibaba + dashscope-sdk-java + + 2.20.6 + + + org.springframework.boot + spring-boot-starter-aop + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + 1.18.36 + true + + + cn.hutool + hutool-all + 5.8.37 + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.5.0 + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.12 + + + + com.baomidou + mybatis-plus-jsqlparser + 3.5.12 + + + + com.mysql + mysql-connector-j + + + + org.apache.poi + poi + 5.2.4 + + + org.apache.poi + poi-ooxml + 5.2.4 + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + com.aliyun + dysmsapi20170525 + 2.0.24 + + + com.aliyun + tea-openapi + 0.2.8 + + + com.aliyun + tea-util + 0.2.21 + + + com.aliyun + credentials-java + 0.2.4 + + + com.aliyun + tea + 1.2.7 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/sql/ddl.sql b/sql/ddl.sql new file mode 100644 index 0000000..b303653 --- /dev/null +++ b/sql/ddl.sql @@ -0,0 +1,272 @@ +-- 聊天消息表 +create database if not exists cpzs; + +-- 切换库 +use cpzs; + +-- 创建历史数据表 +CREATE TABLE IF NOT EXISTS `history_all` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + frequencyPercentage FLOAT NULL COMMENT '出现频率百分比', + averageInterval FLOAT NULL COMMENT '平均隐现期(次)', + maxHiddenInterval INT NULL COMMENT '最长隐现期(次)', + maxConsecutiveCount INT NULL COMMENT '最多连出期(次)', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '全部历史数据表'; + + +-- 创建最近100期数据表 +CREATE TABLE IF NOT EXISTS `history_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + averageInterval FLOAT NULL COMMENT '平均隐现期(次)', + nowInterval INT NULL COMMENT '当前隐现期(次)', + maxConsecutiveCount INT NULL COMMENT '最多连出期(次)', +pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '最近100期数据表'; + +-- 创建历史数据排行表 +CREATE TABLE IF NOT EXISTS `history_top` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + no INT NULL COMMENT '排行', + ballNumber INT NULL COMMENT '球号', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '历史数据排行表'; + +-- 创建100期数据排行表 +CREATE TABLE IF NOT EXISTS `history_top_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + no INT NULL COMMENT '排行', + ballNumber INT NULL COMMENT '球号', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '100期数据排行表'; + + +-- 创建蓝球历史数据表 +CREATE TABLE IF NOT EXISTS `blue_history_all` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + frequencyPercentage FLOAT NULL COMMENT '出现频率百分比', + averageInterval FLOAT NULL COMMENT '平均隐现期(次)', + maxHiddenInterval INT NULL COMMENT '最长隐现期(次)', + maxConsecutiveCount INT NULL COMMENT '最多连出期(次)', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '蓝球全部历史数据表'; + + +-- 创建蓝球最近100期数据表 +CREATE TABLE IF NOT EXISTS `blue_history_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + averageInterval FLOAT NULL COMMENT '平均隐现期(次)', + nowInterval INT NULL COMMENT '当前隐现期(次)', + maxConsecutiveCount INT NULL COMMENT '最多连出期(次)', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '蓝球最近100期数据表'; + +-- 创建蓝球历史数据排行表 +CREATE TABLE IF NOT EXISTS `blue_history_top` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + no INT NULL COMMENT '排行', + ballNumber INT NULL COMMENT '球号', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '蓝球历史数据排行表'; + +-- 创建蓝球100期数据排行表 +CREATE TABLE IF NOT EXISTS `blue_history_top_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + no INT NULL COMMENT '排行', + ballNumber INT NULL COMMENT '球号', + pointCoefficient FLOAT NULL COMMENT '点系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '蓝球100期数据排行表'; + +-- 创建t3表(红球组红球的线系数) +CREATE TABLE IF NOT EXISTS `t3` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + lineCoefficient FLOAT NULL COMMENT '线系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't3表(红球组红球的线系数)'; + + +-- 创建t4表(蓝球组红球的线系数) +CREATE TABLE IF NOT EXISTS `t4` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + lineCoefficient FLOAT NULL COMMENT '线系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't4表(蓝球组红球的线系数)'; + +-- 创建t5表(蓝球组蓝球的线系数) +CREATE TABLE IF NOT EXISTS `t5` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + lineCoefficient FLOAT NULL COMMENT '线系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't5表(蓝球组蓝球的线系数)'; + +-- 创建t6表(红球组蓝球的线系数) +CREATE TABLE IF NOT EXISTS `t6` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + lineCoefficient FLOAT NULL COMMENT '线系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't6表(红球组蓝球的线系数)'; + +-- 创建t7表(红球组红球的面系数) +CREATE TABLE IF NOT EXISTS `t7` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + faceCoefficient FLOAT NULL COMMENT '面系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't7表(红球组红球的面系数)'; + +-- 创建t8表(红球组蓝球的面系数) +CREATE TABLE IF NOT EXISTS `t8` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + faceCoefficient FLOAT NULL COMMENT '面系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't8表(红球组蓝球的面系数)'; + +-- 创建t11表(蓝球组红球的面系数) +CREATE TABLE IF NOT EXISTS `t11` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + faceCoefficient FLOAT NULL COMMENT '面系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 't11表(蓝球组红球的面系数)'; + +CREATE TABLE IF NOT EXISTS `lottery_draws` ( + `drawId` BIGINT NOT NULL COMMENT '开奖期号' PRIMARY KEY, + `drawDate` DATE NOT NULL COMMENT '开奖日期', + `redBall1` INT NOT NULL COMMENT '红1', + `redBall2` INT NOT NULL COMMENT '红2', + `redBall3` INT NOT NULL COMMENT '红3', + `redBall4` INT NOT NULL COMMENT '红4', + `redBall5` INT NOT NULL COMMENT '红5', + `redBall6` INT NOT NULL COMMENT '红6', + `blueBall` INT NOT NULL COMMENT '蓝球' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '彩票开奖信息表'; + +-- 用户表 +create table if not exists user +( + id bigint auto_increment comment 'id' primary key, + userName varchar(256) null comment '用户昵称', + userAccount varchar(256) not null comment '账号', + phone varchar(11) null comment '手机号', + userAvatar varchar(1024) null comment '用户头像', + gender tinyint null comment '性别', + userRole varchar(256) default 'user' not null comment '用户角色:user / admin', + userPassword varchar(512) not null comment '密码', + isVip int default 0 not null comment '是否会员:0-非会员,1-会员', + vipExpire datetime 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 '更新时间', + isDelete tinyint default 0 not null comment '是否删除', + constraint uni_userAccount + unique (userAccount) +) comment '用户'; + +CREATE TABLE IF NOT EXISTS `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 '开奖日期', + `redBall1` INT NOT NULL COMMENT '红1', + `redBall2` INT NOT NULL COMMENT '红2', + `redBall3` INT NOT NULL COMMENT '红3', + `redBall4` INT NOT NULL COMMENT '红4', + `redBall5` INT NOT NULL COMMENT '红5', + `redBall6` INT NOT NULL COMMENT '红6', + `blueBall` INT NOT NULL COMMENT '蓝球', + `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 = '彩票开奖信息表'; + +CREATE TABLE IF NOT EXISTS `vip_code` ( + `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + `code` varchar(36) NOT NULL COMMENT '会员码', + `vipExpireTime` int not null COMMENT '会员有效月数(1/12)', + `vipNumber` int not NULL COMMENT '会员编号', + `isUse` int NOT NULL COMMENT '是否使用', + createdUserId bigint null comment '创建人', + createdUserName varchar(36) null comment '创建人名称', + usedUserId bigint null comment '使用人', + usedUserName varchar(36) null comment '使用人名称', + `createTime` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间', + `updateTime` datetime NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `Order_OrderNo_uindex` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员码表'; + +INSERT INTO `vip_code` (`id`, `code`, `vipExpireTime`, `vipNumber`, `isUse`, `createTime`, `updateTime`) VALUES +(1, 'A3B4C5D6E7F8G9H1', 1, '000001', 0, NOW(), NOW()), +(2, 'I2J3K4L5M6N7O8P9', 12, '000002', 0, NOW(), NOW()); + + +CREATE TABLE IF NOT EXISTS `vip_exchange_record` ( + `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + `userId` bigint NOT NULL COMMENT '用户ID', + `type` varchar(36) NOT NULL COMMENT '月度会员/年度会员', + `exchangeMode` int not null COMMENT '兑换方式', + `orderNo` bigint not NULL COMMENT '订单编号', + `orderAmount` int not NULL COMMENT '订单金额', + `isUse` int NOT NULL COMMENT '是否兑换(未兑换/已兑换)', + `exchangeTime` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '兑换时间', + `updateTime` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员兑换表'; + +CREATE TABLE IF NOT EXISTS `operation_history` ( + `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + `userId` BIGINT NOT NULL COMMENT '操作用户ID', + `operationType` VARCHAR(50) NOT NULL COMMENT '操作类型(批量生成会员码/获取可用会员码/Excel导入等)', + `operationModule` INTEGER NOT NULL COMMENT '操作模块(0-会员码管理/1-Excel导入管理等)', + `operationResult` VARCHAR(20) NOT NULL COMMENT '操作结果(成功/失败)', + `resultMessage` TEXT COMMENT '结果消息', + `operationTime` DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '操作时间', + `updateTime` DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='操作历史记录表'; + + +CREATE TABLE IF NOT EXISTS `chat_message` +( + id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY, + `conversationId` varchar(64) NULL COMMENT '会话ID', + `studentId` varchar(64) NULL COMMENT '用户ID,关联用户表', + `messageType` varchar(64) NULL COMMENT '消息类型(如: 用户提问、AI回答)', + `content` varchar(1024) NULL COMMENT '消息内容', + `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `isDelete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-未删除 1-已删除' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT ='聊天消息表'; + + + diff --git a/src/main/java/com/xy/xyaicpzs/Main.java b/src/main/java/com/xy/xyaicpzs/Main.java new file mode 100644 index 0000000..5cccccd --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/Main.java @@ -0,0 +1,42 @@ +package com.xy.xyaicpzs; + +import com.alibaba.dashscope.app.*; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import io.reactivex.Flowable; + +import java.util.Arrays; +import java.util.List; + +public class Main { + + public static void streamCall() throws NoApiKeyException, InputRequiredException { + ApplicationParam param = ApplicationParam.builder() + // 若没有配置环境变量,可用百炼API Key将下行替换为:.apiKey("sk-xxx")。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。 + .apiKey(System.getenv("DASHSCOPE_API_KEY")) + // 替换为实际的应用 ID + .appId("ec08d5b81ca248e8834228c1133e2c78") + .prompt("你是谁") + // 增量输出 + .incrementalOutput(true) + .build(); + Application application = new Application(); + // .streamCall():流式输出内容 + Flowable result = application.streamCall(param); + result.blockingForEach(data -> { + System.out.printf("%s\n", + data.getOutput().getText()); +// System.out.println("session_id: " + data.getOutput().getSessionId()); + }); + } + public static void main(String[] args) { + try { + streamCall(); + } catch (ApiException | NoApiKeyException | InputRequiredException e) { + System.out.printf("Exception: %s", e.getMessage()); + System.out.println("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code"); + } + System.exit(0); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/Sample.java b/src/main/java/com/xy/xyaicpzs/Sample.java new file mode 100644 index 0000000..07e0534 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/Sample.java @@ -0,0 +1,54 @@ +// This file is auto-generated, don't edit it. Thanks. +package com.xy.xyaicpzs; + +import com.aliyun.tea.*; + +public class Sample { + + /** + * description : + *

使用凭据初始化账号Client

+ * @return Client + * + * @throws Exception + */ + public static com.aliyun.dysmsapi20170525.Client createClient() throws Exception { + // 工程代码建议使用更安全的无AK方式,凭据配置方式请参见:https://help.aliyun.com/document_detail/378657.html。 + com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client(); + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config() + .setCredential(credential); + // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi + config.endpoint = "dysmsapi.aliyuncs.com"; + return new com.aliyun.dysmsapi20170525.Client(config); + } + + public static void main(String[] args_) throws Exception { + + com.aliyun.dysmsapi20170525.Client client = Sample.createClient(); + com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest() + .setSignName("西安精彩数据服务社") + .setTemplateCode("SMS_489840017") + .setPhoneNumbers("13868246742") + .setTemplateParam("{\"code\":\"1234\"}"); + com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions(); + try { + // 复制代码运行请自行打印 API 的返回值 + client.sendSmsWithOptions(sendSmsRequest, runtime); + } catch (TeaException error) { + // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 + // 错误 message + System.out.println(error.getMessage()); + // 诊断地址 + System.out.println(error.getData().get("Recommend")); + com.aliyun.teautil.Common.assertAsString(error.message); + } catch (Exception _error) { + TeaException error = new TeaException(_error.getMessage(), _error); + // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 + // 错误 message + System.out.println(error.getMessage()); + // 诊断地址 + System.out.println(error.getData().get("Recommend")); + com.aliyun.teautil.Common.assertAsString(error.message); + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/XyAiCpzsApplication.java b/src/main/java/com/xy/xyaicpzs/XyAiCpzsApplication.java new file mode 100644 index 0000000..026f9c2 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/XyAiCpzsApplication.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.xy.xyaicpzs.mapper") +public class XyAiCpzsApplication { + + public static void main(String[] args) { + SpringApplication.run(XyAiCpzsApplication.class, args); + } + +} + + diff --git a/src/main/java/com/xy/xyaicpzs/common/DeleteRequest.java b/src/main/java/com/xy/xyaicpzs/common/DeleteRequest.java new file mode 100644 index 0000000..22ea8b0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/DeleteRequest.java @@ -0,0 +1,19 @@ +package com.xy.xyaicpzs.common; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 删除请求 + */ +@Data +public class DeleteRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private Long id; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/ErrorCode.java b/src/main/java/com/xy/xyaicpzs/common/ErrorCode.java new file mode 100644 index 0000000..a6ee793 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/ErrorCode.java @@ -0,0 +1,39 @@ +package com.xy.xyaicpzs.common; + +/** + * 自定义错误码 + */ +public enum ErrorCode { + + SUCCESS(0, "ok"), + PARAMS_ERROR(40000, "请求参数错误"), + NOT_LOGIN_ERROR(40100, "您还未登录,请登录后操作"), + NO_AUTH_ERROR(40101, "无权限"), + NOT_FOUND_ERROR(40400, "请求数据不存在"), + FORBIDDEN_ERROR(40300, "禁止访问"), + SYSTEM_ERROR(50000, "系统内部异常"), + OPERATION_ERROR(50001, "操作失败"); + + /** + * 状态码 + */ + private final int code; + + /** + * 信息 + */ + private final String message; + + ErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/PageRequest.java b/src/main/java/com/xy/xyaicpzs/common/PageRequest.java new file mode 100644 index 0000000..41b592f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/PageRequest.java @@ -0,0 +1,20 @@ +package com.xy.xyaicpzs.common; + +import lombok.Data; + +/** + * 分页请求 + */ +@Data +public class PageRequest { + + /** + * 当前页号 + */ + private long current = 1; + + /** + * 页面大小 + */ + private long pageSize = 10; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java b/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java new file mode 100644 index 0000000..5c8e1a7 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java @@ -0,0 +1,51 @@ +package com.xy.xyaicpzs.common; + +import com.xy.xyaicpzs.common.response.ApiResponse; + +/** + * 返回工具类 + */ +public class ResultUtils { + + /** + * 成功 + * + * @param data + * @param + * @return + */ + public static ApiResponse success(T data) { + return ApiResponse.success(data); + } + + /** + * 失败 + * + * @param errorCode + * @return + */ + public static ApiResponse error(ErrorCode errorCode) { + return ApiResponse.error(errorCode.getMessage()); + } + + /** + * 失败 + * + * @param code + * @param message + * @return + */ + public static ApiResponse error(int code, String message) { + return ApiResponse.error(message); + } + + /** + * 失败 + * + * @param errorCode + * @return + */ + public static ApiResponse error(ErrorCode errorCode, String message) { + return ApiResponse.error(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/BallAnalysisRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/BallAnalysisRequest.java new file mode 100644 index 0000000..ab9dbac --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/BallAnalysisRequest.java @@ -0,0 +1,24 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; + +import java.util.List; + +/** + * 球号分析请求对象 + */ +@Data +public class BallAnalysisRequest { + private String level; + private List redBalls; + private Integer blueBall; + + @Override + public String toString() { + return "BallAnalysisRequest{" + + "level='" + level + '\'' + + ", redBalls=" + redBalls + + ", blueBall=" + blueBall + + '}'; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/BlueBallAnalysisRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/BlueBallAnalysisRequest.java new file mode 100644 index 0000000..f07a61e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/BlueBallAnalysisRequest.java @@ -0,0 +1,29 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; + +import java.util.List; + +/** + * 蓝球分析请求对象 + */ +@Data +public class BlueBallAnalysisRequest { + private String level; + private List predictedRedBalls; + private List predictedBlueBalls; + private List lastRedBalls; + private Integer lastBlueBall; + + + @Override + public String toString() { + return "BlueBallAnalysisRequest{" + + "level='" + level + '\'' + + ", predictedRedBalls=" + predictedRedBalls + + ", predictedBlueBalls=" + predictedBlueBalls + + ", lastRedBalls=" + lastRedBalls + + ", lastBlueBall=" + lastBlueBall + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/FallowBallAnalysisRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/FallowBallAnalysisRequest.java new file mode 100644 index 0000000..8a1207d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/FallowBallAnalysisRequest.java @@ -0,0 +1,26 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; + +import java.util.List; + +/** + * 跟随球号分析请求对象 + */ +@Data +public class FallowBallAnalysisRequest { + private String level; + private List firstThreeRedBalls; + private List lastSixRedBalls; + private Integer blueBall; + + @Override + public String toString() { + return "FallowBallAnalysisRequest{" + + "level='" + level + '\'' + + ", firstThreeRedBalls=" + firstThreeRedBalls + + ", lastSixRedBalls=" + lastSixRedBalls + + ", blueBall=" + blueBall + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/GenerateVipCodesRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/GenerateVipCodesRequest.java new file mode 100644 index 0000000..6b5c722 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/GenerateVipCodesRequest.java @@ -0,0 +1,20 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; + +/** + * 生成会员码请求 + */ +@Data +public class GenerateVipCodesRequest { + + /** + * 生成数量 + */ + private Integer numCodes; + + /** + * 会员有效月数 + */ + private Integer vipExpireTime; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/PredictRecordQueryRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/PredictRecordQueryRequest.java new file mode 100644 index 0000000..8bea26f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/PredictRecordQueryRequest.java @@ -0,0 +1,23 @@ +package com.xy.xyaicpzs.common.requset; + +import com.xy.xyaicpzs.common.PageRequest; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 预测记录查询请求 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PredictRecordQueryRequest extends PageRequest { + + /** + * 用户ID + */ + private Long userId; + + /** + * 预测状态(待开奖/未中奖/已中奖) + */ + private String predictStatus; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/VipCodeActivateRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/VipCodeActivateRequest.java new file mode 100644 index 0000000..be191ee --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/VipCodeActivateRequest.java @@ -0,0 +1,20 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; + +/** + * 会员码激活请求 + */ +@Data +public class VipCodeActivateRequest { + + /** + * 用户ID + */ + private Long userId; + + /** + * 会员码 + */ + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/VipCodeQueryRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/VipCodeQueryRequest.java new file mode 100644 index 0000000..ad106f6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/VipCodeQueryRequest.java @@ -0,0 +1,74 @@ +package com.xy.xyaicpzs.common.requset; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 会员码查询请求 + */ +@Data +@Schema(description = "会员码查询请求") +public class VipCodeQueryRequest { + + /** + * 当前页码 + */ + @Schema(description = "当前页码") + private long current = 1; + + /** + * 页面大小 + */ + @Schema(description = "页面大小") + private long pageSize = 10; + + /** + * 会员码 + */ + @Schema(description = "会员码") + private String code; + + /** + * 会员有效月数(1/12) + */ + @Schema(description = "会员有效月数") + private Integer vipExpireTime; + + /** + * 是否使用:0-未使用,1-已使用 + */ + @Schema(description = "是否使用:0-未使用,1-已使用") + private Integer isUse; + + /** + * 创建人ID + */ + @Schema(description = "创建人ID") + private Long createdUserId; + + /** + * 创建人名称 + */ + @Schema(description = "创建人名称") + private String createdUserName; + + /** + * 使用人ID + */ + @Schema(description = "使用人ID") + private Long usedUserId; + + /** + * 开始时间 + */ + @Schema(description = "开始时间") + private Date startTime; + + /** + * 结束时间 + */ + @Schema(description = "结束时间") + private Date endTime; +} \ 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 new file mode 100644 index 0000000..b09c6bf --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/response/ApiResponse.java @@ -0,0 +1,29 @@ +package com.xy.xyaicpzs.common.response; + +import lombok.Data; + +/** + * API响应对象 + */ +@Data +public class ApiResponse { + private boolean success; + private String message; + private T data; + + public static ApiResponse success(T data) { + ApiResponse response = new ApiResponse<>(); + response.success = true; + response.message = "操作成功"; + response.data = data; + return response; + } + + public static ApiResponse error(String message) { + ApiResponse response = new ApiResponse<>(); + response.success = false; + response.message = message; + return response; + } + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/response/PageResponse.java b/src/main/java/com/xy/xyaicpzs/common/response/PageResponse.java new file mode 100644 index 0000000..085d45d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/response/PageResponse.java @@ -0,0 +1,67 @@ +package com.xy.xyaicpzs.common.response; + +import lombok.Data; + +import java.util.List; + +/** + * 分页响应对象 + * @param 数据类型 + */ +@Data +public class PageResponse { + + /** + * 数据列表 + */ + private List records; + + /** + * 总记录数 + */ + private Long total; + + /** + * 当前页码 + */ + private Integer page; + + /** + * 每页大小 + */ + private Integer size; + + /** + * 总页数 + */ + private Integer totalPages; + + /** + * 是否有下一页 + */ + private Boolean hasNext; + + /** + * 是否有上一页 + */ + private Boolean hasPrevious; + + public PageResponse() {} + + public PageResponse(List records, Long total, Integer page, Integer size) { + this.records = records; + this.total = total; + this.page = page; + this.size = size; + this.totalPages = (int) Math.ceil((double) total / size); + this.hasNext = page < totalPages; + this.hasPrevious = page > 1; + } + + /** + * 创建分页响应对象 + */ + public static PageResponse of(List records, Long total, Integer page, Integer size) { + return new PageResponse<>(records, total, page, size); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/config/CorsConfig.java b/src/main/java/com/xy/xyaicpzs/config/CorsConfig.java new file mode 100644 index 0000000..20a8cd4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/config/CorsConfig.java @@ -0,0 +1,27 @@ +package com.xy.xyaicpzs.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** +// * 全局跨域配置 + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + // 覆盖所有请求 + registry.addMapping("/**") + // 允许发送 Cookie + .allowCredentials(true) + // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突) + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + // 确保SSE相关头信息能被客户端访问 + .exposedHeaders("*", HttpHeaders.CACHE_CONTROL, HttpHeaders.CONTENT_TYPE); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/config/MyBatisPlusConfig.java b/src/main/java/com/xy/xyaicpzs/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..9cec705 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/config/MyBatisPlusConfig.java @@ -0,0 +1,31 @@ +package com.xy.xyaicpzs.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MyBatis Plus 配置 + * + * @author lihanqi + */ +@Configuration +@MapperScan("com.xy.xyaicpzs.mapper") +public class MyBatisPlusConfig { + + /** + * 拦截器配置 + * + * @return + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java b/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java new file mode 100644 index 0000000..a4242f3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * ObjectMapper配置类 + */ +@Configuration +public class ObjectMapperConfig { + + /** + * 配置ObjectMapper Bean + * + * @return ObjectMapper实例 + */ + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/config/RedisConfig.java b/src/main/java/com/xy/xyaicpzs/config/RedisConfig.java new file mode 100644 index 0000000..c2ded59 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/config/RedisConfig.java @@ -0,0 +1,38 @@ +package com.xy.xyaicpzs.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + */ +@Configuration +public class RedisConfig { + + /** + * 自定义RedisTemplate + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + // 设置连接工厂 + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + + // 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value值 + GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); + redisTemplate.setValueSerializer(jsonSerializer); + redisTemplate.setHashValueSerializer(jsonSerializer); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/config/RestTemplateConfig.java b/src/main/java/com/xy/xyaicpzs/config/RestTemplateConfig.java new file mode 100644 index 0000000..b1a489f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/config/RestTemplateConfig.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * RestTemplate配置类 + */ +@Configuration +public class RestTemplateConfig { + + /** + * 配置RestTemplate Bean + * + * @return RestTemplate实例 + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/config/ScheduleConfig.java b/src/main/java/com/xy/xyaicpzs/config/ScheduleConfig.java new file mode 100644 index 0000000..a2a8244 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/config/ScheduleConfig.java @@ -0,0 +1,12 @@ +package com.xy.xyaicpzs.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 定时任务配置类 + */ +@Configuration +@EnableScheduling +public class ScheduleConfig { +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java b/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java new file mode 100644 index 0000000..9db1298 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java @@ -0,0 +1,36 @@ +package com.xy.xyaicpzs.constant; + +/** + * 用户常量 + */ +public interface UserConstant { + + /** + * 用户登录态键 + */ + String USER_LOGIN_STATE = "userLoginState"; + + /** + * 系统用户 id(虚拟用户) + */ + long SYSTEM_USER_ID = 0; + + // region 权限 + + /** + * 默认权限 + */ + String DEFAULT_ROLE = "user"; + + /** + * 管理员权限 + */ + String ADMIN_ROLE = "admin"; + + /** + * 超级管理员权限 + */ + String SUPER_ADMIN_ROLE = "superAdmin"; + + // endregion +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java b/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java new file mode 100644 index 0000000..996bbe1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java @@ -0,0 +1,1805 @@ +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.common.response.PageResponse; +import com.xy.xyaicpzs.domain.entity.LotteryDraws; +import com.xy.xyaicpzs.domain.entity.PredictRecord; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.domain.entity.HistoryAll; +import com.xy.xyaicpzs.domain.entity.History100; +import com.xy.xyaicpzs.domain.entity.HistoryTop; +import com.xy.xyaicpzs.domain.entity.HistoryTop100; +import com.xy.xyaicpzs.domain.entity.BlueHistoryAll; +import com.xy.xyaicpzs.domain.entity.BlueHistory100; +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop; +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop100; +import com.xy.xyaicpzs.domain.entity.T7; +import com.xy.xyaicpzs.domain.entity.T8; +import com.xy.xyaicpzs.domain.entity.T11; +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; +import com.xy.xyaicpzs.service.UserService; +import com.xy.xyaicpzs.service.HistoryAllService; +import com.xy.xyaicpzs.service.History100Service; +import com.xy.xyaicpzs.service.HistoryTopService; +import com.xy.xyaicpzs.service.HistoryTop100Service; +import com.xy.xyaicpzs.service.BlueHistoryAllService; +import com.xy.xyaicpzs.service.BlueHistory100Service; +import com.xy.xyaicpzs.service.BlueHistoryTopService; +import com.xy.xyaicpzs.service.BlueHistoryTop100Service; +import com.xy.xyaicpzs.service.T7Service; +import com.xy.xyaicpzs.service.T8Service; +import com.xy.xyaicpzs.service.T11Service; +import com.xy.xyaicpzs.service.T3Service; +import com.xy.xyaicpzs.service.T4Service; +import com.xy.xyaicpzs.service.T5Service; +import com.xy.xyaicpzs.service.T6Service; +import com.xy.xyaicpzs.domain.vo.BallHitRateVO; +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 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.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Comparator; + +/** + * 球号分析控制器 + * 提供球号分析算法的API接口 + */ +@Slf4j +@RestController +@RequestMapping("/ball-analysis") +@Tag(name = "球号分析", description = "球号分析算法API") +public class BallAnalysisController { + + @Autowired + private BallAnalysisService ballAnalysisService; + + @Autowired + private LotteryDrawsService lotteryDrawsService; + + @Autowired + private PredictRecordService predictRecordService; + + @Autowired + private UserService userService; + + @Autowired + private HistoryAllService historyAllService; + + @Autowired + private History100Service history100Service; + + @Autowired + private HistoryTopService historyTopService; + + @Autowired + private HistoryTop100Service historyTop100Service; + + @Autowired + private BlueHistoryAllService blueHistoryAllService; + + @Autowired + private BlueHistory100Service blueHistory100Service; + + @Autowired + private BlueHistoryTopService blueHistoryTopService; + + @Autowired + private BlueHistoryTop100Service blueHistoryTop100Service; + + @Autowired + private T7Service t7Service; + + @Autowired + private T8Service t8Service; + + @Autowired + private T11Service t11Service; + + @Autowired + private T3Service t3Service; + + @Autowired + private T4Service t4Service; + + @Autowired + private T5Service t5Service; + + @Autowired + private T6Service t6Service; + + /** + * 获取近期开奖信息 + * @param limit 获取条数,可选参数,默认15条 + * @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 = lotteryDrawsService.getRecentDraws(limit); + + 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("/recent-100-draws") + @Operation(summary = "获取最新100条开奖信息", description = "获取lottery_draws表中最新的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 = lotteryDrawsService.getRecentDraws(100); + + 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()); + } + } + + /** + * 根据日期范围查询开奖信息 + * @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, "开始日期不能晚于结束日期"); + } + + // 调用服务按日期范围查询开奖信息 + List result = lotteryDrawsService.getByDateRange(startDate, endDate); + + 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 = "开奖期号,例如:2025056", required = true) + @PathVariable Long drawId, + HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + try { + log.info("接收到根据期号查询开奖信息请求:期号={}", drawId); + + // 调用服务查询开奖信息 + LotteryDraws result = lotteryDrawsService.getByDrawId(drawId); + + 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个中奖球号 + */ + @GetMapping("/draw/{drawId}/numbers") + @Operation(summary = "根据开奖期号查询开奖球号", description = "根据开奖期号查询7个中奖球号(6个红球+1个蓝球)") + public ApiResponse> getDrawNumbersById( + @Parameter(description = "开奖期号,例如:2025056", required = true) + @PathVariable Long drawId, + HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + try { + log.info("接收到根据期号查询开奖球号请求:期号={}", drawId); + + // 调用服务查询开奖信息 + LotteryDraws result = lotteryDrawsService.getByDrawId(drawId); + + if (result == null) { + log.warn("未找到期号为{}的开奖信息", drawId); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "未找到期号为" + drawId + "的开奖信息"); + } + + List winningNumbers = new java.util.ArrayList<>(); + winningNumbers.add(result.getRedBall1()); + winningNumbers.add(result.getRedBall2()); + winningNumbers.add(result.getRedBall3()); + winningNumbers.add(result.getRedBall4()); + winningNumbers.add(result.getRedBall5()); + winningNumbers.add(result.getRedBall6()); + winningNumbers.add(result.getBlueBall()); + + log.info("根据期号查询开奖球号完成:{},球号:{}", drawId, winningNumbers); + return ResultUtils.success(winningNumbers); + + } catch (Exception e) { + log.error("根据期号查询开奖球号失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询开奖球号失败:" + e.getMessage()); + } + } + + /** + * 创建预测记录 + * @param userId 用户ID + * @param drawId 开奖期号 + * @param drawDate 开奖日期 + * @param redBalls 6个红球号码,用逗号分隔 + * @param blueBall 蓝球号码 + * @return 创建的预测记录 + */ + @PostMapping("/create-predict") + @Operation(summary = "创建预测记录", description = "向predict_record表插入一条预测记录数据") + public ApiResponse createPredictRecord( + @Parameter(description = "用户ID,例如:1001", required = true) + @RequestParam Long userId, + + @Parameter(description = "开奖期号,例如:2025056", required = true) + @RequestParam Long drawId, + + @Parameter(description = "开奖日期,格式:yyyy-MM-dd", required = false) + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date drawDate, + + @Parameter(description = "6个红球号码,用逗号分隔,例如:1,5,12,18,25,33", required = true) + @RequestParam String redBalls, + + @Parameter(description = "蓝球号码,例如:8", required = true) + @RequestParam Integer blueBall) { + + try { + log.info("接收到创建预测记录请求:用户ID={},期号={},开奖日期={},红球={},蓝球={}", + userId, drawId, drawDate, redBalls, blueBall); + + // 解析红球号码 + List redBallList = parseRedBalls(redBalls, 6, "红球"); + + // 调用服务创建预测记录 + PredictRecord result = predictRecordService.createPredictRecord(userId, drawId, drawDate, redBallList, blueBall); + + 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()); + } + } + + /** + * 根据用户ID获取预测记录 + * @param userId 用户ID + * @return 用户的所有预测记录列表 + */ + @GetMapping("/predict-records/{userId}") + @Operation(summary = "获取用户预测记录", description = "根据用户ID分页获取该用户的预测记录,按预测时间倒序排列,每页5条") + public ApiResponse> getPredictRecordsByUserId( + @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, "用户未登录"); + } + + try { + log.info("接收到获取用户预测记录请求:用户ID={},页码={}", userId, page); + + // 参数校验 + if (page < 1) { + page = 1; + } + + // 获取总记录数 + Long total = predictRecordService.getPredictRecordsCountByUserId(userId); + + // 调用服务获取用户预测记录(分页,每页5条) + List records = predictRecordService.getPredictRecordsByUserIdWithPaging(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 userId 用户ID + * @param level 高位/中位/低位标识 (H/M/L) + * @param redBalls 6个红球号码,用逗号分隔 + * @param blueBall 蓝球号码 + * @return 分析结果:出现频率最高的前11位数字 + */ + @PostMapping("/analyze") + @Operation(summary = "首球算法", description = "根据输入的级别、红球和蓝球,分析出现频率最高的前11位数字") + public ApiResponse> analyzeBalls( + @Parameter(description = "用户ID,例如:1001", required = true) + @RequestParam Long userId, + + @Parameter(description = "级别:H(高位)/M(中位)/L(低位)", required = true) + @RequestParam String level, + + @Parameter(description = "6个红球号码,用逗号分隔,例如:1,5,12,18,25,33", required = true) + @RequestParam String redBalls, + + @Parameter(description = "蓝球号码,例如:8", required = true) + @RequestParam Integer blueBall) { + + try { + log.info("接收到球号分析请求:用户ID={},级别={},红球={},蓝球={}", userId, level, redBalls, blueBall); + + // 校验用户VIP有效期 + User user = userService.getById(userId); + if (user == null) { + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "用户不存在"); + } + + Date now = new Date(); + if (user.getVipExpire() == null || user.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + // 解析红球号码 + List redBallList = parseRedBalls(redBalls, 6, "红球"); + + // 调用分析服务 + List result = ballAnalysisService.analyzeBalls(level, redBallList, blueBall); + + log.info("球号分析完成,结果:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("球号分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "球号分析失败:" + e.getMessage()); + } + } + + /** + * 跟随球号分析算法 + * @param userId 用户ID + * @param level 高位/中位/低位标识 (H/M/L) + * @param firstThreeRedBalls 前3个红球号码,用逗号分隔 + * @param lastSixRedBalls 后6个红球号码,用逗号分隔 + * @param blueBall 蓝球号码 + * @return 分析结果:出现频率最高的前8位数字 + */ + @PostMapping("/fallow") + @Operation(summary = "跟随球号分析算法", description = "根据输入的级别、前3个红球、后6个红球和蓝球,分析出现频率最高的前8位数字") + public ApiResponse> fallowBallAnalysis( + @Parameter(description = "用户ID,例如:1001", required = true) + @RequestParam Long userId, + + @Parameter(description = "级别:H(高位)/M(中位)/L(低位)", required = true) + @RequestParam String level, + + @Parameter(description = "前3个红球号码,用逗号分隔,例如:7,24,27", required = true) + @RequestParam String firstThreeRedBalls, + + @Parameter(description = "后6个红球号码,用逗号分隔,例如:21,10,5,15,23,28", required = true) + @RequestParam String lastSixRedBalls, + + @Parameter(description = "蓝球号码,例如:16", required = true) + @RequestParam Integer blueBall) { + + try { + log.info("接收到跟随球号分析请求:用户ID={},级别={},前3个红球={},后6个红球={},蓝球={}", + userId, level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + // 校验用户VIP有效期 + User user = userService.getById(userId); + if (user == null) { + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "用户不存在"); + } + + Date now = new Date(); + if (user.getVipExpire() == null || user.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + // 解析红球号码 + List firstThreeRedBallList = parseRedBalls(firstThreeRedBalls, 3, "前3个红球"); + List lastSixRedBallList = parseRedBalls(lastSixRedBalls, 6, "后6个红球"); + + // 调用分析服务 + List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBallList, lastSixRedBallList, blueBall); + + log.info("跟随球号分析完成,结果:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("跟随球号分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "跟随球号分析失败:" + e.getMessage()); + } + } + + /** + * 解析红球号码字符串 + */ + private List parseRedBalls(String redBalls, int expectedCount, String ballType) { + if (redBalls == null || redBalls.trim().isEmpty()) { + throw new IllegalArgumentException(ballType + "号码不能为空"); + } + + try { + String[] parts = redBalls.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 < 1 || ball > 33) { + throw new IllegalArgumentException(ballType + "号码必须在1-33范围内,错误值:" + ball); + } + } + + return result; + + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ballType + "号码格式错误,请使用逗号分隔的数字"); + } + } + + /** + * 蓝球分析算法 + * @param userId 用户ID + * @param level 高位/中位/低位标识 (H/M/L) + * @param predictedRedBalls 6个预测红球号码,用逗号分隔 + * @param predictedBlueBalls 2个预测蓝球号码,用逗号分隔 + * @param lastRedBalls 6个上期红球号码,用逗号分隔 + * @param lastBlueBall 上期蓝球号码 + * @return 分析结果:频率最高的前4个蓝球号码 + */ + @PostMapping("/blue-ball") + @Operation(summary = "蓝球分析算法", description = "根据输入的级别、预测红球、预测蓝球、上期红球和上期蓝球,分析出频率最高的前4个蓝球号码") + public ApiResponse> blueBallAnalysis( + @Parameter(description = "用户ID,例如:1001", required = true) + @RequestParam Long userId, + + @Parameter(description = "级别:H(高位)/M(中位)/L(低位)", required = true) + @RequestParam String level, + + @Parameter(description = "6个预测红球号码,用逗号分隔,例如:26,20,18,32,10,14", required = true) + @RequestParam String predictedRedBalls, + + @Parameter(description = "2个预测蓝球号码,用逗号分隔,例如:5,8", required = true) + @RequestParam String predictedBlueBalls, + + @Parameter(description = "6个上期红球号码,用逗号分隔,例如:7,24,27,21,10,5", required = true) + @RequestParam String lastRedBalls, + + @Parameter(description = "上期蓝球号码,例如:16", required = true) + @RequestParam Integer lastBlueBall) { + + try { + log.info("接收到蓝球分析请求:用户ID={},级别={},预测红球={},预测蓝球={},上期红球={},上期蓝球={}", + userId, level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + + // 校验用户VIP有效期 + User user = userService.getById(userId); + if (user == null) { + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "用户不存在"); + } + + Date now = new Date(); + if (user.getVipExpire() == null || user.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + // 解析球号 + List predictedRedBallList = parseRedBalls(predictedRedBalls, 6, "预测红球"); + List predictedBlueBallList = parseBlueBalls(predictedBlueBalls, 2, "预测蓝球"); + List lastRedBallList = parseRedBalls(lastRedBalls, 6, "上期红球"); + + // 调用分析服务 + List result = ballAnalysisService.blueBallAnalysis( + level, predictedRedBallList, predictedBlueBallList, lastRedBallList, lastBlueBall); + + log.info("蓝球分析完成,结果:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("蓝球分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "蓝球分析失败:" + e.getMessage()); + } + } + + /** + * 解析蓝球号码字符串 + */ + private List parseBlueBalls(String blueBalls, int expectedCount, String ballType) { + if (blueBalls == null || blueBalls.trim().isEmpty()) { + throw new IllegalArgumentException(ballType + "号码不能为空"); + } + + try { + String[] parts = blueBalls.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 < 1 || ball > 16) { + throw new IllegalArgumentException(ballType + "号码必须在1-16范围内,错误值:" + ball); + } + } + + return result; + + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ballType + "号码格式错误,请使用逗号分隔的数字"); + } + } + + /** + * 获取红球历史数据全部记录 + * @return 历史数据全部记录列表 + */ + @GetMapping("/history-all") + @Operation(summary = "获取历史数据全部记录", description = "获取history_all表中的所有历史数据记录") + public ApiResponse> getHistoryAll(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("接收到获取历史数据全部记录请求"); + + // 调用服务获取全部历史数据 + List result = historyAllService.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("/history-100") + @Operation(summary = "获取最近100期数据记录", description = "获取history_100表中的所有最近100期数据记录") + public ApiResponse> getHistory100(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 = history100Service.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("/history-top") + @Operation(summary = "获取历史数据排行记录", description = "获取history_top表中的所有历史数据排行记录") + public ApiResponse> getHistoryTop(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("接收到获取历史数据排行记录请求"); + + // 调用服务获取历史数据排行 + List result = historyTopService.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("/history-top-100") + @Operation(summary = "获取红球100期数据排行记录", description = "获取history_top_100表中的所有100期数据排行记录") + public ApiResponse> getHistoryTop100(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 = historyTop100Service.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("/blue-history-all") + @Operation(summary = "获取蓝球历史数据全部记录", description = "获取blue_history_all表中的所有蓝球历史数据记录") + public ApiResponse> getBlueHistoryAll(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("接收到获取蓝球历史数据全部记录请求"); + + // 调用服务获取蓝球全部历史数据 + List result = blueHistoryAllService.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("/blue-history-100") + @Operation(summary = "获取蓝球最近100期数据记录", description = "获取blue_history_100表中的所有蓝球最近100期数据记录") + public ApiResponse> getBlueHistory100(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 = blueHistory100Service.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("/blue-history-top") + @Operation(summary = "获取蓝球历史数据排行记录", description = "获取blue_history_top表中的所有蓝球历史数据排行记录") + public ApiResponse> getBlueHistoryTop(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("接收到获取蓝球历史数据排行记录请求"); + + // 调用服务获取蓝球历史数据排行 + List result = blueHistoryTopService.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("/blue-history-top-100") + @Operation(summary = "获取蓝球100期数据排行记录", description = "获取blue_history_top_100表中的所有蓝球100期数据排行记录") + public ApiResponse> getBlueHistoryTop100(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 = blueHistoryTop100Service.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()); + } + } + + /** + * 红球与红球的组合性分析 + * @param masterBall 主球号码(第一个红球) + * @param slaveBall 随球号码(第二个红球) + * @return 面系数分析结果 + */ + @GetMapping("/red-ball-combination-analysis") + @Operation(summary = "红球与红球的组合性分析", description = "根据主球和随球号码,查询t7表获取面系数,并计算主球与其他球号的组合情况") + public ApiResponse redBallCombinationAnalysis( + @Parameter(description = "主球号码,例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码,例如:12", required = true) + @RequestParam Integer slaveBall, + + 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("接收到红球与红球的组合性分析请求:主球={},随球={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证红球号码范围 + if (masterBall < 1 || masterBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球号码必须在1-33范围内,错误值:" + masterBall); + } + + if (slaveBall < 1 || slaveBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球号码必须在1-33范围内,错误值:" + slaveBall); + } + + // 查询当前两个球的面系数 + T7 t7Record = t7Service.lambdaQuery() + .eq(T7::getMasterBallNumber, masterBall) + .eq(T7::getSlaveBallNumber, slaveBall) + .one(); + + if (t7Record == null) { + log.warn("未找到主球{}和随球{}的组合记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球%d和随球%d的组合记录", masterBall, slaveBall)); + } + + Double faceCoefficient = t7Record.getFaceCoefficient(); + + // 查询主球与所有其他球的组合系数 + List allCombinations = t7Service.lambdaQuery() + .eq(T7::getMasterBallNumber, masterBall) + .ne(T7::getSlaveBallNumber, masterBall) // 排除自身 + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球{}的组合记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球%d的组合记录", masterBall)); + } + + // 找出系数最高的球号 + T7 highest = allCombinations.stream() + .max(Comparator.comparing(T7::getFaceCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T7 lowest = allCombinations.stream() + .min(Comparator.comparing(T7::getFaceCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T7::getFaceCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallCombinationAnalysisVO result = BallCombinationAnalysisVO.builder() + .faceCoefficient(faceCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getFaceCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getFaceCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + 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("/red-blue-combination-analysis") + @Operation(summary = "红球与蓝球的组合性分析", description = "根据红球主球和蓝球随球号码,查询t8表获取面系数,并返回扩展分析") + public ApiResponse redBlueCombinationAnalysis( + @Parameter(description = "主球号码(红球),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(蓝球),例如:8", required = true) + @RequestParam Integer slaveBall, + + 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("接收到红球与蓝球的组合性分析请求:主球(红球)={},随球(蓝球)={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证红球号码范围 + if (masterBall < 1 || masterBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球(红球)号码必须在1-33范围内,错误值:" + masterBall); + } + + // 验证蓝球号码范围 + if (slaveBall < 1 || slaveBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球(蓝球)号码必须在1-16范围内,错误值:" + slaveBall); + } + + // 查询T8表获取面系数 + T8 t8Record = t8Service.lambdaQuery() + .eq(T8::getMasterBallNumber, masterBall) + .eq(T8::getSlaveBallNumber, slaveBall) + .one(); + + if (t8Record == null) { + log.warn("未找到主球(红球){}和随球(蓝球){}的组合记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(红球)%d和随球(蓝球)%d的组合记录", masterBall, slaveBall)); + } + + Double faceCoefficient = t8Record.getFaceCoefficient(); + + // 查询主球(红球)与所有蓝球的组合系数 + List allCombinations = t8Service.lambdaQuery() + .eq(T8::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球(红球){}的组合记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(红球)%d的组合记录", masterBall)); + } + + // 找出系数最高的球号 + T8 highest = allCombinations.stream() + .max(Comparator.comparing(T8::getFaceCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T8 lowest = allCombinations.stream() + .min(Comparator.comparing(T8::getFaceCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T8::getFaceCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallCombinationAnalysisVO result = BallCombinationAnalysisVO.builder() + .faceCoefficient(faceCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getFaceCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getFaceCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + 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("/blue-red-combination-analysis") + @Operation(summary = "蓝球与红球的组合性分析", description = "根据蓝球主球和红球随球号码,查询t11表获取面系数,并返回扩展分析") + public ApiResponse blueRedCombinationAnalysis( + @Parameter(description = "主球号码(蓝球),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(红球),例如:5", required = true) + @RequestParam Integer slaveBall, + + 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("接收到蓝球与红球的组合性分析请求:主球(蓝球)={},随球(红球)={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证蓝球号码范围 + if (masterBall < 1 || masterBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球(蓝球)号码必须在1-16范围内,错误值:" + masterBall); + } + + // 验证红球号码范围 + if (slaveBall < 1 || slaveBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球(红球)号码必须在1-33范围内,错误值:" + slaveBall); + } + + // 查询T11表获取面系数 + T11 t11Record = t11Service.lambdaQuery() + .eq(T11::getMasterBallNumber, masterBall) + .eq(T11::getSlaveBallNumber, slaveBall) + .one(); + + if (t11Record == null) { + log.warn("未找到主球(蓝球){}和随球(红球){}的组合记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(蓝球)%d和随球(红球)%d的组合记录", masterBall, slaveBall)); + } + + Double faceCoefficient = t11Record.getFaceCoefficient(); + + // 查询主球(蓝球)与所有红球的组合系数 + List allCombinations = t11Service.lambdaQuery() + .eq(T11::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球(蓝球){}的组合记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(蓝球)%d的组合记录", masterBall)); + } + + // 找出系数最高的球号 + T11 highest = allCombinations.stream() + .max(Comparator.comparing(T11::getFaceCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T11 lowest = allCombinations.stream() + .min(Comparator.comparing(T11::getFaceCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T11::getFaceCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallCombinationAnalysisVO result = BallCombinationAnalysisVO.builder() + .faceCoefficient(faceCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getFaceCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getFaceCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + 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("/red-red-persistence-analysis") + @Operation(summary = "红球与红球的持续性分析", description = "根据红球主球和红球随球号码,查询t3表获取线系数,并返回扩展分析") + public ApiResponse redRedPersistenceAnalysis( + @Parameter(description = "主球号码(红球),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(红球),例如:12", required = true) + @RequestParam Integer slaveBall, + + 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("接收到红球与红球的持续性分析请求:主球(红球)={},随球(红球)={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证红球号码范围 + if (masterBall < 1 || masterBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球(红球)号码必须在1-33范围内,错误值:" + masterBall); + } + + if (slaveBall < 1 || slaveBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球(红球)号码必须在1-33范围内,错误值:" + slaveBall); + } + + // 查询T3表获取线系数 + T3 t3Record = t3Service.lambdaQuery() + .eq(T3::getMasterBallNumber, masterBall) + .eq(T3::getSlaveBallNumber, slaveBall) + .one(); + + if (t3Record == null) { + log.warn("未找到主球(红球){}和随球(红球){}的持续性记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(红球)%d和随球(红球)%d的持续性记录", masterBall, slaveBall)); + } + + Double lineCoefficient = t3Record.getLineCoefficient(); + + // 查询主球与其他所有红球的持续性系数 + List allCombinations = t3Service.lambdaQuery() + .eq(T3::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球(红球){}的持续性记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(红球)%d的持续性记录", masterBall)); + } + + // 找出系数最高的球号 + T3 highest = allCombinations.stream() + .max(Comparator.comparing(T3::getLineCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T3 lowest = allCombinations.stream() + .min(Comparator.comparing(T3::getLineCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T3::getLineCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallPersistenceAnalysisVO result = BallPersistenceAnalysisVO.builder() + .lineCoefficient(lineCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getLineCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getLineCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + 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("/blue-blue-persistence-analysis") + @Operation(summary = "蓝球与蓝球的持续性分析", description = "根据蓝球主球和蓝球随球号码,查询t5表获取线系数,并返回扩展分析") + public ApiResponse blueBluePersistenceAnalysis( + @Parameter(description = "主球号码(蓝球),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(蓝球),例如:12", required = true) + @RequestParam Integer slaveBall, + + 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("接收到蓝球与蓝球的持续性分析请求:主球(蓝球)={},随球(蓝球)={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证蓝球号码范围 + if (masterBall < 1 || masterBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球(蓝球)号码必须在1-16范围内,错误值:" + masterBall); + } + + if (slaveBall < 1 || slaveBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球(蓝球)号码必须在1-16范围内,错误值:" + slaveBall); + } + + // 查询T5表获取线系数 + T5 t5Record = t5Service.lambdaQuery() + .eq(T5::getMasterBallNumber, masterBall) + .eq(T5::getSlaveBallNumber, slaveBall) + .one(); + + if (t5Record == null) { + log.warn("未找到主球(蓝球){}和随球(蓝球){}的持续性记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(蓝球)%d和随球(蓝球)%d的持续性记录", masterBall, slaveBall)); + } + + Double lineCoefficient = t5Record.getLineCoefficient(); + + // 查询主球与其他所有蓝球的持续性系数 + List allCombinations = t5Service.lambdaQuery() + .eq(T5::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球(蓝球){}的持续性记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(蓝球)%d的持续性记录", masterBall)); + } + + // 找出系数最高的球号 + T5 highest = allCombinations.stream() + .max(Comparator.comparing(T5::getLineCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T5 lowest = allCombinations.stream() + .min(Comparator.comparing(T5::getLineCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T5::getLineCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallPersistenceAnalysisVO result = BallPersistenceAnalysisVO.builder() + .lineCoefficient(lineCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getLineCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getLineCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + 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("/red-blue-persistence-analysis") + @Operation(summary = "红球与蓝球的持续性分析", description = "根据红球主球和蓝球随球号码,查询t6表获取线系数,并返回扩展分析") + public ApiResponse redBluePersistenceAnalysis( + @Parameter(description = "主球号码(红球),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(蓝球),例如:8", required = true) + @RequestParam Integer slaveBall, + + 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("接收到红球与蓝球的持续性分析请求:主球(红球)={},随球(蓝球)={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证红球号码范围 + if (masterBall < 1 || masterBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球(红球)号码必须在1-33范围内,错误值:" + masterBall); + } + + // 验证蓝球号码范围 + if (slaveBall < 1 || slaveBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球(蓝球)号码必须在1-16范围内,错误值:" + slaveBall); + } + + // 查询T6表获取线系数 + T6 t6Record = t6Service.lambdaQuery() + .eq(T6::getMasterBallNumber, masterBall) + .eq(T6::getSlaveBallNumber, slaveBall) + .one(); + + if (t6Record == null) { + log.warn("未找到主球(红球){}和随球(蓝球){}的持续性记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(红球)%d和随球(蓝球)%d的持续性记录", masterBall, slaveBall)); + } + + Double lineCoefficient = t6Record.getLineCoefficient(); + + // 查询主球(红球)与所有蓝球的持续性系数 + List allCombinations = t6Service.lambdaQuery() + .eq(T6::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球(红球){}的持续性记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(红球)%d的持续性记录", masterBall)); + } + + // 找出系数最高的球号 + T6 highest = allCombinations.stream() + .max(Comparator.comparing(T6::getLineCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T6 lowest = allCombinations.stream() + .min(Comparator.comparing(T6::getLineCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T6::getLineCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallPersistenceAnalysisVO result = BallPersistenceAnalysisVO.builder() + .lineCoefficient(lineCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getLineCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getLineCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + 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("/blue-red-persistence-analysis") + @Operation(summary = "蓝球与红球的持续性分析", description = "根据蓝球主球和红球随球号码,查询t4表获取线系数,并返回扩展分析") + public ApiResponse blueRedPersistenceAnalysis( + @Parameter(description = "主球号码(蓝球),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(红球),例如:5", required = true) + @RequestParam Integer slaveBall, + + 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("接收到蓝球与红球的持续性分析请求:主球(蓝球)={},随球(红球)={}", masterBall, slaveBall); + + // 参数校验 + if (masterBall == null || slaveBall == null) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球和随球号码不能为空"); + } + + // 验证蓝球号码范围 + if (masterBall < 1 || masterBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "主球(蓝球)号码必须在1-16范围内,错误值:" + masterBall); + } + + // 验证红球号码范围 + if (slaveBall < 1 || slaveBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "随球(红球)号码必须在1-33范围内,错误值:" + slaveBall); + } + + // 查询T4表获取线系数 + T4 t4Record = t4Service.lambdaQuery() + .eq(T4::getMasterBallNumber, masterBall) + .eq(T4::getSlaveBallNumber, slaveBall) + .one(); + + if (t4Record == null) { + log.warn("未找到主球(蓝球){}和随球(红球){}的持续性记录", masterBall, slaveBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(蓝球)%d和随球(红球)%d的持续性记录", masterBall, slaveBall)); + } + + Double lineCoefficient = t4Record.getLineCoefficient(); + + // 查询主球(蓝球)与所有红球的持续性系数 + List allCombinations = t4Service.lambdaQuery() + .eq(T4::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + log.warn("未找到主球(蓝球){}的持续性记录", masterBall); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, + String.format("未找到主球(蓝球)%d的持续性记录", masterBall)); + } + + // 找出系数最高的球号 + T4 highest = allCombinations.stream() + .max(Comparator.comparing(T4::getLineCoefficient)) + .orElse(null); + + // 找出系数最低的球号 + T4 lowest = allCombinations.stream() + .min(Comparator.comparing(T4::getLineCoefficient)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(T4::getLineCoefficient) + .average() + .orElse(0.0); + + // 获取最新的一期开奖期号 + Long latestDrawId = lotteryDrawsService.lambdaQuery() + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(LotteryDraws::getDrawId) + .orElse(null); + + // 构建返回结果 + BallPersistenceAnalysisVO result = BallPersistenceAnalysisVO.builder() + .lineCoefficient(lineCoefficient) + .highestBall(highest != null ? highest.getSlaveBallNumber() : null) + .highestCoefficient(highest != null ? highest.getLineCoefficient() : null) + .lowestBall(lowest != null ? lowest.getSlaveBallNumber() : null) + .lowestCoefficient(lowest != null ? lowest.getLineCoefficient() : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + + log.info("蓝球与红球的持续性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("蓝球与红球的持续性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "蓝球与红球的持续性分析失败:" + e.getMessage()); + } + } + + /** + * 获取首球命中率统计 + * @return 首球命中次数和命中率统计 + */ + @GetMapping("/first-ball-hit-rate") + @Operation(summary = "获取首球命中率统计", description = "统计用户的首球命中次数和命中率") + public ApiResponse getFirstBallHitRate(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到获取首球命中率统计请求"); + + // 调用服务获取首球命中率 + BallHitRateVO result = ballAnalysisService.getFirstBallHitRate(loginUser.getId()); + + log.info("获取首球命中率统计完成,命中次数:{},总次数:{},命中率:{}", + result.getHitCount(), result.getTotalCount(), result.getHitRate()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取首球命中率统计失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取首球命中率统计失败:" + e.getMessage()); + } + } + + /** + * 获取蓝球命中率统计 + * @return 蓝球命中次数和命中率统计 + */ + @GetMapping("/blue-ball-hit-rate") + @Operation(summary = "获取蓝球命中率统计", description = "统计用户的蓝球命中次数和命中率") + public ApiResponse getBlueBallHitRate(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到获取蓝球命中率统计请求"); + + // 调用服务获取蓝球命中率 + BallHitRateVO result = ballAnalysisService.getBlueBallHitRate(loginUser.getId()); + + log.info("获取蓝球命中率统计完成,命中次数:{},总次数:{},命中率:{}", + result.getHitCount(), result.getTotalCount(), 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 getPrizeStatistics(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到奖金统计请求"); + + // 调用服务进行奖金统计 + PrizeEstimateVO result = ballAnalysisService.getPrizeStatistics(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()); + } + } + + /** + * 获取近10期开奖期号 + * @return 近10期开奖期号列表 + */ + @GetMapping("/recent-10-draw-ids") + @Operation(summary = "获取近10期开奖期号", description = "获取lottery_draws表中最新的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 = lotteryDrawsService.lambdaQuery() + .select(LotteryDraws::getDrawId) + .orderByDesc(LotteryDraws::getDrawId) + .last("LIMIT 10") + .list() + .stream() + .map(LotteryDraws::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()); + } + } + + /** + * 获取红球命中率统计 + * @return 红球命中总数和命中率 + */ + @GetMapping("/red-ball-hit-rate") + @Operation(summary = "获取红球命中率统计", description = "统计用户的总红球命中次数和命中率") + public ApiResponse getRedBallHitRate(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到获取红球命中率统计请求"); + + // 调用服务获取红球命中率 + RedBallHitRateVO result = ballAnalysisService.getRedBallHitRate(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()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/ChatController.java b/src/main/java/com/xy/xyaicpzs/controller/ChatController.java new file mode 100644 index 0000000..8c98e06 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/ChatController.java @@ -0,0 +1,126 @@ +package com.xy.xyaicpzs.controller; + +import com.alibaba.dashscope.app.Application; +import com.alibaba.dashscope.app.ApplicationParam; +import com.alibaba.dashscope.app.ApplicationResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.InputRequiredException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import com.xy.xyaicpzs.domain.entity.ChatMessage; +import com.xy.xyaicpzs.service.ChatMessageService; +import io.reactivex.Flowable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.atomic.AtomicReference; + +@RestController +public class ChatController { + + @Autowired + private ChatMessageService chatMessageService; + + @Value("${dashscope.api-key}") + private String dashscopeApiKey; + + /** + * SSE流式聊天接口 + * @param message 用户消息 + * @param conversationId 会话ID + * @param userId 用户ID + * @return SseEmitter + */ + @GetMapping("/chat/sse") + public SseEmitter chatSseEmitter( + @RequestParam String message, + @RequestParam(required = false) String conversationId, + @RequestParam(required = false) String userId) { + + // 保存用户消息到数据库 + saveMessage(conversationId, userId, "USER", message); + + // 创建一个超时时间较长的 SseEmitter + SseEmitter emitter = new SseEmitter(180000L); // 3分钟超时 + + // 用于收集完整的AI回复 + AtomicReference fullResponseRef = new AtomicReference<>(new StringBuilder()); + + try { + // 设置AI参数 + ApplicationParam param = ApplicationParam.builder() + .apiKey(dashscopeApiKey) // 使用配置文件中的API密钥 + .appId("ec08d5b81ca248e8834228c1133e2c78") + .prompt(message) + .incrementalOutput(true) + .build(); + + Application application = new Application(); + + // 流式调用 + Flowable result = application.streamCall(param); + + result.subscribe( + // 处理每条消息 + data -> { + try { + String text = data.getOutput().getText(); + // 发送数据到客户端 + emitter.send(text); + // 收集完整响应 + fullResponseRef.get().append(text); + } catch (IOException e) { + emitter.completeWithError(e); + } + }, + // 处理错误 + error -> { + emitter.completeWithError(error); + System.out.println("错误: " + error.getMessage()); + }, + // 处理完成 + () -> { + // 保存AI回复到数据库 + String fullResponse = fullResponseRef.get().toString(); + saveMessage(conversationId, userId, "AI", fullResponse); + emitter.complete(); + } + ); + + } catch (ApiException | NoApiKeyException | InputRequiredException e) { + try { + emitter.send("错误: " + e.getMessage()); + emitter.complete(); + } catch (IOException ex) { + emitter.completeWithError(ex); + } + System.out.println("异常: " + e.getMessage()); + } + + return emitter; + } + + /** + * 保存消息到数据库 + * @param conversationId 会话ID + * @param userId 用户ID + * @param messageType 消息类型(USER/AI) + * @param content 消息内容 + */ + private void saveMessage(String conversationId, String userId, String messageType, String content) { + ChatMessage chatMessage = new ChatMessage(); + chatMessage.setConversationId(conversationId); + chatMessage.setStudentId(userId); + chatMessage.setMessageType(messageType); + chatMessage.setContent(content); + chatMessage.setCreateTime(new Date()); + chatMessage.setUpdateTime(new Date()); + chatMessage.setIsDelete(0); + chatMessageService.save(chatMessage); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/DataAnalysisController.java b/src/main/java/com/xy/xyaicpzs/controller/DataAnalysisController.java new file mode 100644 index 0000000..6458e40 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/DataAnalysisController.java @@ -0,0 +1,138 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.requset.PredictRecordQueryRequest; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.common.response.PageResponse; +import com.xy.xyaicpzs.domain.entity.PredictRecord; +import com.xy.xyaicpzs.domain.vo.UserPredictStatVO; +import com.xy.xyaicpzs.service.DataAnalysisService; +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.HashMap; +import java.util.Map; + +/** + * 数据分析控制器 + * 提供用户预测数据统计分析的API接口 + */ +@Slf4j +@RestController +@RequestMapping("/data-analysis") +@Tag(name = "数据分析", description = "用户预测数据统计分析API") +public class DataAnalysisController { + + @Autowired + private DataAnalysisService dataAnalysisService; + + /** + * 获取用户预测统计数据 + * @param userId 用户ID + * @return 用户预测统计数据 + */ + @GetMapping("/user-predict-stat/{userId}") + @Operation(summary = "获取用户预测统计数据", description = "根据用户ID获取该用户的预测次数、待开奖次数、命中次数、命中率等统计数据") + public ApiResponse getUserPredictStat( + @Parameter(description = "用户ID,例如:1001", required = true) + @PathVariable Long userId) { + + try { + log.info("接收到获取用户预测统计数据请求:用户ID={}", userId); + + // 调用服务获取用户预测统计数据 + UserPredictStatVO result = dataAnalysisService.getUserPredictStat(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 处理结果 + */ + @PostMapping("/process-pending") + @Operation(summary = "手动处理待开奖记录", description = "手动触发处理待开奖的预测记录,匹配开奖结果并更新中奖状态") + public ApiResponse processPendingPredictions() { + + try { + log.info("接收到手动处理待开奖记录请求"); + + // 调用服务处理待开奖记录 + int processedCount = dataAnalysisService.processPendingPredictions(); + + String message = String.format("处理完成,共处理%d条待开奖记录", processedCount); + log.info("手动处理待开奖记录完成:{}", message); + + return ResultUtils.success(message); + + } 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> queryPredictRecords(@RequestBody PredictRecordQueryRequest request) { + try { + log.info("接收到按条件查询预测记录请求:userId={}, predictStatus={}, current={}, pageSize={}", + request.getUserId(), request.getPredictStatus(), request.getCurrent(), request.getPageSize()); + + // 调用服务查询预测记录 + PageResponse result = dataAnalysisService.queryPredictRecords(request); + + log.info("按条件查询预测记录完成,总记录数:{}", result.getTotal()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("按条件查询预测记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询预测记录失败:" + e.getMessage()); + } + } + + /** + * 获取所有预测记录总数 + * @return 预测记录总数 + */ + @GetMapping("/total-predict-count") + @Operation(summary = "获取预测记录总数", description = "获取系统中所有用户的预测记录总数") + public ApiResponse> getTotalPredictCount() { + try { + log.info("接收到获取预测记录总数请求"); + + // 调用服务获取预测记录总数 + long totalCount = dataAnalysisService.getTotalPredictCount(); + + Map result = new HashMap<>(); + result.put("totalCount", totalCount); + + log.info("获取预测记录总数完成,总数:{}", totalCount); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取预测记录总数失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取预测记录总数失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java b/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java new file mode 100644 index 0000000..fac6f55 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java @@ -0,0 +1,156 @@ +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.ExcelImportService; +import com.xy.xyaicpzs.service.OperationHistoryService; +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.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * Excel数据导入控制器 + */ +@Slf4j +@RestController +@RequestMapping("/excel") +@Tag(name = "Excel数据导入", description = "Excel数据导入相关接口") +public class ExcelImportController { + + @Autowired + private ExcelImportService excelImportService; + + @Autowired + private UserService userService; + + @Autowired + private OperationHistoryService operationHistoryService; + + /** + * 上传Excel文件并导入数据 + */ + @PostMapping("/upload") + @Operation(summary = "上传Excel文件导入数据", description = "上传包含T1、T2、T3、T4、T5、T6和T7 sheet的Excel文件,将红球、蓝球、线系数和面系数数据分别导入到十二个数据库表中") + public ApiResponse uploadExcelFile( + @Parameter(description = "Excel文件(.xlsx格式)", required = true) + @RequestParam("file") MultipartFile file, HttpServletRequest httpServletRequest) { + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + String fileName = file.getOriginalFilename(); + log.info("接收到Excel文件上传请求,文件名:{}", fileName); + + try { + String message = excelImportService.importExcelFile(file); + + // 记录操作历史 - 成功 + 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); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%sExcel文件导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "Excel数据导入", 1, "失败", resultMessage); + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "Excel文件导入失败:" + e.getMessage()); + } + } + + /** + * 上传Excel文件并导入开奖数据 + */ + @PostMapping("/upload-lottery-draws") + @Operation(summary = "上传Excel文件导入开奖数据", description = "上传包含T10工作表的Excel文件,只导入开奖数据到lottery_draws表") + public ApiResponse uploadLotteryDrawsFile( + @Parameter(description = "包含T10工作表的Excel文件(.xlsx格式)", required = true) + @RequestParam("file") MultipartFile file, HttpServletRequest httpServletRequest) { + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + String fileName = file.getOriginalFilename(); + log.info("接收到开奖数据上传请求,文件名:{}", fileName); + + try { + String message = excelImportService.importLotteryDrawsFile(file); + + // 记录操作历史 - 成功 + 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); + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "开奖数据文件导入失败:" + e.getMessage()); + } + } + + /** + * 上传Excel文件并追加导入开奖数据 + */ + @PostMapping("/append-lottery-draws") + @Operation(summary = "上传Excel文件追加导入开奖数据", description = "上传包含T10工作表的Excel文件,追加导入开奖数据(不清空现有数据,跳过重复期号)") + public ApiResponse appendLotteryDrawsFile( + @Parameter(description = "包含T10工作表的Excel文件(.xlsx格式)", required = true) + @RequestParam("file") MultipartFile file, HttpServletRequest httpServletRequest) { + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + String fileName = file.getOriginalFilename(); + log.info("接收到开奖数据追加上传请求,文件名:{}", fileName); + + try { + String message = excelImportService.appendLotteryDrawsFile(file); + + // 记录操作历史 - 成功 + 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); + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "开奖数据追加导入失败:" + e.getMessage()); + } + } + + /** + * 获取导入说明 + */ + @GetMapping("/import-info") + @Operation(summary = "获取导入说明", description = "获取Excel数据导入的详细说明") + public String getImportInfo() { + return excelImportService.getImportInfo(); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/HealthController.java b/src/main/java/com/xy/xyaicpzs/controller/HealthController.java new file mode 100644 index 0000000..4af335d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/HealthController.java @@ -0,0 +1,15 @@ +package com.xy.xyaicpzs.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/health") +public class HealthController { + + @GetMapping + public String healthCheck() { + return "ok"; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/controller/JwtController.java b/src/main/java/com/xy/xyaicpzs/controller/JwtController.java new file mode 100644 index 0000000..977ed7d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/JwtController.java @@ -0,0 +1,108 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.service.CozeAuthService; +import com.xy.xyaicpzs.util.JwtUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * JWT令牌控制器 + */ +@RestController +@RequestMapping("/jwt") +@Tag(name = "JWT接口", description = "提供JWT令牌生成功能") +public class JwtController { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private CozeAuthService cozeAuthService; + + /** + * 生成JWT令牌 + * + * @param expireSeconds 过期时间(秒) + * @param sessionName 会话名称(可选) + * @param deviceId 设备ID(可选) + * @return JWT令牌 + */ + @GetMapping("/token") + @Operation(summary = "生成JWT令牌", description = "生成Coze API访问所需的JWT令牌") + public ApiResponse> generateToken( + @Parameter(description = "过期时间(秒)") @RequestParam(defaultValue = "600") int expireSeconds, + @Parameter(description = "会话名称(可选)") @RequestParam(required = false) String sessionName, + @Parameter(description = "设备ID(可选)") @RequestParam(required = false) String deviceId) { + + try { + String token = jwtUtil.generateToken(expireSeconds, sessionName, deviceId); + Map result = new HashMap<>(); + result.put("token", token); + return ResultUtils.success(result); + } catch (Exception e) { + return ResultUtils.error(50000, "JWT生成失败: " + e.getMessage()); + } + } + + /** + * 通过JWT获取访问令牌 + * + * @param jwt JWT令牌 + * @param durationSeconds 访问令牌有效期(秒),默认为86400秒(1天) + * @return 包含访问令牌和过期时间的信息 + */ + @PostMapping("/access-token") + @Operation(summary = "获取访问令牌", description = "通过JWT获取Coze API的OAuth访问令牌") + public ApiResponse> getAccessToken( + @Parameter(description = "JWT令牌") @RequestParam String jwt, + @Parameter(description = "令牌有效期(秒)") @RequestParam(defaultValue = "86400") Integer durationSeconds) { + + try { + Map tokenInfo = cozeAuthService.getAccessToken(jwt, durationSeconds); + return ResultUtils.success(tokenInfo); + } catch (Exception e) { + return ResultUtils.error(50000, "获取访问令牌失败: " + e.getMessage()); + } + } + + /** + * 一站式获取访问令牌(生成JWT并立即获取访问令牌) + * + * @param jwtExpireSeconds JWT过期时间(秒) + * @param sessionName 会话名称(可选) + * @param deviceId 设备ID(可选) + * @param tokenDurationSeconds 访问令牌有效期(秒) + * @return 包含JWT、访问令牌和过期时间的信息 + */ + @PostMapping("/one-step-token") + @Operation(summary = "一站式获取访问令牌", description = "生成JWT并立即获取Coze API的OAuth访问令牌") + public ApiResponse> getOneStepToken( + @Parameter(description = "JWT过期时间(秒)") @RequestParam(defaultValue = "600") int jwtExpireSeconds, + @Parameter(description = "会话名称(可选)") @RequestParam(required = false) String sessionName, + @Parameter(description = "设备ID(可选)") @RequestParam(required = false) String deviceId, + @Parameter(description = "访问令牌有效期(秒)") @RequestParam(defaultValue = "86400") Integer tokenDurationSeconds) { + + try { + // 生成JWT令牌 + String jwt = jwtUtil.generateToken(jwtExpireSeconds, sessionName, deviceId); + + // 获取访问令牌 + Map tokenInfo = cozeAuthService.getAccessToken(jwt, tokenDurationSeconds); + + // 合并结果 + tokenInfo.put("jwt", jwt); + + return ResultUtils.success(tokenInfo); + } catch (Exception e) { + return ResultUtils.error(50000, "获取访问令牌失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/OperationHistoryController.java b/src/main/java/com/xy/xyaicpzs/controller/OperationHistoryController.java new file mode 100644 index 0000000..1e94b03 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/OperationHistoryController.java @@ -0,0 +1,96 @@ +package com.xy.xyaicpzs.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +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.OperationHistory; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.service.OperationHistoryService; +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.web.bind.annotation.*; + +import java.util.List; + +/** + * 操作历史管理控制器 + */ +@RestController +@RequestMapping("/operation-history") +@Slf4j +@Tag(name = "操作历史管理", description = "操作历史记录相关接口") +public class OperationHistoryController { + + @Autowired + private OperationHistoryService operationHistoryService; + + @Autowired + private UserService userService; + + /** + * 获取操作历史记录 + * 支持按操作模块和操作结果进行筛选 + */ + @GetMapping("/list") + @Operation(summary = "获取操作历史记录", description = "获取操作历史记录,支持按操作模块、操作结果筛选和结果信息模糊搜索") + public ApiResponse> getOperationHistory( + @Parameter(description = "操作模块(0-会员码管理/1-Excel导入管理等)") + @RequestParam(value = "operationModule", required = false) Integer operationModule, + @Parameter(description = "操作结果(成功/失败)") + @RequestParam(value = "operationResult", required = false) String operationResult, + @Parameter(description = "结果信息关键词(支持模糊搜索)") + @RequestParam(value = "keyword", required = false) String keyword, + HttpServletRequest httpServletRequest) { + + try { + // 权限校验:仅管理员可以查看 + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限查看操作历史"); + } + + log.info("获取操作历史,操作模块:{},操作结果:{},关键词:{}", operationModule, operationResult, keyword); + + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + + // 添加操作模块筛选条件 + if (operationModule != null && operationModule >= 0) { + queryWrapper.eq("operationModule", operationModule); + } + + // 添加操作结果筛选条件 + if (operationResult != null && !operationResult.trim().isEmpty()) { + if (!operationResult.equals("成功") && !operationResult.equals("失败")) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "操作结果只能是'成功'或'失败'"); + } + queryWrapper.eq("operationResult", operationResult); + } + + // 添加结果信息模糊搜索条件 + if (keyword != null && !keyword.trim().isEmpty()) { + queryWrapper.like("resultMessage", keyword.trim()); + } + + // 按操作时间降序排序 + queryWrapper.orderByDesc("operationTime"); + + // 查询操作历史 + List records = operationHistoryService.list(queryWrapper); + + log.info("操作历史查询成功,共{}条记录", records.size()); + + return ResultUtils.success(records); + + } catch (Exception e) { + log.error("获取操作历史失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取操作历史失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/SmsController.java b/src/main/java/com/xy/xyaicpzs/controller/SmsController.java new file mode 100644 index 0000000..828751b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/SmsController.java @@ -0,0 +1,48 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.service.SmsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 短信控制器 + */ +@RestController +@RequestMapping("/sms") +@Tag(name = "短信接口", description = "提供短信验证码相关功能") +public class SmsController { + + @Autowired + private SmsService smsService; + + /** + * 发送短信验证码 + * + * @param phoneNumber 手机号 + * @return 发送结果 + */ + @PostMapping("/sendCode") + @Operation(summary = "发送短信验证码", description = "向指定手机号发送验证码,每个手机号每天最多发送3次") + public ApiResponse sendVerificationCode( + @Parameter(description = "手机号码", required = true) + @RequestParam String phoneNumber) { + try { + boolean success = smsService.sendVerificationCode(phoneNumber); + if (success) { + return ResultUtils.success(true); + } else { + return ResultUtils.error(40001, "发送验证码失败,请稍后重试或联系客服"); + } + } catch (Exception e) { + return ResultUtils.error(50000, "发送验证码异常:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java b/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java new file mode 100644 index 0000000..ee5b360 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java @@ -0,0 +1,83 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.util.SpeechRecognizerDemo; +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; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 语音识别控制器 + */ +@RestController +@RequestMapping("/api/speech") +public class SpeechRecognitionController { + + @Autowired + private SpeechRecognizerDemo speechRecognizer; + + /** + * 识别本地语音文件 + * @param filePath 文件路径 + * @param sampleRate 采样率 + * @return 识别结果 + */ + @GetMapping("/recognize") + public ApiResponse> recognizeSpeech( + @RequestParam("filePath") String filePath, + @RequestParam(value = "sampleRate", defaultValue = "16000") int sampleRate) { + + String text = speechRecognizer.speechToText(filePath, sampleRate); + Map result = new HashMap<>(); + result.put("text", text); + return ResultUtils.success(result); + } + + /** + * 上传并识别语音文件 + * @param file 上传的语音文件 + * @param sampleRate 采样率 + * @return 识别结果 + */ + @PostMapping("/upload-and-recognize") + public ApiResponse> uploadAndRecognize( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "sampleRate", defaultValue = "16000") int sampleRate) { + + if (file.isEmpty()) { + return ResultUtils.error(40001, "上传文件不能为空"); + } + + try { + // 创建临时目录 + String tempDir = System.getProperty("java.io.tmpdir"); + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); + Path filePath = Paths.get(tempDir, fileName); + + // 保存上传的文件 + file.transferTo(filePath.toFile()); + + // 识别语音 + String text = speechRecognizer.speechToText(filePath.toString(), sampleRate); + + // 删除临时文件 + Files.deleteIfExists(filePath); + + Map result = new HashMap<>(); + result.put("text", text); + return ResultUtils.success(result); + } catch (IOException e) { + return ResultUtils.error(50000, "文件处理失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/UserController.java b/src/main/java/com/xy/xyaicpzs/controller/UserController.java new file mode 100644 index 0000000..c09e0c1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/UserController.java @@ -0,0 +1,578 @@ +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.DeleteRequest; +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +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.exception.BusinessException; +import com.xy.xyaicpzs.service.UserService; +import com.xy.xyaicpzs.service.VipCodeService; +import com.xy.xyaicpzs.service.SmsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 用户接口 + */ +@Slf4j +@RestController +@RequestMapping("/user") +@Tag(name = "用户管理", description = "用户管理相关接口") +public class UserController { + + @Resource + private UserService userService; + + @Resource + private VipCodeService vipCodeService; + + @Resource + private SmsService smsService; + + // region 登录相关 + + /** + * 用户登录 + * + * @param userLoginRequest + * @param request + * @return + */ + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户登录接口") + public ApiResponse userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) { + if (userLoginRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + String userAccount = userLoginRequest.getUserAccount(); + String userPassword = userLoginRequest.getUserPassword(); + if (StringUtils.isAnyBlank(userAccount, userPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + User user = userService.userLogin(userAccount, userPassword, request); + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return ResultUtils.success(userVO); + } + + /** + * 用户注销 + * + * @param request + * @return + */ + @PostMapping("/logout") + @Operation(summary = "用户注销", description = "用户注销接口") + public ApiResponse userLogout(HttpServletRequest request) { + if (request == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + boolean result = userService.userLogout(request); + return ResultUtils.success(result); + } + + /** + * 获取当前登录用户 + * + * @param request + * @return + */ + @GetMapping("/get/login") + @Operation(summary = "获取当前登录用户", description = "获取当前登录用户信息") + public ApiResponse getLoginUser(HttpServletRequest request) { + User user = userService.getLoginUser(request); + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return ResultUtils.success(userVO); + } + + // endregion + + // region 增删改查 + + /** + * 创建用户 + * + * @param userAddRequest + * @param request + * @return + */ + @PostMapping("/add") + @Operation(summary = "创建用户", description = "管理员创建用户") + public ApiResponse addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) { + if (userAddRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + + // 参数校验 + String userAccount = userAddRequest.getUserAccount(); + String userPassword = userAddRequest.getUserPassword(); + String password = userAddRequest.getPassword(); + String phone = userAddRequest.getPhone(); + + // 如果userPassword为空但password不为空,则使用password + if (StringUtils.isBlank(userPassword) && StringUtils.isNotBlank(password)) { + userAddRequest.setUserPassword(password); + userPassword = password; + } + + if (StringUtils.isAnyBlank(userAccount, userPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号或密码不能为空"); + } + + if (phone != null && !phone.isEmpty()) { + // 如果提供了手机号,可以进行手机号格式校验 + if (phone.length() != 11) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式不正确"); + } + } + + User user = new User(); + BeanUtils.copyProperties(userAddRequest, user); + + // 密码加密,使用Service层的加密方法 + String encryptPassword = userService.encryptPassword(userPassword); + user.setUserPassword(encryptPassword); + + boolean result = userService.save(user); + if (!result) { + throw new BusinessException(ErrorCode.OPERATION_ERROR); + } + return ResultUtils.success(user.getId()); + } + + /** + * 修改用户状态 + * + * @param userStatusUpdateRequest 用户状态更新请求 + * @param request HTTP请求 + * @return 修改结果 + */ + @PostMapping("/update-status") + @Operation(summary = "修改用户状态", description = "管理员修改用户状态(正常/封禁)") + public ApiResponse updateUserStatus(@RequestBody UserStatusUpdateRequest userStatusUpdateRequest, + HttpServletRequest request) { + if (userStatusUpdateRequest == null || userStatusUpdateRequest.getId() == null + || userStatusUpdateRequest.getId() <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID不正确"); + } + + Long id = userStatusUpdateRequest.getId(); + Integer status = userStatusUpdateRequest.getStatus(); + + // 校验状态值 + if (status == null || (status != 0 && status != 1)) { + 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) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "用户不存在"); + } + + // 更新用户状态 + user.setStatus(status); + boolean result = userService.updateById(user); + if (!result) { + throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作失败"); + } + + return ResultUtils.success(true); + } + + /** + * 删除用户 + * + * @param deleteRequest + * @param request + * @return + */ + @PostMapping("/delete") + @Operation(summary = "删除用户", description = "管理员删除用户") + public ApiResponse deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { + if (deleteRequest == null || deleteRequest.getId() <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + boolean b = userService.removeById(deleteRequest.getId()); + return ResultUtils.success(b); + } + + /** + * 更新用户 + * + * @param userUpdateRequest + * @param request + * @return + */ + @PostMapping("/update") + @Operation(summary = "更新用户", description = "更新用户信息") + public ApiResponse updateUser(@RequestBody UserUpdateRequest userUpdateRequest, HttpServletRequest request) { + if (userUpdateRequest == null || userUpdateRequest.getId() == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + + // 参数校验 + String userPassword = userUpdateRequest.getUserPassword(); + String password = userUpdateRequest.getPassword(); + String phone = userUpdateRequest.getPhone(); + + // 如果userPassword为空但password不为空,则使用password + if (StringUtils.isBlank(userPassword) && StringUtils.isNotBlank(password)) { + userUpdateRequest.setUserPassword(password); + userPassword = password; + } + + if (phone != null && !phone.isEmpty()) { + // 如果提供了手机号,可以进行手机号格式校验 + if (phone.length() != 11) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式不正确"); + } + } + + User user = new User(); + BeanUtils.copyProperties(userUpdateRequest, user); + + // 如果更新了密码,需要进行加密 + if (StringUtils.isNotBlank(userPassword)) { + String encryptPassword = userService.encryptPassword(userPassword); + user.setUserPassword(encryptPassword); + } + + boolean result = userService.updateById(user); + return ResultUtils.success(result); + } + + /** + * 根据 id 获取用户 + * + * @param id + * @param request + * @return + */ + @GetMapping("/get") + @Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户信息") + public ApiResponse getUserById(@RequestParam("id") long id, HttpServletRequest request) { + if (id <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + User user = userService.getById(id); + if (user == null) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR); + } + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return ResultUtils.success(userVO); + } + + /** + * 获取用户列表 + * + * @param userQueryRequest + * @param request + * @return + */ + @GetMapping("/list") + @Operation(summary = "获取用户列表", description = "获取用户列表,支持用户名/手机号模糊匹配和角色状态筛选") + public ApiResponse> listUser(UserQueryRequest userQueryRequest, HttpServletRequest request) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (userQueryRequest != null) { + // 用户名模糊匹配 + if (StringUtils.isNotBlank(userQueryRequest.getUserName())) { + queryWrapper.like("userName", userQueryRequest.getUserName()); + } + + // 手机号模糊匹配 + if (StringUtils.isNotBlank(userQueryRequest.getPhone())) { + queryWrapper.like("phone", userQueryRequest.getPhone()); + } + + // 账号模糊匹配 + if (StringUtils.isNotBlank(userQueryRequest.getUserAccount())) { + queryWrapper.like("userAccount", userQueryRequest.getUserAccount()); + } + + // 用户角色精确匹配 + if (userQueryRequest.getUserRole() != null) { + queryWrapper.eq("userRole", userQueryRequest.getUserRole()); + } + + // 用户状态精确匹配 + if (userQueryRequest.getStatus() != null) { + queryWrapper.eq("status", userQueryRequest.getStatus()); + } + + // 会员状态匹配 + if (userQueryRequest.getIsVip() != null) { + queryWrapper.eq("isVip", userQueryRequest.getIsVip()); + } + } + + List userList = userService.list(queryWrapper); + List userVOList = userList.stream().map(user -> { + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return userVO; + }).collect(Collectors.toList()); + return ResultUtils.success(userVOList); + } + + /** + * 分页获取用户列表 + * + * @param userQueryRequest + * @param request + * @return + */ + @GetMapping("/list/page") + @Operation(summary = "分页获取用户列表", description = "分页获取用户列表,支持用户名/手机号模糊匹配和角色状态筛选") + public ApiResponse> listUserByPage(UserQueryRequest userQueryRequest, HttpServletRequest request) { + long current = 1; + long size = 10; + + if (userQueryRequest != null) { + current = userQueryRequest.getCurrent(); + size = userQueryRequest.getPageSize(); + } + + QueryWrapper queryWrapper = new QueryWrapper<>(); + + if (userQueryRequest != null) { + // 用户名模糊匹配 + if (StringUtils.isNotBlank(userQueryRequest.getUserName())) { + queryWrapper.like("userName", userQueryRequest.getUserName()); + } + + // 手机号模糊匹配 + if (StringUtils.isNotBlank(userQueryRequest.getPhone())) { + queryWrapper.like("phone", userQueryRequest.getPhone()); + } + + // 账号模糊匹配 + if (StringUtils.isNotBlank(userQueryRequest.getUserAccount())) { + queryWrapper.like("userAccount", userQueryRequest.getUserAccount()); + } + + // 用户角色精确匹配 + if (userQueryRequest.getUserRole() != null) { + queryWrapper.eq("userRole", userQueryRequest.getUserRole()); + } + + // 用户状态精确匹配 + if (userQueryRequest.getStatus() != null) { + queryWrapper.eq("status", userQueryRequest.getStatus()); + } + + // 会员状态匹配 + if (userQueryRequest.getIsVip() != null) { + queryWrapper.eq("isVip", userQueryRequest.getIsVip()); + } + } + + Page userPage = userService.page(new Page<>(current, size), queryWrapper); + + // 创建新的Page对象用于返回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 ResultUtils.success(userVOPage); + } + + /** + * 获取用户统计信息 + * + * @return 包含总用户数和会员数的统计信息 + */ + @GetMapping("/count") + @Operation(summary = "获取用户统计信息", description = "获取系统中总用户数和会员数量") + public ApiResponse> getUserCount() { + // 获取总用户数 + long totalUserCount = userService.count(); + + // 获取会员数量(isVip=1) + QueryWrapper vipQueryWrapper = new QueryWrapper<>(); + vipQueryWrapper.eq("isVip", 1); + long vipUserCount = userService.count(vipQueryWrapper); + + // 获取正常状态用户数量(status=0) + QueryWrapper normalStatusWrapper = new QueryWrapper<>(); + normalStatusWrapper.eq("status", 0); + long normalUserCount = userService.count(normalStatusWrapper); + + // 获取封禁状态用户数量(status=1) + QueryWrapper bannedStatusWrapper = new QueryWrapper<>(); + bannedStatusWrapper.eq("status", 1); + long bannedUserCount = userService.count(bannedStatusWrapper); + + // 构造返回结果 + Map countMap = new HashMap<>(); + countMap.put("totalUserCount", totalUserCount); + countMap.put("vipUserCount", vipUserCount); + countMap.put("normalUserCount", normalUserCount); + countMap.put("bannedUserCount", bannedUserCount); + + return ResultUtils.success(countMap); + } + + /** + * 激活会员码 + * + * @param request 会员码激活请求 + * @return 是否激活成功 + */ + @PostMapping("/activate-vip") + @Operation(summary = "激活会员码", description = "用户使用会员码激活会员服务") + public ApiResponse activateVipCode(@RequestBody VipCodeActivateRequest request) { + if (request == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); + } + if (request.getUserId() == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID不能为空"); + } + if (StringUtils.isBlank(request.getCode())) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员码不能为空"); + } + + try { + boolean result = vipCodeService.activateVipCode(request.getUserId(), request.getCode()); + return ResultUtils.success(result); + } catch (IllegalArgumentException e) { + log.error("会员码激活失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.PARAMS_ERROR, e.getMessage()); + } catch (RuntimeException e) { + log.error("会员码激活系统错误:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "会员码激活失败,请稍后重试"); + } + } + + /** + * 手机号注册 + * + * @param userPhoneRegisterRequest 手机号注册请求 + * @return 用户ID + */ + @PostMapping("/phone/register") + @Operation(summary = "手机号注册", description = "使用手机号和验证码注册用户") + public ApiResponse userPhoneRegister(@RequestBody UserPhoneRegisterRequest userPhoneRegisterRequest) { + if (userPhoneRegisterRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + long result = userService.userPhoneRegister(userPhoneRegisterRequest); + return ResultUtils.success(result); + } + + /** + * 手机号登录 + * + * @param userPhoneLoginRequest 手机号登录请求 + * @param request HTTP请求 + * @return 用户信息 + */ + @PostMapping("/phone/login") + @Operation(summary = "手机号登录", description = "使用手机号和验证码登录") + public ApiResponse userPhoneLogin(@RequestBody UserPhoneLoginRequest userPhoneLoginRequest, HttpServletRequest request) { + if (userPhoneLoginRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR); + } + User user = userService.userPhoneLogin(userPhoneLoginRequest, request); + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return ResultUtils.success(userVO); + } + + /** + * 重置密码 + * + * @param resetPasswordRequest 重置密码请求 + * @return 是否重置成功 + */ + @PostMapping("/reset-password") + @Operation(summary = "重置密码", description = "使用手机号和验证码重置密码") + public ApiResponse resetPassword(@RequestBody ResetPasswordRequest resetPasswordRequest) { + if (resetPasswordRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); + } + + String phone = resetPasswordRequest.getPhone(); + String code = resetPasswordRequest.getCode(); + String newPassword = resetPasswordRequest.getNewPassword(); + String confirmPassword = resetPasswordRequest.getConfirmPassword(); + + // 校验参数 + if (StringUtils.isAnyBlank(phone, code, newPassword, confirmPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空"); + } + + // 校验手机号格式 + if (phone.length() != 11) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式不正确"); + } + + // 校验两次密码是否一致 + if (!newPassword.equals(confirmPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致"); + } + + // 密码长度校验 + if (newPassword.length() < 8) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码长度不能小于8位"); + } + + // 验证短信验证码 + boolean isCodeValid = smsService.verifyCode(phone, code); + if (!isCodeValid) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误或已过期"); + } + + // 查询用户是否存在 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("phone", phone); + User user = userService.getOne(queryWrapper); + if (user == null) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到该手机号注册的用户"); + } + + // 更新密码 + String encryptPassword = userService.encryptPassword(newPassword); + user.setUserPassword(encryptPassword); + boolean result = userService.updateById(user); + + if (!result) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "密码重置失败,请稍后重试"); + } + + ApiResponse response = ResultUtils.success(true); + response.setMessage("密码重置成功"); + return response; + } + + // endregion +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/VipCodeController.java b/src/main/java/com/xy/xyaicpzs/controller/VipCodeController.java new file mode 100644 index 0000000..c646b47 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/VipCodeController.java @@ -0,0 +1,298 @@ +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.GenerateVipCodesRequest; +import com.xy.xyaicpzs.common.requset.VipCodeQueryRequest; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.domain.entity.VipCode; +import com.xy.xyaicpzs.domain.vo.VipCodeVO; +import com.xy.xyaicpzs.exception.BusinessException; +import com.xy.xyaicpzs.service.OperationHistoryService; +import com.xy.xyaicpzs.service.UserService; +import com.xy.xyaicpzs.service.VipCodeService; +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.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 会员码管理接口 + */ +@Slf4j +@RestController +@RequestMapping("/vip-code") +@Tag(name = "会员码管理", description = "会员码管理相关接口") +public class VipCodeController { + + @Resource + private VipCodeService vipCodeService; + + @Autowired + private UserService userService; + + @Autowired + private OperationHistoryService operationHistoryService; + + /** + * 批量生成会员码 + * + * @param request 生成会员码请求 + * @return 生成成功的数量 + */ + @PostMapping("/generate") + @Operation(summary = "批量生成会员码", description = "管理员批量生成会员码") + public ApiResponse generateVipCodes(@RequestBody GenerateVipCodesRequest request, + HttpServletRequest httpServletRequest) { + + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + if (request == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); + } + if (request.getNumCodes() == null || request.getNumCodes() <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成数量必须大于0"); + } + if (request.getVipExpireTime() == null || request.getVipExpireTime() <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数必须大于0"); + } + if (request.getNumCodes() > 1000) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "单次生成数量不能超过1000"); + } + + try { + int result = vipCodeService.generateVipCodes(request.getNumCodes(), request.getVipExpireTime(), userId, userName); + + // 记录操作历史 - 成功 + String resultMessage = String.format("%s成功生成%d个会员码,有效月数:%d", userName, result, request.getVipExpireTime()); + operationHistoryService.recordOperation(userId, "批量生成会员码", 0, "成功", resultMessage); + + return ResultUtils.success(result); + } catch (IllegalArgumentException e) { + log.error("生成会员码参数错误:{}", e.getMessage()); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s生成会员码失败:%s,请求数量:%d,有效月数:%d", + userName, e.getMessage(), request.getNumCodes(), request.getVipExpireTime()); + operationHistoryService.recordOperation(userId, "批量生成会员码", 0, "失败", resultMessage); + + throw new BusinessException(ErrorCode.PARAMS_ERROR, e.getMessage()); + } catch (RuntimeException e) { + log.error("生成会员码系统错误:{}", e.getMessage()); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s生成会员码系统错误:%s,请求数量:%d,有效月数:%d", + userName, e.getMessage(), request.getNumCodes(), request.getVipExpireTime()); + operationHistoryService.recordOperation(userId, "批量生成会员码", 0, "失败", resultMessage); + + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成会员码失败,请稍后重试"); + } + } + + /** + * 获取一个可用的会员码 + * + * @param vipExpireTime 会员有效月数(1或12) + * @return 可用的会员码 + */ + @GetMapping("/available") + @Operation(summary = "获取可用会员码", description = "根据有效月数获取一个可用的会员码") + public ApiResponse getAvailableVipCode(@RequestParam("vipExpireTime") Integer vipExpireTime, + HttpServletRequest httpServletRequest) { + + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + if (vipExpireTime == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数不能为空"); + } + if (vipExpireTime != 1 && vipExpireTime != 12) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数只能是1或12"); + } + + try { + String code = vipCodeService.getAvailableVipCode(vipExpireTime, userId, userName); + if (code == null) { + // 记录操作历史 - 失败 + String resultMessage = String.format("%s获取可用会员码失败:没有找到可用的会员码,有效月数:%d", userName, vipExpireTime); + operationHistoryService.recordOperation(userId, "获取可用会员码", 0, "失败", resultMessage); + + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "没有找到可用的会员码"); + } + + // 记录操作历史 - 成功 + String resultMessage = String.format("%s成功获取可用会员码:%s,有效月数:%d", userName, code, vipExpireTime); + operationHistoryService.recordOperation(userId, "获取可用会员码", 0, "成功", resultMessage); + + return ResultUtils.success(code); + } catch (IllegalArgumentException e) { + log.error("获取可用会员码参数错误:{}", e.getMessage()); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s获取可用会员码参数错误:%s,有效月数:%d", userName, e.getMessage(), vipExpireTime); + operationHistoryService.recordOperation(userId, "获取可用会员码", 0, "失败", resultMessage); + + throw new BusinessException(ErrorCode.PARAMS_ERROR, e.getMessage()); + } catch (Exception e) { + log.error("获取可用会员码系统错误:{}", e.getMessage()); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s获取可用会员码系统错误:%s,有效月数:%d", userName, e.getMessage(), vipExpireTime); + operationHistoryService.recordOperation(userId, "获取可用会员码", 0, "失败", resultMessage); + + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取可用会员码失败,请生成后获取。"); + } + } + + /** + * 分页获取会员码列表 + * + * @param vipCodeQueryRequest 会员码查询请求 + * @param httpServletRequest Http请求 + * @return 分页会员码列表 + */ + @GetMapping("/list/page") + @Operation(summary = "分页获取会员码列表", description = "分页获取会员码列表,支持根据会员码、使用状态和时间筛选") + public ApiResponse> listVipCodesByPage(VipCodeQueryRequest vipCodeQueryRequest, + HttpServletRequest httpServletRequest) { + // 权限校验 + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + + if (vipCodeQueryRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); + } + + long current = vipCodeQueryRequest.getCurrent(); + long pageSize = vipCodeQueryRequest.getPageSize(); + + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + + // 根据会员码模糊查询 + if (StringUtils.isNotBlank(vipCodeQueryRequest.getCode())) { + queryWrapper.like("code", vipCodeQueryRequest.getCode()); + } + + // 根据使用状态筛选 + if (vipCodeQueryRequest.getIsUse() != null) { + queryWrapper.eq("isUse", vipCodeQueryRequest.getIsUse()); + } + + // 根据会员有效月数筛选 + if (vipCodeQueryRequest.getVipExpireTime() != null) { + queryWrapper.eq("vipExpireTime", vipCodeQueryRequest.getVipExpireTime()); + } + + // 根据创建人ID筛选 + if (vipCodeQueryRequest.getCreatedUserId() != null) { + queryWrapper.eq("createdUserId", vipCodeQueryRequest.getCreatedUserId()); + } + + // 根据创建人名称模糊查询 + if (StringUtils.isNotBlank(vipCodeQueryRequest.getCreatedUserName())) { + queryWrapper.like("createdUserName", vipCodeQueryRequest.getCreatedUserName()); + } + + // 根据使用人ID筛选 + if (vipCodeQueryRequest.getUsedUserId() != null) { + queryWrapper.eq("usedUserId", vipCodeQueryRequest.getUsedUserId()); + } + + // 根据创建时间范围筛选 + if (vipCodeQueryRequest.getStartTime() != null && vipCodeQueryRequest.getEndTime() != null) { + queryWrapper.between("createTime", vipCodeQueryRequest.getStartTime(), vipCodeQueryRequest.getEndTime()); + } else if (vipCodeQueryRequest.getStartTime() != null) { + queryWrapper.ge("createTime", vipCodeQueryRequest.getStartTime()); + } else if (vipCodeQueryRequest.getEndTime() != null) { + queryWrapper.le("createTime", vipCodeQueryRequest.getEndTime()); + } + + // 按会员编号升序排序(从小到大) + queryWrapper.orderByAsc("vipNumber"); + + // 执行分页查询 + Page vipCodePage = vipCodeService.page(new Page<>(current, pageSize), queryWrapper); + + // 转换为VO对象 + List vipCodeVOList = vipCodePage.getRecords().stream().map(vipCode -> { + VipCodeVO vipCodeVO = new VipCodeVO(); + BeanUtils.copyProperties(vipCode, vipCodeVO); + return vipCodeVO; + }).collect(Collectors.toList()); + + // 创建VO分页对象,确保正确传递所有分页信息 + Page vipCodeVOPage = new Page<>(vipCodePage.getCurrent(), vipCodePage.getSize(), vipCodePage.getTotal()); + vipCodeVOPage.setRecords(vipCodeVOList); + // 手动设置pages值 + vipCodeVOPage.setPages(vipCodePage.getPages()); + + return ResultUtils.success(vipCodeVOPage); + } + + /** + * 获取会员码统计数量 + * + * @param httpServletRequest Http请求 + * @return 会员码统计数量 + */ + @GetMapping("/count") + @Operation(summary = "获取会员码统计数量", description = "获取系统中会员码总数、可用会员码和已使用会员码的数量") + public ApiResponse> getVipCodeCount(HttpServletRequest httpServletRequest) { + // 权限校验 + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限"); + } + + try { + // 构建查询条件 - 总数 + long totalCount = vipCodeService.count(); + + // 构建查询条件 - 已使用的会员码 + QueryWrapper usedQueryWrapper = new QueryWrapper<>(); + usedQueryWrapper.eq("isUse", 1); + long usedCount = vipCodeService.count(usedQueryWrapper); + + // 构建查询条件 - 可用的会员码 + QueryWrapper availableQueryWrapper = new QueryWrapper<>(); + availableQueryWrapper.eq("isUse", 0); + long availableCount = vipCodeService.count(availableQueryWrapper); + + // 构造返回结果 + Map countMap = new HashMap<>(); + countMap.put("totalCount", totalCount); + countMap.put("availableCount", availableCount); + countMap.put("usedCount", usedCount); + + return ResultUtils.success(countMap); + } catch (Exception e) { + log.error("获取会员码统计数量失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取会员码统计数量失败,请稍后重试"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/VipExchangeRecordController.java b/src/main/java/com/xy/xyaicpzs/controller/VipExchangeRecordController.java new file mode 100644 index 0000000..a18c3e6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/VipExchangeRecordController.java @@ -0,0 +1,139 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.domain.entity.VipExchangeRecord; +import com.xy.xyaicpzs.service.VipExchangeRecordService; +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; + +/** + * VIP兑换记录控制器 + */ +@RestController +@RequestMapping("/vip-exchange-record") +@Slf4j +@Tag(name = "VIP兑换记录管理", description = "VIP兑换记录相关接口") +public class VipExchangeRecordController { + + @Autowired + private VipExchangeRecordService vipExchangeRecordService; + + /** + * 根据用户ID获取所有兑换记录 + */ + @GetMapping("/user/{userId}") + @Operation(summary = "获取用户兑换记录", description = "根据用户ID获取该用户的所有VIP兑换记录") + public ApiResponse> getExchangeRecordsByUserId( + @Parameter(description = "用户ID", required = true) + @PathVariable("userId") Long userId) { + + try { + log.info("获取用户兑换记录,用户ID:{}", userId); + + // 参数校验 + if (userId == null || userId <= 0) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "用户ID不能为空且必须大于0"); + } + + // 查询用户兑换记录 + List records = vipExchangeRecordService.getExchangeRecordsByUserId(userId); + + log.info("用户ID:{} 的兑换记录查询成功,共{}条记录", userId, records.size()); + + return ResultUtils.success(records); + + } catch (Exception e) { + log.error("获取用户兑换记录失败,用户ID:{}", userId, e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取兑换记录失败:" + e.getMessage()); + } + } + + /** + * 根据用户ID获取兑换记录(带分页) + */ + @GetMapping("/user/{userId}/page") + @Operation(summary = "分页获取用户兑换记录", description = "根据用户ID分页获取该用户的VIP兑换记录") + public ApiResponse> getExchangeRecordsByUserIdWithPage( + @Parameter(description = "用户ID", required = true) + @PathVariable("userId") Long userId, + @Parameter(description = "页码,从1开始", required = false) + @RequestParam(value = "page", defaultValue = "1") Integer page, + @Parameter(description = "每页大小", required = false) + @RequestParam(value = "size", defaultValue = "10") Integer size) { + + try { + log.info("分页获取用户兑换记录,用户ID:{},页码:{},每页大小:{}", userId, page, size); + + // 参数校验 + if (userId == null || userId <= 0) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "用户ID不能为空且必须大于0"); + } + + if (page < 1) { + page = 1; + } + if (size < 1 || size > 100) { + size = 10; + } + + // 查询用户兑换记录 + List allRecords = vipExchangeRecordService.getExchangeRecordsByUserId(userId); + + // 手动分页 + int start = (page - 1) * size; + int end = Math.min(start + size, allRecords.size()); + List pageRecords = allRecords.subList(start, end); + + log.info("用户ID:{} 的兑换记录分页查询成功,总记录数:{},当前页记录数:{}", + userId, allRecords.size(), pageRecords.size()); + + return ResultUtils.success(pageRecords); + + } catch (Exception e) { + log.error("分页获取用户兑换记录失败,用户ID:{}", userId, e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取兑换记录失败:" + e.getMessage()); + } + } + + /** + * 根据兑换记录ID获取详情 + */ + @GetMapping("/{recordId}") + @Operation(summary = "获取兑换记录详情", description = "根据兑换记录ID获取详细信息") + public ApiResponse getExchangeRecordById( + @Parameter(description = "兑换记录ID", required = true) + @PathVariable("recordId") Long recordId) { + + try { + log.info("获取兑换记录详情,记录ID:{}", recordId); + + // 参数校验 + if (recordId == null || recordId <= 0) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "兑换记录ID不能为空且必须大于0"); + } + + // 查询兑换记录 + VipExchangeRecord record = vipExchangeRecordService.getById(recordId); + + if (record == null) { + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "兑换记录不存在"); + } + + log.info("兑换记录详情查询成功,记录ID:{}", recordId); + + return ResultUtils.success(record); + + } catch (Exception e) { + log.error("获取兑换记录详情失败,记录ID:{}", recordId, e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取兑换记录详情失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/ResetPasswordRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/ResetPasswordRequest.java new file mode 100644 index 0000000..fbcad14 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/ResetPasswordRequest.java @@ -0,0 +1,34 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 重置密码请求 + */ +@Data +public class ResetPasswordRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户手机号 + */ + private String phone; + + /** + * 短信验证码 + */ + private String code; + + /** + * 新密码 + */ + private String newPassword; + + /** + * 确认新密码 + */ + private String confirmPassword; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserAddRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserAddRequest.java new file mode 100644 index 0000000..0fe715b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserAddRequest.java @@ -0,0 +1,70 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 用户创建请求 + */ +@Data +public class UserAddRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户昵称 + */ + private String userName; + + /** + * 账号 + */ + private String userAccount; + + /** + * 电话 + */ + private String phone; + + /** + * 用户头像 + */ + private String userAvatar; + + /** + * 性别 + */ + private Integer gender; + + /** + * 用户角色:user / admin + */ + private String userRole; + + /** + * 密码 + */ + private String userPassword; + + /** + * 密码(兼容格式) + */ + private String password; + + /** + * 是否会员:0-非会员,1-会员 + */ + private Integer isVip; + + /** + * 会员到期时间 + */ + private Date vipExpire; + + /** + * 状态:0-正常,1-封禁 + */ + private Integer status; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserLoginRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserLoginRequest.java new file mode 100644 index 0000000..2eb2b35 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserLoginRequest.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户登录请求 + */ +@Data +public class UserLoginRequest implements Serializable { + + private static final long serialVersionUID = 3191241716373120793L; + + private String userAccount; + + private String userPassword; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserPhoneLoginRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserPhoneLoginRequest.java new file mode 100644 index 0000000..aa9bf2f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserPhoneLoginRequest.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户手机号登录请求 + */ +@Data +@Schema(description = "用户手机号登录请求") +public class UserPhoneLoginRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "手机号") + private String phone; + + @Schema(description = "验证码") + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserPhoneRegisterRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserPhoneRegisterRequest.java new file mode 100644 index 0000000..f67251e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserPhoneRegisterRequest.java @@ -0,0 +1,34 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户手机号注册请求 + */ +@Data +@Schema(description = "用户手机号注册请求") +public class UserPhoneRegisterRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "用户账号") + private String userAccount; + + @Schema(description = "用户名称") + private String userName; + + @Schema(description = "用户密码") + private String userPassword; + + @Schema(description = "确认密码") + private String checkPassword; + + @Schema(description = "手机号") + private String phone; + + @Schema(description = "验证码") + private String code; +} \ 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 new file mode 100644 index 0000000..e6cab54 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserQueryRequest.java @@ -0,0 +1,57 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import com.xy.xyaicpzs.common.PageRequest; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 用户查询请求 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class UserQueryRequest extends PageRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private Long id; + + /** + * 用户昵称 + */ + private String userName; + + /** + * 账号 + */ + private String userAccount; + + /** + * 手机号 + */ + private String phone; + + /** + * 性别 + */ + private Integer gender; + + /** + * 用户角色:user / admin + */ + private String userRole; + + /** + * 是否会员:0-非会员,1-会员 + */ + private Integer isVip; + + /** + * 状态:0-正常,1-封禁 + */ + private Integer status; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserRegisterRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserRegisterRequest.java new file mode 100644 index 0000000..e16e3de --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserRegisterRequest.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户注册请求 + */ +@Data +public class UserRegisterRequest implements Serializable { + + private static final long serialVersionUID = 3191241716373120793L; + + private String userAccount; + + private String userName; + + private String userPassword; + + private String checkPassword; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserStatusUpdateRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserStatusUpdateRequest.java new file mode 100644 index 0000000..dd47854 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserStatusUpdateRequest.java @@ -0,0 +1,24 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户状态更新请求 + */ +@Data +public class UserStatusUpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户id + */ + private Long id; + + /** + * 状态:0-正常,1-封禁 + */ + private Integer status; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserUpdateRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserUpdateRequest.java new file mode 100644 index 0000000..839a43d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserUpdateRequest.java @@ -0,0 +1,75 @@ +package com.xy.xyaicpzs.domain.dto.user; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 用户更新请求 + */ +@Data +public class UserUpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private Long id; + + /** + * 用户昵称 + */ + private String userName; + + /** + * 账号 + */ + private String userAccount; + + /** + * 电话 + */ + private String phone; + + /** + * 用户头像 + */ + private String userAvatar; + + /** + * 性别 + */ + private Integer gender; + + /** + * 用户角色:user / admin + */ + private String userRole; + + /** + * 密码 + */ + private String userPassword; + + /** + * 密码(兼容格式) + */ + private String password; + + /** + * 是否会员:0-非会员,1-会员 + */ + private Integer isVip; + + /** + * 会员到期时间 + */ + private Date vipExpire; + + /** + * 状态:0-正常,1-封禁 + */ + private Integer status; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistory100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistory100.java new file mode 100644 index 0000000..c2baf7a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistory100.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 blue_history_100 + */ +@TableName(value ="blue_history_100") +@Data +public class BlueHistory100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 平均隐现期(次) + */ + private Double averageInterval; + + /** + * 当前隐现期(次) + */ + private Integer nowInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutiveCount; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryAll.java b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryAll.java new file mode 100644 index 0000000..298813c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryAll.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 blue_history_all + */ +@TableName(value ="blue_history_all") +@Data +public class BlueHistoryAll { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 出现频率百分比 + */ + private Double frequencyPercentage; + + /** + * 平均隐现期(次) + */ + private Double averageInterval; + + /** + * 最长隐现期(次) + */ + private Integer maxHiddenInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutiveCount; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryTop.java b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryTop.java new file mode 100644 index 0000000..81982a0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryTop.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; + +/** + * 蓝球历史数据排行表 + * @TableName blue_history_top + */ +@TableName(value ="blue_history_top") +@Data +public class BlueHistoryTop { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排行 + */ + private Integer no; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryTop100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryTop100.java new file mode 100644 index 0000000..f1ae1dd --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/BlueHistoryTop100.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; + +/** + * 创建蓝球100期数据排行表 + * @TableName blue_history_top_100 + */ +@TableName(value ="blue_history_top_100") +@Data +public class BlueHistoryTop100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排行 + */ + private Integer no; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/ChatMessage.java b/src/main/java/com/xy/xyaicpzs/domain/entity/ChatMessage.java new file mode 100644 index 0000000..2cdcc7c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/ChatMessage.java @@ -0,0 +1,56 @@ +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 chat_message + */ +@TableName(value ="chat_message") +@Data +public class ChatMessage { + /** + * id + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 会话ID + */ + private String conversationId; + + /** + * 用户ID,关联用户表 + */ + private String studentId; + + /** + * 消息类型(如: 用户提问、AI回答) + */ + private String messageType; + + /** + * 消息内容 + */ + private String content; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 是否删除 0-未删除 1-已删除 + */ + private Integer isDelete; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/History100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/History100.java new file mode 100644 index 0000000..6992743 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/History100.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 history_100 + */ +@TableName(value ="history_100") +@Data +public class History100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 平均隐现期(次) + */ + private Double averageInterval; + + /** + * 当前隐现期(次) + */ + private Integer nowInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutiveCount; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryAll.java b/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryAll.java new file mode 100644 index 0000000..80acbce --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryAll.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 history_all + */ +@TableName(value ="history_all") +@Data +public class HistoryAll { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 出现频率百分比 + */ + private Double frequencyPercentage; + + /** + * 平均间隔 + */ + private Double averageInterval; + + /** + * 最长隐藏间隔 + */ + private Integer maxHiddenInterval; + + /** + * 最大连续出现次数 + */ + private Integer maxConsecutiveCount; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryTop.java b/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryTop.java new file mode 100644 index 0000000..6133270 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryTop.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; + +/** + * 历史数据排行表 + * @TableName history_top + */ +@TableName(value ="history_top") +@Data +public class HistoryTop { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排行 + */ + private Integer no; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryTop100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryTop100.java new file mode 100644 index 0000000..eb17fe4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/HistoryTop100.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; + +/** + * 创建100期数据排行表 + * @TableName history_top_100 + */ +@TableName(value ="history_top_100") +@Data +public class HistoryTop100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排行 + */ + private Integer no; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 点系数 + */ + private Double pointCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/LotteryDraws.java b/src/main/java/com/xy/xyaicpzs/domain/entity/LotteryDraws.java new file mode 100644 index 0000000..ca9c840 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/LotteryDraws.java @@ -0,0 +1,60 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.util.Date; +import lombok.Data; + +/** + * 彩票开奖信息表 + * @TableName lottery_draws + */ +@TableName(value ="lottery_draws") +@Data +public class LotteryDraws { + /** + * 开奖期号 + */ + @TableId + private Long drawId; + + /** + * 开奖日期 + */ + private Date drawDate; + + /** + * 红1 + */ + private Integer redBall1; + + /** + * 红2 + */ + private Integer redBall2; + + /** + * 红3 + */ + private Integer redBall3; + + /** + * 红4 + */ + private Integer redBall4; + + /** + * 红5 + */ + private Integer redBall5; + + /** + * 红6 + */ + private Integer redBall6; + + /** + * 蓝球 + */ + private Integer blueBall; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/OperationHistory.java b/src/main/java/com/xy/xyaicpzs/domain/entity/OperationHistory.java new file mode 100644 index 0000000..0539183 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/OperationHistory.java @@ -0,0 +1,56 @@ +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 operation_history + */ +@TableName(value ="operation_history") +@Data +public class OperationHistory { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 操作用户ID + */ + private Long userId; + + /** + * 操作类型(批量生成会员码/获取可用会员码/Excel导入等) + */ + private String operationType; + + /** + * 操作模块(会员码管理/Excel导入管理等) + */ + private Integer operationModule; + + /** + * 操作结果(成功/失败) + */ + private String operationResult; + + /** + * 结果消息 + */ + private String resultMessage; + + /** + * 操作时间 + */ + private Date operationTime; + + /** + * 更新时间 + */ + private Date updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/PredictRecord.java b/src/main/java/com/xy/xyaicpzs/domain/entity/PredictRecord.java new file mode 100644 index 0000000..25f267f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/PredictRecord.java @@ -0,0 +1,96 @@ +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 predict_record + */ +@TableName(value ="predict_record") +@Data +public class PredictRecord { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 开奖期号 + */ + private Long drawId; + + /** + * 开奖日期 + */ + private Date drawDate; + + /** + * 红1 + */ + private Integer redBall1; + + /** + * 红2 + */ + private Integer redBall2; + + /** + * 红3 + */ + private Integer redBall3; + + /** + * 红4 + */ + private Integer redBall4; + + /** + * 红5 + */ + private Integer redBall5; + + /** + * 红6 + */ + private Integer redBall6; + + /** + * 蓝球 + */ + private Integer blueBall; + + /** + * 预测状态(待开奖/已开奖) + */ + private String predictStatus; + + /** + * 预测结果(未中奖/三等奖/二等奖/一等奖) + */ + private String predictResult; + + /** + * 预测时间 + */ + private Date predictTime; + + /** + * 奖金 + */ + private Long bonus; + + /** + * 类型 + */ + private String type; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T11.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T11.java new file mode 100644 index 0000000..82f2b41 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T11.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; + +/** + * t11表(蓝球组红球的面系数) + * @TableName t11 + */ +@TableName(value ="t11") +@Data +public class T11 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 面系数 + */ + private Double faceCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T3.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T3.java new file mode 100644 index 0000000..b805ff3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T3.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; + +/** + * t3表(红球组红球的线系数) + * @TableName t3 + */ +@TableName(value ="t3") +@Data +public class T3 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 线系数 + */ + private Double lineCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T4.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T4.java new file mode 100644 index 0000000..f6a9833 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T4.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; + +/** + * t4表(蓝球组红球的线系数) + * @TableName t4 + */ +@TableName(value ="t4") +@Data +public class T4 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 线系数 + */ + private Double lineCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T5.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T5.java new file mode 100644 index 0000000..24966e9 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T5.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; + +/** + * t5表(蓝球组蓝球的线系数) + * @TableName t5 + */ +@TableName(value ="t5") +@Data +public class T5 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 线系数 + */ + private Double lineCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T6.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T6.java new file mode 100644 index 0000000..685416d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T6.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; + +/** + * t6表(红球组蓝球的线系数) + * @TableName t6 + */ +@TableName(value ="t6") +@Data +public class T6 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 线系数 + */ + private Double lineCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T7.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T7.java new file mode 100644 index 0000000..edede85 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T7.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; + +/** + * t7表(红球组红球的面系数) + * @TableName t7 + */ +@TableName(value ="t7") +@Data +public class T7 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 面系数 + */ + private Double faceCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/T8.java b/src/main/java/com/xy/xyaicpzs/domain/entity/T8.java new file mode 100644 index 0000000..694e23d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/T8.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; + +/** + * t8表(红球组蓝球的面系数) + * @TableName t8 + */ +@TableName(value ="t8") +@Data +public class T8 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 面系数 + */ + private Double faceCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/User.java b/src/main/java/com/xy/xyaicpzs/domain/entity/User.java new file mode 100644 index 0000000..9ff29fc --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/User.java @@ -0,0 +1,86 @@ +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 user + */ +@TableName(value ="user") +@Data +public class User { + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户昵称 + */ + private String userName; + + /** + * 账号 + */ + private String userAccount; + + /** + * 电话 + */ + private String phone; + + /** + * 用户头像 + */ + private String userAvatar; + + /** + * 性别 + */ + private Integer gender; + + /** + * 用户角色:user / admin + */ + private String userRole; + + /** + * 密码 + */ + private String userPassword; + + /** + * 是否会员:0-非会员,1-会员 + */ + private Integer isVip; + + /** + * 会员到期时间 + */ + private Date vipExpire; + + /** + * 状态:0-正常,1-封禁 + */ + private Integer status; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 是否删除 + */ + private Integer isDelete; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/VipCode.java b/src/main/java/com/xy/xyaicpzs/domain/entity/VipCode.java new file mode 100644 index 0000000..40ac723 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/VipCode.java @@ -0,0 +1,72 @@ +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 vip_code + */ +@TableName(value ="vip_code") +@Data +public class VipCode { + /** + * 主键 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 会员码 + */ + private String code; + + /** + * 会员有效月数(1/12) + */ + private Integer vipExpireTime; + + /** + * 会员编号 + */ + private Integer vipNumber; + + /** + * 是否使用:0-未使用,1-已使用 + */ + private Integer isUse; + + /** + * 创建的用户id + */ + private Long createdUserId; + + /** + * 创建的用户名称 + */ + private String createdUserName; + + + /** + * 使用的用户id + */ + private Long usedUserId; + + /** + * 使用的用户名称 + */ + private String usedUserName; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/VipExchangeRecord.java b/src/main/java/com/xy/xyaicpzs/domain/entity/VipExchangeRecord.java new file mode 100644 index 0000000..dfd246b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/VipExchangeRecord.java @@ -0,0 +1,61 @@ +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 vip_exchange_record + */ +@TableName(value ="vip_exchange_record") +@Data +public class VipExchangeRecord { + /** + * 唯一标识符 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 月度会员/年度会员 + */ + private String type; + + /** + * 兑换方式 + */ + private Integer exchangeMode; + + /** + * 订单编号 + */ + private Long orderNo; + + /** + * 订单金额 + */ + private Integer orderAmount; + + /** + * 是否兑换(未兑换/已兑换) + */ + private Integer isUse; + + /** + * 兑换时间 + */ + private Date exchangeTime; + + /** + * 更新时间 + */ + private Date updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/BallCombinationAnalysisVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/BallCombinationAnalysisVO.java new file mode 100644 index 0000000..4ea0d95 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/BallCombinationAnalysisVO.java @@ -0,0 +1,39 @@ +package com.xy.xyaicpzs.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 球号组合分析结果VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "球号组合分析结果") +public class BallCombinationAnalysisVO { + + @Schema(description = "当前两个球的组合系数") + private Double faceCoefficient; + + @Schema(description = "与主球组合系数最高的球号") + private Integer highestBall; + + @Schema(description = "与主球组合系数最高的值") + private Double highestCoefficient; + + @Schema(description = "与主球组合系数最低的球号") + private Integer lowestBall; + + @Schema(description = "与主球组合系数最低的值") + private Double lowestCoefficient; + + @Schema(description = "与主球组合的所有系数平均值") + private Double averageCoefficient; + + @Schema(description = "最新开奖期号") + private Long latestDrawId; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/BallHitRateVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/BallHitRateVO.java new file mode 100644 index 0000000..ef37e1e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/BallHitRateVO.java @@ -0,0 +1,27 @@ +package com.xy.xyaicpzs.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 球号命中率统计VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "球号命中率统计") +public class BallHitRateVO { + + @Schema(description = "命中次数") + private Integer hitCount; + + @Schema(description = "总次数") + private Integer totalCount; + + @Schema(description = "命中率(百分比)") + private Double hitRate; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/BallPersistenceAnalysisVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/BallPersistenceAnalysisVO.java new file mode 100644 index 0000000..1791654 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/BallPersistenceAnalysisVO.java @@ -0,0 +1,39 @@ +package com.xy.xyaicpzs.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 球号持续性分析结果VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "球号持续性分析结果") +public class BallPersistenceAnalysisVO { + + @Schema(description = "当前两个球的组合线系数") + private Double lineCoefficient; + + @Schema(description = "与主球组合线系数最高的球号") + private Integer highestBall; + + @Schema(description = "与主球组合线系数最高的值") + private Double highestCoefficient; + + @Schema(description = "与主球组合线系数最低的球号") + private Integer lowestBall; + + @Schema(description = "与主球组合线系数最低的值") + private Double lowestCoefficient; + + @Schema(description = "与主球组合的所有线系数平均值") + private Double averageCoefficient; + + @Schema(description = "最新开奖期号") + private Long latestDrawId; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/PrizeEstimateVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/PrizeEstimateVO.java new file mode 100644 index 0000000..110e702 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/PrizeEstimateVO.java @@ -0,0 +1,47 @@ +package com.xy.xyaicpzs.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 奖金估算VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "奖金估算信息") +public class PrizeEstimateVO { + + @Schema(description = "总奖金合计") + private BigDecimal totalPrize; + + @Schema(description = "奖项明细") + private List prizeDetails; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + @Schema(description = "奖项明细项") + public static class PrizeDetailItem { + + @Schema(description = "中奖等级,例如:一等奖、二等奖等") + private String prizeLevel; + + @Schema(description = "中奖注数") + private Integer winningCount; + + @Schema(description = "单注奖金(元)") + private BigDecimal singlePrize; + + @Schema(description = "该等级奖金小计(元)") + private BigDecimal subtotal; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/RedBallHitRateVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/RedBallHitRateVO.java new file mode 100644 index 0000000..6843290 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/RedBallHitRateVO.java @@ -0,0 +1,27 @@ +package com.xy.xyaicpzs.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 红球命中率统计VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "红球命中率统计") +public class RedBallHitRateVO { + + @Schema(description = "命中总红球数") + private Integer totalHitCount; + + @Schema(description = "总预测红球数") + private Integer totalPredictedCount; + + @Schema(description = "红球命中率(百分比)") + private Double hitRate; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/UserPredictStatVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/UserPredictStatVO.java new file mode 100644 index 0000000..db55705 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/UserPredictStatVO.java @@ -0,0 +1,46 @@ +package com.xy.xyaicpzs.domain.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 用户预测统计数据VO + */ +@Data +public class UserPredictStatVO { + + /** + * 用户ID + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long userId; + + /** + * 预测次数(总记录数) + */ + private Long predictCount; + + /** + * 待开奖次数 + */ + private Long pendingCount; + + /** + * 命中次数 + */ + private Long hitCount; + + /** + * 命中率(保留4位小数) + */ + private BigDecimal hitRate; + + /** + * 已开奖次数(总次数 - 待开奖次数) + */ + private Long drawnCount; + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/UserVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/UserVO.java new file mode 100644 index 0000000..2fb6ed4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/UserVO.java @@ -0,0 +1,78 @@ +package com.xy.xyaicpzs.domain.vo; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 用户视图(脱敏) + */ +@Data +public class UserVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + + /** + * 用户昵称 + */ + private String userName; + + /** + * 用户昵称 + */ + private String userAccount; + + /** + * 用户头像 + */ + private String userAvatar; + + /** + * 性别 + */ + private Integer gender; + + /** + * 用户角色:user / admin + */ + private String userRole; + + /** + * 用户角色:user / admin + */ + private String phone; + + /** + * 是否会员:0-非会员,1-会员 + */ + private Integer isVip; + + /** + * 会员到期时间 + */ + private Date vipExpire; + + /** + * 状态:0-正常,1-封禁 + */ + private Integer status; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/VipCodeVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/VipCodeVO.java new file mode 100644 index 0000000..28ed0e0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/VipCodeVO.java @@ -0,0 +1,74 @@ +package com.xy.xyaicpzs.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * 会员码视图对象 + */ +@Data +@Schema(description = "会员码视图对象") +public class VipCodeVO { + + /** + * 会员码 + */ + @Schema(description = "会员码") + private String code; + + /** + * 会员有效月数(1/12) + */ + @Schema(description = "会员有效月数") + private Integer vipExpireTime; + + /** + * 会员编号(6位数,如100001) + */ + @Schema(description = "会员编号(6位数)") + private Integer vipNumber; + + /** + * 是否使用:0-未使用,1-已使用 + */ + @Schema(description = "是否使用:0-未使用,1-已使用") + private Integer isUse; + + /** + * 创建人ID + */ + @Schema(description = "创建人ID") + private Long createdUserId; + + /** + * 创建人名称 + */ + @Schema(description = "创建人名称") + private String createdUserName; + + /** + * 使用人ID + */ + @Schema(description = "使用人ID") + private Long usedUserId; + + /** + * 使用人名称 + */ + @Schema(description = "使用人名称") + private String usedUserName; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private Date createTime; + + /** + * 创建时间 + */ + @Schema(description = "更新时间") + private Date updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/exception/BusinessException.java b/src/main/java/com/xy/xyaicpzs/exception/BusinessException.java new file mode 100644 index 0000000..7342547 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/exception/BusinessException.java @@ -0,0 +1,33 @@ +package com.xy.xyaicpzs.exception; + +import com.xy.xyaicpzs.common.ErrorCode; + +/** + * 自定义异常类 + */ +public class BusinessException extends RuntimeException { + + /** + * 错误码 + */ + private final int code; + + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.code = errorCode.getCode(); + } + + public BusinessException(ErrorCode errorCode, String message) { + super(message); + this.code = errorCode.getCode(); + } + + public int getCode() { + return code; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/exception/GlobalExceptionHandler.java b/src/main/java/com/xy/xyaicpzs/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..d6cd857 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/exception/GlobalExceptionHandler.java @@ -0,0 +1,28 @@ +package com.xy.xyaicpzs.exception; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理器 + */ +//@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ApiResponse businessExceptionHandler(BusinessException e) { + log.error("businessException: " + e.getMessage(), e); + return ResultUtils.error(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + public ApiResponse runtimeExceptionHandler(RuntimeException e) { + log.error("runtimeException", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage()); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/BlueHistory100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistory100Mapper.java new file mode 100644 index 0000000..e0cc738 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistory100Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.BlueHistory100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【blue_history_100(蓝球最近100期数据表)】的数据库操作Mapper +* @createDate 2025-06-14 10:40:04 +* @Entity com.xy.xyaicpzs.domain.entity.BlueHistory100 +*/ +public interface BlueHistory100Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryAllMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryAllMapper.java new file mode 100644 index 0000000..04f9d93 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryAllMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.BlueHistoryAll; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【blue_history_all(蓝球全部历史数据表)】的数据库操作Mapper +* @createDate 2025-06-14 10:40:07 +* @Entity com.xy.xyaicpzs.domain.entity.BlueHistoryAll +*/ +public interface BlueHistoryAllMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryTop100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryTop100Mapper.java new file mode 100644 index 0000000..5da1cc0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryTop100Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【blue_history_top_100(创建蓝球100期数据排行表)】的数据库操作Mapper +* @createDate 2025-06-14 10:40:13 +* @Entity com.xy.xyaicpzs.domain.entity.BlueHistoryTop100 +*/ +public interface BlueHistoryTop100Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryTopMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryTopMapper.java new file mode 100644 index 0000000..62f2443 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/BlueHistoryTopMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【blue_history_top(蓝球历史数据排行表)】的数据库操作Mapper +* @createDate 2025-06-14 10:40:10 +* @Entity com.xy.xyaicpzs.domain.entity.BlueHistoryTop +*/ +public interface BlueHistoryTopMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/ChatMessageMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/ChatMessageMapper.java new file mode 100644 index 0000000..2badb1c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/ChatMessageMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.ChatMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【chat_message(聊天消息表)】的数据库操作Mapper +* @createDate 2025-07-07 17:37:15 +* @Entity com.xy.xyaicpzs.domain.entity.ChatMessage +*/ +public interface ChatMessageMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/History100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/History100Mapper.java new file mode 100644 index 0000000..1ca452a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/History100Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.History100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【history_100(最近100期数据表)】的数据库操作Mapper +* @createDate 2025-06-14 09:48:05 +* @Entity generator.domain.History100 +*/ +public interface History100Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/HistoryAllMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/HistoryAllMapper.java new file mode 100644 index 0000000..3737941 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/HistoryAllMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.HistoryAll; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【history_all(历史数据表)】的数据库操作Mapper +* @createDate 2025-06-14 09:48:10 +* @Entity generator.domain.HistoryAll +*/ +public interface HistoryAllMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/HistoryTop100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/HistoryTop100Mapper.java new file mode 100644 index 0000000..0be554b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/HistoryTop100Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.HistoryTop100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【history_top_100(创建100期数据排行表)】的数据库操作Mapper +* @createDate 2025-06-14 09:48:16 +* @Entity generator.domain.HistoryTop100 +*/ +public interface HistoryTop100Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/HistoryTopMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/HistoryTopMapper.java new file mode 100644 index 0000000..67928a9 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/HistoryTopMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.HistoryTop; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【history_top(历史数据排行表)】的数据库操作Mapper +* @createDate 2025-06-14 09:48:13 +* @Entity generator.domain.HistoryTop +*/ +public interface HistoryTopMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/LotteryDrawsMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/LotteryDrawsMapper.java new file mode 100644 index 0000000..b656f11 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/LotteryDrawsMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.LotteryDraws; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【lottery_draws(彩票开奖信息表)】的数据库操作Mapper +* @createDate 2025-06-14 16:41:29 +* @Entity com.xy.xyaicpzs.domain.entity.LotteryDraws +*/ +public interface LotteryDrawsMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/OperationHistoryMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/OperationHistoryMapper.java new file mode 100644 index 0000000..3f154c6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/OperationHistoryMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.OperationHistory; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【operation_history(操作历史记录表)】的数据库操作Mapper +* @createDate 2025-06-19 14:51:51 +* @Entity com.xy.xyaicpzs.domain.entity.OperationHistory +*/ +public interface OperationHistoryMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/PredictRecordMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/PredictRecordMapper.java new file mode 100644 index 0000000..6a781e4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/PredictRecordMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.PredictRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【predict_record(彩票开奖信息表)】的数据库操作Mapper +* @createDate 2025-06-16 13:17:53 +* @Entity com.xy.xyaicpzs.domain.entity.PredictRecord +*/ +public interface PredictRecordMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T11Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T11Mapper.java new file mode 100644 index 0000000..b5c057f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T11Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T11; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【t11(t11表(蓝球组红球的面系数))】的数据库操作Mapper +* @createDate 2025-06-14 16:25:23 +* @Entity com.xy.xyaicpzs.domain.entity.T11 +*/ +public interface T11Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T3Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T3Mapper.java new file mode 100644 index 0000000..05cbca6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T3Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T3; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【t3(t3表(红球组红球的线系数))】的数据库操作Mapper +* @createDate 2025-06-14 11:02:50 +* @Entity com.xy.xyaicpzs.domain.entity.T3 +*/ +public interface T3Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T4Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T4Mapper.java new file mode 100644 index 0000000..3fd5795 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T4Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T4; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【t4(t4表(蓝球组红球的线系数))】的数据库操作Mapper +* @createDate 2025-06-14 11:45:31 +* @Entity com.xy.xyaicpzs.domain.entity.T4 +*/ +public interface T4Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T5Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T5Mapper.java new file mode 100644 index 0000000..a27b2d2 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T5Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T5; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【t5(t5表(蓝球组蓝球的线系数))】的数据库操作Mapper +* @createDate 2025-06-14 12:01:16 +* @Entity com.xy.xyaicpzs.domain.entity.T5 +*/ +public interface T5Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T6Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T6Mapper.java new file mode 100644 index 0000000..e90b630 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T6Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T6; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【t6(t6表(红球组蓝球的线系数))】的数据库操作Mapper +* @createDate 2025-06-14 13:19:12 +* @Entity com.xy.xyaicpzs.domain.entity.T6 +*/ +public interface T6Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T7Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T7Mapper.java new file mode 100644 index 0000000..3841c0d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T7Mapper.java @@ -0,0 +1,21 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T7; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【t7(t7表(红球组红球的面系数))】的数据库操作Mapper +* @createDate 2025-06-14 13:30:50 +* @Entity com.xy.xyaicpzs.domain.entity.T7 +*/ +public interface T7Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/T8Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/T8Mapper.java new file mode 100644 index 0000000..a466238 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/T8Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.T8; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【t8(t8表(红球组蓝球的面系数))】的数据库操作Mapper +* @createDate 2025-06-14 16:11:27 +* @Entity com.xy.xyaicpzs.domain.entity.T8 +*/ +public interface T8Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/UserMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/UserMapper.java new file mode 100644 index 0000000..930176e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/UserMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.User; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【user(用户)】的数据库操作Mapper +* @createDate 2025-06-15 18:29:28 +* @Entity com.xy.xyaicpzs.domain.entity.User +*/ +public interface UserMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/VipCodeMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/VipCodeMapper.java new file mode 100644 index 0000000..399f9cd --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/VipCodeMapper.java @@ -0,0 +1,14 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.VipCode; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【vip_code(会员码表)】的数据库操作Mapper +* @createDate 2025-01-15 16:41:29 +* @Entity com.xy.xyaicpzs.domain.entity.VipCode +*/ +public interface VipCodeMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/VipExchangeRecordMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/VipExchangeRecordMapper.java new file mode 100644 index 0000000..572e8c3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/VipExchangeRecordMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.VipExchangeRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【vip_exchange_record(会员兑换表)】的数据库操作Mapper +* @createDate 2025-06-19 11:27:10 +* @Entity com.xy.xyaicpzs.domain.entity.VipExchangeRecord +*/ +public interface VipExchangeRecordMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java b/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java new file mode 100644 index 0000000..3d6b863 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java @@ -0,0 +1,3154 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.mapper.*; +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.*; +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; + +/** + * 球号分析服务类 + * 实现复杂的球号分析算法 + */ +@Slf4j +@Service +public class BallAnalysisService { + + @Autowired + private T3Mapper t3Mapper; + + @Autowired + private T4Mapper t4Mapper; + + @Autowired + private HistoryTopMapper historyTopMapper; + + @Autowired + private HistoryTop100Mapper historyTop100Mapper; + + @Autowired + private T7Mapper t7Mapper; + + @Autowired + private T5Mapper t5Mapper; + + @Autowired + private T6Mapper t6Mapper; + + @Autowired + private T8Mapper t8Mapper; + + @Autowired + private BlueHistoryTop100Mapper blueHistoryTop100Mapper; + + @Autowired + private BlueHistoryTopMapper blueHistoryTopMapper; + + @Autowired + private PredictRecordMapper predictRecordMapper; + + @Autowired + private LotteryDrawsService lotteryDrawsService; + + /** + * 球号分析算法主方法 + * @param level 高位/中位/低位标识 (H/M/L) + * @param redBalls 6个红球号码 + * @param blueBall 1个蓝球号码 + * @return 分析结果:出现频率最高的前11位数字 + */ + public List analyzeBalls(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<>(); + + // 第一步:处理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 ballNumbers = getTop17FromT3(redBall, level); + allNumbers.addAll(ballNumbers); + + log.info("红球{}获取到{}个数字:{}", redBall, ballNumbers.size(), ballNumbers); + } + + // 第二步:从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个 + List result = getTop11ByFrequency(allNumbers); + + log.info("球号分析算法完成,结果:{}", result); + return result; + } + + /** + * 从T3表获取指定主球的17个从球号 + * 根据不同级别使用不同的选择策略 + * @param masterBallNumber 主球号 + * @param level 级别:H-高位,M-中位,L-低位 + */ + private List getTop17FromT3(Integer masterBallNumber, String level) { + log.debug("从T3表查询主球{}的线系数数据,级别:{}", masterBallNumber, level); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t3List = t3Mapper.selectList(queryWrapper); + + if (t3List.isEmpty()) { + log.warn("T3表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + switch (level) { + case "H": + // 高位:取前17个(按线系数从大到小排列) + result = getHighLevelBalls(t3List); + break; + case "M": + // 中位:取线系数平均值向上9个球(含),向下8个球,共17个 + result = getMiddleLevelBalls(t3List); + break; + case "L": + // 低位:取最小值向上17个球,含最小值 + result = getLowLevelBalls(t3List); + break; + } + + log.debug("T3表主球{}{}级别最终选择的{}个从球:{}", masterBallNumber, level, result.size(), result); + return result; + } + + /** + * 从T4表获取指定主球的17个从球号 + * 根据不同级别使用不同的选择策略 + * @param masterBallNumber 主球号 + * @param level 级别:H-高位,M-中位,L-低位 + */ + private List getTop17FromT4(Integer masterBallNumber, String level) { + log.debug("从T4表查询主球{}的线系数数据,级别:{}", masterBallNumber, level); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t4List = t4Mapper.selectList(queryWrapper); + + if (t4List.isEmpty()) { + log.warn("T4表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + switch (level) { + case "H": + // 高位:取前17个(按线系数从大到小排列) + result = getHighLevelBallsFromT4(t4List); + break; + case "M": + // 中位:取线系数平均值向上9个球(含),向下8个球,共17个 + result = getMiddleLevelBallsFromT4(t4List); + break; + case "L": + // 低位:取最小值向上17个球,含最小值 + result = getLowLevelBallsFromT4(t4List); + break; + } + + log.debug("T4表主球{}{}级别最终选择的{}个从球:{}", masterBallNumber, level, result.size(), result); + return result; + } + + /** + * 高位算法:从T3表取前17个(按线系数从大到小排列) + */ + private List getHighLevelBalls(List t3List) { + if (t3List.size() < 17) { + log.warn("T3表数据不足17条,实际{}条", t3List.size()); + return t3List.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前16个 + List result = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + result.add(t3List.get(i).getSlaveBallNumber()); + } + + // 处理第17个位置:检查是否有相同的线系数 + Double targetCoefficient = t3List.get(16).getLineCoefficient(); + List candidatesFor17th = new ArrayList<>(); + + // 找出所有线系数等于第17个位置线系数的记录 + for (int i = 16; i < t3List.size(); i++) { + if (t3List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor17th.add(t3List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor17th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor17th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第17位有{}个相同线系数的候选:{}", candidatesFor17th.size(), + candidatesFor17th.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesFor17th.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + return result; + } + + /** + * 中位算法:从T3表取线系数平均值向上9个球(含),向下8个球,共17个 + */ + private List getMiddleLevelBalls(List t3List) { + if (t3List.size() < 17) { + log.warn("T3表数据不足17条,实际{}条", t3List.size()); + return t3List.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 计算线系数平均值 + double avgCoefficient = t3List.stream() + .mapToDouble(T3::getLineCoefficient) + .average() + .orElse(0.0); + + log.debug("T3表线系数平均值:{}", avgCoefficient); + + // 找到最接近平均值的位置 + int avgIndex = -1; + double minDiff = Double.MAX_VALUE; + for (int i = 0; i < t3List.size(); i++) { + double diff = Math.abs(t3List.get(i).getLineCoefficient() - avgCoefficient); + if (diff < minDiff) { + minDiff = diff; + avgIndex = i; + } + } + + log.debug("最接近平均值的位置:{},线系数:{}", avgIndex, t3List.get(avgIndex).getLineCoefficient()); + + // 向上9个(含当前),向下8个,共17个 + int startIndex = Math.max(0, avgIndex - 8); + int endIndex = Math.min(t3List.size() - 1, avgIndex + 8); + + // 确保总共17个数字 + while (endIndex - startIndex + 1 < 17 && (startIndex > 0 || endIndex < t3List.size() - 1)) { + if (startIndex > 0) { + startIndex--; + } + if (endIndex < t3List.size() - 1 && endIndex - startIndex + 1 < 17) { + endIndex++; + } + } + + List result = new ArrayList<>(); + for (int i = startIndex; i <= endIndex && result.size() < 17; i++) { + result.add(t3List.get(i).getSlaveBallNumber()); + } + + log.debug("中位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 低位算法:从T3表取最小值向上17个球,含最小值 + */ + private List getLowLevelBalls(List t3List) { + if (t3List.size() < 17) { + log.warn("T3表数据不足17条,实际{}条", t3List.size()); + return t3List.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 从最后17个开始(最小的线系数) + int startIndex = Math.max(0, t3List.size() - 17); + List candidates = new ArrayList<>(); + + for (int i = startIndex; i < t3List.size(); i++) { + candidates.add(t3List.get(i).getSlaveBallNumber()); + } + + // 处理第一个位置(最大线系数)的相同值情况 + if (candidates.size() >= 17) { + Double firstCoefficient = t3List.get(startIndex).getLineCoefficient(); + List candidatesForFirst = new ArrayList<>(); + + // 找出所有线系数等于第一个位置线系数的记录 + for (int i = 0; i < t3List.size(); i++) { + if (t3List.get(i).getLineCoefficient().equals(firstCoefficient)) { + candidatesForFirst.add(t3List.get(i)); + } + } + + if (candidatesForFirst.size() > 1) { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第一位有{}个相同线系数的候选:{}", candidatesForFirst.size(), + candidatesForFirst.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesForFirst.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()) + ); + + // 替换第一个位置 + candidates.set(0, bestBall); + } + } + + log.debug("低位算法选择范围:[{}, {}],共{}个球", startIndex, t3List.size() - 1, candidates.size()); + return candidates.subList(0, Math.min(17, candidates.size())); + } + + /** + * 高位算法:从T4表取前17个(按线系数从大到小排列) + */ + private List getHighLevelBallsFromT4(List t4List) { + if (t4List.size() < 17) { + log.warn("T4表数据不足17条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前16个 + List result = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + result.add(t4List.get(i).getSlaveBallNumber()); + } + + // 处理第17个位置:检查是否有相同的线系数 + Double targetCoefficient = t4List.get(16).getLineCoefficient(); + List candidatesFor17th = new ArrayList<>(); + + // 找出所有线系数等于第17个位置线系数的记录 + for (int i = 16; i < t4List.size(); i++) { + if (t4List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor17th.add(t4List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor17th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor17th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第17位有{}个相同线系数的候选:{}", candidatesFor17th.size(), + candidatesFor17th.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesFor17th.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + return result; + } + + /** + * 中位算法:从T4表取线系数平均值向上9个球(含),向下8个球,共17个 + */ + private List getMiddleLevelBallsFromT4(List t4List) { + if (t4List.size() < 17) { + log.warn("T4表数据不足17条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 计算线系数平均值 + double avgCoefficient = t4List.stream() + .mapToDouble(T4::getLineCoefficient) + .average() + .orElse(0.0); + + log.debug("T4表线系数平均值:{}", avgCoefficient); + + // 找到最接近平均值的位置 + int avgIndex = -1; + double minDiff = Double.MAX_VALUE; + for (int i = 0; i < t4List.size(); i++) { + double diff = Math.abs(t4List.get(i).getLineCoefficient() - avgCoefficient); + if (diff < minDiff) { + minDiff = diff; + avgIndex = i; + } + } + + log.debug("最接近平均值的位置:{},线系数:{}", avgIndex, t4List.get(avgIndex).getLineCoefficient()); + + // 向上9个(含当前),向下8个,共17个 + int startIndex = Math.max(0, avgIndex - 8); + int endIndex = Math.min(t4List.size() - 1, avgIndex + 8); + + // 确保总共17个数字 + while (endIndex - startIndex + 1 < 17 && (startIndex > 0 || endIndex < t4List.size() - 1)) { + if (startIndex > 0) { + startIndex--; + } + if (endIndex < t4List.size() - 1 && endIndex - startIndex + 1 < 17) { + endIndex++; + } + } + + List result = new ArrayList<>(); + for (int i = startIndex; i <= endIndex && result.size() < 17; i++) { + result.add(t4List.get(i).getSlaveBallNumber()); + } + + log.debug("中位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 低位算法:从T4表取线系数最小值向上第4-14个球的10个球号 + */ + private List getLowLevelBallsFromT4(List t4List) { + if (t4List.size() < 14) { + log.warn("T4表数据不足14条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 从最后14个开始,取第4-14个(即倒数第11到倒数第1个) + int startIndex = Math.max(0, t4List.size() - 11); + int endIndex = t4List.size() - 1; + + List candidates = new ArrayList<>(); + for (int i = startIndex; i <= endIndex; i++) { + candidates.add(t4List.get(i).getSlaveBallNumber()); + } + + // 处理边界位置的相同线系数情况 + List result = handleT4BoundaryConflictsFor10(t4List, candidates, startIndex, endIndex); + + log.debug("T4低位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 处理T4表边界位置的线系数冲突(10个球版本) + */ + private List handleT4BoundaryConflictsFor10(List t4List, List candidates, + int startIndex, int endIndex) { + List result = new ArrayList<>(candidates); + + if (result.size() >= 10) { + // 检查第1个位置(最大线系数)的冲突 + Double firstCoefficient = t4List.get(startIndex).getLineCoefficient(); + List candidatesForFirst = new ArrayList<>(); + + for (int i = 0; i < t4List.size(); i++) { + if (t4List.get(i).getLineCoefficient().equals(firstCoefficient)) { + candidatesForFirst.add(t4List.get(i)); + } + } + + if (candidatesForFirst.size() > 1) { + log.debug("第1位有{}个相同线系数的候选:{}", candidatesForFirst.size(), + candidatesForFirst.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesForFirst.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.set(0, bestBall); + } + + // 检查第10个位置(最小线系数)的冲突 + Double lastCoefficient = t4List.get(endIndex).getLineCoefficient(); + List candidatesForLast = new ArrayList<>(); + + for (int i = 0; i < t4List.size(); i++) { + if (t4List.get(i).getLineCoefficient().equals(lastCoefficient)) { + candidatesForLast.add(t4List.get(i)); + } + } + + if (candidatesForLast.size() > 1) { + log.debug("第10位有{}个相同线系数的候选:{}", candidatesForLast.size(), + candidatesForLast.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesForLast.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.set(result.size() - 1, bestBall); + } + } + + return result.subList(0, Math.min(10, result.size())); + } + + /** + * 从history_top表获取前3个球号(按点系数排行) + * 如果点系数相同,通过history_top_100表比较 + */ + private List getTop3FromHistoryTop() { + log.debug("从history_top表获取前3个球号"); + + // 查询前3个排行的数据 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByAsc("no").last("LIMIT 3"); + + List topList = historyTopMapper.selectList(queryWrapper); + + if (topList.size() < 3) { + log.warn("history_top表数据不足3条,实际{}条", topList.size()); + return topList.stream().map(HistoryTop::getBallNumber).collect(Collectors.toList()); + } + + List result = new ArrayList<>(); + + // 处理每个位置,检查是否有相同点系数的情况 + for (int i = 0; i < 3; i++) { + HistoryTop current = topList.get(i); + + // 查找所有具有相同排行的记录 + QueryWrapper sameRankQuery = new QueryWrapper<>(); + sameRankQuery.eq("no", current.getNo()); + List sameRankList = historyTopMapper.selectList(sameRankQuery); + + if (sameRankList.size() == 1) { + // 只有一个,直接添加 + result.add(current.getBallNumber()); + } else { + // 有多个相同排行,通过history_top_100表比较点系数 + log.debug("排行{}有{}个相同的球号:{}", current.getNo(), sameRankList.size(), + sameRankList.stream().map(HistoryTop::getBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + sameRankList.stream().map(HistoryTop::getBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + } + + log.debug("history_top表最终选择的3个球号:{}", result); + return result; + } + + /** + * 从候选球号中选择history_top_100表中点系数最大的球号 + */ + private Integer selectBestBallFromHistoryTop100(List candidates) { + if (candidates.isEmpty()) { + throw new IllegalArgumentException("候选球号列表不能为空"); + } + + if (candidates.size() == 1) { + return candidates.get(0); + } + + log.debug("从history_top_100表中比较候选球号:{}", candidates); + + // 查询这些球号在history_top_100表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidates) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List top100List = historyTop100Mapper.selectList(queryWrapper); + + if (top100List.isEmpty()) { + log.warn("候选球号{}在history_top_100表中未找到数据,返回第一个", candidates); + return candidates.get(0); + } + + Integer bestBall = top100List.get(0).getBallNumber(); + log.debug("从候选{}中选择最佳球号:{}(点系数:{})", candidates, bestBall, top100List.get(0).getPointCoefficient()); + + return bestBall; + } + + /** + * 统计数字出现频率,返回频率最高的前11个数字 + * 如果频次相同的球号超过11个,使用T3表线系数进行二次筛选 + */ + private List getTop11ByFrequency(List allNumbers) { + 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<>(); + + // 按频率从高到低处理 + for (Map.Entry> group : frequencyGroups.entrySet()) { + Integer frequency = group.getKey(); + List balls = group.getValue(); + + // 对同频率的球号按数字升序排序 + Collections.sort(balls); + + log.info("频率{}的球号:{}", frequency, balls); + + // 检查加入这组球号后是否会超过11个 + if (result.size() + balls.size() <= 11) { + // 不会超过11个,直接添加所有球号 + result.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过11个,需要用T3表的线系数进行二次筛选 + int remainingSlots = 11 - result.size(); + log.info("需要从{}个频率相同的球号中选择{}个,使用T3表线系数进行筛选", balls.size(), remainingSlots); + + List selectedBalls = selectBallsByT3LineCoefficient(balls, remainingSlots); + result.addAll(selectedBalls); + + log.info("通过T3表线系数筛选完成,最终选择:{}", selectedBalls); + break; // 已经达到11个,结束 + } + + // 如果已经有11个,结束 + if (result.size() >= 11) { + break; + } + } + + log.info("频率统计最终结果(共{}个):{}", result.size(), result); + + // 打印详细的频率信息 + for (int i = 0; i < result.size(); i++) { + Integer ballNumber = result.get(i); + Integer frequency = frequencyMap.get(ballNumber); + log.info("第{}位:数字{},出现{}次", i + 1, ballNumber, frequency); + } + + return result; + } + + /** + * 根据T3表的线系数总和选择球号,支持多级筛选 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选择的球号列表 + */ + private List selectBallsByT3LineCoefficient(List candidateBalls, int selectCount) { + log.info("开始根据T3表线系数总和选择球号,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选球号数量不超过需要选择的数量,直接返回所有候选球号"); + return new ArrayList<>(candidateBalls); + } + + // 第一级:计算每个球号作为主球时的线系数总和 + Map lineCoefficientSumMap = new HashMap<>(); + + for (Integer ballNumber : candidateBalls) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", ballNumber); + + List t3List = t3Mapper.selectList(queryWrapper); + + double sum = t3List.stream() + .mapToDouble(T3::getLineCoefficient) + .sum(); + + lineCoefficientSumMap.put(ballNumber, sum); + log.debug("球号{}作为主球的线系数总和:{}", ballNumber, sum); + } + + // 按线系数总和分组 + Map> lineCoefficientGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry entry : lineCoefficientSumMap.entrySet()) { + Double sum = entry.getValue(); + Integer ballNumber = entry.getKey(); + lineCoefficientGroups.computeIfAbsent(sum, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("T3表线系数总和分组:{}", lineCoefficientGroups); + + List result = new ArrayList<>(); + + // 按线系数总和从高到低处理 + for (Map.Entry> group : lineCoefficientGroups.entrySet()) { + Double lineSum = group.getKey(); + List balls = group.getValue(); + + log.info("线系数总和{}的球号:{}", lineSum, balls); + + // 检查加入这组球号后是否会超过selectCount个 + if (result.size() + balls.size() <= selectCount) { + // 不会超过,直接添加所有球号(按球号升序排序) + Collections.sort(balls); + result.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过,需要进一步筛选 + int remainingSlots = selectCount - result.size(); + log.info("需要从{}个线系数总和相同的球号中选择{}个,开始多级筛选", balls.size(), remainingSlots); + + List selectedBalls = selectBallsByMultiLevelFiltering(balls, remainingSlots); + result.addAll(selectedBalls); + + log.info("通过多级筛选完成,最终选择:{}", selectedBalls); + break; // 已经达到selectCount个,结束 + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + log.info("T3表线系数筛选最终结果(共{}个):{}", result.size(), result); + + // 打印详细的线系数信息 + for (int i = 0; i < result.size(); i++) { + Integer ballNumber = result.get(i); + Double sum = lineCoefficientSumMap.get(ballNumber); + log.info("第{}位:球号{},线系数总和:{}", i + 1, ballNumber, sum); + } + + return result; + } + + /** + * 多级筛选球号:history_top_100 -> history_top -> 随机选择 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选择的球号列表 + */ + private List selectBallsByMultiLevelFiltering(List candidateBalls, int selectCount) { + log.info("开始多级筛选,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选球号数量不超过需要选择的数量,直接返回所有候选球号"); + return new ArrayList<>(candidateBalls); + } + + // 第二级:使用history_top_100表的排名进行筛选 + List filteredByTop100 = selectBallsByHistoryTop100Ranking(candidateBalls, selectCount); + + if (filteredByTop100.size() == selectCount) { + log.info("通过history_top_100表筛选成功,结果:{}", filteredByTop100); + return filteredByTop100; + } + + // 第三级:使用history_top表的点系数进行筛选 + log.info("history_top_100表筛选后仍有{}个球号,继续使用history_top表筛选", filteredByTop100.size()); + List filteredByTop = selectBallsByHistoryTopPointCoefficient(filteredByTop100, selectCount); + + if (filteredByTop.size() == selectCount) { + log.info("通过history_top表筛选成功,结果:{}", filteredByTop); + return filteredByTop; + } + + // 第四级:随机选择 + log.info("history_top表筛选后仍有{}个球号,进行随机选择", filteredByTop.size()); + List finalResult = selectBallsRandomly(filteredByTop, selectCount); + + log.info("随机选择完成,最终结果:{}", finalResult); + return finalResult; + } + + /** + * 根据history_top_100表的排名筛选球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 筛选后的球号列表 + */ + private List selectBallsByHistoryTop100Ranking(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 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<>(); + + // 按点系数从高到低处理 + 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 { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个球号:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在history_top_100表中的球号 + List notInTop100 = new ArrayList<>(candidateBalls); + notInTop100.removeAll(result); + if (!notInTop100.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + Collections.sort(notInTop100); + 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 result; + } + + /** + * 根据history_top表的点系数筛选球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 筛选后的球号列表 + */ + private List selectBallsByHistoryTopPointCoefficient(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 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<>(); + + // 按点系数从高到低处理 + 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 { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个球号:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在history_top表中的球号 + List notInHistoryTop = new ArrayList<>(candidateBalls); + notInHistoryTop.removeAll(result); + if (!notInHistoryTop.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + Collections.sort(notInHistoryTop); + 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 result; + } + + /** + * 随机选择球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 随机选择的球号列表 + */ + private List selectBallsRandomly(List candidateBalls, int selectCount) { + log.info("随机选择球号,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选球号数量不超过需要选择的数量,直接返回所有候选球号"); + return new ArrayList<>(candidateBalls); + } + + // 创建候选球号的副本并打乱顺序 + List shuffledBalls = new ArrayList<>(candidateBalls); + Collections.shuffle(shuffledBalls, new Random(System.currentTimeMillis())); + + // 选择前selectCount个 + List result = shuffledBalls.subList(0, selectCount); + + // 按球号升序排序结果 + Collections.sort(result); + + log.info("随机选择结果:{}", result); + return result; + } + + /** + * 跟随球号分析算法 + * @param level 高位/中位/低位标识 (H/M/L) + * @param firstThreeRedBalls 前3个红球号码(独立的) + * @param lastSixRedBalls 后6个红球号码(独立的) + * @param blueBall 1个蓝球号码 + * @return 分析结果:出现频率最高的前8位数字 + */ + public List fallowBallAnalysis(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); + + // 第二步:处理后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); + + // 第五步:用上期蓝球从T5表获取12个蓝球号码 + log.info("第五步:用上期蓝球{}从T5表获取12个蓝球号码", blueBall); + List blueNumbers = getTop12FromT5(blueBall); + allNumbers.addAll(blueNumbers); + log.info("蓝球{}获取到{}个数字:{}", blueBall, blueNumbers.size(), blueNumbers); + + log.info("总共收集到{}个数字", allNumbers.size()); + + // 第六步:统计频率并获取前8个 + List result = getTop8ByFrequency(allNumbers); + + log.info("跟随球号分析算法完成,结果:{}", result); + return result; + } + + /** + * 验证球号的有效性和唯一性 + */ + private void validateBallNumbers(List firstThreeRedBalls, List lastSixRedBalls, Integer blueBall) { + // 验证红球范围 + for (Integer ball : firstThreeRedBalls) { + if (ball == null || ball < 1 || ball > 33) { + throw new IllegalArgumentException("前3个红球号码必须在1-33范围内"); + } + } + for (Integer ball : lastSixRedBalls) { + if (ball == null || ball < 1 || ball > 33) { + throw new IllegalArgumentException("后6个红球号码必须在1-33范围内"); + } + } + + // 验证蓝球范围 + if (blueBall < 1 || blueBall > 16) { + throw new IllegalArgumentException("蓝球号码必须在1-16范围内"); + } + + // 验证前3个红球不重复 + Set firstThreeSet = new HashSet<>(firstThreeRedBalls); + if (firstThreeSet.size() != 3) { + throw new IllegalArgumentException("前3个红球号码不能重复"); + } + + // 验证后6个红球不重复 + Set lastSixSet = new HashSet<>(lastSixRedBalls); + if (lastSixSet.size() != 6) { + throw new IllegalArgumentException("后6个红球号码不能重复"); + } + + log.debug("球号验证通过"); + } + + /** + * 从T7表获取指定主球的10个从球号 + * 根据不同级别使用不同的选择策略 + * @param masterBallNumber 主球号 + * @param level 级别:H-高位,M-中位,L-低位 + */ + private List getTop10FromT7(Integer masterBallNumber, String level) { + log.debug("从T7表查询主球{}的面系数数据,级别:{}", masterBallNumber, level); + + // 查询指定主球的所有数据,按面系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("faceCoefficient") + .orderByAsc("slaveBallNumber"); // 面系数相同时按从球号升序 + + List t7List = t7Mapper.selectList(queryWrapper); + + if (t7List.isEmpty()) { + log.warn("T7表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + switch (level) { + case "H": + // 高位:取面系数最大的前10个 + result = getHighLevelBallsFromT7(t7List); + break; + case "M": + // 中位:取面系数平均值向上6个向下4个的10个球号 + result = getMiddleLevelBallsFromT7(t7List); + break; + case "L": + // 低位:取面系数最小值向上第3-13个球共10个球号 + result = getLowLevelBallsFromT7(t7List); + break; + } + + log.debug("T7表主球{}{}级别最终选择的{}个从球:{}", masterBallNumber, level, result.size(), result); + return result; + } + + /** + * 高位算法:从T7表取面系数最大的前10个 + */ + private List getHighLevelBallsFromT7(List t7List) { + if (t7List.size() < 10) { + log.warn("T7表数据不足10条,实际{}条", t7List.size()); + return t7List.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前9个 + List result = new ArrayList<>(); + for (int i = 0; i < 9; i++) { + result.add(t7List.get(i).getSlaveBallNumber()); + } + + // 处理第10个位置:检查是否有相同的面系数 + Double targetCoefficient = t7List.get(9).getFaceCoefficient(); + List candidatesFor10th = new ArrayList<>(); + + // 找出所有面系数等于第10个位置面系数的记录 + for (int i = 9; i < t7List.size(); i++) { + if (t7List.get(i).getFaceCoefficient().equals(targetCoefficient)) { + candidatesFor10th.add(t7List.get(i)); + } else { + break; // 面系数不同,停止查找 + } + } + + if (candidatesFor10th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor10th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第10位有{}个相同面系数的候选:{}", candidatesFor10th.size(), + candidatesFor10th.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesFor10th.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + log.debug("T7高位算法最终选择的10个球:{}", result); + return result; + } + + /** + * 中位算法:从T7表取面系数平均值向上6个向下4个的10个球号 + */ + private List getMiddleLevelBallsFromT7(List t7List) { + if (t7List.size() < 10) { + log.warn("T7表数据不足10条,实际{}条", t7List.size()); + return t7List.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 计算面系数平均值 + double avgCoefficient = t7List.stream() + .mapToDouble(T7::getFaceCoefficient) + .average() + .orElse(0.0); + + log.debug("T7表面系数平均值:{}", avgCoefficient); + + // 找到最接近平均值的位置 + int avgIndex = 0; + double minDiff = Double.MAX_VALUE; + for (int i = 0; i < t7List.size(); i++) { + double diff = Math.abs(t7List.get(i).getFaceCoefficient() - avgCoefficient); + if (diff < minDiff) { + minDiff = diff; + avgIndex = i; + } + } + + log.debug("最接近平均值的位置:{},面系数:{}", avgIndex, t7List.get(avgIndex).getFaceCoefficient()); + + // 向上6个(含当前),向下4个,共10个 + int startIndex = Math.max(0, avgIndex - 4); + int endIndex = Math.min(t7List.size() - 1, avgIndex + 5); + + // 确保总共10个数字 + while (endIndex - startIndex + 1 < 10 && (startIndex > 0 || endIndex < t7List.size() - 1)) { + if (startIndex > 0) { + startIndex--; + } + if (endIndex < t7List.size() - 1 && endIndex - startIndex + 1 < 10) { + endIndex++; + } + } + + List candidates = new ArrayList<>(); + for (int i = startIndex; i <= endIndex && candidates.size() < 10; i++) { + candidates.add(t7List.get(i).getSlaveBallNumber()); + } + + // 处理边界位置的相同面系数情况 + List result = handleT7BoundaryConflicts(t7List, candidates, startIndex, endIndex); + + log.debug("T7中位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 低位算法:从T7表取面系数最小值向上第3-13个球共10个球号 + */ + private List getLowLevelBallsFromT7(List t7List) { + if (t7List.size() < 13) { + log.warn("T7表数据不足13条,实际{}条", t7List.size()); + return t7List.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 从最后13个开始,取第3-13个(即倒数第11到倒数第1个) + int startIndex = Math.max(0, t7List.size() - 11); + int endIndex = t7List.size() - 1; + + List candidates = new ArrayList<>(); + for (int i = startIndex; i <= endIndex; i++) { + candidates.add(t7List.get(i).getSlaveBallNumber()); + } + + // 处理边界位置的相同面系数情况 + List result = handleT7BoundaryConflicts(t7List, candidates, startIndex, endIndex); + + log.debug("T7低位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 处理T7表边界位置的面系数冲突 + */ + private List handleT7BoundaryConflicts(List t7List, List candidates, + int startIndex, int endIndex) { + List result = new ArrayList<>(candidates); + + if (result.size() >= 10) { + // 检查第1个位置(最大面系数)的冲突 + Double firstCoefficient = t7List.get(startIndex).getFaceCoefficient(); + List candidatesForFirst = new ArrayList<>(); + + for (int i = 0; i < t7List.size(); i++) { + if (t7List.get(i).getFaceCoefficient().equals(firstCoefficient)) { + candidatesForFirst.add(t7List.get(i)); + } + } + + if (candidatesForFirst.size() > 1) { + log.debug("第1位有{}个相同面系数的候选:{}", candidatesForFirst.size(), + candidatesForFirst.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesForFirst.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.set(0, bestBall); + } + + // 检查第10个位置(最小面系数)的冲突 + Double lastCoefficient = t7List.get(endIndex).getFaceCoefficient(); + List candidatesForLast = new ArrayList<>(); + + for (int i = 0; i < t7List.size(); i++) { + if (t7List.get(i).getFaceCoefficient().equals(lastCoefficient)) { + candidatesForLast.add(t7List.get(i)); + } + } + + if (candidatesForLast.size() > 1) { + log.debug("第10位有{}个相同面系数的候选:{}", candidatesForLast.size(), + candidatesForLast.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesForLast.stream().map(T7::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.set(result.size() - 1, bestBall); + } + } + + return result.subList(0, Math.min(10, result.size())); + } + + /** + * 从T3表获取指定主球的26个从球号(按线系数从大到小排列) + */ + private List getTop26FromT3(Integer masterBallNumber) { + log.debug("从T3表查询主球{}的线系数数据,取前26个", masterBallNumber); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t3List = t3Mapper.selectList(queryWrapper); + + if (t3List.isEmpty()) { + log.warn("T3表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if (t3List.size() < 26) { + log.warn("T3表数据不足26条,实际{}条", t3List.size()); + return t3List.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前25个 + List result = new ArrayList<>(); + for (int i = 0; i < 25; i++) { + result.add(t3List.get(i).getSlaveBallNumber()); + } + + // 处理第26个位置:检查是否有相同的线系数 + Double targetCoefficient = t3List.get(25).getLineCoefficient(); + List candidatesFor26th = new ArrayList<>(); + + // 找出所有线系数等于第26个位置线系数的记录 + for (int i = 25; i < t3List.size(); i++) { + if (t3List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor26th.add(t3List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor26th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor26th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第26位有{}个相同线系数的候选:{}", candidatesFor26th.size(), + candidatesFor26th.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesFor26th.stream().map(T3::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + log.debug("T3表主球{}最终选择的26个从球:{}", masterBallNumber, result); + return result; + } + + /** + * 从history_top_100表获取前3个球号(按点系数排序) + */ + private List getTop3FromHistoryTop100() { + log.debug("从history_top_100表获取前3个球号"); + + // 查询前3个点系数最高的球号 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List top100List = historyTop100Mapper.selectList(queryWrapper); + + if (top100List.size() < 3) { + log.warn("history_top_100表数据不足3条,实际{}条", top100List.size()); + } + + List result = top100List.stream() + .map(HistoryTop100::getBallNumber) + .collect(Collectors.toList()); + + log.debug("history_top_100表前3个球号:{}", result); + return result; + } + + /** + * 从T4表获取指定主球的10个从球号 + * 根据不同级别使用不同的选择策略 + * @param masterBallNumber 主球号 + * @param level 级别:H-高位,M-中位,L-低位 + */ + private List getTop10FromT4(Integer masterBallNumber, String level) { + log.debug("从T4表查询主球{}的线系数数据,级别:{}", masterBallNumber, level); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t4List = t4Mapper.selectList(queryWrapper); + + if (t4List.isEmpty()) { + log.warn("T4表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + switch (level) { + case "H": + // 高位:取线系数最大的前10个 + result = getHighLevelBallsFromT4For10(t4List); + break; + case "M": + // 中位:取线系数平均值向上6个向下4个的10个球号 + result = getMiddleLevelBallsFromT4For10(t4List); + break; + case "L": + // 低位:取线系数最小值向上第4-14个球的10个球号 + result = getLowLevelBallsFromT4For10(t4List); + break; + } + + log.debug("T4表主球{}{}级别最终选择的{}个从球:{}", masterBallNumber, level, result.size(), result); + return result; + } + + /** + * 高位算法:从T4表取线系数最大的前10个 + */ + private List getHighLevelBallsFromT4For10(List t4List) { + if (t4List.size() < 10) { + log.warn("T4表数据不足10条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前9个 + List result = new ArrayList<>(); + for (int i = 0; i < 9; i++) { + result.add(t4List.get(i).getSlaveBallNumber()); + } + + // 处理第10个位置:检查是否有相同的线系数 + Double targetCoefficient = t4List.get(9).getLineCoefficient(); + List candidatesFor10th = new ArrayList<>(); + + // 找出所有线系数等于第10个位置线系数的记录 + for (int i = 9; i < t4List.size(); i++) { + if (t4List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor10th.add(t4List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor10th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor10th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第10位有{}个相同线系数的候选:{}", candidatesFor10th.size(), + candidatesFor10th.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesFor10th.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + log.debug("T4高位算法最终选择的10个球:{}", result); + return result; + } + + /** + * 中位算法:从T4表取线系数平均值向上6个向下4个的10个球号 + */ + private List getMiddleLevelBallsFromT4For10(List t4List) { + if (t4List.size() < 10) { + log.warn("T4表数据不足10条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 计算线系数平均值 + double avgCoefficient = t4List.stream() + .mapToDouble(T4::getLineCoefficient) + .average() + .orElse(0.0); + + log.debug("T4表线系数平均值:{}", avgCoefficient); + + // 找到最接近平均值的位置 + int avgIndex = 0; + double minDiff = Double.MAX_VALUE; + for (int i = 0; i < t4List.size(); i++) { + double diff = Math.abs(t4List.get(i).getLineCoefficient() - avgCoefficient); + if (diff < minDiff) { + minDiff = diff; + avgIndex = i; + } + } + + log.debug("最接近平均值的位置:{},线系数:{}", avgIndex, t4List.get(avgIndex).getLineCoefficient()); + + // 向上6个(含当前),向下4个,共10个 + int startIndex = Math.max(0, avgIndex - 4); + int endIndex = Math.min(t4List.size() - 1, avgIndex + 5); + + // 确保总共10个数字 + while (endIndex - startIndex + 1 < 10 && (startIndex > 0 || endIndex < t4List.size() - 1)) { + if (startIndex > 0) { + startIndex--; + } + if (endIndex < t4List.size() - 1 && endIndex - startIndex + 1 < 10) { + endIndex++; + } + } + + List candidates = new ArrayList<>(); + for (int i = startIndex; i <= endIndex && candidates.size() < 10; i++) { + candidates.add(t4List.get(i).getSlaveBallNumber()); + } + + // 处理边界位置的相同线系数情况 + List result = handleT4BoundaryConflictsFor10(t4List, candidates, startIndex, endIndex); + + log.debug("T4中位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 低位算法:从T4表取线系数最小值向上第4-14个球的10个球号 + */ + private List getLowLevelBallsFromT4For10(List t4List) { + if (t4List.size() < 14) { + log.warn("T4表数据不足14条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 从最后14个开始,取第4-14个(即倒数第11到倒数第1个) + int startIndex = Math.max(0, t4List.size() - 11); + int endIndex = t4List.size() - 1; + + List candidates = new ArrayList<>(); + for (int i = startIndex; i <= endIndex; i++) { + candidates.add(t4List.get(i).getSlaveBallNumber()); + } + + // 处理边界位置的相同线系数情况 + List result = handleT4BoundaryConflictsFor10(t4List, candidates, startIndex, endIndex); + + log.debug("T4低位算法选择范围:[{}, {}],共{}个球", startIndex, endIndex, result.size()); + return result; + } + + /** + * 统计数字出现频率,返回频率最高的前8个数字 + * 如果频次相同的球号超过8个,使用T7表面系数进行二次筛选 + */ + private List getTop8ByFrequency(List allNumbers) { + 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<>(); + + // 按频率从高到低处理 + for (Map.Entry> group : frequencyGroups.entrySet()) { + Integer frequency = group.getKey(); + List balls = group.getValue(); + + // 对同频率的球号按数字升序排序 + Collections.sort(balls); + + log.info("频率{}的球号:{}", frequency, balls); + + // 检查加入这组球号后是否会超过8个 + if (result.size() + balls.size() <= 8) { + // 不会超过8个,直接添加所有球号 + result.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过8个,需要用T7表的面系数进行二次筛选 + int remainingSlots = 8 - result.size(); + log.info("需要从{}个频率相同的球号中选择{}个,使用T7表面系数进行筛选", balls.size(), remainingSlots); + + List selectedBalls = selectBallsByT7FaceCoefficient(balls, remainingSlots); + result.addAll(selectedBalls); + + log.info("通过T7表面系数筛选完成,最终选择:{}", selectedBalls); + break; // 已经达到8个,结束 + } + + // 如果已经有8个,结束 + if (result.size() >= 8) { + break; + } + } + + log.info("频率统计最终结果(共{}个):{}", result.size(), result); + + // 打印详细的频率信息 + for (int i = 0; i < result.size(); i++) { + Integer ballNumber = result.get(i); + Integer frequency = frequencyMap.get(ballNumber); + log.info("第{}位:数字{},出现{}次", i + 1, ballNumber, frequency); + } + + return result; + } + + /** + * 根据T7表的面系数总和选择球号,支持多级筛选 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选择的球号列表 + */ + private List selectBallsByT7FaceCoefficient(List candidateBalls, int selectCount) { + log.info("开始根据T7表面系数总和选择球号,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选球号数量不超过需要选择的数量,直接返回所有候选球号"); + return new ArrayList<>(candidateBalls); + } + + // 第一级:计算每个球号作为主球时的面系数总和 + Map faceCoefficientSumMap = new HashMap<>(); + + for (Integer ballNumber : candidateBalls) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", ballNumber); + + List t7List = t7Mapper.selectList(queryWrapper); + + double sum = t7List.stream() + .mapToDouble(T7::getFaceCoefficient) + .sum(); + + faceCoefficientSumMap.put(ballNumber, sum); + log.debug("球号{}作为主球的面系数总和:{}", ballNumber, sum); + } + + // 按面系数总和分组 + Map> faceCoefficientGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry entry : faceCoefficientSumMap.entrySet()) { + Double sum = entry.getValue(); + Integer ballNumber = entry.getKey(); + faceCoefficientGroups.computeIfAbsent(sum, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("T7表面系数总和分组:{}", faceCoefficientGroups); + + List result = new ArrayList<>(); + + // 按面系数总和从高到低处理 + for (Map.Entry> group : faceCoefficientGroups.entrySet()) { + Double faceSum = group.getKey(); + List balls = group.getValue(); + + log.info("面系数总和{}的球号:{}", faceSum, balls); + + // 检查加入这组球号后是否会超过selectCount个 + if (result.size() + balls.size() <= selectCount) { + // 不会超过,直接添加所有球号(按球号升序排序) + Collections.sort(balls); + result.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过,需要进一步筛选 + int remainingSlots = selectCount - result.size(); + log.info("需要从{}个面系数总和相同的球号中选择{}个,开始多级筛选", balls.size(), remainingSlots); + + List selectedBalls = selectBallsByMultiLevelFilteringFor8(balls, remainingSlots); + result.addAll(selectedBalls); + + log.info("通过多级筛选完成,最终选择:{}", selectedBalls); + break; // 已经达到selectCount个,结束 + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + log.info("T7表面系数筛选最终结果(共{}个):{}", result.size(), result); + + // 打印详细的面系数信息 + for (int i = 0; i < result.size(); i++) { + Integer ballNumber = result.get(i); + Double sum = faceCoefficientSumMap.get(ballNumber); + log.info("第{}位:球号{},面系数总和:{}", i + 1, ballNumber, sum); + } + + return result; + } + + /** + * 多级筛选球号(8个球版本):history_top_100 -> history_top -> 随机选择 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选择的球号列表 + */ + private List selectBallsByMultiLevelFilteringFor8(List candidateBalls, int selectCount) { + log.info("开始多级筛选(8个球版本),候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选球号数量不超过需要选择的数量,直接返回所有候选球号"); + return new ArrayList<>(candidateBalls); + } + + // 第二级:使用history_top_100表的排名进行筛选 + List filteredByTop100 = selectBallsByHistoryTop100RankingFor8(candidateBalls, selectCount); + + if (filteredByTop100.size() == selectCount) { + log.info("通过history_top_100表筛选成功,结果:{}", filteredByTop100); + return filteredByTop100; + } + + // 第三级:使用history_top表的点系数进行筛选 + log.info("history_top_100表筛选后仍有{}个球号,继续使用history_top表筛选", filteredByTop100.size()); + List filteredByTop = selectBallsByHistoryTopPointCoefficientFor8(filteredByTop100, selectCount); + + if (filteredByTop.size() == selectCount) { + log.info("通过history_top表筛选成功,结果:{}", filteredByTop); + return filteredByTop; + } + + // 第四级:随机选择 + log.info("history_top表筛选后仍有{}个球号,进行随机选择", filteredByTop.size()); + List finalResult = selectBallsRandomlyFor8(filteredByTop, selectCount); + + log.info("随机选择完成,最终结果:{}", finalResult); + return finalResult; + } + + /** + * 根据history_top_100表的排名筛选球号(8个球版本) + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 筛选后的球号列表 + */ + private List selectBallsByHistoryTop100RankingFor8(List candidateBalls, int selectCount) { + log.info("使用history_top_100表排名筛选球号(8个球版本),候选:{},需要选择:{}", 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 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<>(); + + // 按点系数从高到低处理 + 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 { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个球号:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在history_top_100表中的球号 + List notInTop100 = new ArrayList<>(candidateBalls); + notInTop100.removeAll(result); + if (!notInTop100.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + Collections.sort(notInTop100); + 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 result; + } + + /** + * 根据history_top表的点系数筛选球号(8个球版本) + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 筛选后的球号列表 + */ + private List selectBallsByHistoryTopPointCoefficientFor8(List candidateBalls, int selectCount) { + log.info("使用history_top表点系数筛选球号(8个球版本),候选:{},需要选择:{}", 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 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<>(); + + // 按点系数从高到低处理 + 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 { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个球号:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在history_top表中的球号 + List notInHistoryTop = new ArrayList<>(candidateBalls); + notInHistoryTop.removeAll(result); + if (!notInHistoryTop.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + Collections.sort(notInHistoryTop); + 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 result; + } + + /** + * 随机选择球号(8个球版本) + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 随机选择的球号列表 + */ + private List selectBallsRandomlyFor8(List candidateBalls, int selectCount) { + log.info("随机选择球号(8个球版本),候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选球号数量不超过需要选择的数量,直接返回所有候选球号"); + return new ArrayList<>(candidateBalls); + } + + // 创建候选球号的副本并打乱顺序 + List shuffledBalls = new ArrayList<>(candidateBalls); + Collections.shuffle(shuffledBalls, new Random(System.currentTimeMillis())); + + // 选择前selectCount个 + List result = shuffledBalls.subList(0, selectCount); + + // 按球号升序排序结果 + Collections.sort(result); + + log.info("随机选择结果:{}", result); + return result; + } + + /** + * 蓝球分析算法主方法 + * @param level 高位/中位/低位标识 (H/M/L) + * @param predictedRedBalls 6个预测的红球号码 + * @param predictedBlueBalls 2个预测的蓝球号码 + * @param lastRedBalls 6个上期红球号码 + * @param lastBlueBall 1个上期蓝球号码 + * @return 分析结果:出现频率最高的前4个蓝球号码 + */ + public List blueBallAnalysis(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); + + 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表获取5个蓝球号码 + log.info("第五步:用上期蓝球{}从T5表获取5个蓝球号码,使用{}级别算法", lastBlueBall, level); + List t5Numbers = getSimpleTop5FromT5ByLevel(lastBlueBall, level); + allNumbers.addAll(t5Numbers); + log.info("上期蓝球{}从T5表获取到{}个蓝球号码:{}", lastBlueBall, t5Numbers.size(), t5Numbers); + + log.info("总共收集到{}个蓝球号码", allNumbers.size()); + + // 第六步:统计频率并获取前4个 + List result = getSimpleTop4ByFrequency(allNumbers); + + log.info("蓝球分析算法完成,结果:{}", result); + return result; + } + + /** + * 验证蓝球分析算法的输入参数 + */ + private void validateBlueBallAnalysisParams(String level, List predictedRedBalls, + List predictedBlueBalls, List lastRedBalls, + Integer lastBlueBall) { + // 验证级别 + if (!Arrays.asList("H", "M", "L").contains(level)) { + throw new IllegalArgumentException("级别必须为H、M或L"); + } + + // 验证预测红球 + if (predictedRedBalls == null || predictedRedBalls.size() != 6) { + throw new IllegalArgumentException("预测红球数量必须为6个"); + } + for (Integer ball : predictedRedBalls) { + if (ball == null || ball < 1 || ball > 33) { + throw new IllegalArgumentException("预测红球号码必须在1-33范围内,实际:" + ball); + } + } + if (predictedRedBalls.stream().distinct().count() != predictedRedBalls.size()) { + throw new IllegalArgumentException("预测红球号码不能重复"); + } + + // 验证预测蓝球 + if (predictedBlueBalls == null || predictedBlueBalls.size() != 2) { + throw new IllegalArgumentException("预测蓝球数量必须为2个"); + } + for (Integer ball : predictedBlueBalls) { + if (ball == null || ball < 1 || ball > 16) { + throw new IllegalArgumentException("预测蓝球号码必须在1-16范围内,实际:" + ball); + } + } + if (predictedBlueBalls.stream().distinct().count() != predictedBlueBalls.size()) { + throw new IllegalArgumentException("预测蓝球号码不能重复"); + } + + // 验证上期红球 + if (lastRedBalls == null || lastRedBalls.size() != 6) { + throw new IllegalArgumentException("上期红球数量必须为6个"); + } + for (Integer ball : lastRedBalls) { + if (ball == null || ball < 1 || ball > 33) { + throw new IllegalArgumentException("上期红球号码必须在1-33范围内,实际:" + ball); + } + } + if (lastRedBalls.stream().distinct().count() != lastRedBalls.size()) { + throw new IllegalArgumentException("上期红球号码不能重复"); + } + + // 验证上期蓝球 + if (lastBlueBall == null || lastBlueBall < 1 || lastBlueBall > 16) { + throw new IllegalArgumentException("上期蓝球号码必须在1-16范围内,实际:" + lastBlueBall); + } + + log.debug("蓝球分析算法参数验证通过"); + } + + /** + * 从T6表获取指定红球对应的12个蓝球号码(线系数最大的12个) + * @param masterBallNumber 主球号(红球) + * @return 12个蓝球号码列表 + */ + private List getTop12FromT6(Integer masterBallNumber) { + log.debug("从T6表查询主球{}的线系数数据", masterBallNumber); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t6List = t6Mapper.selectList(queryWrapper); + + if (t6List.isEmpty()) { + log.warn("T6表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if (t6List.size() < 12) { + log.warn("T6表数据不足12条,实际{}条", t6List.size()); + return t6List.stream().map(T6::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前11个 + List result = new ArrayList<>(); + for (int i = 0; i < 11; i++) { + result.add(t6List.get(i).getSlaveBallNumber()); + } + + // 处理第12个位置:检查是否有相同的线系数 + Double targetCoefficient = t6List.get(11).getLineCoefficient(); + List candidatesFor12th = new ArrayList<>(); + + // 找出所有线系数等于第12个位置线系数的记录 + for (int i = 11; i < t6List.size(); i++) { + if (t6List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor12th.add(t6List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor12th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor12th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过blue_history_top_100表的点系数排序 + log.debug("第12位有{}个相同线系数的候选:{}", candidatesFor12th.size(), + candidatesFor12th.stream().map(T6::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromBlueHistoryTop100( + candidatesFor12th.stream().map(T6::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + log.debug("T6表主球{}最终选择的{}个从球:{}", masterBallNumber, result.size(), result); + return result; + } + + /** + * 从blue_history_top_100表中选择点系数最高的球号 + * @param candidates 候选球号列表 + * @return 点系数最高的球号 + */ + private Integer selectBestBallFromBlueHistoryTop100(List candidates) { + log.debug("从blue_history_top_100表中选择最佳球号,候选:{}", candidates); + + if (candidates.isEmpty()) { + throw new IllegalArgumentException("候选球号列表不能为空"); + } + + if (candidates.size() == 1) { + return candidates.get(0); + } + + // 查询这些球号在blue_history_top_100表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidates) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List top100List = blueHistoryTop100Mapper.selectList(queryWrapper); + + if (top100List.isEmpty()) { + log.warn("候选球号{}在blue_history_top_100表中未找到数据,返回最小球号", candidates); + return Collections.min(candidates); + } + + // 返回点系数最高的球号 + Integer bestBall = top100List.get(0).getBallNumber(); + log.debug("选择的最佳球号:{},点系数:{}", bestBall, top100List.get(0).getPointCoefficient()); + + return bestBall; + } + + /** + * 从T8表获取指定红球对应的5个蓝球号码(简化版本) + * @param masterBallNumber 主球号(红球) + * @param level 级别:H-高位,M-中位,L-低位 + * @return 5个蓝球号码列表 + */ + private List getSimpleTop5FromT8ByLevel(Integer masterBallNumber, String level) { + log.debug("从T8表查询主球{}的面系数数据,级别:{}", masterBallNumber, level); + + // 查询指定主球的所有数据,按面系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("faceCoefficient") + .orderByAsc("slaveBallNumber"); // 面系数相同时按从球号升序 + + List t8List = t8Mapper.selectList(queryWrapper); + + if (t8List.isEmpty()) { + log.warn("T8表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int size = t8List.size(); + + switch (level) { + case "H": + // 高位:取面系数最大的5个 + for (int i = 0; i < Math.min(5, size); i++) { + result.add(t8List.get(i).getSlaveBallNumber()); + } + break; + case "M": + // 中位:取中间的5个 + int start = Math.max(0, (size - 5) / 2); + for (int i = start; i < Math.min(start + 5, size); i++) { + result.add(t8List.get(i).getSlaveBallNumber()); + } + break; + case "L": + // 低位:取最后5个(面系数最小的) + int startLow = Math.max(0, size - 5); + for (int i = startLow; i < size; i++) { + result.add(t8List.get(i).getSlaveBallNumber()); + } + break; + } + + log.debug("T8表主球{}{}级别最终选择的{}个从球:{}", masterBallNumber, level, result.size(), result); + return result; + } + + /** + * 从blue_history_top_100表获取点系数最高的前2个蓝球号码 + * 如果第2个点系数不唯一,使用blue_history_top表进行二级筛选 + * @return 前2个蓝球号码列表 + */ + private List getTop2FromBlueHistoryTop100() { + log.debug("从blue_history_top_100表获取前2个蓝球号码"); + + // 查询所有数据,按点系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List top100List = blueHistoryTop100Mapper.selectList(queryWrapper); + + if (top100List.isEmpty()) { + log.warn("blue_history_top_100表中没有数据"); + return new ArrayList<>(); + } + + if (top100List.size() == 1) { + List result = Arrays.asList(top100List.get(0).getBallNumber()); + log.debug("blue_history_top_100表只有1条数据:{}", result); + return result; + } + + List result = new ArrayList<>(); + + // 第1个直接取点系数最高的 + result.add(top100List.get(0).getBallNumber()); + log.debug("第1个蓝球号码:{},点系数:{}", top100List.get(0).getBallNumber(), top100List.get(0).getPointCoefficient()); + + // 处理第2个位置:检查是否有相同的点系数 + Double targetCoefficient = top100List.get(1).getPointCoefficient(); + List candidatesFor2nd = new ArrayList<>(); + + // 找出所有点系数等于第2个位置点系数的记录 + for (int i = 1; i < top100List.size(); i++) { + if (top100List.get(i).getPointCoefficient().equals(targetCoefficient)) { + candidatesFor2nd.add(top100List.get(i)); + } else { + break; // 点系数不同,停止查找 + } + } + + if (candidatesFor2nd.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor2nd.get(0).getBallNumber()); + log.debug("第2个蓝球号码:{},点系数:{}", candidatesFor2nd.get(0).getBallNumber(), candidatesFor2nd.get(0).getPointCoefficient()); + } else { + // 有多个候选,通过blue_history_top表的点系数排序 + log.debug("第2位有{}个相同点系数的候选:{}", candidatesFor2nd.size(), + candidatesFor2nd.stream().map(BlueHistoryTop100::getBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromBlueHistoryTop( + candidatesFor2nd.stream().map(BlueHistoryTop100::getBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + log.debug("通过blue_history_top表筛选的第2个蓝球号码:{}", bestBall); + } + + log.debug("从blue_history_top_100表最终获取到{}个蓝球号码:{}", result.size(), result); + return result; + } + + /** + * 从T5表获取指定蓝球对应的5个蓝球号码(简化版本) + * @param masterBallNumber 主球号(蓝球) + * @param level 级别:H-高位,M-中位,L-低位 + * @return 5个蓝球号码列表 + */ + private List getSimpleTop5FromT5ByLevel(Integer masterBallNumber, String level) { + log.debug("从T5表查询主球{}的线系数数据,级别:{}", masterBallNumber, level); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t5List = t5Mapper.selectList(queryWrapper); + + if (t5List.isEmpty()) { + log.warn("T5表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int size = t5List.size(); + + switch (level) { + case "H": + // 高位:取线系数最大的5个 + for (int i = 0; i < Math.min(5, size); i++) { + result.add(t5List.get(i).getSlaveBallNumber()); + } + break; + case "M": + // 中位:取中间的5个 + int start = Math.max(0, (size - 5) / 2); + for (int i = start; i < Math.min(start + 5, size); i++) { + result.add(t5List.get(i).getSlaveBallNumber()); + } + break; + case "L": + // 低位:取最后5个(线系数最小的) + int startLow = Math.max(0, size - 5); + for (int i = startLow; i < size; i++) { + result.add(t5List.get(i).getSlaveBallNumber()); + } + break; + } + + log.debug("T5表主球{}{}级别最终选择的{}个从球:{}", masterBallNumber, level, result.size(), result); + return result; + } + + /** + * 统计数字出现频率,返回频率最高的前4个蓝球号码 + * 如果频次相同的球号超过4个,使用多级筛选机制 + */ + private List getSimpleTop4ByFrequency(List allNumbers) { + 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<>(); + + // 按频率从高到低处理 + 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); + log.info("直接添加{}个蓝球号码,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过4个,需要用多级筛选机制 + int remainingSlots = 4 - result.size(); + log.info("需要从{}个频率相同的蓝球号码中选择{}个,使用多级筛选机制", balls.size(), remainingSlots); + + List selectedBalls = selectBallsByMultiLevelFilteringForBlue4(balls, remainingSlots); + result.addAll(selectedBalls); + + log.info("通过多级筛选完成,最终选择:{}", selectedBalls); + break; // 已经达到4个,结束 + } + + // 如果已经有4个,结束 + if (result.size() >= 4) { + break; + } + } + + log.info("频率统计最终结果(共{}个):{}", result.size(), result); + + // 打印详细的频率信息 + for (int i = 0; i < result.size(); i++) { + Integer ballNumber = result.get(i); + Integer frequency = frequencyMap.get(ballNumber); + log.info("第{}位:蓝球号码{},出现{}次", i + 1, ballNumber, frequency); + } + + return result; + } + + /** + * 从blue_history_top表中选择点系数最高的球号 + * @param candidates 候选球号列表 + * @return 点系数最高的球号 + */ + private Integer selectBestBallFromBlueHistoryTop(List candidates) { + log.debug("从blue_history_top表中选择最佳球号,候选:{}", candidates); + + if (candidates.isEmpty()) { + throw new IllegalArgumentException("候选球号列表不能为空"); + } + + if (candidates.size() == 1) { + return candidates.get(0); + } + + // 查询这些球号在blue_history_top表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidates) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List historyTopList = blueHistoryTopMapper.selectList(queryWrapper); + + if (historyTopList.isEmpty()) { + log.warn("候选球号{}在blue_history_top表中未找到数据,返回最小球号", candidates); + return Collections.min(candidates); + } + + // 返回点系数最高的球号 + Integer bestBall = historyTopList.get(0).getBallNumber(); + log.debug("选择的最佳球号:{},点系数:{}", bestBall, historyTopList.get(0).getPointCoefficient()); + + return bestBall; + } + + /** + * 四级筛选蓝球号码:T8面系数 -> blue_history_top_100排名 -> blue_history_top点系数 -> 随机选择 + * @param candidateBalls 候选蓝球号码列表 + * @param selectCount 需要选择的数量 + * @return 选择的蓝球号码列表 + */ + private List selectBallsByMultiLevelFilteringForBlue4(List candidateBalls, int selectCount) { + log.info("开始四级筛选蓝球号码,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选蓝球号码数量不超过需要选择的数量,直接返回所有候选蓝球号码"); + return new ArrayList<>(candidateBalls); + } + + // 第一级:使用T8表的面系数总和进行筛选 + List filteredByT8 = selectBallsByT8FaceCoefficientSumForBlue4(candidateBalls, selectCount); + + if (filteredByT8.size() == selectCount) { + log.info("通过T8表面系数筛选成功,结果:{}", filteredByT8); + return filteredByT8; + } + + // 第二级:使用blue_history_top_100表的排名进行筛选 + log.info("T8表筛选后仍有{}个蓝球号码,继续使用blue_history_top_100表筛选", filteredByT8.size()); + List filteredByTop100 = selectBallsByBlueHistoryTop100RankingForBlue4(filteredByT8, selectCount); + + if (filteredByTop100.size() == selectCount) { + log.info("通过blue_history_top_100表筛选成功,结果:{}", filteredByTop100); + return filteredByTop100; + } + + // 第三级:使用blue_history_top表的点系数进行筛选 + log.info("blue_history_top_100表筛选后仍有{}个蓝球号码,继续使用blue_history_top表筛选", filteredByTop100.size()); + List filteredByTop = selectBallsByBlueHistoryTopPointCoefficientForBlue4(filteredByTop100, selectCount); + + if (filteredByTop.size() == selectCount) { + log.info("通过blue_history_top表筛选成功,结果:{}", filteredByTop); + return filteredByTop; + } + + // 第四级:随机选择 + log.info("blue_history_top表筛选后仍有{}个蓝球号码,进行随机选择", filteredByTop.size()); + List finalResult = selectBallsRandomlyForBlue4(filteredByTop, selectCount); + + log.info("随机选择完成,最终结果:{}", finalResult); + return finalResult; + } + + /** + * 根据T8表的面系数总和选择蓝球号码(第一级筛选) + * @param candidateBalls 候选蓝球号码列表 + * @param selectCount 需要选择的数量 + * @return 选择的蓝球号码列表 + */ + private List selectBallsByT8FaceCoefficientSumForBlue4(List candidateBalls, int selectCount) { + log.info("开始根据T8表面系数总和选择蓝球号码,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选蓝球号码数量不超过需要选择的数量,直接返回所有候选蓝球号码"); + return new ArrayList<>(candidateBalls); + } + + // 计算每个蓝球号码作为主球时的面系数总和 + Map faceCoefficientSumMap = new HashMap<>(); + + for (Integer ballNumber : candidateBalls) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", ballNumber); + + List t8List = t8Mapper.selectList(queryWrapper); + + double sum = t8List.stream() + .mapToDouble(T8::getFaceCoefficient) + .sum(); + + faceCoefficientSumMap.put(ballNumber, sum); + log.debug("蓝球号码{}作为主球的面系数总和:{}", ballNumber, sum); + } + + // 按面系数总和分组 + Map> faceCoefficientGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry entry : faceCoefficientSumMap.entrySet()) { + Double sum = entry.getValue(); + Integer ballNumber = entry.getKey(); + faceCoefficientGroups.computeIfAbsent(sum, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("T8表面系数总和分组:{}", faceCoefficientGroups); + + List result = new ArrayList<>(); + + // 按面系数总和从高到低处理 + for (Map.Entry> group : faceCoefficientGroups.entrySet()) { + Double faceSum = group.getKey(); + List balls = group.getValue(); + + log.info("面系数总和{}的蓝球号码:{}", faceSum, balls); + + // 检查加入这组球号后是否会超过selectCount个 + if (result.size() + balls.size() <= selectCount) { + // 不会超过,直接添加所有球号(按球号升序排序) + Collections.sort(balls); + result.addAll(balls); + log.info("直接添加{}个蓝球号码,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个蓝球号码:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + log.info("T8表面系数筛选结果:{}", result); + return result; + } + + /** + * 根据blue_history_top_100表的点系数选择蓝球号码(第二级筛选) + * @param candidateBalls 候选蓝球号码列表 + * @param selectCount 需要选择的数量 + * @return 选择的蓝球号码列表 + */ + private List selectBallsByBlueHistoryTop100RankingForBlue4(List candidateBalls, int selectCount) { + log.info("使用blue_history_top_100表点系数筛选蓝球号码,候选:{},需要选择:{}", candidateBalls, selectCount); + + // 查询这些球号在blue_history_top_100表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidateBalls) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List top100List = blueHistoryTop100Mapper.selectList(queryWrapper); + + if (top100List.isEmpty()) { + log.warn("候选球号{}在blue_history_top_100表中未找到数据,按球号升序返回", candidateBalls); + Collections.sort(candidateBalls); + return candidateBalls.subList(0, Math.min(selectCount, candidateBalls.size())); + } + + // 按点系数分组 + Map> pointCoefficientGroups = new TreeMap<>(Collections.reverseOrder()); + for (BlueHistoryTop100 item : top100List) { + Double pointCoefficient = item.getPointCoefficient(); + Integer ballNumber = item.getBallNumber(); + pointCoefficientGroups.computeIfAbsent(pointCoefficient, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("blue_history_top_100表点系数分组:{}", pointCoefficientGroups); + + List result = new ArrayList<>(); + + // 按点系数从高到低处理 + 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 { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个蓝球号码:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在blue_history_top_100表中的球号 + List notInTop100 = new ArrayList<>(candidateBalls); + notInTop100.removeAll(result); + if (!notInTop100.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + Collections.sort(notInTop100); + int addCount = Math.min(remainingSlots, notInTop100.size()); + result.addAll(notInTop100.subList(0, addCount)); + log.info("添加不在blue_history_top_100表中的{}个蓝球号码:{}", addCount, notInTop100.subList(0, addCount)); + } + + log.info("blue_history_top_100表筛选结果:{}", result); + return result; + } + + /** + * 根据blue_history_top表的点系数选择蓝球号码(第三级筛选) + * @param candidateBalls 候选蓝球号码列表 + * @param selectCount 需要选择的数量 + * @return 选择的蓝球号码列表 + */ + private List selectBallsByBlueHistoryTopPointCoefficientForBlue4(List candidateBalls, int selectCount) { + log.info("使用blue_history_top表点系数筛选蓝球号码,候选:{},需要选择:{}", candidateBalls, selectCount); + + // 查询这些球号在blue_history_top表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidateBalls) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List historyTopList = blueHistoryTopMapper.selectList(queryWrapper); + + if (historyTopList.isEmpty()) { + log.warn("候选球号{}在blue_history_top表中未找到数据,按球号升序返回", candidateBalls); + Collections.sort(candidateBalls); + return candidateBalls.subList(0, Math.min(selectCount, candidateBalls.size())); + } + + // 按点系数分组 + Map> pointCoefficientGroups = new TreeMap<>(Collections.reverseOrder()); + for (BlueHistoryTop item : historyTopList) { + Double pointCoefficient = item.getPointCoefficient(); + Integer ballNumber = item.getBallNumber(); + pointCoefficientGroups.computeIfAbsent(pointCoefficient, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("blue_history_top表点系数分组:{}", pointCoefficientGroups); + + List result = new ArrayList<>(); + + // 按点系数从高到低处理 + 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 { + // 会超过,按球号升序选择需要的数量 + int remainingSlots = selectCount - result.size(); + Collections.sort(balls); + result.addAll(balls.subList(0, remainingSlots)); + log.info("选择前{}个蓝球号码:{}", remainingSlots, balls.subList(0, remainingSlots)); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在blue_history_top表中的球号 + List notInHistoryTop = new ArrayList<>(candidateBalls); + notInHistoryTop.removeAll(result); + if (!notInHistoryTop.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + Collections.sort(notInHistoryTop); + int addCount = Math.min(remainingSlots, notInHistoryTop.size()); + result.addAll(notInHistoryTop.subList(0, addCount)); + log.info("添加不在blue_history_top表中的{}个蓝球号码:{}", addCount, notInHistoryTop.subList(0, addCount)); + } + + log.info("blue_history_top表筛选结果:{}", result); + return result; + } + + /** + * 随机选择蓝球号码(第四级筛选) + * @param candidateBalls 候选蓝球号码列表 + * @param selectCount 需要选择的数量 + * @return 随机选择的蓝球号码列表 + */ + private List selectBallsRandomlyForBlue4(List candidateBalls, int selectCount) { + log.info("随机选择蓝球号码,候选:{},需要选择:{}", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + log.info("候选蓝球号码数量不超过需要选择的数量,直接返回所有候选蓝球号码"); + return new ArrayList<>(candidateBalls); + } + + // 创建候选球号的副本并打乱顺序 + List shuffledBalls = new ArrayList<>(candidateBalls); + Collections.shuffle(shuffledBalls, new Random(System.currentTimeMillis())); + + // 选择前selectCount个 + List result = shuffledBalls.subList(0, selectCount); + + // 按球号升序排序结果 + Collections.sort(result); + + log.info("随机选择结果:{}", result); + return result; + } + + /** + * 首球命中率统计 + * @param userId 用户ID + * @return 首球命中率统计信息 + */ + public BallHitRateVO getFirstBallHitRate(Long userId) { + log.info("开始统计用户{}的首球命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .ne("predictStatus", "待开奖"); + + List predictRecords = predictRecordMapper.selectList(queryWrapper); + + // 总预测次数 + int totalCount = predictRecords.size(); + + // 统计首球命中次数 + int hitCount = 0; + + for (PredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + LotteryDraws draw = lotteryDrawsService.getByDrawId(record.getDrawId()); + + if (draw != null && record.getRedBall1() != null) { + Integer predictedFirstBall = record.getRedBall1(); + List drawnRedBalls = Arrays.asList( + draw.getRedBall1(), + draw.getRedBall2(), + draw.getRedBall3(), + draw.getRedBall4(), + draw.getRedBall5(), + draw.getRedBall6() + ); + // 比较预测的第一个红球是否在开奖的六个红球中 + if (drawnRedBalls.contains(predictedFirstBall)) { + hitCount++; + } + } + } + + // 计算命中率 + double hitRate = totalCount > 0 ? (double) hitCount / totalCount * 100 : 0; + + BallHitRateVO result = BallHitRateVO.builder() + .hitCount(hitCount) + .totalCount(totalCount) + .hitRate(hitRate) + .build(); + + log.info("用户{}的首球命中率统计结果:命中{}次,总计{}次,命中率{}%", + userId, hitCount, totalCount, String.format("%.2f", hitRate)); + return result; + } + + /** + * 蓝球命中率统计 + * @param userId 用户ID + * @return 蓝球命中率统计信息 + */ + public BallHitRateVO getBlueBallHitRate(Long userId) { + log.info("开始统计用户{}的蓝球命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .ne("predictStatus", "待开奖"); + + List predictRecords = predictRecordMapper.selectList(queryWrapper); + + // 总预测次数 + int totalCount = predictRecords.size(); + + // 统计蓝球命中次数 + int hitCount = 0; + + for (PredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + LotteryDraws draw = lotteryDrawsService.getByDrawId(record.getDrawId()); + + if (draw != null && record.getBlueBall() != null) { + // 比较预测的蓝球与开奖的蓝球是否相等 + if (record.getBlueBall().equals(draw.getBlueBall())) { + hitCount++; + } + } + } + + // 计算命中率 + double hitRate = totalCount > 0 ? (double) hitCount / totalCount * 100 : 0; + + BallHitRateVO result = BallHitRateVO.builder() + .hitCount(hitCount) + .totalCount(totalCount) + .hitRate(hitRate) + .build(); + + log.info("用户{}的蓝球命中率统计结果:命中{}次,总计{}次,命中率{}%", + userId, hitCount, totalCount, String.format("%.2f", hitRate)); + return result; + } + + /** + * 奖金统计 + * @param userId 用户ID + * @param predictId 预测记录ID,如果为null则统计所有记录 + * @return 奖金统计信息 + */ + public PrizeEstimateVO getPrizeStatistics(Long userId, Long predictId) { + log.info("开始为用户{}进行奖金统计,predictId={}", userId, predictId); + + // 查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId); + // 排除待开奖状态 + queryWrapper.ne("predictStatus", "待开奖"); + // 如果指定了预测记录ID,则只查询该记录 + if (predictId != null) { + queryWrapper.eq("id", predictId); + } + + // 查询用户的预测记录 + List records = predictRecordMapper.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 (PredictRecord record : records) { + String prizeLevel = record.getPredictResult(); + if (prizeLevel == null) { + 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) { + 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); + return result; + } + + /** + * 红球命中率统计 + * @param userId 用户ID + * @return 红球命中率统计信息 + */ + public RedBallHitRateVO getRedBallHitRate(Long userId) { + log.info("开始统计用户{}的红球命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .ne("predictStatus", "待开奖"); + + List predictRecords = predictRecordMapper.selectList(queryWrapper); + + int totalHitCount = 0; + int totalRecords = predictRecords.size(); + + for (PredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + LotteryDraws draw = lotteryDrawsService.getByDrawId(record.getDrawId()); + + if (draw != null) { + List predictedRedBalls = Arrays.asList( + record.getRedBall1(), + record.getRedBall2(), + record.getRedBall3(), + record.getRedBall4(), + record.getRedBall5(), + record.getRedBall6() + ); + + List drawnRedBalls = Arrays.asList( + draw.getRedBall1(), + draw.getRedBall2(), + draw.getRedBall3(), + draw.getRedBall4(), + draw.getRedBall5(), + draw.getRedBall6() + ); + + // 计算当前记录命中的红球数 + for (Integer predictedBall : predictedRedBalls) { + if (predictedBall != null && drawnRedBalls.contains(predictedBall)) { + totalHitCount++; + } + } + } + } + + int totalPredictedCount = totalRecords * 6; + 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; + } + + /** + * 从T4表获取指定主球的26个从球号,取线系数最大值向下26个数字 + * @param masterBallNumber 主球号 + */ + private List getTop26FromT4(Integer masterBallNumber) { + log.debug("从T4表查询主球{}的线系数数据,取前26个", masterBallNumber); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t4List = t4Mapper.selectList(queryWrapper); + + if (t4List.isEmpty()) { + log.warn("T4表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if (t4List.size() < 26) { + log.warn("T4表数据不足26条,实际{}条", t4List.size()); + return t4List.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前25个 + List result = new ArrayList<>(); + for (int i = 0; i < 25; i++) { + result.add(t4List.get(i).getSlaveBallNumber()); + } + + // 处理第26个位置:检查是否有相同的线系数 + Double targetCoefficient = t4List.get(25).getLineCoefficient(); + List candidatesFor26th = new ArrayList<>(); + + // 找出所有线系数等于第26个位置线系数的记录 + for (int i = 25; i < t4List.size(); i++) { + if (t4List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor26th.add(t4List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor26th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor26th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过history_top_100表的点系数排序 + log.debug("第26位有{}个相同线系数的候选:{}", candidatesFor26th.size(), + candidatesFor26th.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromHistoryTop100( + candidatesFor26th.stream().map(T4::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + log.debug("T4表主球{}最终选择的26个从球:{}", masterBallNumber, result); + return result; + } + + /** + * 从T5表获取指定主球的12个从球号,取线系数最大值向下12个数字 + * @param masterBallNumber 主球号 + */ + private List getTop12FromT5(Integer masterBallNumber) { + log.debug("从T5表查询主球{}的线系数数据,取前12个", masterBallNumber); + + // 查询指定主球的所有数据,按线系数降序排列 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient") + .orderByAsc("slaveBallNumber"); // 线系数相同时按从球号升序 + + List t5List = t5Mapper.selectList(queryWrapper); + + if (t5List.isEmpty()) { + log.warn("T5表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if (t5List.size() < 12) { + log.warn("T5表数据不足12条,实际{}条", t5List.size()); + return t5List.stream().map(T5::getSlaveBallNumber).collect(Collectors.toList()); + } + + // 获取前11个 + List result = new ArrayList<>(); + for (int i = 0; i < 11; i++) { + result.add(t5List.get(i).getSlaveBallNumber()); + } + + // 处理第12个位置:检查是否有相同的线系数 + Double targetCoefficient = t5List.get(11).getLineCoefficient(); + List candidatesFor12th = new ArrayList<>(); + + // 找出所有线系数等于第12个位置线系数的记录 + for (int i = 11; i < t5List.size(); i++) { + if (t5List.get(i).getLineCoefficient().equals(targetCoefficient)) { + candidatesFor12th.add(t5List.get(i)); + } else { + break; // 线系数不同,停止查找 + } + } + + if (candidatesFor12th.size() == 1) { + // 只有一个候选,直接添加 + result.add(candidatesFor12th.get(0).getSlaveBallNumber()); + } else { + // 有多个候选,通过blue_history_top_100表的点系数排序 + log.debug("第12位有{}个相同线系数的候选:{}", candidatesFor12th.size(), + candidatesFor12th.stream().map(T5::getSlaveBallNumber).collect(Collectors.toList())); + + Integer bestBall = selectBestBallFromBlueHistoryTop100( + candidatesFor12th.stream().map(T5::getSlaveBallNumber).collect(Collectors.toList()) + ); + result.add(bestBall); + } + + log.debug("T5表主球{}最终选择的{}个从球:{}", masterBallNumber, result.size(), result); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/BlueHistory100Service.java b/src/main/java/com/xy/xyaicpzs/service/BlueHistory100Service.java new file mode 100644 index 0000000..278eaa4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/BlueHistory100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.BlueHistory100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【blue_history_100(蓝球最近100期数据表)】的数据库操作Service +* @createDate 2025-06-14 10:40:04 +*/ +public interface BlueHistory100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/BlueHistoryAllService.java b/src/main/java/com/xy/xyaicpzs/service/BlueHistoryAllService.java new file mode 100644 index 0000000..39ba6a7 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/BlueHistoryAllService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.BlueHistoryAll; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【blue_history_all(蓝球全部历史数据表)】的数据库操作Service +* @createDate 2025-06-14 10:40:07 +*/ +public interface BlueHistoryAllService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/BlueHistoryTop100Service.java b/src/main/java/com/xy/xyaicpzs/service/BlueHistoryTop100Service.java new file mode 100644 index 0000000..0b273b9 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/BlueHistoryTop100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【blue_history_top_100(创建蓝球100期数据排行表)】的数据库操作Service +* @createDate 2025-06-14 10:40:13 +*/ +public interface BlueHistoryTop100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/BlueHistoryTopService.java b/src/main/java/com/xy/xyaicpzs/service/BlueHistoryTopService.java new file mode 100644 index 0000000..e62d0ce --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/BlueHistoryTopService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【blue_history_top(蓝球历史数据排行表)】的数据库操作Service +* @createDate 2025-06-14 10:40:10 +*/ +public interface BlueHistoryTopService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/ChatMessageService.java b/src/main/java/com/xy/xyaicpzs/service/ChatMessageService.java new file mode 100644 index 0000000..7a5aaff --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/ChatMessageService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.ChatMessage; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【chat_message(聊天消息表)】的数据库操作Service +* @createDate 2025-07-07 17:37:15 +*/ +public interface ChatMessageService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/CozeAuthService.java b/src/main/java/com/xy/xyaicpzs/service/CozeAuthService.java new file mode 100644 index 0000000..43e888f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/CozeAuthService.java @@ -0,0 +1,84 @@ +package com.xy.xyaicpzs.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +/** + * Coze认证服务 + * 提供通过JWT获取访问令牌的功能 + */ +@Service +public class CozeAuthService { + + private static final Logger logger = LoggerFactory.getLogger(CozeAuthService.class); + private static final String COZE_TOKEN_URL = "https://api.coze.cn/api/permission/oauth2/token"; + private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + /** + * 通过JWT获取访问令牌 + * + * @param jwt JWT令牌 + * @param durationSeconds 令牌有效期(秒) + * @return 包含访问令牌和过期时间的Map + */ + public Map getAccessToken(String jwt, Integer durationSeconds) { + try { + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + jwt); + + // 设置请求体 + Map requestBody = new HashMap<>(); + requestBody.put("grant_type", GRANT_TYPE); + if (durationSeconds != null && durationSeconds > 0) { + requestBody.put("duration_seconds", durationSeconds); + } + + // 创建请求实体 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // 发送请求 + ResponseEntity response = restTemplate.postForEntity(COZE_TOKEN_URL, requestEntity, String.class); + + // 解析响应 + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + JsonNode jsonNode = objectMapper.readTree(response.getBody()); + + Map result = new HashMap<>(); + result.put("access_token", jsonNode.get("access_token").asText()); + result.put("expires_in", jsonNode.get("expires_in").asLong()); + + return result; + } else { + throw new RuntimeException("获取访问令牌失败,HTTP状态码:" + response.getStatusCodeValue()); + } + + } catch (JsonProcessingException e) { + logger.error("解析响应JSON失败", e); + throw new RuntimeException("解析响应JSON失败: " + e.getMessage()); + } catch (Exception e) { + logger.error("获取访问令牌失败", e); + throw new RuntimeException("获取访问令牌失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/DataAnalysisService.java b/src/main/java/com/xy/xyaicpzs/service/DataAnalysisService.java new file mode 100644 index 0000000..5dcf545 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DataAnalysisService.java @@ -0,0 +1,38 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.common.requset.PredictRecordQueryRequest; +import com.xy.xyaicpzs.common.response.PageResponse; +import com.xy.xyaicpzs.domain.entity.PredictRecord; +import com.xy.xyaicpzs.domain.vo.UserPredictStatVO; + +/** + * 数据分析服务接口 + */ +public interface DataAnalysisService { + + /** + * 获取用户预测统计数据 + * @param userId 用户ID + * @return 用户预测统计数据 + */ + UserPredictStatVO getUserPredictStat(Long userId); + + /** + * 处理待开奖记录,匹配开奖结果 + * @return 处理的记录数量 + */ + int processPendingPredictions(); + + /** + * 按条件查询预测记录 + * @param request 查询条件 + * @return 分页预测记录 + */ + PageResponse queryPredictRecords(PredictRecordQueryRequest request); + + /** + * 获取预测记录总数 + * @return 预测记录总数 + */ + long getTotalPredictCount(); +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/ExcelImportService.java b/src/main/java/com/xy/xyaicpzs/service/ExcelImportService.java new file mode 100644 index 0000000..b5fd6a0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/ExcelImportService.java @@ -0,0 +1,249 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.util.ExcelDataImporter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; + +/** + * Excel导入服务类 + */ +@Slf4j +@Service +public class ExcelImportService { + + @Autowired + private ExcelDataImporter excelDataImporter; + + /** + * 导入Excel文件数据 + * @param file 上传的Excel文件 + * @return 导入结果消息 + */ + public String importExcelFile(MultipartFile file) { + if (file.isEmpty()) { + return "文件为空,请选择有效的Excel文件"; + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".xlsx")) { + return "请上传.xlsx格式的Excel文件"; + } + + try { + // 创建临时文件 + File tempFile = File.createTempFile("import_", ".xlsx"); + file.transferTo(tempFile); + + // 执行数据导入 + excelDataImporter.importExcelData(tempFile.getAbsolutePath()); + + // 删除临时文件 + tempFile.delete(); + + return "数据导入成功"; + + } catch (IOException e) { + log.error("文件处理失败:{}", e.getMessage(), e); + return "文件处理失败:" + e.getMessage(); + } catch (Exception e) { + log.error("数据导入失败:{}", e.getMessage(), e); + return "数据导入失败:" + e.getMessage(); + } + } + + /** + * 导入指定路径的Excel文件 + * @param filePath 文件路径 + * @return 导入结果消息 + */ + public String importExcelFileByPath(String filePath) { + try { + File file = new File(filePath); + if (!file.exists()) { + return "文件不存在:" + filePath; + } + + if (!filePath.toLowerCase().endsWith(".xlsx")) { + return "请提供.xlsx格式的Excel文件路径"; + } + + // 执行数据导入 + excelDataImporter.importExcelData(filePath); + + return "数据导入成功"; + + } catch (Exception e) { + log.error("数据导入失败:{}", e.getMessage(), e); + return "数据导入失败:" + e.getMessage(); + } + } + + /** + * 获取Excel导入信息说明 + * @return 导入信息说明 + */ + public String getImportInfo() { + StringBuilder info = new StringBuilder(); + info.append("=== Excel数据导入说明 ===\n"); + info.append("支持的工作表:\n"); + info.append("1. T1工作表 - 红球数据(4个表:history_all, history_100, history_top, history_top_100)\n"); + info.append("2. T2工作表 - 蓝球数据(4个表:blue_history_all, blue_history_100, blue_history_top, blue_history_top_100)\n"); + info.append("3. T3工作表 - 红球线系数数据(1个表:t3,33×33=1089条记录)\n"); + info.append("4. T4工作表 - 蓝球组红球线系数数据(1个表:t4,16×33=528条记录)\n"); + info.append("5. T5工作表 - 蓝球组蓝球线系数数据(1个表:t5,16×16=256条记录)\n"); + info.append("6. T6工作表 - 红球组蓝球线系数数据(1个表:t6,33×16=528条记录)\n"); + info.append("7. T7工作表 - 红球组红球面系数数据(1个表:t7,33×33=1089条记录)\n"); + info.append("8. T8工作表 - 红球组蓝球面系数数据(1个表:t8,33×16=528条记录)\n"); + info.append("9. T10工作表 - 彩票开奖信息数据(1个表:lottery_draws,历史开奖记录)\n"); + info.append("10. T11工作表 - 蓝球组红球面系数数据(1个表:t11,16×33=528条记录)\n"); + info.append("\n数据结构:\n"); + info.append("- T1/T2:标准表格结构,按列映射到对应字段\n"); + info.append("- T3:每三列为一组,红球1-33号,线系数在C、F、I、L...列\n"); + info.append("- T4:每三列为一组,蓝球1-16号,红球1-33号,线系数在C、F、I、L...列\n"); + info.append("- T5:每三列为一组,蓝球1-16号,蓝球1-16号,线系数在C、F、I、L...列\n"); + info.append("- T6:每三列为一组,红球1-33号,蓝球1-16号,线系数在C、F、I、L...列\n"); + info.append("- T7:每三列为一组,红球1-33号,红球1-33号,面系数在C、F、I、L...列\n"); + info.append("- T8:每三列为一组,红球1-33号,蓝球1-16号,面系数在C、F、I、L...列\n"); + info.append("- T10:标准表格结构,A列开奖期号,B列开奖日期,C-H列红球1-6,I列蓝球\n"); + info.append("- T11:每三列为一组,蓝球1-16号,红球1-33号,面系数在C、F、I、L...列\n"); + info.append("\n导入特性:\n"); + info.append("- 自动清空现有数据\n"); + info.append("- 数值自动保留两位小数\n"); + info.append("- 完善的错误处理和日志记录\n"); + info.append("- 支持.xlsx和.xls格式\n"); + return info.toString(); + } + + /** + * 导入指定路径的开奖数据Excel文件 + * @param filePath 文件路径 + * @return 导入结果消息 + */ + public String importLotteryDrawsByPath(String filePath) { + try { + File file = new File(filePath); + if (!file.exists()) { + return "文件不存在:" + filePath; + } + + if (!filePath.toLowerCase().endsWith(".xlsx")) { + return "请提供.xlsx格式的Excel文件路径"; + } + + // 执行开奖数据导入 + excelDataImporter.importLotteryDrawsData(filePath); + + return "开奖数据导入成功"; + + } catch (Exception e) { + log.error("开奖数据导入失败:{}", e.getMessage(), e); + return "开奖数据导入失败:" + e.getMessage(); + } + } + + /** + * 导入上传的开奖数据Excel文件 + * @param file 上传的Excel文件 + * @return 导入结果消息 + */ + public String importLotteryDrawsFile(MultipartFile file) { + if (file.isEmpty()) { + return "文件为空,请选择有效的Excel文件"; + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".xlsx")) { + return "请上传.xlsx格式的Excel文件"; + } + + try { + // 创建临时文件 + File tempFile = File.createTempFile("lottery_import_", ".xlsx"); + file.transferTo(tempFile); + + // 执行开奖数据导入 + excelDataImporter.importLotteryDrawsData(tempFile.getAbsolutePath()); + + // 删除临时文件 + tempFile.delete(); + + return "开奖数据导入成功"; + + } catch (IOException e) { + log.error("文件处理失败:{}", e.getMessage(), e); + return "文件处理失败:" + e.getMessage(); + } catch (Exception e) { + log.error("开奖数据导入失败:{}", e.getMessage(), e); + return "开奖数据导入失败:" + e.getMessage(); + } + } + + /** + * 追加导入指定路径的开奖数据Excel文件(不清空现有数据) + * @param filePath 文件路径 + * @return 导入结果消息 + */ + public String appendLotteryDrawsByPath(String filePath) { + try { + File file = new File(filePath); + if (!file.exists()) { + return "文件不存在:" + filePath; + } + + if (!filePath.toLowerCase().endsWith(".xlsx")) { + return "请提供.xlsx格式的Excel文件路径"; + } + + // 执行开奖数据追加导入 + excelDataImporter.appendLotteryDrawsData(filePath); + + return "开奖数据追加导入成功"; + + } catch (Exception e) { + log.error("开奖数据追加导入失败:{}", e.getMessage(), e); + return "开奖数据追加导入失败:" + e.getMessage(); + } + } + + /** + * 追加导入上传的开奖数据Excel文件(不清空现有数据) + * @param file 上传的Excel文件 + * @return 导入结果消息 + */ + public String appendLotteryDrawsFile(MultipartFile file) { + if (file.isEmpty()) { + return "文件为空,请选择有效的Excel文件"; + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".xlsx")) { + return "请上传.xlsx格式的Excel文件"; + } + + try { + // 创建临时文件 + File tempFile = File.createTempFile("lottery_append_", ".xlsx"); + file.transferTo(tempFile); + + // 执行开奖数据追加导入 + excelDataImporter.appendLotteryDrawsData(tempFile.getAbsolutePath()); + + // 删除临时文件 + tempFile.delete(); + + return "开奖数据追加导入成功"; + + } catch (IOException e) { + log.error("文件处理失败:{}", e.getMessage(), e); + return "文件处理失败:" + e.getMessage(); + } catch (Exception e) { + log.error("开奖数据追加导入失败:{}", e.getMessage(), e); + return "开奖数据追加导入失败:" + e.getMessage(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/History100Service.java b/src/main/java/com/xy/xyaicpzs/service/History100Service.java new file mode 100644 index 0000000..481cabc --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/History100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.History100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【history_100(最近100期数据表)】的数据库操作Service +* @createDate 2025-06-14 09:48:05 +*/ +public interface History100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/HistoryAllService.java b/src/main/java/com/xy/xyaicpzs/service/HistoryAllService.java new file mode 100644 index 0000000..0dafcef --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/HistoryAllService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.HistoryAll; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【history_all(历史数据表)】的数据库操作Service +* @createDate 2025-06-14 09:48:10 +*/ +public interface HistoryAllService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/HistoryTop100Service.java b/src/main/java/com/xy/xyaicpzs/service/HistoryTop100Service.java new file mode 100644 index 0000000..d5bb701 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/HistoryTop100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.HistoryTop100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【history_top_100(创建100期数据排行表)】的数据库操作Service +* @createDate 2025-06-14 09:48:16 +*/ +public interface HistoryTop100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/HistoryTopService.java b/src/main/java/com/xy/xyaicpzs/service/HistoryTopService.java new file mode 100644 index 0000000..b2fc82c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/HistoryTopService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.HistoryTop; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【history_top(历史数据排行表)】的数据库操作Service +* @createDate 2025-06-14 09:48:13 +*/ +public interface HistoryTopService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/LotteryDrawsService.java b/src/main/java/com/xy/xyaicpzs/service/LotteryDrawsService.java new file mode 100644 index 0000000..512a9ad --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/LotteryDrawsService.java @@ -0,0 +1,47 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.LotteryDraws; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【lottery_draws(彩票开奖信息表)】的数据库操作Service +* @createDate 2025-06-14 16:41:29 +*/ +public interface LotteryDrawsService extends IService { + + /** + * 获取近期开奖信息 + * @param limit 获取条数,默认15条 + * @return 开奖信息列表,按开奖期号倒序排列 + */ + List getRecentDraws(Integer limit); + + /** + * 根据期号精准查询开奖信息 + * @param drawId 开奖期号 + * @return 开奖信息 + */ + LotteryDraws getByDrawId(Long drawId); + + /** + * 根据日期范围查询开奖信息 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 开奖信息列表,按开奖期号倒序排列 + */ + List getByDateRange(Date startDate, Date endDate); + + /** + * 综合查询开奖信息 + * @param drawId 开奖期号(可选) + * @param startDate 开始日期(可选) + * @param endDate 结束日期(可选) + * @return 开奖信息列表 + */ + List queryDraws(Long drawId, Date startDate, Date endDate); + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/OperationHistoryService.java b/src/main/java/com/xy/xyaicpzs/service/OperationHistoryService.java new file mode 100644 index 0000000..4ccbec0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/OperationHistoryService.java @@ -0,0 +1,41 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.OperationHistory; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【operation_history(操作历史记录表)】的数据库操作Service +* @createDate 2025-06-19 14:51:51 +*/ +public interface OperationHistoryService extends IService { + + /** + * 记录操作历史 + * @param userId 操作用户ID + * @param operationType 操作类型 + * @param operationModule 操作模块(0-会员码管理/1-Excel导入管理等) + * @param operationResult 操作结果(成功/失败) + * @param resultMessage 结果消息 + */ + void recordOperation(Long userId, String operationType, Integer operationModule, + String operationResult, String resultMessage); + + /** + * 根据用户ID和操作模块获取操作历史列表 + * @param userId 用户ID + * @param operationModule 操作模块(0-会员码管理/1-Excel导入管理等) + * @return 操作历史列表,按操作时间倒序排列 + */ + List getOperationHistoryByUserIdAndModule(Long userId, Integer operationModule); + + /** + * 根据操作模块获取操作历史列表 + * @param operationModule 操作模块(0-会员码管理/1-Excel导入管理等) + * @return 操作历史列表,按操作时间倒序排列 + */ + List getOperationHistoryByModule(Integer operationModule); + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/PredictRecordService.java b/src/main/java/com/xy/xyaicpzs/service/PredictRecordService.java new file mode 100644 index 0000000..0a356a1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/PredictRecordService.java @@ -0,0 +1,50 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.PredictRecord; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【predict_record(彩票开奖信息表)】的数据库操作Service +* @createDate 2025-06-16 13:17:53 +*/ +public interface PredictRecordService extends IService { + + /** + * 创建预测记录 + * @param userId 用户ID + * @param drawId 开奖期号 + * @param drawDate 开奖日期 + * @param redBalls 6个红球号码 + * @param blueBall 蓝球号码 + * @return 创建的预测记录 + */ + PredictRecord createPredictRecord(Long userId, Long drawId, Date drawDate, List redBalls, Integer blueBall); + + /** + * 根据用户ID获取所有预测记录 + * @param userId 用户ID + * @return 用户的所有预测记录列表,按预测时间倒序排列 + */ + List getPredictRecordsByUserId(Long userId); + + /** + * 根据用户ID分页获取预测记录 + * @param userId 用户ID + * @param page 页码,从1开始 + * @param size 每页大小 + * @return 用户的预测记录列表,按预测时间倒序排列 + */ + List getPredictRecordsByUserIdWithPaging(Long userId, Integer page, Integer size); + + /** + * 根据用户ID获取预测记录总数 + * @param userId 用户ID + * @return 用户的预测记录总数 + */ + Long getPredictRecordsCountByUserId(Long userId); + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/SmsService.java b/src/main/java/com/xy/xyaicpzs/service/SmsService.java new file mode 100644 index 0000000..5c49320 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/SmsService.java @@ -0,0 +1,25 @@ +package com.xy.xyaicpzs.service; + +/** + * 短信服务接口 + */ +public interface SmsService { + + /** + * 发送短信验证码 + * + * @param phoneNumber 手机号码 + * @return 发送是否成功 + * @throws Exception 发送异常 + */ + boolean sendVerificationCode(String phoneNumber) throws Exception; + + /** + * 验证短信验证码 + * + * @param phoneNumber 手机号码 + * @param code 验证码 + * @return 验证是否通过 + */ + boolean verifyCode(String phoneNumber, String code); +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/T11Service.java b/src/main/java/com/xy/xyaicpzs/service/T11Service.java new file mode 100644 index 0000000..7619054 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T11Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.T11; + +/** +* @author XY003 +* @description 针对表【t11(t11表(蓝球组红球的面系数))】的数据库操作Service +* @createDate 2025-06-14 16:25:23 +*/ +public interface T11Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/T3Service.java b/src/main/java/com/xy/xyaicpzs/service/T3Service.java new file mode 100644 index 0000000..5d24cdd --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T3Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.T3; + +/** +* @author XY003 +* @description 针对表【t3(t3表(红球组红球的线系数))】的数据库操作Service +* @createDate 2025-06-14 11:02:50 +*/ +public interface T3Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/T4Service.java b/src/main/java/com/xy/xyaicpzs/service/T4Service.java new file mode 100644 index 0000000..ddd0019 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T4Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.T4; + +/** +* @author XY003 +* @description 针对表【t4(t4表(蓝球组红球的线系数))】的数据库操作Service +* @createDate 2025-06-14 11:45:31 +*/ +public interface T4Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/T5Service.java b/src/main/java/com/xy/xyaicpzs/service/T5Service.java new file mode 100644 index 0000000..37a5078 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T5Service.java @@ -0,0 +1,14 @@ +package com.xy.xyaicpzs.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.T5; + +/** +* @author XY003 +* @description 针对表【t5(t5表(蓝球组蓝球的线系数))】的数据库操作Service +* @createDate 2025-06-14 12:01:16 +*/ +public interface T5Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/T6Service.java b/src/main/java/com/xy/xyaicpzs/service/T6Service.java new file mode 100644 index 0000000..8c4eef5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T6Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.T6; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【t6(t6表(红球组蓝球的线系数))】的数据库操作Service +* @createDate 2025-06-14 13:19:12 +*/ +public interface T6Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/T7Service.java b/src/main/java/com/xy/xyaicpzs/service/T7Service.java new file mode 100644 index 0000000..5f72460 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T7Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.T7; + +/** +* @author XY003 +* @description 针对表【t7(t7表(红球组红球的面系数))】的数据库操作Service +* @createDate 2025-06-14 13:30:50 +*/ +public interface T7Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/T8Service.java b/src/main/java/com/xy/xyaicpzs/service/T8Service.java new file mode 100644 index 0000000..4138022 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/T8Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.T8; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【t8(t8表(红球组蓝球的面系数))】的数据库操作Service +* @createDate 2025-06-14 16:11:27 +*/ +public interface T8Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/UserService.java b/src/main/java/com/xy/xyaicpzs/service/UserService.java new file mode 100644 index 0000000..798828e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/UserService.java @@ -0,0 +1,99 @@ +package com.xy.xyaicpzs.service; + +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 jakarta.servlet.http.HttpServletRequest; + +/** + * 用户服务 +*/ +public interface UserService extends IService { + + /** + * 用户注册 + * + * @param userAccount 用户账户 + * @param userPassword 用户密码 + * @param checkPassword 校验密码 + * @return 新用户 id + */ + long userRegister(String userAccount, String userName, String userPassword, String checkPassword); + + /** + * 用户登录 + * + * @param userAccount 用户账户 + * @param userPassword 用户密码 + * @param request + * @return 脱敏后的用户信息 + */ + User userLogin(String userAccount, String userPassword, HttpServletRequest request); + + /** + * 获取当前登录用户 + * + * @param request + * @return + */ + User getLoginUser(HttpServletRequest request); + + /** + * 用户注销 + * + * @param request + * @return + */ + boolean userLogout(HttpServletRequest request); + + /** + * 获取脱敏的用户信息 + * + * @param originUser + * @return + */ + User getSafetyUser(User originUser); + + /** + * 是否为管理员 + * + * @param request + * @return + */ + boolean isAdmin(HttpServletRequest request); + + /** + * 是否为管理员 + * + * @param user + * @return + */ + boolean isAdmin(User user); + + /** + * 手机号注册 + * + * @param userPhoneRegisterRequest 手机号注册请求 + * @return 新用户 id + */ + long userPhoneRegister(UserPhoneRegisterRequest userPhoneRegisterRequest); + + /** + * 手机号登录 + * + * @param userPhoneLoginRequest 手机号登录请求 + * @param request HTTP请求 + * @return 脱敏后的用户信息 + */ + User userPhoneLogin(UserPhoneLoginRequest userPhoneLoginRequest, HttpServletRequest request); + + /** + * 加密用户密码 + * + * @param password 原始密码 + * @return 加密后的密码 + */ + String encryptPassword(String password); +} diff --git a/src/main/java/com/xy/xyaicpzs/service/VipCodeService.java b/src/main/java/com/xy/xyaicpzs/service/VipCodeService.java new file mode 100644 index 0000000..e4ed167 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/VipCodeService.java @@ -0,0 +1,40 @@ +package com.xy.xyaicpzs.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xy.xyaicpzs.domain.entity.VipCode; + +/** +* @author XY003 +* @description 针对表【vip_code(会员码表)】的数据库操作Service +* @createDate 2025-01-15 16:41:29 +*/ +public interface VipCodeService extends IService { + + /** + * 激活会员码 + * @param userId 用户ID + * @param code 会员码 + * @return 是否激活成功 + */ + boolean activateVipCode(Long userId, String code); + + /** + * 批量生成会员码 + * @param numCodes 生成数量 + * @param vipExpireTime 会员有效月数 + * @param createdUserId 创建人ID + * @param createdUserName 创建人名称 + * @return 生成成功的数量 + */ + int generateVipCodes(int numCodes, int vipExpireTime, Long createdUserId, String createdUserName); + + /** + * 获取一个可用的会员码 + * @param vipExpireTime 会员有效月数(1或12) + * @param createdUserId 创建人ID + * @param createdUserName 创建人名称 + * @return 可用的会员码,如果没有则返回null + */ + String getAvailableVipCode(int vipExpireTime, Long createdUserId, String createdUserName); + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/VipExchangeRecordService.java b/src/main/java/com/xy/xyaicpzs/service/VipExchangeRecordService.java new file mode 100644 index 0000000..20a8e3c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/VipExchangeRecordService.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.VipExchangeRecord; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【vip_exchange_record(会员兑换表)】的数据库操作Service +* @createDate 2025-06-19 11:27:10 +*/ +public interface VipExchangeRecordService extends IService { + + /** + * 根据用户ID获取所有兑换记录 + * @param userId 用户ID + * @return 用户的所有兑换记录列表,按兑换时间倒序排列 + */ + List getExchangeRecordsByUserId(Long userId); + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistory100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistory100ServiceImpl.java new file mode 100644 index 0000000..84e35f8 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistory100ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.BlueHistory100; +import com.xy.xyaicpzs.mapper.BlueHistory100Mapper; +import com.xy.xyaicpzs.service.BlueHistory100Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【blue_history_100(蓝球最近100期历史数据表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class BlueHistory100ServiceImpl extends ServiceImpl + implements BlueHistory100Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryAllServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryAllServiceImpl.java new file mode 100644 index 0000000..5292b3a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryAllServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.BlueHistoryAll; +import com.xy.xyaicpzs.mapper.BlueHistoryAllMapper; +import com.xy.xyaicpzs.service.BlueHistoryAllService; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【blue_history_all(蓝球历史数据表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class BlueHistoryAllServiceImpl extends ServiceImpl + implements BlueHistoryAllService{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryTop100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryTop100ServiceImpl.java new file mode 100644 index 0000000..6d29967 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryTop100ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop100; +import com.xy.xyaicpzs.mapper.BlueHistoryTop100Mapper; +import com.xy.xyaicpzs.service.BlueHistoryTop100Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【blue_history_top_100(蓝球100期数据排行表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class BlueHistoryTop100ServiceImpl extends ServiceImpl + implements BlueHistoryTop100Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryTopServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryTopServiceImpl.java new file mode 100644 index 0000000..5a8529f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/BlueHistoryTopServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop; +import com.xy.xyaicpzs.mapper.BlueHistoryTopMapper; +import com.xy.xyaicpzs.service.BlueHistoryTopService; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【blue_history_top(蓝球历史数据排行表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class BlueHistoryTopServiceImpl extends ServiceImpl + implements BlueHistoryTopService{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/ChatMessageServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/ChatMessageServiceImpl.java new file mode 100644 index 0000000..62b901e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/ChatMessageServiceImpl.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.ChatMessage; +import com.xy.xyaicpzs.service.ChatMessageService; +import com.xy.xyaicpzs.mapper.ChatMessageMapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【chat_message(聊天消息表)】的数据库操作Service实现 +* @createDate 2025-07-07 17:37:15 +*/ +@Service +public class ChatMessageServiceImpl extends ServiceImpl + implements ChatMessageService{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java new file mode 100644 index 0000000..4ce210a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java @@ -0,0 +1,217 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xy.xyaicpzs.common.requset.PredictRecordQueryRequest; +import com.xy.xyaicpzs.common.response.PageResponse; +import com.xy.xyaicpzs.domain.entity.LotteryDraws; +import com.xy.xyaicpzs.domain.entity.PredictRecord; +import com.xy.xyaicpzs.domain.vo.UserPredictStatVO; +import com.xy.xyaicpzs.mapper.LotteryDrawsMapper; +import com.xy.xyaicpzs.mapper.PredictRecordMapper; +import com.xy.xyaicpzs.service.DataAnalysisService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 数据分析服务实现类 + */ +@Service +public class DataAnalysisServiceImpl implements DataAnalysisService { + + @Autowired + private PredictRecordMapper predictRecordMapper; + + @Autowired + private LotteryDrawsMapper lotteryDrawsMapper; + + @Override + public UserPredictStatVO getUserPredictStat(Long userId) { + UserPredictStatVO statVO = new UserPredictStatVO(); + + // 查询用户的所有预测记录 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId); + List predictRecords = predictRecordMapper.selectList(queryWrapper); + + if (predictRecords == null || predictRecords.isEmpty()) { + statVO.setUserId(userId); + statVO.setPredictCount(0L); + statVO.setHitCount(0L); + statVO.setPendingCount(0L); + statVO.setDrawnCount(0L); + statVO.setHitRate(java.math.BigDecimal.ZERO); + return statVO; + } + + // 统计数据 + long totalPredicts = predictRecords.size(); + long hitCount = 0L; + long pendingCount = 0L; + long drawnCount = 0L; + + for (PredictRecord record : predictRecords) { + if ("待开奖".equals(record.getPredictStatus())) { + pendingCount++; + } else if ("已中奖".equals(record.getPredictStatus())) { + hitCount++; + drawnCount++; + } else if ("未中奖".equals(record.getPredictStatus())) { + drawnCount++; + } + } + + // 计算命中率 + java.math.BigDecimal hitRate = drawnCount > 0 ? + java.math.BigDecimal.valueOf(hitCount).divide(java.math.BigDecimal.valueOf(drawnCount), 4, java.math.RoundingMode.HALF_UP) : + java.math.BigDecimal.ZERO; + + statVO.setUserId(userId); + statVO.setPredictCount(totalPredicts); + statVO.setHitCount(hitCount); + statVO.setPendingCount(pendingCount); + statVO.setDrawnCount(drawnCount); + statVO.setHitRate(hitRate); + + return statVO; + } + + @Override + public int processPendingPredictions() { + // 查询所有待开奖的预测记录 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("predictStatus", "待开奖"); + List pendingRecords = predictRecordMapper.selectList(queryWrapper); + + int processedCount = 0; + + for (PredictRecord record : pendingRecords) { + // 查询对应期号的开奖结果 + LotteryDraws draws = lotteryDrawsMapper.selectById(record.getDrawId()); + if (draws != null) { + // 比较预测号码与开奖号码,计算中奖结果 + String result = calculatePredictResult(record, draws); + long bonus = calculateBonus(result); + if(result.equals("未中奖")){ + record.setPredictStatus("未中奖"); + }else{ + record.setPredictStatus("已中奖"); + } + + // 更新预测记录 + + record.setPredictResult(result); + record.setBonus(bonus); + predictRecordMapper.updateById(record); + + processedCount++; + } + } + + return processedCount; + } + + @Override + public PageResponse queryPredictRecords(PredictRecordQueryRequest request) { + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + + // 添加用户ID筛选条件 + if (request.getUserId() != null) { + queryWrapper.eq("userId", request.getUserId()); + } + + // 添加预测状态筛选条件 + if (StringUtils.hasText(request.getPredictStatus())) { + queryWrapper.eq("predictStatus", request.getPredictStatus()); + } + + // 按预测时间降序排序 + queryWrapper.orderByDesc("predictTime"); + + // 执行分页查询 + Page page = new Page<>(request.getCurrent(), request.getPageSize()); + Page resultPage = predictRecordMapper.selectPage(page, queryWrapper); + + // 构建分页响应对象 + return PageResponse.of( + resultPage.getRecords(), + resultPage.getTotal(), + (int) resultPage.getCurrent(), + (int) resultPage.getSize() + ); + } + + @Override + public long getTotalPredictCount() { + // 获取全部预测记录数量 + return predictRecordMapper.selectCount(null); + } + + /** + * 计算预测结果 + */ + private String calculatePredictResult(PredictRecord record, LotteryDraws draws) { + // 比较红球 + int redMatches = 0; + Integer[] predictReds = {record.getRedBall1(), record.getRedBall2(), record.getRedBall3(), + record.getRedBall4(), record.getRedBall5(), record.getRedBall6()}; + Integer[] drawReds = {draws.getRedBall1(), draws.getRedBall2(), draws.getRedBall3(), + draws.getRedBall4(), draws.getRedBall5(), draws.getRedBall6()}; + + for (Integer predictRed : predictReds) { + for (Integer drawRed : drawReds) { + if (predictRed != null && predictRed.equals(drawRed)) { + redMatches++; + break; + } + } + } + + // 比较蓝球 + boolean blueMatch = record.getBlueBall() != null && + record.getBlueBall().equals(draws.getBlueBall()); + + // 根据中奖规则判断奖项 + if (redMatches == 6 && blueMatch) { + return "一等奖"; + } else if (redMatches == 6) { + return "二等奖"; + } else if (redMatches == 5 && blueMatch) { + return "三等奖"; + } else if ((redMatches == 5) || (redMatches == 4 && blueMatch)) { + return "四等奖"; + } else if ((redMatches == 4) || (redMatches == 3 && blueMatch)) { + return "五等奖"; + } else if (blueMatch && (redMatches == 0 || redMatches == 1 || redMatches == 2)) { + return "六等奖"; + } else { + return "未中奖"; + } + } + + /** + * 计算奖金 + */ + private long calculateBonus(String result) { + switch (result) { + case "一等奖": + return 5000000L; // 500万 + case "二等奖": + return 1000000L; // 100万 + case "三等奖": + return 3000L; // 3000元 + case "四等奖": + return 200L; // 200元 + case "五等奖": + return 10L; // 10元 + case "六等奖": + return 5L; // 5元 + default: + return 0L; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/History100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/History100ServiceImpl.java new file mode 100644 index 0000000..41f3860 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/History100ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.History100; +import com.xy.xyaicpzs.mapper.History100Mapper; +import com.xy.xyaicpzs.service.History100Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【history_100(最近100期历史数据表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class History100ServiceImpl extends ServiceImpl + implements History100Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/HistoryAllServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/HistoryAllServiceImpl.java new file mode 100644 index 0000000..e319f24 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/HistoryAllServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.HistoryAll; +import com.xy.xyaicpzs.mapper.HistoryAllMapper; +import com.xy.xyaicpzs.service.HistoryAllService; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【history_all(历史数据表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class HistoryAllServiceImpl extends ServiceImpl + implements HistoryAllService{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/HistoryTop100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/HistoryTop100ServiceImpl.java new file mode 100644 index 0000000..6a219d6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/HistoryTop100ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.HistoryTop100; +import com.xy.xyaicpzs.mapper.HistoryTop100Mapper; +import com.xy.xyaicpzs.service.HistoryTop100Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【history_top_100(100期数据排行表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class HistoryTop100ServiceImpl extends ServiceImpl + implements HistoryTop100Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/HistoryTopServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/HistoryTopServiceImpl.java new file mode 100644 index 0000000..48594f3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/HistoryTopServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.HistoryTop; +import com.xy.xyaicpzs.mapper.HistoryTopMapper; +import com.xy.xyaicpzs.service.HistoryTopService; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【history_top(历史数据排行表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class HistoryTopServiceImpl extends ServiceImpl + implements HistoryTopService{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/LotteryDrawsServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/LotteryDrawsServiceImpl.java new file mode 100644 index 0000000..c460dfb --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/LotteryDrawsServiceImpl.java @@ -0,0 +1,65 @@ +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.LotteryDraws; +import com.xy.xyaicpzs.mapper.LotteryDrawsMapper; +import com.xy.xyaicpzs.service.LotteryDrawsService; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【lottery_draws(开奖结果表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class LotteryDrawsServiceImpl extends ServiceImpl + implements LotteryDrawsService{ + + @Override + public List getRecentDraws(Integer limit) { + if (limit == null || limit <= 0) { + limit = 15; + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByDesc("drawId").last("LIMIT " + limit); + return list(queryWrapper); + } + + @Override + public LotteryDraws getByDrawId(Long drawId) { + return getById(drawId); + } + + @Override + public List getByDateRange(Date startDate, Date endDate) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (startDate != null) { + queryWrapper.ge("drawDate", startDate); + } + if (endDate != null) { + queryWrapper.le("drawDate", endDate); + } + queryWrapper.orderByDesc("drawId"); + return list(queryWrapper); + } + + @Override + public List queryDraws(Long drawId, Date startDate, Date endDate) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (drawId != null) { + queryWrapper.eq("drawId", drawId); + } + if (startDate != null) { + queryWrapper.ge("drawDate", startDate); + } + if (endDate != null) { + queryWrapper.le("drawDate", endDate); + } + queryWrapper.orderByDesc("drawId"); + return list(queryWrapper); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/OperationHistoryServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/OperationHistoryServiceImpl.java new file mode 100644 index 0000000..3353709 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/OperationHistoryServiceImpl.java @@ -0,0 +1,67 @@ +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.OperationHistory; +import com.xy.xyaicpzs.service.OperationHistoryService; +import com.xy.xyaicpzs.mapper.OperationHistoryMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【operation_history(操作历史记录表)】的数据库操作Service实现 +* @createDate 2025-06-19 14:51:51 +*/ +@Service +@Slf4j +public class OperationHistoryServiceImpl extends ServiceImpl + implements OperationHistoryService{ + + @Override + public void recordOperation(Long userId, String operationType, Integer operationModule, + String operationResult, String resultMessage) { + try { + OperationHistory operationHistory = new OperationHistory(); + operationHistory.setUserId(userId); + operationHistory.setOperationType(operationType); + operationHistory.setOperationModule(operationModule); + operationHistory.setOperationResult(operationResult); + operationHistory.setResultMessage(resultMessage); + operationHistory.setOperationTime(new Date()); + operationHistory.setUpdateTime(new Date()); + + save(operationHistory); + + log.info("操作历史记录成功 - 用户ID: {}, 操作类型: {}, 操作模块: {}, 操作结果: {}", + userId, operationType, operationModule, operationResult); + } catch (Exception e) { + log.error("记录操作历史失败 - 用户ID: {}, 操作类型: {}, 错误信息: {}", + userId, operationType, e.getMessage(), e); + } + } + + @Override + public List getOperationHistoryByUserIdAndModule(Long userId, Integer operationModule) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .eq("operationModule", operationModule) + .orderByDesc("operationTime"); + return list(queryWrapper); + } + + @Override + public List getOperationHistoryByModule(Integer operationModule) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("operationModule", operationModule) + .orderByDesc("operationTime"); + return list(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 new file mode 100644 index 0000000..a684d83 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/PredictRecordServiceImpl.java @@ -0,0 +1,77 @@ +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.PredictRecord; +import com.xy.xyaicpzs.mapper.PredictRecordMapper; +import com.xy.xyaicpzs.service.PredictRecordService; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【predict_record(彩票开奖信息表)】的数据库操作Service实现 +* @createDate 2025-06-16 13:17:53 +*/ +@Service +public class PredictRecordServiceImpl extends ServiceImpl + implements PredictRecordService{ + + @Override + public PredictRecord createPredictRecord(Long userId, Long drawId, Date drawDate, List redBalls, Integer blueBall) { + PredictRecord predictRecord = new PredictRecord(); + predictRecord.setUserId(userId); + predictRecord.setDrawId(drawId); + predictRecord.setDrawDate(drawDate); + predictRecord.setPredictTime(new Date()); + + // 设置红球号码 + if (redBalls != null && redBalls.size() >= 6) { + predictRecord.setRedBall1(redBalls.get(0)); + predictRecord.setRedBall2(redBalls.get(1)); + predictRecord.setRedBall3(redBalls.get(2)); + predictRecord.setRedBall4(redBalls.get(3)); + predictRecord.setRedBall5(redBalls.get(4)); + predictRecord.setRedBall6(redBalls.get(5)); + } + + // 设置蓝球号码 + if (blueBall != null) { + predictRecord.setBlueBall(blueBall); + } + + // 默认状态为待开奖 + predictRecord.setPredictStatus("待开奖"); + + save(predictRecord); + return predictRecord; + } + + @Override + public List getPredictRecordsByUserId(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId).orderByDesc("predictTime"); + return list(queryWrapper); + } + + @Override + public List getPredictRecordsByUserIdWithPaging(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 getPredictRecordsCountByUserId(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId); + return count(queryWrapper); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/SmsServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/SmsServiceImpl.java new file mode 100644 index 0000000..5ba5f74 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/SmsServiceImpl.java @@ -0,0 +1,153 @@ +package com.xy.xyaicpzs.service.impl; + +import com.aliyun.dysmsapi20170525.Client; +import com.aliyun.dysmsapi20170525.models.SendSmsRequest; +import com.aliyun.tea.TeaException; +import com.aliyun.teautil.models.RuntimeOptions; +import com.xy.xyaicpzs.service.SmsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * 短信服务实现类 + */ +@Service +public class SmsServiceImpl implements SmsService { + + private static final Logger logger = LoggerFactory.getLogger(SmsServiceImpl.class); + + @Value("${aliyun.sms.sign-name:西安精彩数据服务社}") + private String signName; + + @Value("${aliyun.sms.template-code:SMS_489840017}") + private String templateCode; + + @Value("${aliyun.sms.access-key-id}") + private String accessKeyId; + + @Value("${aliyun.sms.access-key-secret}") + private String accessKeySecret; + + @Autowired + private RedisTemplate redisTemplate; + + // 短信验证码Redis前缀 + private static final String SMS_CODE_PREFIX = "sms:code:"; + // 短信发送次数Redis前缀 + private static final String SMS_COUNT_PREFIX = "sms:count:"; + // 短信验证码有效期(分钟) + private static final long SMS_CODE_EXPIRE = 5; + // 每天最大发送次数 + private static final int MAX_SMS_COUNT_PER_DAY = 10; + + /** + * 创建阿里云短信客户端 + */ + private Client createSmsClient() throws Exception { + // 从配置文件中获取阿里云AccessKey配置 + com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config(); + config.accessKeyId = accessKeyId; + config.accessKeySecret = accessKeySecret; + config.endpoint = "dysmsapi.aliyuncs.com"; + return new Client(config); + } + + /** + * 生成6位随机数字验证码 + */ + private String generateVerificationCode() { + Random random = new Random(); + StringBuilder code = new StringBuilder(); + for (int i = 0; i < 6; i++) { + code.append(random.nextInt(10)); + } + return code.toString(); + } + + /** + * 获取当天结束时间的剩余秒数 + */ + private long getSecondsUntilEndOfDay() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59); + Duration duration = Duration.between(now, endOfDay); + return duration.getSeconds(); + } + + @Override + public boolean sendVerificationCode(String phoneNumber) throws Exception { + // 检查当天发送次数是否达到上限 + String countKey = SMS_COUNT_PREFIX + phoneNumber + ":" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + Integer count = (Integer) redisTemplate.opsForValue().get(countKey); + + if (count != null && count >= MAX_SMS_COUNT_PER_DAY) { + logger.warn("手机号{}今日短信发送次数已达上限: {}", phoneNumber, MAX_SMS_COUNT_PER_DAY); + return false; + } + + // 生成6位随机验证码 + String verificationCode = generateVerificationCode(); + + // 构建短信请求 + Client client = createSmsClient(); + SendSmsRequest sendSmsRequest = new SendSmsRequest() + .setSignName(signName) + .setTemplateCode(templateCode) + .setPhoneNumbers(phoneNumber) + .setTemplateParam("{\"code\":\"" + verificationCode + "\"}"); + + RuntimeOptions runtime = new RuntimeOptions(); + try { + // 发送短信 + client.sendSmsWithOptions(sendSmsRequest, runtime); + logger.info("短信验证码发送成功,手机号: {}", phoneNumber); + + // 将验证码保存到Redis,设置过期时间 + String codeKey = SMS_CODE_PREFIX + phoneNumber; + redisTemplate.opsForValue().set(codeKey, verificationCode, SMS_CODE_EXPIRE, TimeUnit.MINUTES); + + // 增加当天发送次数,并设置过期时间为当天结束 + if (count == null) { + count = 0; + } + redisTemplate.opsForValue().set(countKey, count + 1, getSecondsUntilEndOfDay(), TimeUnit.SECONDS); + + return true; + } catch (TeaException error) { + logger.error("短信发送失败, 手机号: {}, 错误信息: {}, 诊断信息: {}", + phoneNumber, error.getMessage(), error.getData().get("Recommend")); + return false; + } catch (Exception error) { + logger.error("短信发送异常, 手机号: {}", phoneNumber, error); + return false; + } + } + + @Override + public boolean verifyCode(String phoneNumber, String code) { + if (phoneNumber == null || code == null) { + return false; + } + + String codeKey = SMS_CODE_PREFIX + phoneNumber; + String savedCode = (String) redisTemplate.opsForValue().get(codeKey); + + if (savedCode != null && savedCode.equals(code)) { + // 验证成功后删除验证码 + redisTemplate.delete(codeKey); + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T11ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T11ServiceImpl.java new file mode 100644 index 0000000..c6a7e5c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T11ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T11; +import com.xy.xyaicpzs.mapper.T11Mapper; +import com.xy.xyaicpzs.service.T11Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t11(蓝球组蓝球面系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T11ServiceImpl extends ServiceImpl + implements T11Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T3ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T3ServiceImpl.java new file mode 100644 index 0000000..0e57ef0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T3ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T3; +import com.xy.xyaicpzs.mapper.T3Mapper; +import com.xy.xyaicpzs.service.T3Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t3(红球线系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T3ServiceImpl extends ServiceImpl + implements T3Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T4ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T4ServiceImpl.java new file mode 100644 index 0000000..631e6e7 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T4ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T4; +import com.xy.xyaicpzs.mapper.T4Mapper; +import com.xy.xyaicpzs.service.T4Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t4(蓝球组红球线系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T4ServiceImpl extends ServiceImpl + implements T4Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T5ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T5ServiceImpl.java new file mode 100644 index 0000000..0910d26 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T5ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T5; +import com.xy.xyaicpzs.mapper.T5Mapper; +import com.xy.xyaicpzs.service.T5Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t5(蓝球组蓝球线系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T5ServiceImpl extends ServiceImpl + implements T5Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T6ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T6ServiceImpl.java new file mode 100644 index 0000000..d67fb30 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T6ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T6; +import com.xy.xyaicpzs.mapper.T6Mapper; +import com.xy.xyaicpzs.service.T6Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t6(红球组蓝球线系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T6ServiceImpl extends ServiceImpl + implements T6Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T7ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T7ServiceImpl.java new file mode 100644 index 0000000..57c4106 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T7ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T7; +import com.xy.xyaicpzs.mapper.T7Mapper; +import com.xy.xyaicpzs.service.T7Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t7(红球组红球面系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T7ServiceImpl extends ServiceImpl + implements T7Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/T8ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/T8ServiceImpl.java new file mode 100644 index 0000000..0b55f98 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/T8ServiceImpl.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.T8; +import com.xy.xyaicpzs.mapper.T8Mapper; +import com.xy.xyaicpzs.service.T8Service; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【t8(红球组蓝球面系数表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +public class T8ServiceImpl extends ServiceImpl + implements T8Service{ + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/UserServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..45f779f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/UserServiceImpl.java @@ -0,0 +1,332 @@ +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.common.ErrorCode; +import com.xy.xyaicpzs.constant.UserConstant; +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.exception.BusinessException; +import com.xy.xyaicpzs.mapper.UserMapper; +import com.xy.xyaicpzs.service.SmsService; +import com.xy.xyaicpzs.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** +* @author XY003 +* @description 针对表【user(用户表)】的数据库操作Service实现 +* @createDate 2025-06-14 09:48:10 +*/ +@Service +@Slf4j +public class UserServiceImpl extends ServiceImpl + implements UserService{ + + /** + * 盐值,混淆密码 + */ + private static final String SALT = "xy"; + + @Autowired + private SmsService smsService; + + @Override + public long userRegister(String userAccount, String userName, String userPassword, String checkPassword) { + // 1. 校验 + if (StringUtils.isAnyBlank(userAccount, userName, userPassword, checkPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); + } + if (userAccount.length() < 4) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短"); + } + if (userName.length() > 40) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户名过长"); + } + if (userPassword.length() < 8 || checkPassword.length() < 8) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短"); + } + + // 密码和校验密码相同 + if (!userPassword.equals(checkPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致"); + } + + synchronized (userAccount.intern()) { + // 账户不能重复 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userAccount", userAccount); + long count = this.baseMapper.selectCount(queryWrapper); + if (count > 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复"); + } + + // 2. 加密 + String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); + + // 3. 插入数据 + User user = new User(); + user.setUserAccount(userAccount); + user.setUserName(userName); + user.setUserPassword(encryptPassword); + user.setCreateTime(new Date()); + user.setUpdateTime(new Date()); + // 设置为VIP用户,有效期10天 + user.setIsVip(1); + Date vipExpireDate = new Date(System.currentTimeMillis() + 10L * 24 * 60 * 60 * 1000); + user.setVipExpire(vipExpireDate); + boolean saveResult = this.save(user); + if (!saveResult) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误"); + } + return user.getId(); + } + } + + @Override + public User userLogin(String userAccount, String userPassword, HttpServletRequest request) { + // 1. 校验 + if (StringUtils.isAnyBlank(userAccount, userPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); + } + if (userAccount.length() < 4) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误"); + } + if (userPassword.length() < 8) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误"); + } + + // 2. 加密 + String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); + + // 查询用户是否存在 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userAccount", userAccount); + queryWrapper.eq("userPassword", encryptPassword); + User user = this.baseMapper.selectOne(queryWrapper); + + // 用户不存在 + if (user == null) { + log.info("user login failed, userAccount cannot match userPassword"); + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误"); + } + + // 3. 记录用户的登录态 + request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); + return getSafetyUser(user); + } + + /** + * 获取当前登录用户 + * + * @param request + * @return + */ + @Override + public User getLoginUser(HttpServletRequest request) { + // 先判断是否已登录 + Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE); + User currentUser = (User) userObj; + if (currentUser == null || currentUser.getId() == null) { + throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); + } + // 从数据库查询(追求性能的话可以注释,直接走缓存) + long userId = currentUser.getId(); + currentUser = this.getById(userId); + if (currentUser == null) { + throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); + } + return currentUser; + } + + /** + * 用户注销 + * + * @param request + */ + @Override + public boolean userLogout(HttpServletRequest request) { + if (request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE) == null) { + throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录"); + } + // 移除登录态 + request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE); + return true; + } + + @Override + public User getSafetyUser(User originUser) { + if (originUser == null) { + return null; + } + User safetyUser = new User(); + safetyUser.setId(originUser.getId()); + safetyUser.setUserName(originUser.getUserName()); + safetyUser.setUserAccount(originUser.getUserAccount()); + safetyUser.setUserAvatar(originUser.getUserAvatar()); + safetyUser.setUserRole(originUser.getUserRole()); + safetyUser.setCreateTime(originUser.getCreateTime()); + safetyUser.setUpdateTime(originUser.getUpdateTime()); + return safetyUser; + } + + @Override + public boolean isAdmin(HttpServletRequest request) { + // 仅管理员可查询 + Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE); + User user = (User) userObj; + return isAdmin(user); + } + + @Override + public boolean isAdmin(User user) { + return user != null && (UserConstant.ADMIN_ROLE.equals(user.getUserRole()) + || UserConstant.SUPER_ADMIN_ROLE.equals(user.getUserRole())); + } + + @Override + public long userPhoneRegister(UserPhoneRegisterRequest userPhoneRegisterRequest) { + if (userPhoneRegisterRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空"); + } + + String userAccount = userPhoneRegisterRequest.getUserAccount(); + String userPassword = userPhoneRegisterRequest.getUserPassword(); + String checkPassword = userPhoneRegisterRequest.getCheckPassword(); + String phone = userPhoneRegisterRequest.getPhone(); + String code = userPhoneRegisterRequest.getCode(); + String userName = userPhoneRegisterRequest.getUserName(); + + // 1. 校验 + if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, phone, code)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); + } + // 用户名可以为空,如果为空则默认使用账号 + if (StringUtils.isBlank(userName)) { + userName = userAccount; + } + if (userAccount.length() < 4) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短"); + } + if (userPassword.length() < 8 || checkPassword.length() < 8) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短"); + } + + // 密码和校验密码相同 + if (!userPassword.equals(checkPassword)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致"); + } + + // 验证手机号格式 + String phoneRegex = "^1[3-9]\\d{9}$"; + if (!phone.matches(phoneRegex)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式错误"); + } + + // 验证短信验证码 + boolean isCodeValid = smsService.verifyCode(phone, code); + if (!isCodeValid) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误或已过期"); + } + + synchronized (userAccount.intern()) { + // 账户不能重复 + QueryWrapper accountQueryWrapper = new QueryWrapper<>(); + accountQueryWrapper.eq("userAccount", userAccount); + long accountCount = this.baseMapper.selectCount(accountQueryWrapper); + if (accountCount > 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号已存在"); + } + + // 手机号不能重复 + QueryWrapper phoneQueryWrapper = new QueryWrapper<>(); + phoneQueryWrapper.eq("phone", phone); + long phoneCount = this.baseMapper.selectCount(phoneQueryWrapper); + if (phoneCount > 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号已注册"); + } + + // 2. 加密 + String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); + + // 3. 插入数据 + User user = new User(); + user.setUserAccount(userAccount); + user.setUserPassword(encryptPassword); + user.setPhone(phone); + // 设置用户名 + user.setUserName(userName); + user.setCreateTime(new Date()); + user.setUpdateTime(new Date()); + // 设置为VIP用户,有效期10天 + user.setIsVip(0); + Date vipExpireDate = new Date(System.currentTimeMillis() + 10L * 24 * 60 * 60 * 1000); + user.setVipExpire(vipExpireDate); + + boolean saveResult = this.save(user); + if (!saveResult) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误"); + } + return user.getId(); + } + } + + @Override + public User userPhoneLogin(UserPhoneLoginRequest userPhoneLoginRequest, HttpServletRequest request) { + if (userPhoneLoginRequest == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空"); + } + + String phone = userPhoneLoginRequest.getPhone(); + String code = userPhoneLoginRequest.getCode(); + + // 1. 校验参数 + if (StringUtils.isAnyBlank(phone, code)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); + } + + // 验证手机号格式 + String phoneRegex = "^1[3-9]\\d{9}$"; + if (!phone.matches(phoneRegex)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式错误"); + } + + // 验证短信验证码 + boolean isCodeValid = smsService.verifyCode(phone, code); + if (!isCodeValid) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "验证码错误或已过期"); + } + + // 查询用户是否存在 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("phone", phone); + User user = this.baseMapper.selectOne(queryWrapper); + + // 用户不存在 + if (user == null) { + log.info("user login failed, phone number not registered"); + throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号未注册"); + } + + // 3. 记录用户的登录态 + request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); + return getSafetyUser(user); + } + + @Override + public String encryptPassword(String password) { + if (StringUtils.isBlank(password)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码不能为空"); + } + return DigestUtils.md5DigestAsHex((SALT + password).getBytes()); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/VipCodeServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/VipCodeServiceImpl.java new file mode 100644 index 0000000..ed5c68f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/VipCodeServiceImpl.java @@ -0,0 +1,273 @@ +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.User; +import com.xy.xyaicpzs.domain.entity.VipCode; +import com.xy.xyaicpzs.domain.entity.VipExchangeRecord; +import com.xy.xyaicpzs.mapper.UserMapper; +import com.xy.xyaicpzs.mapper.VipCodeMapper; +import com.xy.xyaicpzs.service.VipCodeService; +import com.xy.xyaicpzs.service.VipExchangeRecordService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.UUID; + +/** + * @author XY003 + * @description 针对表【vip_code(会员码表)】的数据库操作Service实现 + * @createDate 2025-01-15 16:41:29 + */ +@Slf4j +@Service +public class VipCodeServiceImpl extends ServiceImpl implements VipCodeService { + + @Autowired + private VipCodeMapper vipCodeMapper; + + @Autowired + private UserMapper userMapper; + + @Autowired + private VipExchangeRecordService vipExchangeRecordService; + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean activateVipCode(Long userId, String code) { + log.info("开始激活会员码,用户ID:{},会员码:{}", userId, code); + + // 1. 校验参数 + if (userId == null || code == null || code.trim().isEmpty()) { + throw new IllegalArgumentException("用户ID和会员码不能为空"); + } + + // 2. 查询会员码信息 + QueryWrapper vipCodeQuery = new QueryWrapper<>(); + vipCodeQuery.eq("code", code); + VipCode vipCode = vipCodeMapper.selectOne(vipCodeQuery); + + if (vipCode == null) { + throw new IllegalArgumentException("会员码不存在"); + } + + if (vipCode.getIsUse() == 1) { + throw new IllegalArgumentException("会员码已被使用"); + } + + // 3. 查询用户信息 + User user = userMapper.selectById(userId); + if (user == null) { + throw new IllegalArgumentException("用户不存在"); + } + + // 4. 根据会员码的vipExpireTime判断会员类型 + String memberType; + int orderAmount; + if (vipCode.getVipExpireTime() == 1) { + memberType = "月度会员"; + orderAmount = 10; + } else if (vipCode.getVipExpireTime() == 12) { + memberType = "年度会员"; + orderAmount = 100; + } else { + throw new IllegalArgumentException("无效的会员有效期:" + vipCode.getVipExpireTime()); + } + + // 5. 先在vip_exchange_record表插入兑换记录 + VipExchangeRecord exchangeRecord = new VipExchangeRecord(); + exchangeRecord.setUserId(userId); + exchangeRecord.setType(memberType); + exchangeRecord.setExchangeMode(1); // 1表示会员码兑换 + //生成随机16位订单号 + exchangeRecord.setOrderNo(generateOrderNumber()); + exchangeRecord.setOrderAmount(orderAmount); + exchangeRecord.setIsUse(1); // 1表示已兑换 + exchangeRecord.setExchangeTime(new Date()); + exchangeRecord.setUpdateTime(new Date()); + + boolean recordSaved = vipExchangeRecordService.save(exchangeRecord); + if (!recordSaved) { + log.error("保存兑换记录失败,用户ID:{},会员码:{}", userId, code); + throw new RuntimeException("保存兑换记录失败"); + } + + log.info("成功插入兑换记录,用户ID:{},会员类型:{}", userId, memberType); + + // 6. 计算新的会员到期时间 + Date newVipExpire = calculateNewVipExpire(user.getVipExpire(), vipCode.getVipExpireTime()); + + // 7. 更新用户会员状态 + User updateUser = new User(); + updateUser.setId(userId); + updateUser.setIsVip(1); // 设置为会员 + updateUser.setVipExpire(newVipExpire); + updateUser.setUpdateTime(new Date()); + + int userUpdateResult = userMapper.updateById(updateUser); + if (userUpdateResult == 0) { + log.error("更新用户会员状态失败,用户ID:{}", userId); + throw new RuntimeException("更新用户会员状态失败"); + } + + // 8. 标记会员码为已使用,并记录使用人信息和使用时间 + Date now = new Date(); + VipCode updateVipCode = new VipCode(); + updateVipCode.setId(vipCode.getId()); + updateVipCode.setIsUse(1); + updateVipCode.setUpdateTime(now); + // 添加使用人ID、使用人名称 + updateVipCode.setUsedUserId(userId); + updateVipCode.setUsedUserName(user.getUserName()); + + int vipCodeUpdateResult = vipCodeMapper.updateById(updateVipCode); + if (vipCodeUpdateResult == 0) { + log.error("更新会员码状态失败,会员码:{}", code); + throw new RuntimeException("更新会员码状态失败"); + } + + log.info("会员码激活成功,用户ID:{},会员类型:{},新的到期时间:{}", userId, memberType, newVipExpire); + return true; + } + + /** + * 计算新的会员到期时间 + * @param currentVipExpire 当前会员到期时间 + * @param vipExpireTime 会员有效月数 + * @return 新的会员到期时间 + */ + private Date calculateNewVipExpire(Date currentVipExpire, int vipExpireTime) { + LocalDate baseDate; + + // 如果当前已经是会员且未过期,则从当前到期时间开始延长 + if (currentVipExpire != null) { + LocalDate currentExpireDate = currentVipExpire.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate today = LocalDate.now(); + + if (currentExpireDate.isAfter(today)) { + // 当前会员未过期,从到期时间开始延长 + baseDate = currentExpireDate; + } else { + // 当前会员已过期,从今天开始计算 + baseDate = today; + } + } else { + // 从未开通过会员,从今天开始计算 + baseDate = LocalDate.now(); + } + + // 增加对应的月数 + LocalDate newExpireDate = baseDate.plusMonths(vipExpireTime); + + return Date.from(newExpireDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + @Override + public int generateVipCodes(int numCodes, int vipExpireTime, Long createdUserId, String createdUserName) { + log.info("开始生成会员码,数量:{},有效月数:{},创建人ID:{}", numCodes, vipExpireTime, createdUserId); + + if (numCodes <= 0) { + throw new IllegalArgumentException("生成数量必须大于0"); + } + + if (vipExpireTime != 1 && vipExpireTime != 12) { + throw new IllegalArgumentException("会员有效月数只能是1或12"); + } + + if (numCodes > 1000) { + throw new IllegalArgumentException("单次生成数量不能超过1000"); + } + + // 获取当前最大的vipNumber + Integer maxVipNumber = 100000; // 默认从100001开始(6位数) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.orderByDesc("vipNumber").last("LIMIT 1"); + VipCode maxVipCode = vipCodeMapper.selectOne(queryWrapper); + if (maxVipCode != null && maxVipCode.getVipNumber() != null) { + maxVipNumber = Math.max(maxVipNumber, maxVipCode.getVipNumber()); + } + + int successCount = 0; + Date now = new Date(); + + for (int i = 0; i < numCodes; i++) { + try { + VipCode vipCode = new VipCode(); + vipCode.setCode(generateUniqueCode()); + vipCode.setVipExpireTime(vipExpireTime); + + // 设置会员编号,从最大值+1开始,确保是6位数 + int nextVipNumber = maxVipNumber + i + 1; + vipCode.setVipNumber(nextVipNumber); + + vipCode.setIsUse(0); // 0表示未使用 + vipCode.setCreatedUserId(createdUserId); // 设置创建人ID + vipCode.setCreatedUserName(createdUserName); // 设置创建人名称 + vipCode.setCreateTime(now); +/* vipCode.setUpdateTime(now);*/ + + int result = vipCodeMapper.insert(vipCode); + if (result > 0) { + successCount++; + } + } catch (Exception e) { + log.error("生成第{}个会员码失败:{}", i + 1, e.getMessage()); + } + } + + log.info("会员码生成完成,请求生成:{}个,实际成功:{}个", numCodes, successCount); + return successCount; + } + + @Override + public String getAvailableVipCode(int vipExpireTime, Long createdUserId, String createdUserName) { + log.info("查找可用会员码,有效月数:{},创建人ID:{}", vipExpireTime, createdUserId); + + if (vipExpireTime != 1 && vipExpireTime != 12) { + throw new IllegalArgumentException("会员有效月数只能是1或12"); + } + + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("vipExpireTime", vipExpireTime) + .eq("isUse", 0) + .orderByAsc("createTime") + .last("LIMIT 1"); + + VipCode vipCode = vipCodeMapper.selectOne(queryWrapper); + + if (vipCode != null) { + log.info("找到可用会员码:{}", vipCode.getCode()); + return vipCode.getCode(); + } else { + log.warn("没有找到有效月数为{}的可用会员码", vipExpireTime); + return null; + } + } + + /** + * 生成唯一的会员码 + * @return 会员码字符串 + */ + private String generateUniqueCode() { + // 生成16位的随机字符串作为会员码 + String uuid = UUID.randomUUID().toString().replace("-", ""); + return uuid.substring(0, 16).toUpperCase(); + } + + /** + * 生成16位随机订单号 + * @return 16位订单号 + */ + private Long generateOrderNumber() { + // 生成16位随机数字订单号 + // 方法1: 时间戳 + 随机数组合(推荐,避免重复) + long timestamp = System.currentTimeMillis() % 1000000L; // 取6位时间戳 + long random = (long) (Math.random() * 9000000000L) + 1000000000L; // 10位随机数 + return Long.parseLong(String.valueOf(timestamp) + String.valueOf(random)); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/VipExchangeRecordServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/VipExchangeRecordServiceImpl.java new file mode 100644 index 0000000..6f8d6a3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/VipExchangeRecordServiceImpl.java @@ -0,0 +1,32 @@ +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.VipExchangeRecord; +import com.xy.xyaicpzs.service.VipExchangeRecordService; +import com.xy.xyaicpzs.mapper.VipExchangeRecordMapper; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【vip_exchange_record(会员兑换表)】的数据库操作Service实现 +* @createDate 2025-06-19 11:27:10 +*/ +@Service +public class VipExchangeRecordServiceImpl extends ServiceImpl + implements VipExchangeRecordService{ + + @Override + public List getExchangeRecordsByUserId(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .orderByDesc("exchangeTime"); + return list(queryWrapper); + } +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java b/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java new file mode 100644 index 0000000..7b16505 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java @@ -0,0 +1,44 @@ +package com.xy.xyaicpzs.task; + +import com.xy.xyaicpzs.service.DataAnalysisService; +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 +public class PredictResultTask { + + @Autowired + private DataAnalysisService dataAnalysisService; + + /** + * 每5分钟执行一次预测结果匹配 + * cron表达式:秒 分 时 日 月 周 + * 0 0/5 * * * ? 表示每5分钟执行一次 + */ + @Scheduled(cron = "0 0/1 * * * ?") + public void processPendingPredictions() { + try { + log.info("=== 开始执行预测结果匹配定时任务 ==="); + + long startTime = System.currentTimeMillis(); + + // 调用服务处理待开奖记录 + int processedCount = dataAnalysisService.processPendingPredictions(); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + log.info("=== 预测结果匹配定时任务执行完成 === 处理记录数:{},耗时:{}ms", processedCount, duration); + + } catch (Exception e) { + log.error("预测结果匹配定时任务执行失败:{}", e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/util/ExcelDataImporter.java b/src/main/java/com/xy/xyaicpzs/util/ExcelDataImporter.java new file mode 100644 index 0000000..db7fa85 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/ExcelDataImporter.java @@ -0,0 +1,1737 @@ +package com.xy.xyaicpzs.util; + +import com.xy.xyaicpzs.domain.entity.History100; +import com.xy.xyaicpzs.domain.entity.HistoryAll; +import com.xy.xyaicpzs.domain.entity.HistoryTop; +import com.xy.xyaicpzs.domain.entity.HistoryTop100; +import com.xy.xyaicpzs.domain.entity.BlueHistory100; +import com.xy.xyaicpzs.domain.entity.BlueHistoryAll; +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop; +import com.xy.xyaicpzs.domain.entity.BlueHistoryTop100; +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.domain.entity.T7; +import com.xy.xyaicpzs.domain.entity.T8; +import com.xy.xyaicpzs.domain.entity.T11; +import com.xy.xyaicpzs.domain.entity.LotteryDraws; +import com.xy.xyaicpzs.mapper.History100Mapper; +import com.xy.xyaicpzs.mapper.HistoryAllMapper; +import com.xy.xyaicpzs.mapper.HistoryTop100Mapper; +import com.xy.xyaicpzs.mapper.HistoryTopMapper; +import com.xy.xyaicpzs.mapper.BlueHistory100Mapper; +import com.xy.xyaicpzs.mapper.BlueHistoryAllMapper; +import com.xy.xyaicpzs.mapper.BlueHistoryTop100Mapper; +import com.xy.xyaicpzs.mapper.BlueHistoryTopMapper; +import com.xy.xyaicpzs.mapper.T3Mapper; +import com.xy.xyaicpzs.mapper.T4Mapper; +import com.xy.xyaicpzs.mapper.T5Mapper; +import com.xy.xyaicpzs.mapper.T6Mapper; +import com.xy.xyaicpzs.mapper.T7Mapper; +import com.xy.xyaicpzs.mapper.T8Mapper; +import com.xy.xyaicpzs.mapper.T11Mapper; +import com.xy.xyaicpzs.mapper.LotteryDrawsMapper; +import com.xy.xyaicpzs.service.T3Service; +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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * Excel数据导入工具类 + * 用于读取T1 sheet的数据并插入到四个数据库表中 + */ +@Slf4j +@Component +public class ExcelDataImporter { + + @Autowired + private HistoryAllMapper historyAllMapper; + + @Autowired + private History100Mapper history100Mapper; + + @Autowired + private HistoryTopMapper historyTopMapper; + + @Autowired + private HistoryTop100Mapper historyTop100Mapper; + + @Autowired + private BlueHistoryAllMapper blueHistoryAllMapper; + + @Autowired + private BlueHistory100Mapper blueHistory100Mapper; + + @Autowired + private BlueHistoryTopMapper blueHistoryTopMapper; + + @Autowired + private BlueHistoryTop100Mapper blueHistoryTop100Mapper; + + @Autowired + private T3Mapper t3Mapper; + + @Autowired + private T4Mapper t4Mapper; + + @Autowired + private T5Mapper t5Mapper; + + @Autowired + private T6Mapper t6Mapper; + + @Autowired + private T7Mapper t7Mapper; + + @Autowired + private T8Mapper t8Mapper; + + @Autowired + private T11Mapper t11Mapper; + + @Autowired + private LotteryDrawsMapper lotteryDrawsMapper; + + @Autowired + private T3Service t3Service; + + /** + * 导入Excel数据到数据库 + * @param filePath Excel文件路径 + */ + public void importExcelData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 清空现有数据 + clearExistingData(); + + // 处理T1 sheet(红球数据) + Sheet t1Sheet = workbook.getSheet("T1"); + if (t1Sheet != null) { + int lastRowNum = t1Sheet.getLastRowNum(); + log.info("开始导入T1数据(红球),共{}行", lastRowNum); + + // 读取数据并插入到各个表 + importHistoryAllData(t1Sheet, lastRowNum); + importHistory100Data(t1Sheet, lastRowNum); + importHistoryTopData(t1Sheet, lastRowNum); + importHistoryTop100Data(t1Sheet, lastRowNum); + + log.info("T1数据(红球)导入完成"); + } else { + log.warn("未找到T1工作表"); + } + + // 处理T2 sheet(蓝球数据) + Sheet t2Sheet = workbook.getSheet("T2"); + if (t2Sheet != null) { + int lastRowNum = t2Sheet.getLastRowNum(); + log.info("开始导入T2数据(蓝球),共{}行", lastRowNum); + + // 读取数据并插入到蓝球相关表 + importBlueHistoryAllData(t2Sheet, lastRowNum); + importBlueHistory100Data(t2Sheet, lastRowNum); + importBlueHistoryTopData(t2Sheet, lastRowNum); + importBlueHistoryTop100Data(t2Sheet, lastRowNum); + + log.info("T2数据(蓝球)导入完成"); + } else { + log.warn("未找到T2工作表"); + } + + // 处理T3 sheet(红球线系数数据) + Sheet t3Sheet = workbook.getSheet("T3"); + if (t3Sheet != null) { + log.info("开始导入T3数据(红球线系数)"); + List dataList = readT3DataFromSheet(t3Sheet); + if (!dataList.isEmpty()) { + // clearExistingData() is called at the beginning, so we just save. + t3Service.saveBatch(dataList); + log.info("成功导入{}条T3数据(红球线系数)", dataList.size()); + } else { + log.warn("未找到有效的T3数据"); + } + log.info("T3数据(红球线系数)导入完成"); + } else { + log.warn("未找到T3工作表"); + } + + // 处理T4 sheet(蓝球组红球线系数数据) + Sheet t4Sheet = workbook.getSheet("T4"); + if (t4Sheet != null) { + log.info("开始导入T4数据(蓝球组红球线系数)"); + + // 读取T4数据并插入到t4表 + importT4Data(t4Sheet); + + log.info("T4数据(蓝球组红球线系数)导入完成"); + } else { + log.warn("未找到T4工作表"); + } + + // 处理T5 sheet(蓝球组蓝球线系数数据) + Sheet t5Sheet = workbook.getSheet("T5"); + if (t5Sheet != null) { + log.info("开始导入T5数据(蓝球组蓝球线系数)"); + + // 读取T5数据并插入到t5表 + importT5Data(t5Sheet); + + log.info("T5数据(蓝球组蓝球线系数)导入完成"); + } else { + log.warn("未找到T5工作表"); + } + + // 处理T6 sheet(红球组蓝球线系数数据) + Sheet t6Sheet = workbook.getSheet("T6"); + if (t6Sheet != null) { + log.info("开始导入T6数据(红球组蓝球线系数)"); + + // 读取T6数据并插入到t6表 + importT6Data(t6Sheet); + + log.info("T6数据(红球组蓝球线系数)导入完成"); + } else { + log.warn("未找到T6工作表"); + } + + // 处理T7 sheet(红球组红球面系数数据) + Sheet t7Sheet = workbook.getSheet("T7"); + if (t7Sheet != null) { + log.info("开始导入T7数据(红球组红球面系数)"); + + // 读取T7数据并插入到t7表 + importT7Data(t7Sheet); + + log.info("T7数据(红球组红球面系数)导入完成"); + } else { + log.warn("未找到T7工作表"); + } + + // 处理T8 sheet(红球组蓝球面系数数据) + Sheet t8Sheet = workbook.getSheet("T8"); + if (t8Sheet != null) { + log.info("开始导入T8数据(红球组蓝球面系数)"); + + // 读取T8数据并插入到t8表 + importT8Data(t8Sheet); + + log.info("T8数据(红球组蓝球面系数)导入完成"); + } else { + log.warn("未找到T8工作表"); + } + +// // 处理T10 sheet(彩票开奖信息数据) +// Sheet t10Sheet = workbook.getSheet("T10"); +// if (t10Sheet != null) { +// log.info("开始导入T10数据(彩票开奖信息)"); +// +// // 读取T10数据并插入到lottery_draws表 +// importT10Data(t10Sheet); +// +// log.info("T10数据(彩票开奖信息)导入完成"); +// } else { +// log.warn("未找到T10工作表"); +// } + + // 处理T11 sheet(蓝球组红球面系数数据) + Sheet t11Sheet = workbook.getSheet("T11"); + if (t11Sheet != null) { + log.info("开始导入T11数据(蓝球组红球面系数)"); + + // 读取T11数据并插入到t11表 + importT11Data(t11Sheet); + + log.info("T11数据(蓝球组红球面系数)导入完成"); + } else { + log.warn("未找到T11工作表"); + } + + log.info("数据导入完成"); + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 导入Excel的开奖数据到数据库 + * @param filePath Excel文件路径 + */ + public void importLotteryDrawsData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + // 清空现有数据 + clearLotteryDrawsData(); + + // 处理T10 sheet(彩票开奖信息数据) + Sheet t10Sheet = workbook.getSheet("T10"); + if (t10Sheet != null) { + log.info("开始导入T10数据(彩票开奖信息)"); + + // 读取T10数据并插入到lottery_draws表 + importT10Data(t10Sheet); + + log.info("T10数据(彩票开奖信息)导入完成"); + } else { + log.warn("未找到T10工作表"); + } + + log.info("数据导入完成"); + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 追加导入Excel的开奖数据到数据库(不清空现有数据) + * @param filePath Excel文件路径 + */ + public void appendLotteryDrawsData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 处理T10 sheet(彩票开奖信息数据) + Sheet t10Sheet = workbook.getSheet("T10"); + if (t10Sheet != null) { + log.info("开始追加导入T10数据(彩票开奖信息)"); + + // 读取T10数据并追加到lottery_draws表 + appendT10Data(t10Sheet); + + log.info("T10数据(彩票开奖信息)追加导入完成"); + } else { + log.warn("未找到T10工作表"); + } + + log.info("追加数据导入完成"); + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 清空现有数据 + */ + private void clearExistingData() { + log.info("清空现有数据..."); + // 使用MyBatis-Plus的delete方法清空表 + // 清空红球数据表 + historyAllMapper.delete(null); + history100Mapper.delete(null); + historyTopMapper.delete(null); + historyTop100Mapper.delete(null); + + // 清空蓝球数据表 + blueHistoryAllMapper.delete(null); + blueHistory100Mapper.delete(null); + blueHistoryTopMapper.delete(null); + blueHistoryTop100Mapper.delete(null); + + // 清空T3数据表 + t3Mapper.delete(null); + + // 清空T4数据表 + t4Mapper.delete(null); + + // 清空T5数据表 + t5Mapper.delete(null); + + // 清空T6数据表 + t6Mapper.delete(null); + + // 清空T7数据表 + t7Mapper.delete(null); + + // 清空T8数据表 + t8Mapper.delete(null); + + // 清空T10数据表 + lotteryDrawsMapper.delete(null); + + // 清空T11数据表 + t11Mapper.delete(null); + } + + /** + * 清空现有开奖数据 + */ + private void clearLotteryDrawsData() { + log.info("清空现有开奖数据..."); + // 使用MyBatis-Plus的delete方法清空表 + // 清空T10数据表 + lotteryDrawsMapper.delete(null); + } + + /** + * 导入全部历史数据 (列A-G) + */ + private void importHistoryAllData(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; + + HistoryAll entity = new HistoryAll(); + + // 列A: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列B: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(1))); + + // 列C: 出现频率百分比 + entity.setFrequencyPercentage(getCellNumericValue(row.getCell(2))); + + // 列D: 平均隐现期 + entity.setAverageInterval(getCellNumericValue(row.getCell(3))); + + // 列E: 最长隐现期 + entity.setMaxHiddenInterval(getCellIntegerValue(row.getCell(4))); + + // 列F: 最多连出期 + entity.setMaxConsecutiveCount(getCellIntegerValue(row.getCell(5))); + + // 列G: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(6))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(historyAllMapper::insert); + log.info("成功导入{}条全部历史数据", dataList.size()); + } + } + + /** + * 导入最近100期数据 (列H-L) + */ + private void importHistory100Data(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; + + History100 entity = new History100(); + + // 使用第一列的球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列H: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(8))); + + // 列J: 平均隐现期 + entity.setAverageInterval(getCellNumericValue(row.getCell(9))); + + // 列K: 当前隐现期 + entity.setNowInterval(getCellIntegerValue(row.getCell(10))); + + // 列L: 最多连出期 + entity.setMaxConsecutiveCount(getCellIntegerValue(row.getCell(11))); + + // 列M: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(12))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(history100Mapper::insert); + log.info("成功导入{}条最近100期数据", dataList.size()); + } + } + + /** + * 导入历史数据排行 (列N-P) + */ + private void importHistoryTopData(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; + + HistoryTop entity = new HistoryTop(); + + // 列N: 排行 + entity.setNo(getCellIntegerValue(row.getCell(14))); + + // 列O: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(15))); + + // 列P: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(16))); + + if (entity.getBallNumber() != null && entity.getNo() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(historyTopMapper::insert); + log.info("成功导入{}条历史数据排行", dataList.size()); + } + } + + /** + * 导入100期数据排行 (列Q-S) + */ + private void importHistoryTop100Data(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; + + HistoryTop100 entity = new HistoryTop100(); + + // 列Q: 排行 + entity.setNo(getCellIntegerValue(row.getCell(18))); + + // 列R: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(19))); + + // 列S: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(20))); + + if (entity.getBallNumber() != null && entity.getNo() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(historyTop100Mapper::insert); + log.info("成功导入{}条100期数据排行", dataList.size()); + } + } + + /** + * 获取单元格的数值(保留两位小数) + */ + 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; + } + + /** + * 将数值保留两位小数 + * @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; + } + } + + // ==================== 蓝球数据导入方法 ==================== + + /** + * 导入蓝球全部历史数据 (列A-G) + */ + private void importBlueHistoryAllData(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; + + BlueHistoryAll entity = new BlueHistoryAll(); + + // 列A: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列B: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(1))); + + // 列C: 出现频率百分比 + entity.setFrequencyPercentage(getCellNumericValue(row.getCell(2))); + + // 列D: 平均隐现期 + entity.setAverageInterval(getCellNumericValue(row.getCell(3))); + + // 列E: 最长隐现期 + entity.setMaxHiddenInterval(getCellIntegerValue(row.getCell(4))); + + // 列F: 最多连出期 + entity.setMaxConsecutiveCount(getCellIntegerValue(row.getCell(5))); + + // 列G: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(6))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(blueHistoryAllMapper::insert); + log.info("成功导入{}条蓝球全部历史数据", dataList.size()); + } + } + + /** + * 导入蓝球最近100期数据 (列H-M) + */ + private void importBlueHistory100Data(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; + + BlueHistory100 entity = new BlueHistory100(); + + // 使用第一列的球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列H: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(8))); + + // 列J: 平均隐现期 + entity.setAverageInterval(getCellNumericValue(row.getCell(9))); + + // 列K: 当前隐现期 + entity.setNowInterval(getCellIntegerValue(row.getCell(10))); + + // 列L: 最多连出期 + entity.setMaxConsecutiveCount(getCellIntegerValue(row.getCell(11))); + + // 列M: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(12))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(blueHistory100Mapper::insert); + log.info("成功导入{}条蓝球最近100期数据", dataList.size()); + } + } + + /** + * 导入蓝球历史数据排行 (列N-P) + */ + private void importBlueHistoryTopData(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; + + BlueHistoryTop entity = new BlueHistoryTop(); + + // 列N: 排行 + entity.setNo(getCellIntegerValue(row.getCell(14))); + + // 列O: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(15))); + + // 列P: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(16))); + + if (entity.getBallNumber() != null && entity.getNo() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(blueHistoryTopMapper::insert); + log.info("成功导入{}条蓝球历史数据排行", dataList.size()); + } + } + + /** + * 导入蓝球100期数据排行 (列Q-S) + */ + private void importBlueHistoryTop100Data(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; + + BlueHistoryTop100 entity = new BlueHistoryTop100(); + + // 列Q: 排行 + entity.setNo(getCellIntegerValue(row.getCell(18))); + + // 列R: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(19))); + + // 列S: 点系数 + entity.setPointCoefficient(getCellNumericValue(row.getCell(20))); + + if (entity.getBallNumber() != null && entity.getNo() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(blueHistoryTop100Mapper::insert); + log.info("成功导入{}条蓝球100期数据排行", dataList.size()); + } + } + + // ==================== T3数据导入方法 ==================== + + /** + * 导入T3数据(红球线系数) + * 数据结构:每三列为一组,每组有33行数据 + * 从球号固定为1-33(行号),线系数在C、F、I、L...列 + */ + private void importT3Data(Sheet sheet) { + log.info("开始导入T3数据(红球线系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有33行数据 + 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; // 每组3列 + log.info("检测到{}组数据,共{}列,预期33行数据", groupCount, totalCols); + + // 遍历每组数据 + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主球号从1开始 + int lineCoefCol = group * 3 + 2; // 线系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(主球{}号),线系数列位置:{}", group + 1, masterBallNumber, lineCoefCol); + + // 遍历33行数据(跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 33; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T3 entity = new T3(); + entity.setMasterBallNumber(masterBallNumber); + + // 从球号固定为行号(1-33) + Integer slaveBallNumber = i; + + // 线系数(从对应组的第二列读取) + Double lineCoefficient = getCellNumericValue(row.getCell(lineCoefCol)); + + if (lineCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setLineCoefficient(lineCoefficient); + dataList.add(entity); + } else { + log.warn("第{}组第{}行线系数为空,跳过", group + 1, i); + } + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t3Mapper::insert); + log.info("成功导入{}条T3数据(红球线系数)", dataList.size()); + } else { + log.warn("未找到有效的T3数据"); + } + } + + // ==================== T4数据导入方法 ==================== + + /** + * 导入T4数据(蓝球组红球的线系数) + * 数据结构:每三列为一组,每组有33行数据 + * 蓝球号码范围:1-16(主球) + * 红球号码范围:1-33(从球,对应行号) + * 线系数在C、F、I、L...列 + */ + private void importT4Data(Sheet sheet) { + log.info("开始导入T4数据(蓝球组红球的线系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有33行数据 + 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; // 每组3列 + // 蓝球号码范围是1-16,所以最多16组 + groupCount = Math.min(groupCount, 16); + + log.info("检测到{}组数据,共{}列,预期33行数据", groupCount, totalCols); + + // 遍历每组数据(蓝球1-16号) + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 蓝球号从1开始 + int lineCoefCol = group * 3 + 2; // 线系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(蓝球{}号),线系数列位置:{}", group + 1, masterBallNumber, lineCoefCol); + + // 遍历33行数据(红球1-33号,跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 33; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T4 entity = new T4(); + entity.setMasterBallNumber(masterBallNumber); // 蓝球号 + + // 从球号固定为行号(红球1-33) + Integer slaveBallNumber = i; + + // 线系数(从对应组的第三列读取) + Double lineCoefficient = getCellNumericValue(row.getCell(lineCoefCol)); + + if (lineCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setLineCoefficient(roundToTwoDecimalPlaces(lineCoefficient)); + dataList.add(entity); + } else { + log.warn("第{}组第{}行线系数为空,跳过", group + 1, i); + } + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t4Mapper::insert); + log.info("成功导入{}条T4数据(蓝球组红球的线系数)", dataList.size()); + log.info("预期数据量:16个蓝球 × 33个红球 = 528条记录"); + } else { + log.warn("未找到有效的T4数据"); + } + } + + // ==================== T5数据导入方法 ==================== + + /** + * 导入T5数据(蓝球组蓝球的线系数) + * 数据结构:每三列为一组,每组有16行数据 + * 蓝球号码范围:1-16(主球和从球都是) + * 线系数在C、F、I、L...列 + */ + private void importT5Data(Sheet sheet) { + log.info("开始导入T5数据(蓝球组蓝球的线系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有16行数据 + 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; // 每组3列 + // 蓝球号码范围是1-16,所以最多16组 + groupCount = Math.min(groupCount, 16); + + log.info("检测到{}组数据,共{}列,预期16行数据", groupCount, totalCols); + + // 遍历每组数据(蓝球1-16号) + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主蓝球号从1开始 + int lineCoefCol = group * 3 + 2; // 线系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(主蓝球{}号),线系数列位置:{}", group + 1, masterBallNumber, lineCoefCol); + + // 遍历16行数据(从蓝球1-16号,跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 16; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T5 entity = new T5(); + entity.setMasterBallNumber(masterBallNumber); // 主蓝球号 + + // 从球号固定为行号(从蓝球1-16) + Integer slaveBallNumber = i; + + // 线系数(从对应组的第三列读取) + Double lineCoefficient = getCellNumericValue(row.getCell(lineCoefCol)); + + if (lineCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setLineCoefficient(roundToTwoDecimalPlaces(lineCoefficient)); + dataList.add(entity); + } else { + log.warn("第{}组第{}行线系数为空,跳过", group + 1, i); + } + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t5Mapper::insert); + log.info("成功导入{}条T5数据(蓝球组蓝球的线系数)", dataList.size()); + log.info("预期数据量:16个蓝球 × 16个蓝球 = 256条记录"); + } else { + log.warn("未找到有效的T5数据"); + } + } + + // ==================== T6数据导入方法 ==================== + + /** + * 导入T6数据(红球组蓝球的线系数) + * 数据结构:每三列为一组,每组有16行数据 + * 红球号码范围:1-33(主球) + * 蓝球号码范围:1-16(从球) + * 线系数在C、F、I、L...列 + */ + private void importT6Data(Sheet sheet) { + log.info("开始导入T6数据(红球组蓝球的线系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有16行数据 + 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; // 每组3列 + // 红球号码范围是1-33,所以最多33组 + groupCount = Math.min(groupCount, 33); + + log.info("检测到{}组数据,共{}列,预期16行数据", groupCount, totalCols); + + // 遍历每组数据(红球1-33号) + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主红球号从1开始 + int lineCoefCol = group * 3 + 2; // 线系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(主红球{}号),线系数列位置:{}", group + 1, masterBallNumber, lineCoefCol); + + // 遍历16行数据(从蓝球1-16号,跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 16; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T6 entity = new T6(); + entity.setMasterBallNumber(masterBallNumber); // 主红球号 + + // 从球号固定为行号(从蓝球1-16) + Integer slaveBallNumber = i; + + // 线系数(从对应组的第三列读取) + Double lineCoefficient = getCellNumericValue(row.getCell(lineCoefCol)); + + if (lineCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setLineCoefficient(roundToTwoDecimalPlaces(lineCoefficient)); + dataList.add(entity); + } else { + log.warn("第{}组第{}行线系数为空,跳过", group + 1, i); + } + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t6Mapper::insert); + log.info("成功导入{}条T6数据(红球组蓝球的线系数)", dataList.size()); + log.info("预期数据量:33个红球 × 16个蓝球 = 528条记录"); + } else { + log.warn("未找到有效的T6数据"); + } + } + + // ==================== T7数据导入方法 ==================== + + /** + * 导入T7数据(红球组红球的面系数) + * 数据结构:每三列为一组,每组有33行数据,但需要排除自己和自己组合的情况 + * 红球号码范围:1-33(主球和从球都是) + * 面系数在C、F、I、L...列 + * 特殊处理:1号只能与2-33组合,2号只能与1,3-33组合,以此类推 + */ + private void importT7Data(Sheet sheet) { + log.info("开始导入T7数据(红球组红球的面系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有33行数据 + 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; // 每组3列 + // 红球号码范围是1-33,所以最多33组 + groupCount = Math.min(groupCount, 33); + + log.info("检测到{}组数据,共{}列,数据行数{},预期33行数据", groupCount, totalCols, lastRowNum); + + // 遍历每组数据(红球1-33号作为主球) + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主红球号从1开始 + int ballNumberCol = group * 3 + 1; // 球号列:1,4,7,10...(B,E,H,K列) + int faceCoefCol = group * 3 + 2; // 面系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(主红球{}号),球号列:{},面系数列:{}", group + 1, masterBallNumber, ballNumberCol, faceCoefCol); + + int validDataCount = 0; // 统计这组有效数据数量 + int skippedDiagonal = 0; // 统计跳过的对角线数量 + int emptyBallNumber = 0; // 统计球号为空的数量 + int emptyCoefficient = 0; // 统计面系数为空的数量 + + // 遍历所有数据行(跳过标题行) + // 需要读取到最后一行数据,确保包含33号球 + for (int rowIndex = 1; rowIndex <= lastRowNum; rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + continue; + } + + // 从Excel中读取球号(从球号列读取) + Integer slaveBallNumber = getCellIntegerValue(row.getCell(ballNumberCol)); + if (slaveBallNumber == null) { + emptyBallNumber++; + continue; // 如果球号为空,跳过这行 + } + + // 不排除对角线,读取到什么就插入什么 + // if (masterBallNumber == slaveBallNumber) { + // skippedDiagonal++; + // log.debug("跳过对角线组合:主球{},从球{}", masterBallNumber, slaveBallNumber); + // continue; + // } + + // 面系数(从对应组的面系数列读取) + Double faceCoefficient = getCellNumericValue(row.getCell(faceCoefCol)); + + if (faceCoefficient != null) { + T7 entity = new T7(); + entity.setMasterBallNumber(masterBallNumber); // 主红球号 + entity.setSlaveBallNumber(slaveBallNumber); // 从红球号 + entity.setFaceCoefficient(roundToTwoDecimalPlaces(faceCoefficient)); + dataList.add(entity); + validDataCount++; + + // 特别记录1号组33号球的数据 + if (masterBallNumber == 1 && slaveBallNumber == 33) { + log.info("★ 重要数据:主球1,从球33,Excel行{},面系数{}", rowIndex + 1, faceCoefficient); + } + + log.debug("添加T7数据:主球{},从球{},Excel行{},面系数{}", + masterBallNumber, slaveBallNumber, rowIndex + 1, faceCoefficient); + } else { + emptyCoefficient++; + log.debug("第{}组第{}行面系数为空,球号{},跳过", group + 1, rowIndex + 1, slaveBallNumber); + } + } + + // 统计这组的处理结果 + log.info("第{}组统计:有效数据{}条,跳过对角线{}条,球号为空{}条,面系数为空{}条", + group + 1, validDataCount, skippedDiagonal, emptyBallNumber, emptyCoefficient); + + if (validDataCount != 33) { + log.warn("第{}组数据异常:应该有33条有效数据,实际{}条", group + 1, validDataCount); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t7Mapper::insert); + log.info("成功导入{}条T7数据(红球组红球的面系数)", dataList.size()); + log.info("预期数据量:33个红球 × 32个红球(排除自己) = 1056条记录"); + + // 验证关键数据点 + boolean found1_33 = dataList.stream().anyMatch(t7 -> + t7.getMasterBallNumber() == 1 && t7.getSlaveBallNumber() == 33); + if (found1_33) { + T7 record1_33 = dataList.stream() + .filter(t7 -> t7.getMasterBallNumber() == 1 && t7.getSlaveBallNumber() == 33) + .findFirst().orElse(null); + log.info("验证成功:找到(1,33)记录,面系数={}", record1_33.getFaceCoefficient()); + } else { + log.warn("验证失败:未找到(1,33)记录"); + } + + // 统计每组的记录数(应该都是32条,排除对角线) + for (int master = 1; master <= Math.min(groupCount, 5); master++) { + final int masterNum = master; + long count = dataList.stream().filter(t7 -> t7.getMasterBallNumber() == masterNum).count(); + log.info("主球{}号记录数:{}(应该是32)", masterNum, count); + } + + // 验证对角线数据不存在 + long diagonalCount = dataList.stream() + .filter(t7 -> t7.getMasterBallNumber().equals(t7.getSlaveBallNumber())) + .count(); + log.info("对角线记录数:{}(应该是0)", diagonalCount); + + // 验证从球号范围 + Set slaveBallNumbers = dataList.stream() + .map(T7::getSlaveBallNumber) + .collect(java.util.stream.Collectors.toSet()); + log.info("从球号范围:{}到{},总数:{}(应该是1到33)", + slaveBallNumbers.stream().min(Integer::compareTo).orElse(0), + slaveBallNumbers.stream().max(Integer::compareTo).orElse(0), + slaveBallNumbers.size()); + + // 验证是否包含33号球 + boolean has33 = slaveBallNumbers.contains(33); + log.info("是否包含33号球:{}(应该是true)", has33); + + // 详细分析缺失的球号 + List missingSlaves = new ArrayList<>(); + for (int i = 1; i <= 33; i++) { + if (!slaveBallNumbers.contains(i)) { + missingSlaves.add(i); + } + } + if (!missingSlaves.isEmpty()) { + log.warn("缺失的从球号:{}", missingSlaves); + } + + // 统计每个主球的记录数,找出缺失的组合 + for (int master = 1; master <= 33; master++) { + final int masterNum = master; + long count = dataList.stream().filter(t7 -> t7.getMasterBallNumber() == masterNum).count(); + if (count != 33) { + log.warn("主球{}号记录数异常:{}(应该是33)", masterNum, count); + + // 找出这个主球缺失的从球号 + Set existingSlaves = dataList.stream() + .filter(t7 -> t7.getMasterBallNumber() == masterNum) + .map(T7::getSlaveBallNumber) + .collect(java.util.stream.Collectors.toSet()); + + List missingForThisMaster = new ArrayList<>(); + for (int slave = 1; slave <= 33; slave++) { + if (!existingSlaves.contains(slave)) { + missingForThisMaster.add(slave); + } + } + if (!missingForThisMaster.isEmpty()) { + log.warn("主球{}号缺失的从球号:{}", masterNum, missingForThisMaster); + } + } + } + + // 验证总记录数 + log.info("实际记录数:{},预期记录数:{}(33×33=1089),差异:{}", + dataList.size(), 33 * 33, (33 * 33) - dataList.size()); + } else { + log.warn("未找到有效的T7数据"); + } + } + + // ==================== T8数据导入方法 ==================== + + /** + * 导入T8数据(红球组蓝球的面系数) + * 数据结构:每三列为一组,每组有16行数据 + * 红球号码范围:1-33(主球) + * 蓝球号码范围:1-16(从球) + * 面系数在C、F、I、L...列 + */ + private void importT8Data(Sheet sheet) { + log.info("开始导入T8数据(红球组蓝球的面系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有16行数据 + 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; // 每组3列 + // 红球号码范围是1-33,所以最多33组 + groupCount = Math.min(groupCount, 33); + + log.info("检测到{}组数据,共{}列,预期16行数据", groupCount, totalCols); + + // 遍历每组数据(红球1-33号) + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主红球号从1开始 + int faceCoefCol = group * 3 + 2; // 面系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(主红球{}号),面系数列位置:{}", group + 1, masterBallNumber, faceCoefCol); + + // 遍历16行数据(从蓝球1-16号,跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 16; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T8 entity = new T8(); + entity.setMasterBallNumber(masterBallNumber); // 主红球号 + + // 从球号固定为行号(从蓝球1-16) + Integer slaveBallNumber = i; + + // 面系数(从对应组的第三列读取) + Double faceCoefficient = getCellNumericValue(row.getCell(faceCoefCol)); + + if (faceCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setFaceCoefficient(roundToTwoDecimalPlaces(faceCoefficient)); + dataList.add(entity); + } else { + log.warn("第{}组第{}行面系数为空,跳过", group + 1, i); + } + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t8Mapper::insert); + log.info("成功导入{}条T8数据(红球组蓝球的面系数)", dataList.size()); + log.info("预期数据量:33个红球 × 16个蓝球 = 528条记录"); + } else { + log.warn("未找到有效的T8数据"); + } + } + + // ==================== T11数据导入方法 ==================== + + /** + * 导入T11数据(蓝球组红球的面系数) + * 数据结构:每三列为一组,每组有33行数据 + * 蓝球号码范围:1-16(主球) + * 红球号码范围:1-33(从球) + * 面系数在C、F、I、L...列 + */ + private void importT11Data(Sheet sheet) { + log.info("开始导入T11数据(蓝球组红球的面系数)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行,应该有33行数据 + 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; // 每组3列 + // 蓝球号码范围是1-16,所以最多16组 + groupCount = Math.min(groupCount, 16); + + log.info("检测到{}组数据,共{}列,预期33行数据", groupCount, totalCols); + + // 遍历每组数据(蓝球1-16号) + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 蓝球号从1开始 + int faceCoefCol = group * 3 + 2; // 面系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(蓝球{}号),面系数列位置:{}", group + 1, masterBallNumber, faceCoefCol); + + // 遍历33行数据(红球1-33号,跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 33; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T11 entity = new T11(); + entity.setMasterBallNumber(masterBallNumber); // 蓝球号 + + // 从球号固定为行号(红球1-33) + Integer slaveBallNumber = i; + + // 面系数(从对应组的第三列读取) + Double faceCoefficient = getCellNumericValue(row.getCell(faceCoefCol)); + + if (faceCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setFaceCoefficient(roundToTwoDecimalPlaces(faceCoefficient)); + dataList.add(entity); + } else { + log.warn("第{}组第{}行面系数为空,跳过", group + 1, i); + } + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(t11Mapper::insert); + log.info("成功导入{}条T11数据(蓝球组红球的面系数)", dataList.size()); + log.info("预期数据量:16个蓝球 × 33个红球 = 528条记录"); + } else { + log.warn("未找到有效的T11数据"); + } + } + + // ==================== T10数据导入方法 ==================== + + /** + * 导入T10数据(彩票开奖信息) + * 数据结构:标准表格结构 + * A列:开奖期号,B列:开奖日期 + * C-H列:红球1-6,I列:蓝球 + */ + private void importT10Data(Sheet sheet) { + log.info("开始导入T10数据(彩票开奖信息)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行 + int lastRowNum = sheet.getLastRowNum(); + log.info("T10工作表共{}行数据(包含标题行)", lastRowNum + 1); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + LotteryDraws entity = new LotteryDraws(); + + // A列: 开奖期号 + Long drawId = getCellLongValue(row.getCell(0)); + if (drawId == null) { + log.warn("第{}行开奖期号为空,跳过", i + 1); + continue; + } + entity.setDrawId(drawId); + + // B列: 开奖日期 + Date drawDate = getCellDateValue(row.getCell(1)); + if (drawDate == null) { + log.warn("第{}行开奖日期为空,跳过", i + 1); + continue; + } + entity.setDrawDate(drawDate); + + // C列: 红球1 + entity.setRedBall1(getCellIntegerValue(row.getCell(2))); + + // D列: 红球2 + entity.setRedBall2(getCellIntegerValue(row.getCell(3))); + + // E列: 红球3 + entity.setRedBall3(getCellIntegerValue(row.getCell(4))); + + // F列: 红球4 + entity.setRedBall4(getCellIntegerValue(row.getCell(5))); + + // G列: 红球5 + entity.setRedBall5(getCellIntegerValue(row.getCell(6))); + + // H列: 红球6 + entity.setRedBall6(getCellIntegerValue(row.getCell(7))); + + // I列: 蓝球 + entity.setBlueBall(getCellIntegerValue(row.getCell(8))); + + // 验证必要字段 + if (entity.getRedBall1() != null && entity.getRedBall2() != null && + entity.getRedBall3() != null && entity.getRedBall4() != null && + entity.getRedBall5() != null && entity.getRedBall6() != null && + entity.getBlueBall() != null) { + dataList.add(entity); + log.debug("添加开奖记录:期号{},日期{},红球{}-{}-{}-{}-{}-{},蓝球{}", + entity.getDrawId(), entity.getDrawDate(), + entity.getRedBall1(), entity.getRedBall2(), entity.getRedBall3(), + entity.getRedBall4(), entity.getRedBall5(), entity.getRedBall6(), + entity.getBlueBall()); + } else { + log.warn("第{}行数据不完整,跳过", i + 1); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dataList.forEach(lotteryDrawsMapper::insert); + log.info("成功导入{}条T10数据(彩票开奖信息)", dataList.size()); + } else { + log.warn("未找到有效的T10数据"); + } + } + + /** + * 获取单元格的Long值 + */ + private Long getCellLongValue(Cell cell) { + if (cell == null) return null; + + try { + switch (cell.getCellType()) { + case NUMERIC: + return (long) cell.getNumericCellValue(); + case STRING: + String strValue = cell.getStringCellValue().trim(); + if (strValue.isEmpty()) return null; + return Long.parseLong(strValue); + case FORMULA: + return (long) cell.getNumericCellValue(); + case BLANK: + return null; + default: + log.warn("不支持的单元格类型:{}", cell.getCellType()); + return null; + } + } catch (Exception e) { + log.warn("读取Long值失败:{}", e.getMessage()); + return null; + } + } + + /** + * 获取单元格的Date值 + */ + private Date getCellDateValue(Cell cell) { + if (cell == null) return null; + + try { + switch (cell.getCellType()) { + case NUMERIC: + if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue(); + } else { + // 如果是数字,尝试转换为日期 + return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(cell.getNumericCellValue()); + } + case STRING: + String strValue = cell.getStringCellValue().trim(); + if (strValue.isEmpty()) return null; + // 尝试解析字符串日期,支持多种格式 + return parseStringToDate(strValue); + case FORMULA: + if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue(); + } else { + return org.apache.poi.ss.usermodel.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 { + // 支持多种日期格式 + java.text.SimpleDateFormat[] formats = { + new java.text.SimpleDateFormat("yyyy-MM-dd"), + new java.text.SimpleDateFormat("yyyy/MM/dd"), + new java.text.SimpleDateFormat("yyyy-M-d"), + new java.text.SimpleDateFormat("yyyy/M/d") + }; + + for (java.text.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; + } + } + + /** + * 追加导入T10数据(彩票开奖信息),检查重复数据 + * 数据结构:标准表格结构 + * A列:开奖期号,B列:开奖日期 + * C-H列:红球1-6,I列:蓝球 + */ + private void appendT10Data(Sheet sheet) { + log.info("开始追加导入T10数据(彩票开奖信息)..."); + List dataList = new ArrayList<>(); + int duplicateCount = 0; + int newCount = 0; + + // 获取数据行数,跳过标题行 + int lastRowNum = sheet.getLastRowNum(); + log.info("T10工作表共{}行数据(包含标题行)", lastRowNum + 1); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + LotteryDraws entity = new LotteryDraws(); + + // A列: 开奖期号 + Long drawId = getCellLongValue(row.getCell(0)); + if (drawId == null) { + log.warn("第{}行开奖期号为空,跳过", i + 1); + continue; + } + entity.setDrawId(drawId); + + // 检查该期号是否已存在 + LotteryDraws existingRecord = lotteryDrawsMapper.selectById(drawId); + if (existingRecord != null) { + duplicateCount++; + log.debug("开奖期号{}已存在,跳过", drawId); + continue; + } + + // B列: 开奖日期 + Date drawDate = getCellDateValue(row.getCell(1)); + if (drawDate == null) { + log.warn("第{}行开奖日期为空,跳过", i + 1); + continue; + } + entity.setDrawDate(drawDate); + + // C列: 红球1 + entity.setRedBall1(getCellIntegerValue(row.getCell(2))); + + // D列: 红球2 + entity.setRedBall2(getCellIntegerValue(row.getCell(3))); + + // E列: 红球3 + entity.setRedBall3(getCellIntegerValue(row.getCell(4))); + + // F列: 红球4 + entity.setRedBall4(getCellIntegerValue(row.getCell(5))); + + // G列: 红球5 + entity.setRedBall5(getCellIntegerValue(row.getCell(6))); + + // H列: 红球6 + entity.setRedBall6(getCellIntegerValue(row.getCell(7))); + + // I列: 蓝球 + entity.setBlueBall(getCellIntegerValue(row.getCell(8))); + + // 验证必要字段 + if (entity.getRedBall1() != null && entity.getRedBall2() != null && + entity.getRedBall3() != null && entity.getRedBall4() != null && + entity.getRedBall5() != null && entity.getRedBall6() != null && + entity.getBlueBall() != null) { + dataList.add(entity); + newCount++; + log.debug("添加新开奖记录:期号{},日期{},红球{}-{}-{}-{}-{}-{},蓝球{}", + entity.getDrawId(), entity.getDrawDate(), + entity.getRedBall1(), entity.getRedBall2(), entity.getRedBall3(), + entity.getRedBall4(), entity.getRedBall5(), entity.getRedBall6(), + entity.getBlueBall()); + } else { + log.warn("第{}行数据不完整,跳过", i + 1); + } + } + + // 批量插入新数据 + if (!dataList.isEmpty()) { + dataList.forEach(lotteryDrawsMapper::insert); + log.info("成功追加导入{}条新的T10数据(彩票开奖信息)", dataList.size()); + } else { + log.info("没有新的T10数据需要导入"); + } + + log.info("追加导入统计:新增{}条,跳过重复{}条", newCount, duplicateCount); + } + + /** + * 读取并保存T3表数据 + * @param filePath 文件路径 + * @throws IOException IO异常 + */ + public void readAndSaveT3Data(String filePath) throws IOException { + log.info("开始读取并保存T3表数据,文件路径: {}", filePath); + try (InputStream inputStream = new FileInputStream(filePath)) { + readAndSaveT3Data(inputStream); + } + } + + public void readAndSaveT3Data(InputStream inputStream) throws IOException { + log.info("开始读取并保存T3表数据..."); + List dataList; + try (Workbook workbook = new XSSFWorkbook(inputStream)) { + Sheet sheet = workbook.getSheet("T3"); + if (sheet == null) { + log.warn("在提供的文件中未找到T3工作表"); + return; + } + dataList = readT3DataFromSheet(sheet); + } + + if (!dataList.isEmpty()) { + // 清空T3表 + t3Service.remove(null); + log.info("已清空T3表"); + t3Service.saveBatch(dataList); + log.info("成功保存 {} 条T3数据", dataList.size()); + } else { + log.warn("没有读取到T3数据"); + } + } + + private List readT3DataFromSheet(Sheet sheet) { + List dataList = new ArrayList<>(); + + int lastRowNum = sheet.getLastRowNum(); + int groupCount = 33; // T3表有33组数据 + + // 遍历每组数据 + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主球号从1开始 + int slaveBallCol = group * 3 + 1; // 从球号列:1,4,7,10...(B,E,H,K列) + int lineCoefCol = group * 3 + 2; // 线系数列:2,5,8,11...(C,F,I,L列) + + log.info("处理第{}组数据(主球{}号),从球号列: {}, 线系数列: {}", group + 1, masterBallNumber, slaveBallCol, lineCoefCol); + + // 遍历33行数据(跳过标题行) + for (int i = 1; i <= lastRowNum && i <= 33; i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + T3 entity = new T3(); + entity.setMasterBallNumber(masterBallNumber); + + // 从对应组的第一列读取从球号 + Double slaveBallDouble = getCellNumericValue(row.getCell(slaveBallCol)); + Integer slaveBallNumber = (slaveBallDouble != null) ? slaveBallDouble.intValue() : null; + + // 线系数(从对应组的第二列读取) + Double lineCoefficient = getCellNumericValue(row.getCell(lineCoefCol)); + + if (slaveBallNumber != null && lineCoefficient != null) { + entity.setSlaveBallNumber(slaveBallNumber); + entity.setLineCoefficient(lineCoefficient); + dataList.add(entity); + } else { + log.warn("第{}组第{}行从球号或线系数为空,跳过", group + 1, i); + } + } + } + return dataList; + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/util/ExcelTestRunner.java b/src/main/java/com/xy/xyaicpzs/util/ExcelTestRunner.java new file mode 100644 index 0000000..bdc81b7 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/ExcelTestRunner.java @@ -0,0 +1,48 @@ +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/JwtUtil.java b/src/main/java/com/xy/xyaicpzs/util/JwtUtil.java new file mode 100644 index 0000000..c8739d3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/JwtUtil.java @@ -0,0 +1,140 @@ +package com.xy.xyaicpzs.util; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Instant; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * JWT工具类,用于生成和验证JWT令牌 + */ +@Component +public class JwtUtil { + + private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); + + private static final String COZE_APP_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7ZkUY3DNf0wf0\n" + + "K4MX9oytqYl2HFfZ8O5qZvN/MaLcyojzios97DZba9kj0ZBkhQNAZe5IZkBZ1Vud\n" + + "sQu3sxXHW1YEazDqMDEm8x8QspCXniiNUb9RX3kk+vxIczqXhn/xxAIcR1jnq2Pr\n" + + "Pt0j8kKr9FjhS1xuKEzBhU2YGJfMzN/ARqr7Q9zw5ONYjU8s6ejkUw240m/TvyMl\n" + + "eSBNkqLfWM8o7CMPCcvAnvGw7n7lpxQ8EQV4J5rfjl02cW5wZHLEtOEGlcQsx1KF\n" + + "gukcRkpGi6M7ifAENtqUrqZXghq7BlVRDDk3gXLP++fyOklL57eyhdKaY/bIlsXn\n" + + "N9WQc/YJAgMBAAECggEAEvH8Y06DrRjXEYLCno4Q28+rc3LbGM/oMn6U+FNqzRp2\n" + + "rPRhLlHujuCA08OwbtEZqo2DgPNBZzOQLwmCj0A+H+jmcczqZYqYtYDXr6b1wjY9\n" + + "kEfm6RGXXDs5fROVJVN2JNl1gyldXhJwKAyza5pmzh6GS8SEJCjT1a6l0Q4PbqdN\n" + + "XIKJzwXKZO0LCqlA3mQV5liEDe5XQM3ygkdYgLFvl+lUHws9TfCxpvKgn87b3OmT\n" + + "9k9bbWN1TOfP64Fhdwho2hoH0G7jiYjvm1fUuMxX0cYjIAmcuwokj9l/aTBBzc8+\n" + + "XT1zGxZ1ewSd0Xuc9+Xn+lKCScgSkbpzMZcQ5Eka3wKBgQDgrXIpI59V1U5jYiNn\n" + + "WX0G+8/UyraNxzv3rl+cJX43bvAN9KYgCq6qnJBdFDPF3Z9jEIU3LE06fz1gxrN4\n" + + "NbdM2GpnBQJ/wkMM5EG+0Zec01n5PZdES6NWbA5AZlRmsAxE71L2OOwbj0gVnyAj\n" + + "w5f9m44YaL2ZWpLbY93XKmEWzwKBgQDVhmbqdDjEl7xxt2H7xvJjCoSPLGruMfIH\n" + + "94o7xAIYCyjnNc4aTyQawo+cfzq+wjtRbZEvz4S6CtfrNfJHlgrLEF1V4WaaKHcN\n" + + "V43IdC18H4/8F502D4SZCwvhdmXdYjOcBbGZD3w5HZOBuH4RYcspplsj4Ot+w2Lz\n" + + "YQy8kHXbpwKBgHYZleWcDPggTLke7/82YesW2FNBTx5NeO8joKxCZQscbGDZla1I\n" + + "EDIsZBIZOXGrokl/eJbc2aeDFK9XIyVwDu7830lRr9OaOIaBQTHdmDVeP3As+ON0\n" + + "YuJdoEwnvfvQVFKz/kbg+vMtqLV81HcYLD+p0dJw1CDVJFbgMs8UPr6vAoGBAK+2\n" + + "MXzgdVK6dzWxLi3OMurz3dAgQkKP0VdjCltSWlLE5D0YVunplJF86Edln8cY+U1x\n" + + "99c7U0Lx52tE8oKcY3dlmRkyKofTotzU3vVFPaT2KDvQhuvU604x++3my77ZsBTF\n" + + "zrdhOd2ajCdk1kGhk1lL75Zf4gtn6EbV53BJBOPjAoGASVU6chk0zAEg7c9xOjCd\n" + + "uFlGeNqfiTJGL6aHHaaJN3EhCeWc36l4NGJ/JwWCm0ETydzYAtcUaj2biUAlEm0Q\n" + + "s1Vgl2ulxkpw32IclM0oND1VdJfwZRzcl4OO9jyF+993icaY+dAtZ9U37Jk9QazY\n" + + "L+Cmvmq59YO4yS9+WeBEGV8=\n" + + "-----END PRIVATE KEY-----"; + + private static final String COZE_APP_ID = "1102723433113"; + private static final String COZE_APP_FINGERPRINT = "WNHPUWKXL_PDNeynptov7GsiwW1eqgPP-2OfCIFlEc0"; + private static final String COZE_API_ENDPOINT = "api.coze.cn"; + + /** + * 生成JWT令牌 + * + * @param expireSeconds 过期时间(秒) + * @param sessionName 会话名称(可选) + * @param deviceId 设备ID(可选) + * @return JWT令牌字符串 + */ + public String generateToken(int expireSeconds, String sessionName, String deviceId) { + try { + // 当前时间戳(秒) + long currentTimeSeconds = Instant.now().getEpochSecond(); + + // 构建Header + Map headers = new HashMap<>(); + headers.put("alg", "RS256"); // 固定为RS256 + headers.put("typ", "JWT"); // 固定为JWT + headers.put("kid", COZE_APP_FINGERPRINT); // OAuth应用的公钥指纹 + + // 构建Payload + Map claims = new HashMap<>(); + claims.put("iss", COZE_APP_ID); // OAuth应用的ID + claims.put("aud", COZE_API_ENDPOINT); // 扣子API的Endpoint + claims.put("iat", currentTimeSeconds); // JWT开始生效的时间 + claims.put("exp", currentTimeSeconds + expireSeconds); // JWT过期的时间 + claims.put("jti", UUID.randomUUID().toString()); // 随机字符串,防止重放攻击 + + // 可选参数 + if (sessionName != null && !sessionName.isEmpty()) { + claims.put("session_name", sessionName); + } + + // 如果提供了设备ID,则添加设备信息 + if (deviceId != null && !deviceId.isEmpty()) { + Map sessionContext = new HashMap<>(); + Map deviceInfo = new HashMap<>(); + deviceInfo.put("device_id", deviceId); + sessionContext.put("device_info", deviceInfo); + claims.put("session_context", sessionContext); + } + + // 从私钥字符串中获取PrivateKey对象 + PrivateKey privateKey = getPrivateKeyFromString(COZE_APP_PRIVATE_KEY); + + // 生成JWT + return Jwts.builder() + .setHeader(headers) + .setClaims(claims) + .signWith(privateKey, SignatureAlgorithm.RS256) + .compact(); + } catch (Exception e) { + logger.error("JWT生成失败", e); + throw new RuntimeException("JWT生成失败: " + e.getMessage()); + } + } + + /** + * 从字符串中获取PrivateKey对象 + * + * @param privateKeyStr 私钥字符串(PEM格式) + * @return PrivateKey对象 + */ + private PrivateKey getPrivateKeyFromString(String privateKeyStr) throws Exception { + // 去除PEM格式的头尾和换行符 + String privateKeyPEM = privateKeyStr + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", ""); + + // Base64解码 + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyPEM); + + // 生成PKCS8EncodedKeySpec对象 + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + + // 获取KeyFactory实例 + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + // 生成PrivateKey对象 + return keyFactory.generatePrivate(keySpec); + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/util/SpeechRecognizerDemo.java b/src/main/java/com/xy/xyaicpzs/util/SpeechRecognizerDemo.java new file mode 100644 index 0000000..2be8814 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/SpeechRecognizerDemo.java @@ -0,0 +1,231 @@ +package com.xy.xyaicpzs.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import com.alibaba.nls.client.AccessToken; +import com.alibaba.nls.client.protocol.InputFormatEnum; +import com.alibaba.nls.client.protocol.NlsClient; +import com.alibaba.nls.client.protocol.SampleRateEnum; +import com.alibaba.nls.client.protocol.asr.SpeechRecognizer; +import com.alibaba.nls.client.protocol.asr.SpeechRecognizerListener; +import com.alibaba.nls.client.protocol.asr.SpeechRecognizerResponse; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * 语音识别服务类 + * 可以将语音文件转换为文本 + */ +@Service +public class SpeechRecognizerDemo { + private static final Logger logger = LoggerFactory.getLogger(SpeechRecognizerDemo.class); + private String appKey; + private NlsClient client; + + @Value("${aliyun.speech.app-key:}") + private String nLsAppKey; + + @Value("${aliyun.speech.access-key-id}") + private String accessKeyId; + + @Value("${aliyun.speech.access-key-secret}") + private String accessKeySecret; + + @Value("${aliyun.speech.gateway-url}") + private String gatewayUrl; + + public SpeechRecognizerDemo() { + } + + @PostConstruct + public void initialize() { + this.appKey = nLsAppKey; + AccessToken accessToken = new AccessToken(accessKeyId, accessKeySecret); + try { + accessToken.apply(); + logger.info("get token: " + accessToken.getToken() + ", expire time: " + accessToken.getExpireTime()); + if(gatewayUrl.isEmpty()) { + client = new NlsClient(accessToken.getToken()); + } else { + client = new NlsClient(gatewayUrl, accessToken.getToken()); + } + } catch (IOException e) { + logger.error("初始化语音识别客户端失败", e); + } + } + + public SpeechRecognizerDemo(String appKey, String id, String secret, String url) { + this.appKey = appKey; + //应用全局创建一个NlsClient实例,默认服务地址为阿里云线上服务地址。 + //获取Token,实际使用时注意在accessToken.getExpireTime()过期前再次获取。 + AccessToken accessToken = new AccessToken(id, secret); + try { + accessToken.apply(); + System.out.println("get token: " + accessToken.getToken() + ", expire time: " + accessToken.getExpireTime()); + if(url.isEmpty()) { + client = new NlsClient(accessToken.getToken()); + } else { + client = new NlsClient(url, accessToken.getToken()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 获取语音识别监听器 + */ + private static SpeechRecognizerListener getRecognizerListener(CompletableFuture resultFuture) { + SpeechRecognizerListener listener = new SpeechRecognizerListener() { + // 最终识别结果 + private String finalResult = ""; + + //识别出中间结果。仅当setEnableIntermediateResult为true时,才会返回该消息。 + @Override + public void onRecognitionResultChanged(SpeechRecognizerResponse response) { + //getName是获取事件名称,getStatus是获取状态码,getRecognizedText是语音识别文本。 + logger.info("name: " + response.getName() + ", status: " + response.getStatus() + ", result: " + response.getRecognizedText()); + } + + //识别完毕 + @Override + public void onRecognitionCompleted(SpeechRecognizerResponse response) { + //getName是获取事件名称,getStatus是获取状态码,getRecognizedText是语音识别文本。 + logger.info("name: " + response.getName() + ", status: " + response.getStatus() + ", result: " + response.getRecognizedText()); + finalResult = response.getRecognizedText(); + resultFuture.complete(finalResult); + } + + @Override + public void onStarted(SpeechRecognizerResponse response) { + logger.info("task_id: " + response.getTaskId() + " started"); + } + + @Override + public void onFail(SpeechRecognizerResponse response) { + //task_id是调用方和服务端通信的唯一标识,当遇到问题时,需要提供此task_id。 + logger.error("task_id: " + response.getTaskId() + ", status: " + response.getStatus() + ", status_text: " + response.getStatusText()); + resultFuture.completeExceptionally(new RuntimeException("语音识别失败: " + response.getStatusText())); + } + }; + return listener; + } + + //根据二进制数据大小计算对应的同等语音长度 + //sampleRate仅支持8000或16000。 + public static int getSleepDelta(int dataSize, int sampleRate) { + // 仅支持16位采样。 + int sampleBytes = 16; + // 仅支持单通道。 + int soundChannel = 1; + return (dataSize * 10 * 8000) / (160 * sampleRate); + } + + /** + * 将语音文件转换为文本 + * @param filepath 语音文件路径 + * @param sampleRate 采样率 + * @return 识别出的文本 + */ + public String speechToText(String filepath, int sampleRate) { + CompletableFuture resultFuture = new CompletableFuture<>(); + + try { + process(filepath, sampleRate, resultFuture); + // 等待异步结果完成 + return resultFuture.get(); + } catch (Exception e) { + logger.error("语音识别失败", e); + return "语音识别失败: " + e.getMessage(); + } + } + + private void process(String filepath, int sampleRate, CompletableFuture resultFuture) { + SpeechRecognizer recognizer = null; + try { + SpeechRecognizerListener listener = getRecognizerListener(resultFuture); + recognizer = new SpeechRecognizer(client, listener); + recognizer.setAppKey(appKey); + //设置音频编码格式。如果是OPUS文件,请设置为InputFormatEnum.OPUS。 + recognizer.setFormat(InputFormatEnum.PCM); + //设置音频采样率 + if(sampleRate == 16000) { + recognizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_16K); + } else if(sampleRate == 8000) { + recognizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_8K); + } + //设置是否返回中间识别结果 + recognizer.setEnableIntermediateResult(true); + //设置是否打开语音检测(即vad) + recognizer.addCustomedParam("enable_voice_detection",true); + //此方法将以上参数设置序列化为JSON发送给服务端,并等待服务端确认。 + long now = System.currentTimeMillis(); + recognizer.start(); + logger.info("ASR start latency : " + (System.currentTimeMillis() - now) + " ms"); + File file = new File(filepath); + FileInputStream fis = new FileInputStream(file); + byte[] b = new byte[3200]; + int len; + while ((len = fis.read(b)) > 0) { + logger.debug("send data pack length: " + len); + recognizer.send(b, len); + //本案例用读取本地文件的形式模拟实时获取语音流,因为读取速度较快,这里需要设置sleep时长。 + // 如果实时获取语音则无需设置sleep时长,如果是8k采样率语音第二个参数设置为8000。 + int deltaSleep = getSleepDelta(len, sampleRate); + Thread.sleep(deltaSleep); + } + //通知服务端语音数据发送完毕,等待服务端处理完成。 + now = System.currentTimeMillis(); + //计算实际延迟,调用stop返回之后一般即是识别结果返回时间。 + logger.info("ASR wait for complete"); + recognizer.stop(); + logger.info("ASR stop latency : " + (System.currentTimeMillis() - now) + " ms"); + fis.close(); + } catch (Exception e) { + logger.error("语音识别处理异常", e); + resultFuture.completeExceptionally(e); + } finally { + //关闭连接 + if (null != recognizer) { + recognizer.close(); + } + } + } + + public void process(String filepath, int sampleRate) { + CompletableFuture resultFuture = new CompletableFuture<>(); + process(filepath, sampleRate, resultFuture); + } + + public void shutdown() { + if (client != null) { + client.shutdown(); + } + } + + public static void main(String[] args) throws Exception { + // 这个main方法用于独立测试,实际应用中不会使用 + SpeechRecognizerDemo demo = new SpeechRecognizerDemo(); + // 手动设置参数进行测试 + demo.appKey = System.getenv().getOrDefault("NLS_APP_KEY", ""); + String id = System.getenv().getOrDefault("ALIYUN_AK_ID", ""); + String secret = System.getenv().getOrDefault("ALIYUN_AK_SECRET", ""); + String url = System.getenv().getOrDefault("NLS_GATEWAY_URL", "wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1"); + + // 初始化客户端 + AccessToken accessToken = new AccessToken(id, secret); + accessToken.apply(); + demo.client = new NlsClient(url, accessToken.getToken()); + + //转换为接口调用方式 + String result = demo.speechToText("D:\\code\\xy-ai-cpzs\\nls-sample-16k.wav", 16000); + System.out.println("语音识别结果: " + result); + demo.shutdown(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..c21ab00 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,75 @@ +spring: + application: + name: xy-ai-cpzs + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/cpzs + username: root + password: root +# datasource: +# driver-class-name: com.mysql.cj.jdbc.Driver +# url: jdbc:mysql://47.117.22.239:3306/cpzs +# username: cpzs_root +# password: cpzs_123456 + data: + redis: + host: localhost + port: 6379 + database: 0 +# data: +# redis: +# host: 47.117.22.239 +# port: 6379 +# database: 0 +# password: cpzs_123456 +server: + port: 8123 + servlet: + context-path: /api + session: + timeout: 86400 +# springdoc-openapi +springdoc: + swagger-ui: + path: /swagger-ui.html + tags-sorter: alpha + operations-sorter: alpha + api-docs: + path: /v3/api-docs + group-configs: + - group: 'default' + paths-to-match: '/**' + packages-to-scan: com.xy.xyaicpzs.controller +# knife4j +knife4j: + enable: true + setting: + language: zh_cn +mybatis-plus: + configuration: + # MyBatis ?? + map-underscore-to-camel-case: false + # ?????????? + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + logic-delete-field: isDelete # ???????????? + logic-delete-value: 1 # ?????????? 1? + logic-not-delete-value: 0 # ?????????? 0? + +# DashScope API配置 +dashscope: + api-key: sk-2ca3c5503a594147bffb0a8be73a125a + +# 阿里云语音识别服务配置 +aliyun: + speech: + app-key: scUTVRDNy1LVzsYz + access-key-id: LTAI5tR18rXPYazi3y8kAuep + access-key-secret: KZ1aKZOupilVc332SXE1g1DfKsqHPu + gateway-url: wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1 + sms: + sign-name: 西安精彩数据服务社 + template-code: SMS_489840017 + access-key-id: LTAI5tR18rXPYazi3y8kAuep + access-key-secret: KZ1aKZOupilVc332SXE1g1DfKsqHPu \ No newline at end of file diff --git a/src/main/resources/generator/mapper/BlueHistory100Mapper.xml b/src/main/resources/generator/mapper/BlueHistory100Mapper.xml new file mode 100644 index 0000000..c5d79dc --- /dev/null +++ b/src/main/resources/generator/mapper/BlueHistory100Mapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,averageInterval,nowInterval,maxConsecutiveCount, + pointCoefficient + + diff --git a/src/main/resources/generator/mapper/BlueHistoryAllMapper.xml b/src/main/resources/generator/mapper/BlueHistoryAllMapper.xml new file mode 100644 index 0000000..d307cda --- /dev/null +++ b/src/main/resources/generator/mapper/BlueHistoryAllMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,frequencyPercentage,averageInterval,maxHiddenInterval, + maxConsecutiveCount,pointCoefficient + + diff --git a/src/main/resources/generator/mapper/BlueHistoryTop100Mapper.xml b/src/main/resources/generator/mapper/BlueHistoryTop100Mapper.xml new file mode 100644 index 0000000..e1dc984 --- /dev/null +++ b/src/main/resources/generator/mapper/BlueHistoryTop100Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,no,ballNumber,pointCoefficient + + diff --git a/src/main/resources/generator/mapper/BlueHistoryTopMapper.xml b/src/main/resources/generator/mapper/BlueHistoryTopMapper.xml new file mode 100644 index 0000000..bc33554 --- /dev/null +++ b/src/main/resources/generator/mapper/BlueHistoryTopMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,no,ballNumber,pointCoefficient + + diff --git a/src/main/resources/generator/mapper/ChatMessageMapper.xml b/src/main/resources/generator/mapper/ChatMessageMapper.xml new file mode 100644 index 0000000..df9fdfd --- /dev/null +++ b/src/main/resources/generator/mapper/ChatMessageMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + id,conversationId,studentId,messageType,content,createTime, + updateTime,isDelete + + diff --git a/src/main/resources/generator/mapper/History100Mapper.xml b/src/main/resources/generator/mapper/History100Mapper.xml new file mode 100644 index 0000000..4b6dcf7 --- /dev/null +++ b/src/main/resources/generator/mapper/History100Mapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,averageInterval,nowInterval,maxConsecutiveCount, + pointCoefficient + + diff --git a/src/main/resources/generator/mapper/HistoryAllMapper.xml b/src/main/resources/generator/mapper/HistoryAllMapper.xml new file mode 100644 index 0000000..42abea0 --- /dev/null +++ b/src/main/resources/generator/mapper/HistoryAllMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,frequencyPercentage,averageInterval,maxHiddenInterval, + maxConsecutiveCount,pointCoefficient + + diff --git a/src/main/resources/generator/mapper/HistoryTop100Mapper.xml b/src/main/resources/generator/mapper/HistoryTop100Mapper.xml new file mode 100644 index 0000000..de06ff6 --- /dev/null +++ b/src/main/resources/generator/mapper/HistoryTop100Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,no,ballNumber,pointCoefficient + + diff --git a/src/main/resources/generator/mapper/HistoryTopMapper.xml b/src/main/resources/generator/mapper/HistoryTopMapper.xml new file mode 100644 index 0000000..b9a5296 --- /dev/null +++ b/src/main/resources/generator/mapper/HistoryTopMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,no,ballNumber,pointCoefficient + + diff --git a/src/main/resources/generator/mapper/LotteryDrawsMapper.xml b/src/main/resources/generator/mapper/LotteryDrawsMapper.xml new file mode 100644 index 0000000..7ff8e2a --- /dev/null +++ b/src/main/resources/generator/mapper/LotteryDrawsMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + drawId,drawDate,redBall1,redBall2,redBall3,redBall4, + redBall5,redBall6,blueBall + + diff --git a/src/main/resources/generator/mapper/OperationHistoryMapper.xml b/src/main/resources/generator/mapper/OperationHistoryMapper.xml new file mode 100644 index 0000000..fd1da29 --- /dev/null +++ b/src/main/resources/generator/mapper/OperationHistoryMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + id,userId,operationType,operationModule,operationResult,resultMessage, + operationTime,updateTime + + diff --git a/src/main/resources/generator/mapper/PredictRecordMapper.xml b/src/main/resources/generator/mapper/PredictRecordMapper.xml new file mode 100644 index 0000000..818ef7e --- /dev/null +++ b/src/main/resources/generator/mapper/PredictRecordMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id,userId,drawId,drawDate,redBall1,redBall2, + redBall3,redBall4,redBall5,redBall6,blueBall, + predictStatus,predictResult,predictTime,bonus,type + + diff --git a/src/main/resources/generator/mapper/T11Mapper.xml b/src/main/resources/generator/mapper/T11Mapper.xml new file mode 100644 index 0000000..3a01e2c --- /dev/null +++ b/src/main/resources/generator/mapper/T11Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,faceCoefficient + + diff --git a/src/main/resources/generator/mapper/T3Mapper.xml b/src/main/resources/generator/mapper/T3Mapper.xml new file mode 100644 index 0000000..8b15c33 --- /dev/null +++ b/src/main/resources/generator/mapper/T3Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,lineCoefficient + + diff --git a/src/main/resources/generator/mapper/T4Mapper.xml b/src/main/resources/generator/mapper/T4Mapper.xml new file mode 100644 index 0000000..60f9f16 --- /dev/null +++ b/src/main/resources/generator/mapper/T4Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,lineCoefficient + + diff --git a/src/main/resources/generator/mapper/T5Mapper.xml b/src/main/resources/generator/mapper/T5Mapper.xml new file mode 100644 index 0000000..d6af954 --- /dev/null +++ b/src/main/resources/generator/mapper/T5Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,lineCoefficient + + diff --git a/src/main/resources/generator/mapper/T6Mapper.xml b/src/main/resources/generator/mapper/T6Mapper.xml new file mode 100644 index 0000000..6aa4cd3 --- /dev/null +++ b/src/main/resources/generator/mapper/T6Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,lineCoefficient + + diff --git a/src/main/resources/generator/mapper/T7Mapper.xml b/src/main/resources/generator/mapper/T7Mapper.xml new file mode 100644 index 0000000..1800877 --- /dev/null +++ b/src/main/resources/generator/mapper/T7Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,faceCoefficient + + diff --git a/src/main/resources/generator/mapper/T8Mapper.xml b/src/main/resources/generator/mapper/T8Mapper.xml new file mode 100644 index 0000000..6307f14 --- /dev/null +++ b/src/main/resources/generator/mapper/T8Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,faceCoefficient + + diff --git a/src/main/resources/generator/mapper/UserMapper.xml b/src/main/resources/generator/mapper/UserMapper.xml new file mode 100644 index 0000000..667f48a --- /dev/null +++ b/src/main/resources/generator/mapper/UserMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + id,userName,userAccount,phone,userAvatar,gender,userRole, + userPassword,isVip,vipExpire,status,createTime,updateTime, + isDelete + + diff --git a/src/main/resources/generator/mapper/VipCodeMapper.xml b/src/main/resources/generator/mapper/VipCodeMapper.xml new file mode 100644 index 0000000..49ebd62 --- /dev/null +++ b/src/main/resources/generator/mapper/VipCodeMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + id,code,vipExpireTime, + vipNumber,isUse,createdUserId,createdUserName + usedUserId,usedUserName,createTime,updateTime + + + \ No newline at end of file diff --git a/src/main/resources/generator/mapper/VipExchangeRecordMapper.xml b/src/main/resources/generator/mapper/VipExchangeRecordMapper.xml new file mode 100644 index 0000000..b34f08b --- /dev/null +++ b/src/main/resources/generator/mapper/VipExchangeRecordMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + id,userId,type,exchangeMode,orderNo,orderAmount, + isUse,exchangeTime,updateTime + + diff --git a/src/main/resources/private_key.pem b/src/main/resources/private_key.pem new file mode 100644 index 0000000..ee03bdb --- /dev/null +++ b/src/main/resources/private_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZ1yKyaaWauaTX +7UhYFQsbZ87oHIHRdb74FPv3qtwpFFCVcNNKbK9uI2IrrrSUmVpExn89r1wT7KkW +pkohlI2EysBLuRlFAe2mr/2UGJD1VwHIla3GVgOEnIlnvFCLGFHDbEQB32XUmGuJ +7Q5aB+4p8pXtEOatv0wtp09g6nAF5i/YXgD9F/L9ejHN7UDAZV7XI5316bEg5suC +HZM40eEM+060A1oTM4byn7DocW5JUfktozIe61VUKFWXsI8dSXv9cMuia9e1sde3 +T+M2ykBkUX/XKUpLCOejZR2tlBnF4JdtYcydRtzT/9GzU4roz6i83OVD9dHKqwAz +NUar6Qr1AgMBAAECggEABynJjjmq8fYAII7GU4BNV1jBfV+AcEv1g7CEcqnkck7l +bq6azrMaBbyaNbL+6mbBRC+4R9tvuTzH5cuslXzeNxHyxhkyaEXDTAOTVjnrnATn +CsY6GctdAr0q4mdAxS1GzLJF8Llh0OD1AZkwayLwj8hMJgZSBhfreI+7EOXxp+Xs +u76AVuI8OwpXeRjs+dkoMGYIRasBcUL4PLZlWFd5EHuEx5PvI+yCAbLY2oQEViqX +hSnmrdybrjbp0cBh3cvSh4UqTCfuHcH82KAh2+u+vlSkPEAl5mkiXKXvVKXrrkIf +TUDFnv20hTMJwyggd0BuWQjusm/1hjUxVNZ2TppEuQKBgQDQYDtJxBX287VqKiDe +JN2QTYVDIwBmqD4YhoR9OxtLFEUmSGVVVGWzD9DoB2SUkV1lheOiYc3N89w1p6dy +K3wxzNLVKNKvsUw1k8QsX61BSk7xyzDYgcg19EmowcISV1eU2Xa/9p4Wgsh3chad +sNwjUlRFWo3BRX4CbmBLrMnirQKBgQC9ABwqS7AsoTpCyi4R6NTvL/vLflU+AU26 +AeK5KOzgyr1H1h6pLvCQeCVSG64H/W/frIXPDeYtooHhyePpW475OHABgfYVpnjy +j0qaKobqeNns2HNGqxWF+WG3lFmgAd014zW3Q1N6SQEFvxOdNC9QImKPiTZng9mp +05YLDJuaaQKBgHRNsenLQ0Mx5Yja+LtksjHPwtilGZ0yvG3t7tcC/YnbR3lFoSGO +WLEn278WGUJ/HDUFFPfWRQt11+wuplwn/0rAetC011tuxw2Ea1MlF8vAUO3OLnY5 +gejzb4V8l/iqvVx8QDaGzNfdqUxZ5lufkk84rYaEM2izg9CRkLfMOmaFAoGAHbQy +uQMEQT9lpLGZ4/6v2Je1/xlv1DroOF+rfmZMNFgov22LjBNwg4QeeXUYOPE/7+tx +bEVG880ilJXiV/6nzcts7w+52VU8lQKoOuqMIbW0qmCqXWTuEQxMq0mJfHhOh/WZ +r5/JMuLeYkTRlFdrSDDfRKfQARzbzLEQV9Q3cGECgYB/xL4h2CyBroQFNlu/PuP3 +AqO8Aexj42EAzfB6whb6UbyrFF94cNxdxBiLVMEw3cDHidpc8OwAksM8DPrrpKBJ +ARC0Tishf9DimuFjA0aamLu9jIxm+kNrvY51mmyqdAl/BS2UJN4hYk9Y4bR/bt8E +llF8Ty61JhMnJgiFqt4Yog== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/BallAnalysisFrequencyTest.java b/src/test/java/com/xy/xyaicpzs/BallAnalysisFrequencyTest.java new file mode 100644 index 0000000..170ffa5 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/BallAnalysisFrequencyTest.java @@ -0,0 +1,173 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.mapper.T7Mapper; +import com.xy.xyaicpzs.service.BallAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +/** + * 球号分析算法频次筛选测试 + * 验证当频次相同的球号超过11个时,使用T3表线系数进行二次筛选的功能 + */ +@Slf4j +@SpringBootTest +public class BallAnalysisFrequencyTest { + + @Autowired + private BallAnalysisService ballAnalysisService; + + /** + * 测试高位算法的频次筛选功能 + */ + @Test + public void testHighLevelFrequencySelection() { + log.info("=== 测试高位算法的频次筛选功能 ==="); + + // 输入参数 + String level = "H"; + List redBalls = Arrays.asList(7, 24, 27, 21, 10, 5); + Integer blueBall = 16; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("高位算法结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + } + + /** + * 测试中位算法的频次筛选功能 + */ + @Test + public void testMiddleLevelFrequencySelection() { + log.info("=== 测试中位算法的频次筛选功能 ==="); + + // 输入参数 + String level = "M"; + List redBalls = Arrays.asList(2, 9, 14, 20, 25, 32); + Integer blueBall = 12; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("中位算法结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + } + + /** + * 测试低位算法的频次筛选功能 + */ + @Test + public void testLowLevelFrequencySelection() { + log.info("=== 测试低位算法的频次筛选功能 ==="); + + // 输入参数 + String level = "L"; + List redBalls = Arrays.asList(3, 7, 11, 17, 22, 29); + Integer blueBall = 8; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("低位算法结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + } + + /** + * 测试用户提供的示例场景 + * 模拟频次相同导致超过11个球号的情况 + */ + @Test + public void testUserExampleScenario() { + log.info("=== 测试用户示例场景 ==="); + log.info("模拟场景:"); + log.info("26出现了8次 - 直接选择"); + log.info("17出现了7次 - 直接选择"); + log.info("14出现了7次 - 直接选择"); + log.info("7出现了6次 - 直接选择"); + log.info("3出现了6次 - 直接选择"); + log.info("20出现了6次 - 直接选择"); + log.info("2,9,1,22,32,13出现了5次 - 需要T3表线系数筛选"); + + // 使用特定的红球和蓝球组合来模拟这种情况 + String level = "H"; + List redBalls = Arrays.asList(26, 17, 14, 7, 3, 20); + Integer blueBall = 2; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("示例场景结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("频次筛选测试完成!"); + } + + /** + * 测试边界情况:恰好11个球号 + */ + @Test + public void testExactly11Balls() { + log.info("=== 测试边界情况:恰好11个球号 ==="); + + String level = "M"; + List redBalls = Arrays.asList(5, 10, 15, 18, 24, 30); + Integer blueBall = 6; + + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + log.info("边界情况结果:{}", result); + assert result.size() == 11 : "结果应该包含11个球号"; + } + + /** + * 测试极端情况:验证T3表线系数筛选的稳定性 + */ + @Test + public void testT3LineCoefficientStability() { + log.info("=== 测试T3表线系数筛选的稳定性 ==="); + + String level = "H"; + List redBalls = Arrays.asList(1, 2, 3, 4, 5, 6); + Integer blueBall = 1; + + // 多次执行同样的分析,结果应该一致 + List result1 = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + List result2 = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + log.info("第一次结果:{}", result1); + log.info("第二次结果:{}", result2); + + assert result1.equals(result2) : "相同输入应该产生相同输出"; + log.info("T3表线系数筛选稳定性测试通过!"); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/BallAnalysisMultiLevelFilteringTest.java b/src/test/java/com/xy/xyaicpzs/BallAnalysisMultiLevelFilteringTest.java new file mode 100644 index 0000000..3123fba --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/BallAnalysisMultiLevelFilteringTest.java @@ -0,0 +1,232 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.BallAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +/** + * 球号分析多级筛选测试 + * 测试analyzeBalls方法的多级筛选功能 + */ +@Slf4j +@SpringBootTest +public class BallAnalysisMultiLevelFilteringTest { + + @Autowired + private BallAnalysisService ballAnalysisService; + + /** + * 测试高位算法的多级筛选 + */ + @Test + public void testHighLevelMultiLevelFiltering() { + log.info("=== 测试高位算法的多级筛选 ==="); + + // 输入参数 + String level = "H"; + List redBalls = Arrays.asList(1, 5, 12, 18, 25, 33); + Integer blueBall = 8; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("高位算法多级筛选结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("高位算法多级筛选测试通过!"); + } + + /** + * 测试中位算法的多级筛选 + */ + @Test + public void testMiddleLevelMultiLevelFiltering() { + log.info("=== 测试中位算法的多级筛选 ==="); + + // 输入参数 + String level = "M"; + List redBalls = Arrays.asList(2, 8, 15, 22, 28, 31); + Integer blueBall = 12; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("中位算法多级筛选结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("中位算法多级筛选测试通过!"); + } + + /** + * 测试低位算法的多级筛选 + */ + @Test + public void testLowLevelMultiLevelFiltering() { + log.info("=== 测试低位算法的多级筛选 ==="); + + // 输入参数 + String level = "L"; + List redBalls = Arrays.asList(3, 9, 16, 23, 29, 32); + Integer blueBall = 4; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("低位算法多级筛选结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("低位算法多级筛选测试通过!"); + } + + /** + * 测试可能触发多级筛选的特殊情况 + * 使用容易产生频率冲突的球号组合 + */ + @Test + public void testSpecialCaseForMultiLevelFiltering() { + log.info("=== 测试可能触发多级筛选的特殊情况 ==="); + + // 输入参数 - 使用一些常见的球号,可能会产生更多的频率冲突 + String level = "H"; + List redBalls = Arrays.asList(7, 14, 21, 28, 1, 8); + Integer blueBall = 16; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("特殊情况多级筛选结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("特殊情况多级筛选测试通过!"); + } + + /** + * 测试多次运行的一致性 + * 验证随机选择部分的稳定性 + */ + @Test + public void testConsistencyOfMultipleRuns() { + log.info("=== 测试多次运行的一致性 ==="); + + // 输入参数 + String level = "M"; + List redBalls = Arrays.asList(6, 13, 20, 27, 2, 9); + Integer blueBall = 11; + + // 执行多次分析 + List result1 = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + List result2 = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + List result3 = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("第1次结果:{}", result1); + log.info("第2次结果:{}", result2); + log.info("第3次结果:{}", result3); + + // 断言 - 每次结果都应该是11个不重复的球号 + assert result1.size() == 11 : "第1次结果应该包含11个球号"; + assert result2.size() == 11 : "第2次结果应该包含11个球号"; + assert result3.size() == 11 : "第3次结果应该包含11个球号"; + + assert result1.stream().distinct().count() == result1.size() : "第1次结果不应该有重复的球号"; + assert result2.stream().distinct().count() == result2.size() : "第2次结果不应该有重复的球号"; + assert result3.stream().distinct().count() == result3.size() : "第3次结果不应该有重复的球号"; + + // 注意:由于有随机选择的部分,结果可能不完全相同,但大部分应该是相同的 + log.info("多次运行一致性测试通过!"); + } + + /** + * 测试边界情况:所有球号频率都不同 + */ + @Test + public void testBoundaryCase_AllDifferentFrequencies() { + log.info("=== 测试边界情况:所有球号频率都不同 ==="); + + // 输入参数 - 使用差异较大的球号,减少频率冲突 + String level = "H"; + List redBalls = Arrays.asList(1, 11, 22, 33, 5, 15); + Integer blueBall = 1; + + // 执行分析 + List result = ballAnalysisService.analyzeBalls(level, redBalls, blueBall); + + // 验证结果 + log.info("边界情况结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 11 : "结果应该包含11个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("边界情况测试通过!"); + } + + /** + * 测试三种级别算法的对比 + */ + @Test + public void testAllLevelsComparison() { + log.info("=== 测试三种级别算法的对比 ==="); + + // 输入参数 + List redBalls = Arrays.asList(4, 10, 17, 24, 30, 6); + Integer blueBall = 14; + + // 执行三种级别的分析 + List resultH = ballAnalysisService.analyzeBalls("H", redBalls, blueBall); + List resultM = ballAnalysisService.analyzeBalls("M", redBalls, blueBall); + List resultL = ballAnalysisService.analyzeBalls("L", redBalls, blueBall); + + // 验证结果 + log.info("高位算法结果:{}", resultH); + log.info("中位算法结果:{}", resultM); + log.info("低位算法结果:{}", resultL); + + // 断言 + assert resultH.size() == 11 : "高位算法结果应该包含11个球号"; + assert resultM.size() == 11 : "中位算法结果应该包含11个球号"; + assert resultL.size() == 11 : "低位算法结果应该包含11个球号"; + + assert resultH.stream().distinct().count() == resultH.size() : "高位算法结果不应该有重复的球号"; + assert resultM.stream().distinct().count() == resultM.size() : "中位算法结果不应该有重复的球号"; + assert resultL.stream().distinct().count() == resultL.size() : "低位算法结果不应该有重复的球号"; + + // 分析结果差异 + log.info("高位与中位的交集:{}", resultH.stream().filter(resultM::contains).collect(java.util.stream.Collectors.toList())); + log.info("高位与低位的交集:{}", resultH.stream().filter(resultL::contains).collect(java.util.stream.Collectors.toList())); + log.info("中位与低位的交集:{}", resultM.stream().filter(resultL::contains).collect(java.util.stream.Collectors.toList())); + + log.info("三种级别算法对比测试通过!"); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/BlueBallAnalysisTest.java b/src/test/java/com/xy/xyaicpzs/BlueBallAnalysisTest.java new file mode 100644 index 0000000..71f5c1a --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/BlueBallAnalysisTest.java @@ -0,0 +1,204 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.BallAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +/** + * 蓝球分析算法测试类 + */ +@Slf4j +@SpringBootTest +public class BlueBallAnalysisTest { + + @Autowired + private BallAnalysisService ballAnalysisService; + + @Test + public void testBlueBallAnalysisHigh() { + log.info("=== 测试蓝球分析算法 - 高位算法 ==="); + + // 测试数据26 20 18 32 10 14 + String level = "H"; + List predictedRedBalls = Arrays.asList(26, 20, 18, 32, 10, 14); // 6个预测红球 + List predictedBlueBalls = Arrays.asList(5, 8); // 2个预测蓝球 + List lastRedBalls = Arrays.asList(7, 24, 27, 21, 10, 5); // 6个上期红球 + Integer lastBlueBall = 16; // 1个上期蓝球 + + log.info("输入参数:"); + log.info("级别:{}", level); + log.info("预测红球:{}", predictedRedBalls); + log.info("预测蓝球:{}", predictedBlueBalls); + log.info("上期红球:{}", lastRedBalls); + log.info("上期蓝球:{}", lastBlueBall); + + List result = ballAnalysisService.blueBallAnalysis( + level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + + log.info("高位算法结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为空"; + assert result.size() <= 4 : "结果数量不能超过4个"; + log.info("高位算法测试通过"); + } + + @Test + public void testBlueBallAnalysisMiddle() { + log.info("=== 测试蓝球分析算法 - 中位算法 ==="); + + // 测试数据 + String level = "M"; + List predictedRedBalls = Arrays.asList(3, 9, 14, 21, 27, 32); // 6个预测红球 + List predictedBlueBalls = Arrays.asList(5, 11); // 2个预测蓝球 + List lastRedBalls = Arrays.asList(4, 10, 16, 22, 29, 30); // 6个上期红球 + Integer lastBlueBall = 7; // 1个上期蓝球 + + log.info("输入参数:"); + log.info("级别:{}", level); + log.info("预测红球:{}", predictedRedBalls); + log.info("预测蓝球:{}", predictedBlueBalls); + log.info("上期红球:{}", lastRedBalls); + log.info("上期蓝球:{}", lastBlueBall); + + List result = ballAnalysisService.blueBallAnalysis( + level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + + log.info("中位算法结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为空"; + assert result.size() <= 4 : "结果数量不能超过4个"; + log.info("中位算法测试通过"); + } + + @Test + public void testBlueBallAnalysisLow() { + log.info("=== 测试蓝球分析算法 - 低位算法 ==="); + + // 测试数据 + String level = "L"; + List predictedRedBalls = Arrays.asList(6, 11, 17, 23, 26, 33); // 6个预测红球 + List predictedBlueBalls = Arrays.asList(2, 14); // 2个预测蓝球 + List lastRedBalls = Arrays.asList(1, 8, 13, 19, 24, 32); // 6个上期红球 + Integer lastBlueBall = 16; // 1个上期蓝球 + + log.info("输入参数:"); + log.info("级别:{}", level); + log.info("预测红球:{}", predictedRedBalls); + log.info("预测蓝球:{}", predictedBlueBalls); + log.info("上期红球:{}", lastRedBalls); + log.info("上期蓝球:{}", lastBlueBall); + + List result = ballAnalysisService.blueBallAnalysis( + level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + + log.info("低位算法结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为空"; + assert result.size() <= 4 : "结果数量不能超过4个"; + log.info("低位算法测试通过"); + } + + @Test + public void testBlueBallAnalysisAllLevels() { + log.info("=== 测试蓝球分析算法 - 三种级别对比 ==="); + + // 相同的测试数据 + List predictedRedBalls = Arrays.asList(7, 12, 18, 24, 29, 33); // 6个预测红球 + List predictedBlueBalls = Arrays.asList(4, 9); // 2个预测蓝球 + List lastRedBalls = Arrays.asList(3, 8, 14, 20, 25, 31); // 6个上期红球 + Integer lastBlueBall = 6; // 1个上期蓝球 + + log.info("使用相同输入测试三种级别算法:"); + log.info("预测红球:{}", predictedRedBalls); + log.info("预测蓝球:{}", predictedBlueBalls); + log.info("上期红球:{}", lastRedBalls); + log.info("上期蓝球:{}", lastBlueBall); + + // 高位算法 + List highResult = ballAnalysisService.blueBallAnalysis( + "H", predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + log.info("高位算法结果:{}", highResult); + + // 中位算法 + List middleResult = ballAnalysisService.blueBallAnalysis( + "M", predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + log.info("中位算法结果:{}", middleResult); + + // 低位算法 + List lowResult = ballAnalysisService.blueBallAnalysis( + "L", predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + log.info("低位算法结果:{}", lowResult); + + // 验证结果 + assert highResult != null && highResult.size() <= 4 : "高位算法结果异常"; + assert middleResult != null && middleResult.size() <= 4 : "中位算法结果异常"; + assert lowResult != null && lowResult.size() <= 4 : "低位算法结果异常"; + + log.info("三种级别算法对比测试通过"); + } + + @Test + public void testBlueBallAnalysisParameterValidation() { + log.info("=== 测试蓝球分析算法 - 参数验证 ==="); + + // 测试无效级别 + try { + ballAnalysisService.blueBallAnalysis( + "X", Arrays.asList(1, 2, 3, 4, 5, 6), Arrays.asList(1, 2), + Arrays.asList(7, 8, 9, 10, 11, 12), 1); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("无效级别测试通过:{}", e.getMessage()); + } + + // 测试预测红球数量错误 + try { + ballAnalysisService.blueBallAnalysis( + "H", Arrays.asList(1, 2, 3, 4, 5), Arrays.asList(1, 2), + Arrays.asList(7, 8, 9, 10, 11, 12), 1); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("预测红球数量错误测试通过:{}", e.getMessage()); + } + + // 测试预测蓝球数量错误 + try { + ballAnalysisService.blueBallAnalysis( + "H", Arrays.asList(1, 2, 3, 4, 5, 6), Arrays.asList(1), + Arrays.asList(7, 8, 9, 10, 11, 12), 1); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("预测蓝球数量错误测试通过:{}", e.getMessage()); + } + + // 测试红球号码超出范围 + try { + ballAnalysisService.blueBallAnalysis( + "H", Arrays.asList(1, 2, 3, 4, 5, 34), Arrays.asList(1, 2), + Arrays.asList(7, 8, 9, 10, 11, 12), 1); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("红球号码超出范围测试通过:{}", e.getMessage()); + } + + // 测试蓝球号码超出范围 + try { + ballAnalysisService.blueBallAnalysis( + "H", Arrays.asList(1, 2, 3, 4, 5, 6), Arrays.asList(1, 17), + Arrays.asList(7, 8, 9, 10, 11, 12), 1); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("蓝球号码超出范围测试通过:{}", e.getMessage()); + } + + log.info("参数验证测试全部通过"); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/DecimalPrecisionTest.java b/src/test/java/com/xy/xyaicpzs/DecimalPrecisionTest.java new file mode 100644 index 0000000..8358e76 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/DecimalPrecisionTest.java @@ -0,0 +1,77 @@ +package com.xy.xyaicpzs; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 小数精度处理测试类 + */ +@SpringBootTest +public class DecimalPrecisionTest { + + /** + * 测试小数位数处理方法 + */ + @Test + public void testDecimalPrecision() { + System.out.println("=== 小数位数处理测试 ==="); + + // 测试数据 + double[] testValues = { + 123.456789, // 多位小数 + 12.1, // 一位小数 + 5.0, // 整数 + 0.999, // 四舍五入测试 + 0.001, // 小数点后多位 + 99.999 // 进位测试 + }; + + for (double value : testValues) { + Double result = roundToTwoDecimalPlaces(value); + System.out.printf("原值:%f → 结果:%.2f%n", value, result); + } + } + + /** + * 将数值保留两位小数的辅助方法(与ExcelDataImporter中的方法相同) + */ + 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) { + System.err.println("数值格式化失败:" + value); + return value; + } + } + + /** + * 测试边界情况 + */ + @Test + public void testEdgeCases() { + System.out.println("=== 边界情况测试 ==="); + + // 测试特殊值 + double[] edgeCases = { + Double.NaN, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, + 0.0, + -0.0, + -123.456 + }; + + for (double value : edgeCases) { + Double result = roundToTwoDecimalPlaces(value); + System.out.printf("特殊值:%f → 结果:%s%n", value, result); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/ExcelImportTest.java b/src/test/java/com/xy/xyaicpzs/ExcelImportTest.java new file mode 100644 index 0000000..6906e9a --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/ExcelImportTest.java @@ -0,0 +1,63 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.ExcelImportService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Excel导入功能测试类 + */ +@SpringBootTest +public class ExcelImportTest { + + @Autowired + private ExcelImportService excelImportService; + + /** + * 测试通过文件路径导入Excel数据 + * 需要将kaifa1.xlsx文件放在项目根目录下 + * 现在会同时导入T1(红球)和T2(蓝球)的数据 + */ + @Test + public void testImportExcelByPath() { + // 假设Excel文件在项目根目录下 + String filePath = "kaifa1.xlsx"; + + try { + String result = excelImportService.importExcelFileByPath(filePath); + System.out.println("导入结果:" + result); + System.out.println("注意:系统会自动导入T1(红球)、T2(蓝球)、T3(红球线系数)、T4(蓝球组红球线系数)、T5(蓝球组蓝球线系数)、T6(红球组蓝球线系数)和T7(红球组红球面系数)七个工作表的数据"); + } catch (Exception e) { + System.err.println("导入失败:" + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 测试导入绝对路径的Excel文件 + * 请根据实际情况修改文件路径 + * 现在会同时导入T1(红球)和T2(蓝球)的数据 + */ + @Test + public void testImportExcelByAbsolutePath() { + // 请根据实际情况修改文件路径 + String filePath = "D:/code/xy-ai-cpzs/kaifa1.xlsx"; + + try { + String result = excelImportService.importExcelFileByPath(filePath); + System.out.println("导入结果:" + result); + System.out.println("数据导入详情:"); + System.out.println("- T1工作表 → 红球数据表(history_all, history_100, history_top, history_top_100)"); + System.out.println("- T2工作表 → 蓝球数据表(blue_history_all, blue_history_100, blue_history_top, blue_history_top_100)"); + System.out.println("- T3工作表 → 红球线系数表(t3,33×33=1089条记录)"); + System.out.println("- T4工作表 → 蓝球组红球线系数表(t4,16×33=528条记录)"); + System.out.println("- T5工作表 → 蓝球组蓝球线系数表(t5,16×16=256条记录)"); + System.out.println("- T6工作表 → 红球组蓝球线系数表(t6,33×16=528条记录)"); + System.out.println("- T7工作表 → 红球组红球面系数表(t7,33×32=1056条记录,排除自己组合)"); + } catch (Exception e) { + System.err.println("导入失败:" + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisApiTest.java b/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisApiTest.java new file mode 100644 index 0000000..8d754c5 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisApiTest.java @@ -0,0 +1,245 @@ +package com.xy.xyaicpzs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xy.xyaicpzs.common.requset.FallowBallAnalysisRequest; +import com.xy.xyaicpzs.controller.BallAnalysisController; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; + +import java.util.Arrays; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; + +/** + * 跟随球号分析API接口测试 + * 测试REST API的功能和响应 + */ +@Slf4j +@SpringBootTest +@AutoConfigureMockMvc +public class FallowBallAnalysisApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + /** + * 测试表单参数方式的跟随球号分析接口 + */ + @Test + public void testFallowBallAnalysisWithFormParams() throws Exception { + log.info("=== 测试表单参数方式的跟随球号分析接口 ==="); + + mockMvc.perform(post("/api/ball-analysis/fallow") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("level", "H") + .param("firstThreeRedBalls", "7,24,27") + .param("lastSixRedBalls", "21,10,5,15,23,28") + .param("blueBall", "16")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(8)); + + log.info("表单参数方式测试通过!"); + } + + /** + * 测试JSON请求体方式的跟随球号分析接口 + */ + @Test + public void testFallowBallAnalysisWithJsonBody() throws Exception { + log.info("=== 测试JSON请求体方式的跟随球号分析接口 ==="); + + // 构建请求对象 + FallowBallAnalysisRequest request = + new FallowBallAnalysisRequest(); + request.setLevel("M"); + request.setFirstThreeRedBalls(Arrays.asList(26, 31, 28)); + request.setLastSixRedBalls(Arrays.asList(7, 24, 27, 21, 10, 5)); + request.setBlueBall(16); + + String requestJson = objectMapper.writeValueAsString(request); + log.info("请求JSON: {}", requestJson); + + mockMvc.perform(post("/api/ball-analysis/fallow-json") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(8)); + + log.info("JSON请求体方式测试通过!"); + } + + /** + * 测试获取算法说明接口 + */ + @Test + public void testGetFallowAlgorithmInfo() throws Exception { + log.info("=== 测试获取算法说明接口 ==="); + + mockMvc.perform(get("/api/ball-analysis/fallow-info")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isString()) + .andExpect(jsonPath("$.data").value(org.hamcrest.Matchers.containsString("跟随球号分析算法说明"))); + + log.info("获取算法说明接口测试通过!"); + } + + /** + * 测试不同级别的算法 + */ + @Test + public void testAllLevelsWithJsonBody() throws Exception { + log.info("=== 测试不同级别的算法 ==="); + + String[] levels = {"H", "M", "L"}; + + for (String level : levels) { + log.info("测试{}级别算法", level); + + // 构建请求对象 + FallowBallAnalysisRequest request = + new FallowBallAnalysisRequest(); + request.setLevel(level); + request.setFirstThreeRedBalls(Arrays.asList(1, 15, 23)); + request.setLastSixRedBalls(Arrays.asList(28, 31, 33, 2, 8, 12)); + request.setBlueBall(8); + + String requestJson = objectMapper.writeValueAsString(request); + + mockMvc.perform(post("/api/ball-analysis/fallow-json") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(8)); + + log.info("{}级别算法测试通过!", level); + } + } + + /** + * 测试参数验证 - 错误的级别 + */ + @Test + public void testInvalidLevel() throws Exception { + log.info("=== 测试参数验证 - 错误的级别 ==="); + + mockMvc.perform(post("/api/ball-analysis/fallow") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("level", "X") // 错误的级别 + .param("firstThreeRedBalls", "7,24,27") + .param("lastSixRedBalls", "21,10,5,15,23,28") + .param("blueBall", "16")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("级别必须为H、M或L"))); + + log.info("错误级别参数验证测试通过!"); + } + + /** + * 测试参数验证 - 前3个红球数量错误 + */ + @Test + public void testInvalidFirstThreeRedBalls() throws Exception { + log.info("=== 测试参数验证 - 前3个红球数量错误 ==="); + + mockMvc.perform(post("/api/ball-analysis/fallow") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("level", "H") + .param("firstThreeRedBalls", "7,24") // 只有2个球,应该是3个 + .param("lastSixRedBalls", "21,10,5,15,23,28") + .param("blueBall", "16")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("前3个红球数量必须为3个"))); + + log.info("前3个红球数量错误参数验证测试通过!"); + } + + /** + * 测试参数验证 - 后6个红球数量错误 + */ + @Test + public void testInvalidLastSixRedBalls() throws Exception { + log.info("=== 测试参数验证 - 后6个红球数量错误 ==="); + + mockMvc.perform(post("/api/ball-analysis/fallow") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("level", "H") + .param("firstThreeRedBalls", "7,24,27") + .param("lastSixRedBalls", "21,10,5,15,23") // 只有5个球,应该是6个 + .param("blueBall", "16")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("后6个红球数量必须为6个"))); + + log.info("后6个红球数量错误参数验证测试通过!"); + } + + /** + * 测试参数验证 - 红球号码超出范围 + */ + @Test + public void testRedBallOutOfRange() throws Exception { + log.info("=== 测试参数验证 - 红球号码超出范围 ==="); + + mockMvc.perform(post("/api/ball-analysis/fallow") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("level", "H") + .param("firstThreeRedBalls", "7,24,35") // 35超出范围 + .param("lastSixRedBalls", "21,10,5,15,23,28") + .param("blueBall", "16")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("前3个红球号码必须在1-33范围内"))); + + log.info("红球号码超出范围参数验证测试通过!"); + } + + /** + * 测试边界情况 - 前3个红球和后6个红球有重叠 + */ + @Test + public void testOverlappingRedBalls() throws Exception { + log.info("=== 测试边界情况 - 前3个红球和后6个红球有重叠 ==="); + + // 前3个红球和后6个红球有重叠是允许的 + mockMvc.perform(post("/api/ball-analysis/fallow") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("level", "H") + .param("firstThreeRedBalls", "7,24,27") + .param("lastSixRedBalls", "7,24,5,15,23,28") // 7和24重叠 + .param("blueBall", "16")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(8)); + + log.info("前3个红球和后6个红球重叠测试通过!"); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisMultiLevelFilteringTest.java b/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisMultiLevelFilteringTest.java new file mode 100644 index 0000000..59d639d --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisMultiLevelFilteringTest.java @@ -0,0 +1,250 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.BallAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +/** + * fallowBallAnalysis方法多级筛选功能测试 + * 测试T7面系数筛选后的多级筛选机制: + * 1. T7面系数总和筛选 + * 2. history_top_100排名筛选 + * 3. history_top点系数筛选 + * 4. 随机选择 + */ +@Slf4j +@SpringBootTest +public class FallowBallAnalysisMultiLevelFilteringTest { + + @Autowired + private BallAnalysisService ballAnalysisService; + + /** + * 测试高级算法的多级筛选 + */ + @Test + public void testHighLevelMultiLevelFiltering() { + log.info("=== 测试fallowBallAnalysis高级算法多级筛选 ==="); + + // 使用可能触发多级筛选的输入 + List firstThreeRedBalls = Arrays.asList(1, 2, 3); + List lastSixRedBalls = Arrays.asList(10, 15, 20, 25, 30, 33); + Integer blueBall = 5; + + List result = ballAnalysisService.fallowBallAnalysis("H", firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("高级算法结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号,实际:" + result.size(); + + // 验证球号范围 + for (Integer ball : result) { + assert ball >= 1 && ball <= 33 : "球号应该在1-33范围内,实际:" + ball; + } + + // 验证无重复 + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + + log.info("高级算法多级筛选测试通过"); + } + + /** + * 测试中级算法的多级筛选 + */ + @Test + public void testMiddleLevelMultiLevelFiltering() { + log.info("=== 测试fallowBallAnalysis中级算法多级筛选 ==="); + + List firstThreeRedBalls = Arrays.asList(5, 8, 12); + List lastSixRedBalls = Arrays.asList(14, 18, 22, 26, 29, 32); + Integer blueBall = 8; + + List result = ballAnalysisService.fallowBallAnalysis("M", firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("中级算法结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号,实际:" + result.size(); + + // 验证球号范围 + for (Integer ball : result) { + assert ball >= 1 && ball <= 33 : "球号应该在1-33范围内,实际:" + ball; + } + + // 验证无重复 + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + + log.info("中级算法多级筛选测试通过"); + } + + /** + * 测试低级算法的多级筛选 + */ + @Test + public void testLowLevelMultiLevelFiltering() { + log.info("=== 测试fallowBallAnalysis低级算法多级筛选 ==="); + + List firstThreeRedBalls = Arrays.asList(7, 11, 16); + List lastSixRedBalls = Arrays.asList(19, 21, 24, 27, 31, 33); + Integer blueBall = 12; + + List result = ballAnalysisService.fallowBallAnalysis("L", firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("低级算法结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号,实际:" + result.size(); + + // 验证球号范围 + for (Integer ball : result) { + assert ball >= 1 && ball <= 33 : "球号应该在1-33范围内,实际:" + ball; + } + + // 验证无重复 + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + + log.info("低级算法多级筛选测试通过"); + } + + /** + * 测试可能触发多级筛选的特殊情况 + */ + @Test + public void testSpecialCasesForMultiLevelFiltering() { + log.info("=== 测试fallowBallAnalysis特殊情况多级筛选 ==="); + + // 使用连续数字,可能产生相同的面系数 + List firstThreeRedBalls = Arrays.asList(1, 2, 3); + List lastSixRedBalls = Arrays.asList(4, 5, 6, 7, 8, 9); + Integer blueBall = 1; + + List result = ballAnalysisService.fallowBallAnalysis("H", firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("特殊情况结果:{}", result); + + // 验证结果 + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号,实际:" + result.size(); + + // 验证球号范围 + for (Integer ball : result) { + assert ball >= 1 && ball <= 33 : "球号应该在1-33范围内,实际:" + ball; + } + + // 验证无重复 + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + + log.info("特殊情况多级筛选测试通过"); + } + + /** + * 测试多次运行的一致性(在相同输入下) + */ + @Test + public void testConsistencyAcrossMultipleRuns() { + log.info("=== 测试fallowBallAnalysis多次运行一致性 ==="); + + List firstThreeRedBalls = Arrays.asList(6, 13, 17); + List lastSixRedBalls = Arrays.asList(20, 23, 25, 28, 30, 32); + Integer blueBall = 10; + + // 运行多次并比较结果 + List result1 = ballAnalysisService.fallowBallAnalysis("M", firstThreeRedBalls, lastSixRedBalls, blueBall); + List result2 = ballAnalysisService.fallowBallAnalysis("M", firstThreeRedBalls, lastSixRedBalls, blueBall); + List result3 = ballAnalysisService.fallowBallAnalysis("M", firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("第1次结果:{}", result1); + log.info("第2次结果:{}", result2); + log.info("第3次结果:{}", result3); + + // 验证所有结果都有效 + for (List result : Arrays.asList(result1, result2, result3)) { + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + } + + // 注意:由于最后一级是随机选择,结果可能不完全一致 + // 但前面几级的筛选应该是确定性的 + log.info("多次运行一致性测试完成(注意:最后一级随机选择可能导致结果不同)"); + } + + /** + * 测试边界情况 + */ + @Test + public void testBoundaryCases() { + log.info("=== 测试fallowBallAnalysis边界情况 ==="); + + // 测试最小值 + List firstThreeRedBalls = Arrays.asList(1, 2, 3); + List lastSixRedBalls = Arrays.asList(4, 5, 6, 7, 8, 9); + Integer blueBall = 1; + + List result1 = ballAnalysisService.fallowBallAnalysis("L", firstThreeRedBalls, lastSixRedBalls, blueBall); + log.info("最小值边界结果:{}", result1); + + // 测试最大值 + List firstThreeRedBalls2 = Arrays.asList(31, 32, 33); + List lastSixRedBalls2 = Arrays.asList(25, 26, 27, 28, 29, 30); + Integer blueBall2 = 16; + + List result2 = ballAnalysisService.fallowBallAnalysis("H", firstThreeRedBalls2, lastSixRedBalls2, blueBall2); + log.info("最大值边界结果:{}", result2); + + // 验证结果 + for (List result : Arrays.asList(result1, result2)) { + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + + for (Integer ball : result) { + assert ball >= 1 && ball <= 33 : "球号应该在1-33范围内,实际:" + ball; + } + } + + log.info("边界情况测试通过"); + } + + /** + * 测试三种级别算法的比较 + */ + @Test + public void testComparisonBetweenLevels() { + log.info("=== 测试fallowBallAnalysis三种级别算法比较 ==="); + + List firstThreeRedBalls = Arrays.asList(9, 14, 18); + List lastSixRedBalls = Arrays.asList(21, 24, 26, 29, 31, 33); + Integer blueBall = 7; + + List highResult = ballAnalysisService.fallowBallAnalysis("H", firstThreeRedBalls, lastSixRedBalls, blueBall); + List midResult = ballAnalysisService.fallowBallAnalysis("M", firstThreeRedBalls, lastSixRedBalls, blueBall); + List lowResult = ballAnalysisService.fallowBallAnalysis("L", firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("高级算法结果:{}", highResult); + log.info("中级算法结果:{}", midResult); + log.info("低级算法结果:{}", lowResult); + + // 验证所有结果都有效 + for (List result : Arrays.asList(highResult, midResult, lowResult)) { + assert result != null : "结果不能为null"; + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().distinct().count() == result.size() : "结果不应该有重复球号"; + + for (Integer ball : result) { + assert ball >= 1 && ball <= 33 : "球号应该在1-33范围内,实际:" + ball; + } + } + + log.info("三种级别算法比较测试通过"); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisTest.java b/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisTest.java new file mode 100644 index 0000000..2d46d29 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/FallowBallAnalysisTest.java @@ -0,0 +1,291 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.BallAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +/** + * 跟随球号分析算法测试 + * 验证fallowBallAnalysis方法的功能 + */ +@Slf4j +@SpringBootTest +public class FallowBallAnalysisTest { + + @Autowired + private BallAnalysisService ballAnalysisService; + + /** + * 测试高位算法的跟随球号分析 + */ + @Test + public void testHighLevelFallowBallAnalysis() { + log.info("=== 测试高位算法的跟随球号分析 ==="); + + // 输入参数 + String level = "H"; + List firstThreeRedBalls = Arrays.asList(26, 31, 28); // 前3个红球(独立的) + List lastSixRedBalls = Arrays.asList(7, 24, 27, 21, 10, 5);; // 后6个红球(独立的) + Integer blueBall = 16; + + // 执行分析 + List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + // 验证结果 + log.info("高位算法结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("高位算法测试通过!"); + } + + /** + * 测试中位算法的跟随球号分析 + */ + @Test + public void testMiddleLevelFallowBallAnalysis() { + log.info("=== 测试中位算法的跟随球号分析 ==="); + + // 输入参数 + String level = "M"; + List firstThreeRedBalls = Arrays.asList(2, 9, 14); // 前3个红球(独立的) + List lastSixRedBalls = Arrays.asList(20, 25, 32, 1, 15, 23); // 后6个红球(独立的) + Integer blueBall = 12; + + // 执行分析 + List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + // 验证结果 + log.info("中位算法结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("中位算法测试通过!"); + } + + /** + * 测试低位算法的跟随球号分析 + */ + @Test + public void testLowLevelFallowBallAnalysis() { + log.info("=== 测试低位算法的跟随球号分析 ==="); + + // 输入参数 + String level = "L"; + List firstThreeRedBalls = Arrays.asList(3, 7, 11); // 前3个红球(独立的) + List lastSixRedBalls = Arrays.asList(17, 22, 29, 4, 8, 13); // 后6个红球(独立的) + Integer blueBall = 8; + + // 执行分析 + List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + // 验证结果 + log.info("低位算法结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("低位算法测试通过!"); + } + + /** + * 测试参数验证功能 + */ + @Test + public void testParameterValidation() { + log.info("=== 测试参数验证功能 ==="); + + String level = "H"; + List validFirstThree = Arrays.asList(1, 2, 3); + List validLastSix = Arrays.asList(4, 5, 6, 7, 8, 9); + Integer validBlueBall = 1; + + // 测试前3个红球数量错误 + try { + ballAnalysisService.fallowBallAnalysis(level, Arrays.asList(1, 2), validLastSix, validBlueBall); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("前3个红球数量验证通过:{}", e.getMessage()); + } + + // 测试后6个红球数量错误 + try { + ballAnalysisService.fallowBallAnalysis(level, validFirstThree, Arrays.asList(1, 2, 3, 4, 5), validBlueBall); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("后6个红球数量验证通过:{}", e.getMessage()); + } + + // 测试红球范围错误 + try { + ballAnalysisService.fallowBallAnalysis(level, Arrays.asList(1, 2, 34), validLastSix, validBlueBall); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("红球范围验证通过:{}", e.getMessage()); + } + + // 测试蓝球范围错误 + try { + ballAnalysisService.fallowBallAnalysis(level, validFirstThree, validLastSix, 17); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("蓝球范围验证通过:{}", e.getMessage()); + } + + // 测试前3个红球重复 + try { + ballAnalysisService.fallowBallAnalysis(level, Arrays.asList(1, 1, 3), validLastSix, validBlueBall); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("前3个红球重复验证通过:{}", e.getMessage()); + } + + // 测试后6个红球重复 + try { + ballAnalysisService.fallowBallAnalysis(level, validFirstThree, Arrays.asList(4, 5, 6, 7, 8, 8), validBlueBall); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("后6个红球重复验证通过:{}", e.getMessage()); + } + + // 测试级别错误 + try { + ballAnalysisService.fallowBallAnalysis("X", validFirstThree, validLastSix, validBlueBall); + assert false : "应该抛出异常"; + } catch (IllegalArgumentException e) { + log.info("级别验证通过:{}", e.getMessage()); + } + + log.info("参数验证测试全部通过!"); + } + + /** + * 测试数据收集的完整性 + */ + @Test + public void testDataCollectionCompleteness() { + log.info("=== 测试数据收集的完整性 ==="); + + String level = "H"; + List firstThreeRedBalls = Arrays.asList(1, 2, 3); + List lastSixRedBalls = Arrays.asList(4, 5, 6, 7, 8, 9); + Integer blueBall = 1; + + List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("数据收集完整性测试结果:{}", result); + + // 验证数据收集的逻辑 + // 第1个红球从T7表获取10个数字 + // 后6个红球每个从T3表获取26个数字,共6*26=156个数字 + // history_top_100表获取3个数字 + // 前3个红球的后两个,共2个数字 + // 蓝球从T4表获取10个数字 + // 总计:10 + 156 + 3 + 2 + 10 = 181个数字 + + log.info("预期数据收集:10(T7) + 156(T3) + 3(history_top_100) + 2(前3红球后2个) + 10(T4) = 181个数字"); + log.info("数据收集完整性测试通过!"); + } + + /** + * 测试算法稳定性 + */ + @Test + public void testAlgorithmStability() { + log.info("=== 测试算法稳定性 ==="); + + String level = "M"; + List firstThreeRedBalls = Arrays.asList(5, 10, 15); + List lastSixRedBalls = Arrays.asList(18, 24, 30, 12, 19, 26); + Integer blueBall = 6; + + // 多次执行同样的分析,结果应该一致 + List result1 = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + List result2 = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + log.info("第一次结果:{}", result1); + log.info("第二次结果:{}", result2); + + assert result1.equals(result2) : "相同输入应该产生相同输出"; + log.info("算法稳定性测试通过!"); + } + + /** + * 测试边界情况 + */ + @Test + public void testBoundaryConditions() { + log.info("=== 测试边界情况 ==="); + + // 测试最小值边界 + String level = "L"; + List firstThreeRedBalls = Arrays.asList(1, 2, 3); + List lastSixRedBalls = Arrays.asList(4, 5, 6, 7, 8, 9); + Integer blueBall = 1; + + List result1 = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + log.info("最小值边界测试结果:{}", result1); + + // 测试最大值边界 + firstThreeRedBalls = Arrays.asList(31, 32, 33); + lastSixRedBalls = Arrays.asList(25, 26, 27, 28, 29, 30); + blueBall = 16; + + List result2 = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + log.info("最大值边界测试结果:{}", result2); + + // 验证结果 + assert result1.size() == 8 : "最小值边界结果应该包含8个球号"; + assert result2.size() == 8 : "最大值边界结果应该包含8个球号"; + + log.info("边界情况测试通过!"); + } + + /** + * 测试用户提供的示例场景 + * 模拟频次相同导致超过8个球号的情况 + */ + @Test + public void testUserExampleScenario() { + log.info("=== 测试用户示例场景 ==="); + log.info("模拟场景:"); + log.info("当频次相同的球号超过8个时"); + log.info("使用T7表面系数总和进行二次筛选"); + + // 使用特定的红球和蓝球组合来模拟这种情况 + String level = "H"; + List firstThreeRedBalls = Arrays.asList(26, 31, 28); + List lastSixRedBalls = Arrays.asList(7, 24, 27, 21, 10, 5); + Integer blueBall = 16; + + // 执行分析 + List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + // 验证结果 + log.info("示例场景结果:{}", result); + log.info("结果数量:{}", result.size()); + + // 断言 + assert result.size() == 8 : "结果应该包含8个球号"; + assert result.stream().allMatch(ball -> ball >= 1 && ball <= 33) : "所有球号应该在1-33范围内"; + assert result.stream().distinct().count() == result.size() : "不应该有重复的球号"; + + log.info("频次筛选测试完成!"); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/GenerateVipCodesTest.java b/src/test/java/com/xy/xyaicpzs/GenerateVipCodesTest.java new file mode 100644 index 0000000..42e9935 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/GenerateVipCodesTest.java @@ -0,0 +1,126 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.VipCodeService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import jakarta.annotation.Resource; + +/** + * 生成会员码功能测试 + */ +@Slf4j +@SpringBootTest +@ActiveProfiles("test") +public class GenerateVipCodesTest { + + @Resource + private VipCodeService vipCodeService; + + @Test + public void testGenerateVipCodes() { + try { + // 测试生成10个1个月的会员码 + int numCodes = 10; + int vipExpireTime = 1; + + log.info("开始测试生成会员码功能,数量:{},有效月数:{}", numCodes, vipExpireTime); + + int result = vipCodeService.generateVipCodes(numCodes, vipExpireTime); + + log.info("生成会员码测试结果:{}", result); + + } catch (Exception e) { + log.error("生成会员码测试失败:{}", e.getMessage(), e); + } + } + + @Test + public void testGenerateVipCodesWithLargeQuantity() { + try { + // 测试生成50个12个月的会员码 + int numCodes = 50; + int vipExpireTime = 12; + + log.info("开始测试大批量生成会员码功能,数量:{},有效月数:{}", numCodes, vipExpireTime); + + int result = vipCodeService.generateVipCodes(numCodes, vipExpireTime); + + log.info("大批量生成会员码测试结果:{}", result); + + } catch (Exception e) { + log.error("大批量生成会员码测试失败:{}", e.getMessage(), e); + } + } + + @Test + public void testGenerateVipCodesWithInvalidParams() { + try { + // 测试无效参数 + int numCodes = -1; + int vipExpireTime = 1; + + log.info("开始测试无效参数的会员码生成,数量:{},有效月数:{}", numCodes, vipExpireTime); + + vipCodeService.generateVipCodes(numCodes, vipExpireTime); + + } catch (IllegalArgumentException e) { + log.info("预期的异常:{}", e.getMessage()); + } catch (Exception e) { + log.error("意外的异常:{}", e.getMessage(), e); + } + } + + @Test + public void testGetAvailableVipCode() { + try { + // 测试获取1个月的可用会员码 + int vipExpireTime = 1; + + log.info("开始测试获取可用会员码功能,有效月数:{}", vipExpireTime); + + String code = vipCodeService.getAvailableVipCode(vipExpireTime); + + log.info("获取可用会员码测试结果:{}", code); + + } catch (Exception e) { + log.error("获取可用会员码测试失败:{}", e.getMessage(), e); + } + } + + @Test + public void testGetAvailableVipCodeWith12Months() { + try { + // 测试获取12个月的可用会员码 + int vipExpireTime = 12; + + log.info("开始测试获取12个月可用会员码功能,有效月数:{}", vipExpireTime); + + String code = vipCodeService.getAvailableVipCode(vipExpireTime); + + log.info("获取12个月可用会员码测试结果:{}", code); + + } catch (Exception e) { + log.error("获取12个月可用会员码测试失败:{}", e.getMessage(), e); + } + } + + @Test + public void testGetAvailableVipCodeWithInvalidMonth() { + try { + // 测试无效的月数参数 + int vipExpireTime = 6; + + log.info("开始测试无效月数参数的获取可用会员码,有效月数:{}", vipExpireTime); + + vipCodeService.getAvailableVipCode(vipExpireTime); + + } catch (IllegalArgumentException e) { + log.info("预期的异常:{}", e.getMessage()); + } catch (Exception e) { + log.error("意外的异常:{}", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/UserControllerTest.java b/src/test/java/com/xy/xyaicpzs/UserControllerTest.java new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/UserControllerTest.java @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/VipCodeActivateTest.java b/src/test/java/com/xy/xyaicpzs/VipCodeActivateTest.java new file mode 100644 index 0000000..072158c --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/VipCodeActivateTest.java @@ -0,0 +1,76 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.service.VipCodeService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import jakarta.annotation.Resource; + +/** + * 会员码激活功能测试 + */ +@Slf4j +@SpringBootTest +@ActiveProfiles("test") +public class VipCodeActivateTest { + + @Resource + private VipCodeService vipCodeService; + + @Test + public void testActivateVipCode() { + try { + // 测试参数:用户ID=1,会员码=TEST_CODE_001 + // 实际测试时需要确保数据库中有对应的测试数据 + Long userId = 1L; + String code = "TEST_CODE_001"; + + log.info("开始测试会员码激活功能,用户ID:{},会员码:{}", userId, code); + + boolean result = vipCodeService.activateVipCode(userId, code); + + log.info("会员码激活测试结果:{}", result); + + } catch (Exception e) { + log.error("会员码激活测试失败:{}", e.getMessage(), e); + } + } + + @Test + public void testActivateVipCodeWithInvalidUser() { + try { + // 测试无效用户ID + Long userId = 999999L; + String code = "TEST_CODE_001"; + + log.info("开始测试无效用户ID的会员码激活,用户ID:{},会员码:{}", userId, code); + + vipCodeService.activateVipCode(userId, code); + + } catch (IllegalArgumentException e) { + log.info("预期的异常:{}", e.getMessage()); + } catch (Exception e) { + log.error("意外的异常:{}", e.getMessage(), e); + } + } + + @Test + public void testActivateVipCodeWithInvalidCode() { + try { + // 测试无效会员码 + Long userId = 1L; + String code = "INVALID_CODE"; + + log.info("开始测试无效会员码的激活,用户ID:{},会员码:{}", userId, code); + + vipCodeService.activateVipCode(userId, code); + + } catch (IllegalArgumentException e) { + log.info("预期的异常:{}", e.getMessage()); + } catch (Exception e) { + log.error("意外的异常:{}", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/XyAiCpzsApplicationTests.java b/src/test/java/com/xy/xyaicpzs/XyAiCpzsApplicationTests.java new file mode 100644 index 0000000..5fc41f3 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/XyAiCpzsApplicationTests.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class XyAiCpzsApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/xy/xyaicpzs/mapper/T7MapperTest.java b/src/test/java/com/xy/xyaicpzs/mapper/T7MapperTest.java new file mode 100644 index 0000000..2a322fa --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/mapper/T7MapperTest.java @@ -0,0 +1,15 @@ +package com.xy.xyaicpzs.mapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +class T7MapperTest { + + @Autowired + private T7Mapper t7Mapper; + +} \ No newline at end of file diff --git a/userController.txt b/userController.txt new file mode 100644 index 0000000..8348b60 --- /dev/null +++ b/userController.txt @@ -0,0 +1,164 @@ +2 签署 JWT +扣子的 JWT 生成方式及部分参数定义沿用业界统一流程规范,但 JWT 中 Header 和 Payload 部分由扣子平台自行定义。 +在 JWT(JSON Web Tokens)的流程中,通常使用私钥来签署(sign)token。JWT 包含三部分,即 Header、Payload 和 signature,其中 header 和 payload 由参数拼接而成,signature 根据指定的签名算法和私钥对 Header 和 Payload 自动计算生成。三部分之间用点(.)分隔。详细的签署方式可参考JWT 官方文档。 +Header 和 Payload +扣子平台对 Header 和 Payload 的定义如下: +Header +Header 部分的参数定义如下: +参数 +类型 +是否必选 +说明 +alg +String +必选 +签名使用的加密算法。固定为 RS256,即非对称加密算法,一种基于 RSA(非对称加密算法)+ SHA256(安全哈希函数)的签名算法,该算法使用私钥进行签名,公钥进行验证。 +typ +String +必选 +固定为 JWT。 +kid +String +必选 +OAuth 应用的公钥指纹,可以在OAuth 应用页面找到这个应用,在操作列单击编辑图标,进入配置页面查看公钥指纹。 + + +Header 示例如下: +{ + "alg": "RS256", // 固定为RS256 + "typ": "JWT", // 固定为 JWT + "kid": "gdehvaDegW....." // OAuth 应用的公钥指纹 +} + +Payload: +Payload 部分的参数定义如下: +参数 +类型 +是否必选 +说明 +iss +String +必选 +OAuth 应用的 ID,可以在OAuth 应用页面查看。 +aud +String +必选 +扣子 API 的 Endpoint,即 api.coze.cn。 +iat +Integer +必选 +JWT 开始生效的时间,Unixtime 时间戳格式,精确到秒。一般为当前时刻。 +exp +Integer +必选 +JWT 过期的时间,Unixtime 时间戳格式,精确到秒。必须晚于 iat。 +jti +String +必选 +随机字符串,用于防止重放攻击。建议长度大于 32 字节。每次签署 JWT 时应指定为不同的字符串。 +session_name +String +可选 +访问令牌的会话标识。目前仅限在会话隔离场景下使用,即将 session_name 指定为用户在业务侧的 UID,以此区分不同业务侧用户的对话历史。 +若未指定 session_name,不同用户的对话历史可能会掺杂在一起。 +会话隔离的详细实现方法请参见如何实现会话隔离。 + +session_context +Object +可选 +会话上下文信息,包含设备相关信息等。 + +session_context.device_info +Object +可选 +用于配置设备相关信息,扣子平台基于该部分信息对设备做用量管控以及账单记录。 +该参数为企业白版白名单功能,需要联系扣子商务经理开通后才能使用。硬件设备用量管控的具体操作可参考硬件设备用量查询和配额管控。 +session_context.device_info.device_id +String +可选 +IoT 等硬件设备 ID,一个设备对应一个唯一的设备号。 +当需要记录设备用量或对设备用量进行管控时,需要填写该参数,否则,无法对设备进行用量管控,用量统计页面对应的设备 ID 将显示为 N/A。 +session_context.device_info.custom_consumer + +String +可选 +自定义维度的实体 ID,你可以根据业务需要进行设置,例如 APP 上的用户名等。 +当需要记录设备用量或对设备用量进行管控,需要填写该参数,否则,无法对设备进行用量管控,用量统计页面对应的自定义 ID 将显示为 N/A。 +device_id 和 custom_consumer 建议选择其中一个即可。 +custom_consumer 参数用于设备用量管控,与对话等 API 传入的 user_id 无关,user_id 主要用于上下文、数据库隔离等场景。 +出于数据隐私及信息安全等方面的考虑,不建议使用业务系统中定义的用户敏感标识(如手机号等)作为 custom_consumer 的值。 + + +Payload 示例如下: +{ + "iss": "310000000002", // OAuth 应用的 ID + "aud": "api.coze.cn", // 扣子 API 的 Endpoint + "iat": 1516239022, // JWT 开始生效的时间,秒级时间戳 + "exp": 1516259022, // JWT 过期时间,秒级时间戳 + "jti": "fhjashjgkhalskj", // 随机字符串,防止重放攻击 + "session_name": "user_2222", //用户在业务侧的 UID + "session_context": { + "device_info": { + "device_id": "1234567890" // IoT 等硬件设备的唯一标识 ID + } + } +} + +示例代码 +你可以直接参考以下示例代码签署 JWT。 +# You must run `pip install PyJWT cryptography` to install the PyJWT and the cryptography packages in order to use this script. + +#!/usr/bin/env python3 +import sys +import time +import uuid + +import jwt + +# 替换为你的实际 Coze App 私钥 +signing_key = ''' +-----BEGIN PRIVATE KEY----- +xxxxxxxxxxxxxxxxxx +-----END PRIVATE KEY----- +''' + +payload = { + 'iat': int(time.time()), + 'exp': int(time.time()) + 600, + "jti": str(uuid.uuid4()), + 'aud': 'api.coze.cn', + 'iss': '1127900106117' # 替换为你的实际 Coze App ID +} + +headers = { + 'kid': '_v0VjcMlLdQc3tRTD3jC5Xz29TUnKQOhtuD5k-gpyf4' # 替换为你的实际 Coze App 公钥指纹 +} + +# Create JWT with headers +encoded_jwt = jwt.encode(payload, signing_key, algorithm='RS256', headers=headers) + +print(f"JWT: {encoded_jwt}") + +最终生成的 JWT 示例如下: +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InZZd2ZsdFR1OWZBbWtwWFhSdnR5UmREc3RONVMzZWNFcDFqVzB6dVQyRE****.eyJpc3MiOiIzMTAwMDAwMDAwMDIiLCJhdWQiOiJhcGkuY296ZS5jb20iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTkxNjI1OTAyMiwianRpIjoiZmhqaGFsc2tqZmFkc2pld3F****.CuoiCCF-nHFyGmu2EKlwFoyd3uDyKQ3Drc1CrXQyMVySTzZlZd2M7zKWsziB3AktwbUZiRJlQ1HbghR05CW2YRHwKL4-dlJ4koR3onU7iQAO5DkPCaIxbAuTsQobtCAdkkZTg8gav9EnN1QN_1xq0w8BzuuhS7wCeY8UbaskkTK9GnO4eU9tEINmVw-2CrfB-kNbEHlEDwXfcrb4YPpkw3GhmuPShenNLObfSWS0CqIyakXL8qD5AgXLoB-SejAsRdzloSUInNXENJHfSVMkThxRhJy7yEjX3BmculC54fMKENRfLElBqwJyLLUjeRHsYnaru2ca4W8_yaPJ7F**** + + + + + +3 获取访问令牌 +应用程序调用 通过 JWT 获取 Oauth Access Token API ,请求 Header 中携带 JWT,扣子服务端会在响应中通过 access_token 字段返回访问令牌。 +请求示例如下: +curl --location --request POST 'https://api.coze.cn/api/permission/oauth2/token' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InZZd2ZsdFR1OWZBbWtwWFhSdnR5UmREc3RONVMzZWNFcDFqVzB6dVQyRE****.eyJpc3MiOiIzMTAwMDAwMDAwMDIiLCJhdWQiOiJhcGkuY296ZS5jb20iLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTkxNjI1OTAyMiwianRpIjoiZmhqaGFsc2tqZmFkc2pld3F****.CuoiCCF-nHFyGmu2EKlwFoyd3uDyKQ3Drc1CrXQyMVySTzZlZd2M7zKWsziB3AktwbUZiRJlQ1HbghR05CW2YRHwKL4-dlJ4koR3onU7iQAO5DkPCaIxbAuTsQobtCAdkkZTg8gav9EnN1QN_1xq0w8BzuuhS7wCeY8UbaskkTK9GnO4eU9tEINmVw-2CrfB-kNbEHlEDwXfcrb4YPpkw3GhmuPShenNLObfSWS0CqIyakXL8qD5AgXLoB-SejAsRdzloSUInNXENJHfSVMkThxRhJy7yEjX3BmculC54fMKENRfLElBqwJyLLUjeRHsYnaru2ca4W8_yaPJ7F****' \ +--data '{ + "duration_seconds": 86399, + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer" +}' + +返回示例如下: +{ + "access_token": "czs_RQOhsc7vmUzK4bNgb7hn4wqOgRBYAO6xvpFHNbnl6RiQJX3cSXSguIhFDzgy****", + "expires_in": 1721135859 +} \ No newline at end of file diff --git a/会员码激活API使用说明.md b/会员码激活API使用说明.md new file mode 100644 index 0000000..c3f0a42 --- /dev/null +++ b/会员码激活API使用说明.md @@ -0,0 +1,355 @@ +# 会员码管理API使用说明 + +## 功能概述 + +会员码管理系统包含两个主要功能: +1. **会员码生成**:管理员可以批量生成会员码,用于分发给用户 +2. **会员码激活**:用户通过输入有效的会员码来延长或激活会员服务 + +系统会校验会员码的有效性,并根据会员码设定的月数来更新用户的会员到期时间。 + +## 接口信息 + +### 1. 批量生成会员码 + +**接口地址:** `POST /vip-code/generate` + +**请求参数:** + +```json +{ + "numCodes": 100, + "vipExpireTime": 12 +} +``` + +**参数说明:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| numCodes | Integer | 是 | 生成数量(1-1000) | +| vipExpireTime | Integer | 是 | 会员有效月数 | + +**响应示例:** + +成功响应: +```json +{ + "code": 0, + "data": 100, + "message": "ok" +} +``` + +失败响应: +```json +{ + "code": 40000, + "data": null, + "message": "生成数量必须大于0" +} +``` + +### 2. 获取可用会员码 + +**接口地址:** `GET /vip-code/available?vipExpireTime=1` + +**请求参数:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| vipExpireTime | Integer | 是 | 会员有效月数(1或12) | + +**响应示例:** + +成功响应: +```json +{ + "code": 0, + "data": "ABC123DEF456GHI7", + "message": "ok" +} +``` + +失败响应: +```json +{ + "code": 40400, + "data": null, + "message": "没有找到可用的会员码" +} +``` + +### 3. 激活会员码 + +**接口地址:** `POST /user/activate-vip` + +**请求参数:** + +```json +{ + "userId": 1, + "code": "VIP_CODE_123456" +} +``` + +**参数说明:** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| userId | Long | 是 | 用户ID | +| code | String | 是 | 会员码 | + +**响应示例:** + +成功响应: +```json +{ + "code": 0, + "data": true, + "message": "ok" +} +``` + +失败响应: +```json +{ + "code": 40000, + "data": null, + "message": "会员码不存在或无效" +} +``` + +## 业务逻辑 + +### 会员码生成流程 + +1. **参数校验** + - 验证生成数量大于0且不超过1000 + - 验证会员有效月数大于0 + +2. **生成唯一会员码** + - 使用随机算法生成16位会员码 + - 检查数据库确保会员码唯一性 + - 重复生成直到达到指定数量 + +3. **会员编号分配** + - 查询当前最大会员编号 + - 从最大编号+1开始连续分配 + +4. **批量插入数据库** + - 使用事务确保数据一致性 + - 批量插入提高性能 + +### 获取可用会员码流程 + +1. **参数校验** + - 验证会员有效月数只能是1或12 + +2. **数据库查询** + - 查询指定月数的未使用会员码 + - 按创建时间升序排列,获取最早的一个 + - 确保返回的会员码处于可用状态 + +3. **结果返回** + - 如果找到可用会员码,返回会员码字符串 + - 如果没有找到,返回错误信息 + +### 会员码激活流程 + +1. **参数校验** + - 验证userId不为空 + - 验证会员码不为空或空字符串 + +2. **用户存在性校验** + - 检查用户ID对应的用户是否存在 + - 不存在则抛出"用户不存在"异常 + +3. **会员码有效性校验** + - 根据会员码查询vip_code表 + - 检查会员码是否存在 + - 检查会员码是否已被使用(isUse字段) + +4. **会员时间计算** + - 获取用户当前的会员到期时间(vipExpire) + - 如果当前时间晚于会员到期时间或会员到期时间为空,则从当前时间开始计算 + - 如果当前时间早于会员到期时间,则从会员到期时间开始计算 + - 添加会员码对应的月数(vipExpireTime字段) + +5. **数据库更新** + - 更新用户表:设置isVip=1,更新vipExpire为新计算的时间 + - 更新会员码表:设置isUse=1,标记为已使用 + +### 数据库表结构 + +**vip_code表:** +- `id`: 主键 +- `code`: 会员码(唯一) +- `vipExpireTime`: 会员有效月数 +- `vipNumber`: 会员编号 +- `isUse`: 是否使用(0-未使用,1-已使用) +- `createTime`: 创建时间 +- `updateTime`: 更新时间 + +**user表:** +- `isVip`: 是否会员(0-非会员,1-会员) +- `vipExpire`: 会员到期时间 + +## 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 40000 | 参数错误(用户ID为空、会员码为空、用户不存在、会员码无效等) | +| 50000 | 系统错误(数据库操作失败等) | + +## 使用示例 + +### cURL 示例 + +**生成会员码:** +```bash +curl -X POST http://localhost:8080/vip-code/generate \ + -H "Content-Type: application/json" \ + -d '{ + "numCodes": 100, + "vipExpireTime": 12 + }' +``` + +**获取可用会员码:** +```bash +curl -X GET "http://localhost:8080/vip-code/available?vipExpireTime=1" +``` + +**激活会员码:** +```bash +curl -X POST http://localhost:8080/user/activate-vip \ + -H "Content-Type: application/json" \ + -d '{ + "userId": 1, + "code": "VIP_CODE_123456" + }' +``` + +### JavaScript 示例 + +**生成会员码:** +```javascript +const generateVipCodes = async (numCodes, vipExpireTime) => { + try { + const response = await fetch('/vip-code/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + numCodes: numCodes, + vipExpireTime: vipExpireTime + }) + }); + + const result = await response.json(); + if (result.code === 0) { + console.log('会员码生成成功,数量:', result.data); + return result.data; + } else { + console.error('会员码生成失败:', result.message); + return 0; + } + } catch (error) { + console.error('请求失败:', error); + return 0; + } +}; +``` + +**获取可用会员码:** +```javascript +const getAvailableVipCode = async (vipExpireTime) => { + try { + const response = await fetch(`/vip-code/available?vipExpireTime=${vipExpireTime}`, { + method: 'GET' + }); + + const result = await response.json(); + if (result.code === 0) { + console.log('获取可用会员码成功:', result.data); + return result.data; + } else { + console.error('获取可用会员码失败:', result.message); + return null; + } + } catch (error) { + console.error('请求失败:', error); + return null; + } +}; +``` + +**激活会员码:** +```javascript +const activateVipCode = async (userId, code) => { + try { + const response = await fetch('/user/activate-vip', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userId: userId, + code: code + }) + }); + + const result = await response.json(); + if (result.code === 0) { + console.log('会员码激活成功'); + return true; + } else { + console.error('会员码激活失败:', result.message); + return false; + } + } catch (error) { + console.error('请求失败:', error); + return false; + } +}; +``` + +## 注意事项 + +### 会员码生成 +1. **批量限制**:单次最多生成1000个会员码,避免系统负载过高 +2. **唯一性保证**:系统会检查数据库确保生成的会员码唯一 +3. **事务管理**:使用数据库事务确保批量插入的数据一致性 +4. **性能优化**:使用批量插入提高大量数据的插入性能 + +### 获取可用会员码 +1. **参数限制**:只支持1个月和12个月两种类型的会员码 +2. **优先级策略**:按创建时间升序返回,优先返回最早创建的会员码 +3. **状态检查**:只返回未使用状态的会员码 +4. **库存管理**:如果指定类型的会员码已用完,会返回相应错误信息 + +### 会员码激活 +1. **事务管理**:整个激活过程使用数据库事务,确保数据一致性 +2. **重复使用**:每个会员码只能使用一次,使用后会被标记为已使用 +3. **时间计算**:会员时间会根据用户当前状态智能计算,不会丢失已有的会员时间 +4. **日志记录**:所有操作都有详细的日志记录,便于问题排查 +5. **异常处理**:完善的异常处理机制,提供清晰的错误信息 + +## 测试数据 + +在测试环境中,可以在vip_code表中插入测试数据: + +```sql +INSERT INTO vip_code (id, code, vipExpireTime, vipNumber, isUse, createTime, updateTime) +VALUES (1, 'TEST_CODE_001', 1, 1001, 0, NOW(), NOW()); + +INSERT INTO vip_code (id, code, vipExpireTime, vipNumber, isUse, createTime, updateTime) +VALUES (2, 'TEST_CODE_002', 3, 1002, 0, NOW(), NOW()); + +INSERT INTO vip_code (id, code, vipExpireTime, vipNumber, isUse, createTime, updateTime) +VALUES (3, 'TEST_CODE_003', 12, 1003, 0, NOW(), NOW()); +``` + +这些测试数据分别对应1个月、3个月和12个月的会员时长。 \ No newline at end of file