update: 提交所有修改和新增功能代码
This commit is contained in:
484
Excel导入使用说明.md
484
Excel导入使用说明.md
@@ -1,484 +0,0 @@
|
||||
# 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导入接口。
|
||||
@@ -1,182 +0,0 @@
|
||||
# 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. 分页查询采用内存分页,大数据量时建议使用数据库分页优化
|
||||
13
pom.xml
13
pom.xml
@@ -40,6 +40,12 @@
|
||||
<version>3.1.1281</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 腾讯云 COS 依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.qcloud</groupId>
|
||||
<artifactId>cos_api</artifactId>
|
||||
<version>5.6.155</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.nls</groupId>
|
||||
@@ -83,7 +89,12 @@
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 腾讯云 COS 依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java</artifactId>
|
||||
<version>3.1.1412</version><!-- 注:这里只是示例版本号(可直接使用),可获取并替换为 最新的版本号,注意不要使用4.0.x版本(非最新版本) -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
11
sql/add_prize_pool_column.sql
Normal file
11
sql/add_prize_pool_column.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- 为 lottery_draws 表添加奖池字段
|
||||
ALTER TABLE `lottery_draws`
|
||||
ADD COLUMN `prizePool` BIGINT DEFAULT 0 COMMENT '奖池' AFTER `blueBall`;
|
||||
|
||||
-- 为 dlt_draw_record 表添加奖池字段
|
||||
ALTER TABLE `dlt_draw_record`
|
||||
ADD COLUMN `prizePool` BIGINT DEFAULT 0 COMMENT '奖池' AFTER `backBall2`;
|
||||
|
||||
-- 验证字段是否添加成功
|
||||
-- SELECT * FROM lottery_draws LIMIT 1;
|
||||
-- SELECT * FROM dlt_draw_record LIMIT 1;
|
||||
9
sql/add_special_rule_status.sql
Normal file
9
sql/add_special_rule_status.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- 为双色球开奖记录表添加特别规定期间标识字段
|
||||
-- 用于标识该期是否处于特别规定期间(福运奖期间)
|
||||
|
||||
ALTER TABLE lottery_draws ADD COLUMN is_special_period TINYINT DEFAULT 0 COMMENT '是否处于特别规定期间:0-否,1-是';
|
||||
|
||||
-- 更新说明:
|
||||
-- 特别规定启动条件:当奖池资金 >= 15亿元时开始执行
|
||||
-- 特别规定停止条件:执行特别规定后,某期开奖后奖池资金 < 3亿元时停止
|
||||
-- 在特别规定期间,3个红球匹配可获得福运奖(5元)
|
||||
37
sql/ddl.sql
37
sql/ddl.sql
@@ -166,7 +166,8 @@ CREATE TABLE IF NOT EXISTS `lottery_draws` (
|
||||
`redBall4` INT NOT NULL COMMENT '红4',
|
||||
`redBall5` INT NOT NULL COMMENT '红5',
|
||||
`redBall6` INT NOT NULL COMMENT '红6',
|
||||
`blueBall` INT NOT NULL COMMENT '蓝球'
|
||||
`blueBall` INT NOT NULL COMMENT '蓝球',
|
||||
`prizePool` BIGINT NULL COMMENT '奖池'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT = '彩票开奖信息表';
|
||||
|
||||
@@ -183,6 +184,10 @@ create table if not exists user
|
||||
userPassword varchar(512) not null comment '密码',
|
||||
isVip int default 0 not null comment '是否会员:0-非会员,1-会员',
|
||||
vipExpire datetime null comment '会员到期时间',
|
||||
vipType varchar(50) default '体验会员' null comment '套餐类别(体验会员/月度会员/年度会员)',
|
||||
location varchar(100) null comment '所在省市',
|
||||
preference varchar(100) null comment '彩票偏好(双色球/大乐透等)',
|
||||
channel varchar(100) null comment '获客渠道',
|
||||
# vipNum int not null comment '会员编号',
|
||||
`status` tinyint DEFAULT '0' COMMENT '状态0正常1不正常',
|
||||
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
|
||||
@@ -273,7 +278,8 @@ CREATE TABLE IF NOT EXISTS `dlt_draw_record` (
|
||||
`frontBall4` INT NOT NULL COMMENT '前区4',
|
||||
`frontBall5` INT NOT NULL COMMENT '前区5',
|
||||
`backBall1` INT NOT NULL COMMENT '后区1',
|
||||
`backBall2` INT NOT NULL COMMENT '后区2'
|
||||
`backBall2` INT NOT NULL COMMENT '后区2',
|
||||
`prizePool` BIGINT NULL COMMENT '奖池'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透开奖信息表';
|
||||
|
||||
@@ -438,3 +444,30 @@ CREATE TABLE IF NOT EXISTS `dlt_predict_record` (
|
||||
`bonus` BIGINT default 0 NOT NULL COMMENT '奖金'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透推测记录表';
|
||||
|
||||
|
||||
-- 公告管理表
|
||||
CREATE TABLE IF NOT EXISTS `announcement` (
|
||||
`id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
`title` VARCHAR(200) NOT NULL COMMENT '公告标题',
|
||||
`content` TEXT NOT NULL COMMENT '公告详情内容',
|
||||
`publisherId` BIGINT NOT NULL COMMENT '发布人ID',
|
||||
`publisherName` VARCHAR(256) NOT NULL COMMENT '发布人名称',
|
||||
`status` TINYINT DEFAULT 1 NOT NULL COMMENT '公告状态:0-草稿,1-已发布,2-已下架',
|
||||
`priority` TINYINT DEFAULT 0 NOT NULL COMMENT '优先级:0-普通,1-置顶',
|
||||
`viewCount` INT DEFAULT 0 NOT NULL COMMENT '浏览次数',
|
||||
`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 '是否删除:0-未删除,1-已删除'
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4 COMMENT = '公告管理表';
|
||||
|
||||
-- 为已存在的user表添加新字段
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `vipType` VARCHAR(50) DEFAULT '体验会员' NULL COMMENT '套餐类别(体验会员/月度会员/年度会员)' AFTER `vipExpire`,
|
||||
ADD COLUMN `location` VARCHAR(100) NULL COMMENT '所在省市' AFTER `vipType`,
|
||||
ADD COLUMN `preference` VARCHAR(100) NULL COMMENT '彩票偏好(双色球/大乐透等)' AFTER `location`,
|
||||
ADD COLUMN `channel` VARCHAR(100) NULL COMMENT '获客渠道' AFTER `preference`;
|
||||
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `vipType` VARCHAR(50) DEFAULT '体验会员' NULL COMMENT '套餐类别(体验会员/月度会员/年度会员)' AFTER `vipExpire`
|
||||
|
||||
14
sql/update_prize_pool_to_decimal.sql
Normal file
14
sql/update_prize_pool_to_decimal.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- 修改奖池字段类型:从 BIGINT 改为 DECIMAL(10,2)
|
||||
-- 单位从"元"改为"亿元"
|
||||
|
||||
-- 修改 lottery_draws 表的奖池字段
|
||||
ALTER TABLE `lottery_draws`
|
||||
MODIFY COLUMN `prizePool` DECIMAL(10,2) DEFAULT 0.00 COMMENT '奖池(单位:亿元)';
|
||||
|
||||
-- 修改 dlt_draw_record 表的奖池字段
|
||||
ALTER TABLE `dlt_draw_record`
|
||||
MODIFY COLUMN `prizePool` DECIMAL(10,2) DEFAULT 0.00 COMMENT '奖池(单位:亿元)';
|
||||
|
||||
-- 验证字段类型是否修改成功
|
||||
-- SHOW COLUMNS FROM lottery_draws LIKE 'prizePool';
|
||||
-- SHOW COLUMNS FROM dlt_draw_record LIKE 'prizePool';
|
||||
93
sql/user_viptype_update.sql
Normal file
93
sql/user_viptype_update.sql
Normal file
@@ -0,0 +1,93 @@
|
||||
-- ============================================
|
||||
-- 用户表vipType字段更新SQL脚本
|
||||
-- 功能:为user表添加vipType等字段,并设置默认值
|
||||
-- 日期:2026-01-26
|
||||
-- ============================================
|
||||
|
||||
-- 方案一:为已存在的user表添加新字段(推荐用于已有数据的表)
|
||||
-- 如果字段已存在,请先删除后再添加,或者直接跳过此步骤
|
||||
|
||||
-- 添加vipType字段,默认值为'体验会员'
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `vipType` VARCHAR(50) DEFAULT '体验会员' NULL COMMENT '套餐类别(体验会员/月度会员/年度会员)' AFTER `vipExpire`;
|
||||
|
||||
-- 添加location字段
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `location` VARCHAR(100) NULL COMMENT '所在省市' AFTER `vipType`;
|
||||
|
||||
-- 添加preference字段
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `preference` VARCHAR(100) NULL COMMENT '彩票偏好(双色球/大乐透等)' AFTER `location`;
|
||||
|
||||
-- 添加channel字段
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `channel` VARCHAR(100) NULL COMMENT '获客渠道' AFTER `preference`;
|
||||
|
||||
-- ============================================
|
||||
-- 如果字段已存在但需要修改默认值,使用以下语句
|
||||
-- ============================================
|
||||
|
||||
-- 修改vipType字段,设置默认值为'体验会员'
|
||||
ALTER TABLE `user`
|
||||
MODIFY COLUMN `vipType` VARCHAR(50) DEFAULT '体验会员' NULL COMMENT '套餐类别(体验会员/月度会员/年度会员)';
|
||||
|
||||
-- ============================================
|
||||
-- 为已有用户设置默认vipType(可选)
|
||||
-- ============================================
|
||||
|
||||
-- 将所有vipType为NULL的用户设置为'体验会员'
|
||||
UPDATE `user` SET `vipType` = '体验会员' WHERE `vipType` IS NULL;
|
||||
|
||||
-- 将所有非会员用户设置为'体验会员'
|
||||
UPDATE `user` SET `vipType` = '体验会员' WHERE `isVip` = 0 AND (`vipType` IS NULL OR `vipType` = '');
|
||||
|
||||
-- ============================================
|
||||
-- 验证SQL
|
||||
-- ============================================
|
||||
|
||||
-- 查看user表结构
|
||||
DESC `user`;
|
||||
|
||||
-- 查看vipType字段的分布情况
|
||||
SELECT `vipType`, COUNT(*) as count FROM `user` GROUP BY `vipType`;
|
||||
|
||||
-- 查看会员类型分布
|
||||
SELECT
|
||||
`isVip`,
|
||||
`vipType`,
|
||||
COUNT(*) as count,
|
||||
CASE
|
||||
WHEN `vipExpire` IS NULL THEN '无到期时间'
|
||||
WHEN `vipExpire` < NOW() THEN '已过期'
|
||||
ELSE '未过期'
|
||||
END as expire_status
|
||||
FROM `user`
|
||||
GROUP BY `isVip`, `vipType`, expire_status;
|
||||
|
||||
-- ============================================
|
||||
-- 说明
|
||||
-- ============================================
|
||||
-- vipType字段取值说明:
|
||||
-- 1. '体验会员' - 默认值,所有新注册用户和非会员用户
|
||||
-- 2. '月度会员' - 激活码时效 < 12个月的用户
|
||||
-- 3. '年度会员' - 激活码时效 >= 12个月的用户
|
||||
--
|
||||
-- 激活会员码时的逻辑:
|
||||
-- - 如果激活码时效 < 12个月,设置vipType为'月度会员'
|
||||
-- - 如果激活码时效 >= 12个月,设置vipType为'年度会员'
|
||||
-- ============================================
|
||||
|
||||
|
||||
|
||||
-- 为 lottery_draws 表添加奖池字段
|
||||
ALTER TABLE `lottery_draws`
|
||||
ADD COLUMN `prizePool` BIGINT DEFAULT 0 COMMENT '奖池' AFTER `blueBall`;
|
||||
|
||||
-- 为 dlt_draw_record 表添加奖池字段
|
||||
ALTER TABLE `dlt_draw_record`
|
||||
ADD COLUMN `prizePool` BIGINT DEFAULT 0 COMMENT '奖池' AFTER `backBall2`;
|
||||
|
||||
-- 验证字段是否添加成功
|
||||
-- SELECT * FROM lottery_draws LIMIT 1;
|
||||
-- SELECT * FROM dlt_draw_record LIMIT 1;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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<ApplicationResult> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.xy.xyaicpzs.common.requset;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 公告添加请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "公告添加请求")
|
||||
public class AnnouncementAddRequest {
|
||||
|
||||
/**
|
||||
* 公告标题
|
||||
*/
|
||||
@Schema(description = "公告标题", required = true)
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 公告详情内容
|
||||
*/
|
||||
@Schema(description = "公告详情内容", required = true)
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 公告状态:0-草稿,1-已发布,2-已下架
|
||||
*/
|
||||
@Schema(description = "公告状态:0-草稿,1-已发布,2-已下架,默认为1-已发布")
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 优先级:0-普通,1-置顶
|
||||
*/
|
||||
@Schema(description = "优先级:0-普通,1-置顶,默认为0-普通")
|
||||
private Integer priority = 0;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.xy.xyaicpzs.common.requset;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 公告查询请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "公告查询请求")
|
||||
public class AnnouncementQueryRequest {
|
||||
|
||||
/**
|
||||
* 当前页码
|
||||
*/
|
||||
@Schema(description = "当前页码")
|
||||
private long current = 1;
|
||||
|
||||
/**
|
||||
* 页面大小
|
||||
*/
|
||||
@Schema(description = "页面大小")
|
||||
private long pageSize = 10;
|
||||
|
||||
/**
|
||||
* 公告标题(模糊查询)
|
||||
*/
|
||||
@Schema(description = "公告标题(模糊查询)")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 公告状态:0-草稿,1-已发布,2-已下架
|
||||
*/
|
||||
@Schema(description = "公告状态:0-草稿,1-已发布,2-已下架")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 优先级:0-普通,1-置顶
|
||||
*/
|
||||
@Schema(description = "优先级:0-普通,1-置顶")
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 发布人ID
|
||||
*/
|
||||
@Schema(description = "发布人ID")
|
||||
private Long publisherId;
|
||||
|
||||
/**
|
||||
* 发布人名称(模糊查询)
|
||||
*/
|
||||
@Schema(description = "发布人名称(模糊查询)")
|
||||
private String publisherName;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.xy.xyaicpzs.common.requset;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 公告更新请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "公告更新请求")
|
||||
public class AnnouncementUpdateRequest {
|
||||
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
@Schema(description = "唯一标识符", required = true)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 公告标题
|
||||
*/
|
||||
@Schema(description = "公告标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 公告详情内容
|
||||
*/
|
||||
@Schema(description = "公告详情内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 公告状态:0-草稿,1-已发布,2-已下架
|
||||
*/
|
||||
@Schema(description = "公告状态:0-草稿,1-已发布,2-已下架")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 优先级:0-普通,1-置顶
|
||||
*/
|
||||
@Schema(description = "优先级:0-普通,1-置顶")
|
||||
private Integer priority;
|
||||
}
|
||||
63
src/main/java/com/xy/xyaicpzs/config/CosConfig.java
Normal file
63
src/main/java/com/xy/xyaicpzs/config/CosConfig.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.xy.xyaicpzs.config;
|
||||
|
||||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.ClientConfig;
|
||||
import com.qcloud.cos.auth.BasicCOSCredentials;
|
||||
import com.qcloud.cos.auth.COSCredentials;
|
||||
import com.qcloud.cos.region.Region;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS 配置类
|
||||
*
|
||||
* @author xy
|
||||
*/
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "cos")
|
||||
@Data
|
||||
public class CosConfig {
|
||||
|
||||
/**
|
||||
* SecretId
|
||||
*/
|
||||
private String secretId;
|
||||
|
||||
/**
|
||||
* SecretKey
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 存储桶名称
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 地域
|
||||
*/
|
||||
private String region;
|
||||
|
||||
/**
|
||||
* 访问域名
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 创建 COSClient 实例
|
||||
*/
|
||||
@Bean
|
||||
public COSClient cosClient() {
|
||||
// 初始化用户身份信息(secretId, secretKey)
|
||||
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
|
||||
|
||||
// 设置 bucket 的地域
|
||||
Region regionObj = new Region(region);
|
||||
ClientConfig clientConfig = new ClientConfig(regionObj);
|
||||
|
||||
// 生成 cos 客户端
|
||||
return new COSClient(cred, clientConfig);
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/xy/xyaicpzs/config/JacksonConfig.java
Normal file
37
src/main/java/com/xy/xyaicpzs/config/JacksonConfig.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.xy.xyaicpzs.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Jackson 配置类
|
||||
* 解决前端无法接收完整 Long 类型 ID 的问题
|
||||
*
|
||||
* @author xy
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
/**
|
||||
* Jackson 全局配置
|
||||
* 将 Long、BigInteger 类型序列化为字符串,避免前端精度丢失
|
||||
*
|
||||
* @return Jackson2ObjectMapperBuilderCustomizer
|
||||
*/
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
|
||||
return jacksonObjectMapperBuilder -> {
|
||||
jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance);
|
||||
jacksonObjectMapperBuilder.serializerByType(Long.TYPE, ToStringSerializer.instance);
|
||||
jacksonObjectMapperBuilder.serializerByType(BigInteger.class, ToStringSerializer.instance);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
package com.xy.xyaicpzs.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* ObjectMapper配置类
|
||||
* 解决Long类型精度丢失问题
|
||||
*/
|
||||
@Configuration
|
||||
public class ObjectMapperConfig {
|
||||
|
||||
/**
|
||||
* 配置ObjectMapper Bean
|
||||
* 将Long类型序列化为String,避免前端JavaScript精度丢失
|
||||
*
|
||||
* @return ObjectMapper实例
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 创建自定义模块
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
|
||||
// 将Long类型序列化为String
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
|
||||
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
|
||||
|
||||
// 注册模块
|
||||
objectMapper.registerModule(simpleModule);
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,21 @@ public interface UserConstant {
|
||||
* 用户登录态键
|
||||
*/
|
||||
String USER_LOGIN_STATE = "userLoginState";
|
||||
|
||||
/**
|
||||
* 用户登录token键(存储在Session中)
|
||||
*/
|
||||
String USER_LOGIN_TOKEN = "userLoginToken";
|
||||
|
||||
/**
|
||||
* Redis中用户登录token的key前缀
|
||||
*/
|
||||
String REDIS_USER_LOGIN_TOKEN_PREFIX = "user:login:token:";
|
||||
|
||||
/**
|
||||
* 用户登录token过期时间(秒),24小时
|
||||
*/
|
||||
long USER_LOGIN_TOKEN_EXPIRE = 86400;
|
||||
|
||||
/**
|
||||
* 系统用户 id(虚拟用户)
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
package com.xy.xyaicpzs.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
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.AnnouncementAddRequest;
|
||||
import com.xy.xyaicpzs.common.requset.AnnouncementQueryRequest;
|
||||
import com.xy.xyaicpzs.common.requset.AnnouncementUpdateRequest;
|
||||
import com.xy.xyaicpzs.common.response.ApiResponse;
|
||||
import com.xy.xyaicpzs.domain.entity.Announcement;
|
||||
import com.xy.xyaicpzs.domain.entity.User;
|
||||
import com.xy.xyaicpzs.domain.vo.AnnouncementVO;
|
||||
import com.xy.xyaicpzs.exception.BusinessException;
|
||||
import com.xy.xyaicpzs.service.AnnouncementService;
|
||||
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.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 公告管理接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/announcement")
|
||||
@Tag(name = "公告管理", description = "公告管理相关接口")
|
||||
public class AnnouncementController {
|
||||
|
||||
@Autowired
|
||||
private AnnouncementService announcementService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private OperationHistoryService operationHistoryService;
|
||||
|
||||
/**
|
||||
* 添加公告
|
||||
*
|
||||
* @param request 公告添加请求
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 添加成功的公告ID
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "添加公告", description = "管理员添加新公告")
|
||||
public ApiResponse<Long> addAnnouncement(@RequestBody AnnouncementAddRequest 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 (StringUtils.isBlank(request.getTitle())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告标题不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getContent())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告内容不能为空");
|
||||
}
|
||||
if (request.getTitle().length() > 200) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告标题不能超过200个字符");
|
||||
}
|
||||
|
||||
try {
|
||||
Announcement announcement = new Announcement();
|
||||
BeanUtils.copyProperties(request, announcement);
|
||||
announcement.setPublisherId(userId);
|
||||
announcement.setPublisherName(userName);
|
||||
announcement.setViewCount(0);
|
||||
|
||||
boolean result = announcementService.save(announcement);
|
||||
if (!result) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "添加公告失败");
|
||||
}
|
||||
|
||||
// 记录操作历史 - 成功
|
||||
String resultMessage = String.format("%s成功添加公告:%s", userName, request.getTitle());
|
||||
operationHistoryService.recordOperation(userId, "添加公告", 1, "成功", resultMessage);
|
||||
|
||||
log.info("管理员{}添加公告成功,公告ID:{}", userName, announcement.getId());
|
||||
return ResultUtils.success(announcement.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("添加公告失败:{}", e.getMessage(), e);
|
||||
|
||||
// 记录操作历史 - 失败
|
||||
String resultMessage = String.format("%s添加公告失败:%s,公告标题:%s",
|
||||
userName, e.getMessage(), request.getTitle());
|
||||
operationHistoryService.recordOperation(userId, "添加公告", 1, "失败", resultMessage);
|
||||
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "添加公告失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新公告
|
||||
*
|
||||
* @param request 公告更新请求
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
@Operation(summary = "更新公告", description = "管理员更新公告信息")
|
||||
public ApiResponse<Boolean> updateAnnouncement(@RequestBody AnnouncementUpdateRequest 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 || request.getId() == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
|
||||
}
|
||||
if (request.getTitle() != null && request.getTitle().length() > 200) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告标题不能超过200个字符");
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查公告是否存在
|
||||
Announcement existingAnnouncement = announcementService.getById(request.getId());
|
||||
if (existingAnnouncement == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "公告不存在");
|
||||
}
|
||||
|
||||
Announcement announcement = new Announcement();
|
||||
BeanUtils.copyProperties(request, announcement);
|
||||
boolean result = announcementService.updateById(announcement);
|
||||
|
||||
if (!result) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告失败");
|
||||
}
|
||||
|
||||
// 记录操作历史 - 成功
|
||||
String resultMessage = String.format("%s成功更新公告:%s(ID:%d)",
|
||||
userName, existingAnnouncement.getTitle(), request.getId());
|
||||
operationHistoryService.recordOperation(userId, "更新公告", 1, "成功", resultMessage);
|
||||
|
||||
log.info("管理员{}更新公告成功,公告ID:{}", userName, request.getId());
|
||||
return ResultUtils.success(true);
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("更新公告失败:{}", e.getMessage(), e);
|
||||
|
||||
// 记录操作历史 - 失败
|
||||
String resultMessage = String.format("%s更新公告失败:%s,公告ID:%d",
|
||||
userName, e.getMessage(), request.getId());
|
||||
operationHistoryService.recordOperation(userId, "更新公告", 1, "失败", resultMessage);
|
||||
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除公告(逻辑删除)
|
||||
*
|
||||
* @param id 公告ID
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 删除结果
|
||||
*/
|
||||
@DeleteMapping("/delete/{id}")
|
||||
@Operation(summary = "删除公告", description = "管理员删除公告(逻辑删除)")
|
||||
public ApiResponse<Boolean> deleteAnnouncement(@Parameter(description = "公告ID", required = true)
|
||||
@PathVariable Long id,
|
||||
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 (id == null || id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告ID无效");
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查公告是否存在
|
||||
Announcement announcement = announcementService.getById(id);
|
||||
if (announcement == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "公告不存在");
|
||||
}
|
||||
|
||||
// 逻辑删除
|
||||
UpdateWrapper<Announcement> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", id);
|
||||
updateWrapper.set("isDelete", 1);
|
||||
boolean result = announcementService.update(updateWrapper);
|
||||
|
||||
if (!result) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除公告失败");
|
||||
}
|
||||
|
||||
// 记录操作历史 - 成功
|
||||
String resultMessage = String.format("%s成功删除公告:%s(ID:%d)",
|
||||
userName, announcement.getTitle(), id);
|
||||
operationHistoryService.recordOperation(userId, "删除公告", 1, "成功", resultMessage);
|
||||
|
||||
log.info("管理员{}删除公告成功,公告ID:{}", userName, id);
|
||||
return ResultUtils.success(true);
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("删除公告失败:{}", e.getMessage(), e);
|
||||
|
||||
// 记录操作历史 - 失败
|
||||
String resultMessage = String.format("%s删除公告失败:%s,公告ID:%d",
|
||||
userName, e.getMessage(), id);
|
||||
operationHistoryService.recordOperation(userId, "删除公告", 1, "失败", resultMessage);
|
||||
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除公告失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取公告列表
|
||||
*
|
||||
* @param queryRequest 查询请求
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 分页公告列表
|
||||
*/
|
||||
@GetMapping("/list/page")
|
||||
@Operation(summary = "分页获取公告列表", description = "分页获取公告列表,支持多条件查询")
|
||||
public ApiResponse<Page<AnnouncementVO>> listAnnouncementsByPage(AnnouncementQueryRequest queryRequest,
|
||||
HttpServletRequest httpServletRequest) {
|
||||
// 权限校验
|
||||
User loginUser = userService.getLoginUser(httpServletRequest);
|
||||
if (!userService.isAdmin(loginUser)) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限");
|
||||
}
|
||||
|
||||
if (queryRequest == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
|
||||
}
|
||||
|
||||
long current = queryRequest.getCurrent();
|
||||
long pageSize = queryRequest.getPageSize();
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<Announcement> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("isDelete", 0); // 只查询未删除的
|
||||
|
||||
// 标题模糊查询
|
||||
if (StringUtils.isNotBlank(queryRequest.getTitle())) {
|
||||
queryWrapper.like("title", queryRequest.getTitle());
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (queryRequest.getStatus() != null) {
|
||||
queryWrapper.eq("status", queryRequest.getStatus());
|
||||
}
|
||||
|
||||
// 优先级筛选
|
||||
if (queryRequest.getPriority() != null) {
|
||||
queryWrapper.eq("priority", queryRequest.getPriority());
|
||||
}
|
||||
|
||||
// 发布人ID筛选
|
||||
if (queryRequest.getPublisherId() != null) {
|
||||
queryWrapper.eq("publisherId", queryRequest.getPublisherId());
|
||||
}
|
||||
|
||||
// 发布人名称模糊查询
|
||||
if (StringUtils.isNotBlank(queryRequest.getPublisherName())) {
|
||||
queryWrapper.like("publisherName", queryRequest.getPublisherName());
|
||||
}
|
||||
|
||||
// 排序:优先级降序,然后创建时间降序
|
||||
queryWrapper.orderByDesc("priority", "createTime");
|
||||
|
||||
// 执行分页查询
|
||||
Page<Announcement> announcementPage = announcementService.page(new Page<>(current, pageSize), queryWrapper);
|
||||
|
||||
// 转换为VO对象
|
||||
List<AnnouncementVO> announcementVOList = announcementPage.getRecords().stream().map(announcement -> {
|
||||
AnnouncementVO announcementVO = new AnnouncementVO();
|
||||
BeanUtils.copyProperties(announcement, announcementVO);
|
||||
return announcementVO;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 创建VO分页对象
|
||||
Page<AnnouncementVO> announcementVOPage = new Page<>(announcementPage.getCurrent(),
|
||||
announcementPage.getSize(),
|
||||
announcementPage.getTotal());
|
||||
announcementVOPage.setRecords(announcementVOList);
|
||||
announcementVOPage.setPages(announcementPage.getPages());
|
||||
|
||||
return ResultUtils.success(announcementVOPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个公告详情
|
||||
*
|
||||
* @param id 公告ID
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 公告详情
|
||||
*/
|
||||
@GetMapping("/get/{id}")
|
||||
@Operation(summary = "获取公告详情", description = "根据ID获取公告详情")
|
||||
public ApiResponse<AnnouncementVO> getAnnouncement(@Parameter(description = "公告ID", required = true)
|
||||
@PathVariable Long id,
|
||||
HttpServletRequest httpServletRequest) {
|
||||
if (id == null || id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告ID无效");
|
||||
}
|
||||
|
||||
try {
|
||||
Announcement announcement = announcementService.getById(id);
|
||||
if (announcement == null || announcement.getIsDelete() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "公告不存在");
|
||||
}
|
||||
|
||||
AnnouncementVO announcementVO = new AnnouncementVO();
|
||||
BeanUtils.copyProperties(announcement, announcementVO);
|
||||
|
||||
return ResultUtils.success(announcementVO);
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("获取公告详情失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取公告详情失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新公告状态
|
||||
*
|
||||
* @param id 公告ID
|
||||
* @param status 状态:0-草稿,1-已发布,2-已下架
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PutMapping("/status/{id}")
|
||||
@Operation(summary = "更新公告状态", description = "管理员更新公告状态(发布/下架)")
|
||||
public ApiResponse<Boolean> updateAnnouncementStatus(
|
||||
@Parameter(description = "公告ID", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "状态:0-草稿,1-已发布,2-已下架", required = true) @RequestParam Integer status,
|
||||
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 (id == null || id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告ID无效");
|
||||
}
|
||||
if (status == null || status < 0 || status > 2) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "状态值无效");
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查公告是否存在
|
||||
Announcement announcement = announcementService.getById(id);
|
||||
if (announcement == null || announcement.getIsDelete() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "公告不存在");
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
UpdateWrapper<Announcement> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", id);
|
||||
updateWrapper.set("status", status);
|
||||
boolean result = announcementService.update(updateWrapper);
|
||||
|
||||
if (!result) {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告状态失败");
|
||||
}
|
||||
|
||||
// 记录操作历史 - 成功
|
||||
String statusText = status == 0 ? "草稿" : (status == 1 ? "已发布" : "已下架");
|
||||
String resultMessage = String.format("%s成功将公告状态更新为%s:%s(ID:%d)",
|
||||
userName, statusText, announcement.getTitle(), id);
|
||||
operationHistoryService.recordOperation(userId, "更新公告状态", 1, "成功", resultMessage);
|
||||
|
||||
log.info("管理员{}更新公告状态成功,公告ID:{},新状态:{}", userName, id, statusText);
|
||||
return ResultUtils.success(true);
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("更新公告状态失败:{}", e.getMessage(), e);
|
||||
|
||||
// 记录操作历史 - 失败
|
||||
String resultMessage = String.format("%s更新公告状态失败:%s,公告ID:%d",
|
||||
userName, e.getMessage(), id);
|
||||
operationHistoryService.recordOperation(userId, "更新公告状态", 1, "失败", resultMessage);
|
||||
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告状态失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加公告浏览次数
|
||||
*
|
||||
* @param id 公告ID
|
||||
* @return 更新结果
|
||||
*/
|
||||
@PutMapping("/view/{id}")
|
||||
@Operation(summary = "增加浏览次数", description = "用户查看公告时增加浏览次数")
|
||||
public ApiResponse<Boolean> increaseViewCount(@Parameter(description = "公告ID", required = true)
|
||||
@PathVariable Long id) {
|
||||
if (id == null || id <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告ID无效");
|
||||
}
|
||||
|
||||
try {
|
||||
Announcement announcement = announcementService.getById(id);
|
||||
if (announcement == null || announcement.getIsDelete() == 1) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "公告不存在");
|
||||
}
|
||||
|
||||
// 增加浏览次数
|
||||
UpdateWrapper<Announcement> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", id);
|
||||
updateWrapper.setSql("viewCount = viewCount + 1");
|
||||
boolean result = announcementService.update(updateWrapper);
|
||||
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("增加浏览次数失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "增加浏览次数失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公告统计数据
|
||||
*
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 公告统计数据
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
@Operation(summary = "获取公告统计数据", description = "获取公告总数、已发布、草稿和已下架的数量")
|
||||
public ApiResponse<Map<String, Long>> getAnnouncementStatistics(HttpServletRequest httpServletRequest) {
|
||||
// 权限校验
|
||||
User loginUser = userService.getLoginUser(httpServletRequest);
|
||||
if (!userService.isAdmin(loginUser)) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限");
|
||||
}
|
||||
|
||||
try {
|
||||
// 总数(未删除的)
|
||||
QueryWrapper<Announcement> totalWrapper = new QueryWrapper<>();
|
||||
totalWrapper.eq("isDelete", 0);
|
||||
long totalCount = announcementService.count(totalWrapper);
|
||||
|
||||
// 已发布的
|
||||
QueryWrapper<Announcement> publishedWrapper = new QueryWrapper<>();
|
||||
publishedWrapper.eq("isDelete", 0).eq("status", 1);
|
||||
long publishedCount = announcementService.count(publishedWrapper);
|
||||
|
||||
// 草稿
|
||||
QueryWrapper<Announcement> draftWrapper = new QueryWrapper<>();
|
||||
draftWrapper.eq("isDelete", 0).eq("status", 0);
|
||||
long draftCount = announcementService.count(draftWrapper);
|
||||
|
||||
// 已下架的
|
||||
QueryWrapper<Announcement> offlineWrapper = new QueryWrapper<>();
|
||||
offlineWrapper.eq("isDelete", 0).eq("status", 2);
|
||||
long offlineCount = announcementService.count(offlineWrapper);
|
||||
|
||||
// 构造返回结果
|
||||
Map<String, Long> statistics = new HashMap<>();
|
||||
statistics.put("totalCount", totalCount);
|
||||
statistics.put("publishedCount", publishedCount);
|
||||
statistics.put("draftCount", draftCount);
|
||||
statistics.put("offlineCount", offlineCount);
|
||||
|
||||
return ResultUtils.success(statistics);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取公告统计数据失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取公告统计数据失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新的公告列表(用于首页展示)
|
||||
*
|
||||
* @param limit 限制数量,默认5条
|
||||
* @return 最新公告列表
|
||||
*/
|
||||
@GetMapping("/latest")
|
||||
@Operation(summary = "获取最新公告", description = "获取最新的已发布公告列表,用于首页展示")
|
||||
public ApiResponse<List<AnnouncementVO>> getLatestAnnouncements(
|
||||
@Parameter(description = "限制数量,默认5条") @RequestParam(defaultValue = "5") Integer limit) {
|
||||
if (limit == null || limit <= 0) {
|
||||
limit = 5;
|
||||
}
|
||||
if (limit > 20) {
|
||||
limit = 20; // 最多返回20条
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询已发布的公告,按优先级和创建时间排序
|
||||
QueryWrapper<Announcement> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("isDelete", 0)
|
||||
.eq("status", 1) // 只查已发布的
|
||||
.orderByDesc("priority", "createTime")
|
||||
.last("LIMIT " + limit);
|
||||
|
||||
List<Announcement> announcements = announcementService.list(queryWrapper);
|
||||
|
||||
// 转换为VO
|
||||
List<AnnouncementVO> announcementVOList = announcements.stream().map(announcement -> {
|
||||
AnnouncementVO announcementVO = new AnnouncementVO();
|
||||
BeanUtils.copyProperties(announcement, announcementVO);
|
||||
return announcementVO;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return ResultUtils.success(announcementVOList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取最新公告失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取最新公告失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取置顶公告列表
|
||||
*
|
||||
* @param limit 限制数量,默认10条
|
||||
* @return 置顶公告列表
|
||||
*/
|
||||
@GetMapping("/top")
|
||||
@Operation(summary = "获取置顶公告", description = "获取所有置顶的已发布公告列表")
|
||||
public ApiResponse<List<AnnouncementVO>> getTopAnnouncements(
|
||||
@Parameter(description = "限制数量,默认10条") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
if (limit == null || limit <= 0) {
|
||||
limit = 10;
|
||||
}
|
||||
if (limit > 50) {
|
||||
limit = 50; // 最多返回50条
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询置顶且已发布的公告,按创建时间降序排序
|
||||
QueryWrapper<Announcement> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("isDelete", 0)
|
||||
.eq("status", 1) // 只查已发布的
|
||||
.eq("priority", 1) // 只查置顶的
|
||||
.orderByDesc("createTime")
|
||||
.last("LIMIT " + limit);
|
||||
|
||||
List<Announcement> announcements = announcementService.list(queryWrapper);
|
||||
|
||||
// 转换为VO
|
||||
List<AnnouncementVO> announcementVOList = announcements.stream().map(announcement -> {
|
||||
AnnouncementVO announcementVO = new AnnouncementVO();
|
||||
BeanUtils.copyProperties(announcement, announcementVO);
|
||||
return announcementVO;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return ResultUtils.success(announcementVOList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取置顶公告失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取置顶公告失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户端获取所有已发布公告(置顶优先)
|
||||
*
|
||||
* @return 已发布公告列表,置顶优先
|
||||
*/
|
||||
@GetMapping("/published")
|
||||
@Operation(summary = "获取所有已发布公告", description = "用户端获取所有已发布的公告,置顶优先,按创建时间降序排序")
|
||||
public ApiResponse<List<AnnouncementVO>> getPublishedAnnouncements() {
|
||||
try {
|
||||
// 查询所有已发布的公告,置顶优先,然后按创建时间降序
|
||||
QueryWrapper<Announcement> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("isDelete", 0)
|
||||
.eq("status", 1) // 只查已发布的
|
||||
.orderByDesc("priority", "createTime");
|
||||
|
||||
List<Announcement> announcements = announcementService.list(queryWrapper);
|
||||
|
||||
// 转换为VO
|
||||
List<AnnouncementVO> announcementVOList = announcements.stream().map(announcement -> {
|
||||
AnnouncementVO announcementVO = new AnnouncementVO();
|
||||
BeanUtils.copyProperties(announcement, announcementVO);
|
||||
return announcementVO;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return ResultUtils.success(announcementVOList);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取已发布公告失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取已发布公告失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
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.response.ApiResponse;
|
||||
@@ -1744,6 +1746,54 @@ public class BallAnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员获取双色球中奖记录统计(返回每一条中奖记录)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param prizeGrade 奖项等级(可选)
|
||||
* @param current 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @param request HTTP请求
|
||||
* @return 分页的中奖记录列表
|
||||
*/
|
||||
@GetMapping("/admin/prize-statistics")
|
||||
@Operation(summary = "管理员获取中奖记录明细", description = "管理员获取所有用户的双色球中奖记录明细,支持根据用户ID和奖项等级筛选")
|
||||
public ApiResponse<PageResponse<PredictRecord>> getAdminPrizeStatistics(
|
||||
@Parameter(description = "用户ID(可选)")
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@Parameter(description = "奖项等级(可选),例如:一等奖、二等奖")
|
||||
@RequestParam(value = "prizeGrade", required = false) String prizeGrade,
|
||||
@Parameter(description = "当前页码,从1开始,默认为1")
|
||||
@RequestParam(value = "current", defaultValue = "1") Integer current,
|
||||
@Parameter(description = "每页大小,默认为10")
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// 验证管理员权限
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
|
||||
}
|
||||
|
||||
if (!userService.isAdmin(loginUser)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限访问,仅管理员可用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("管理员获取双色球中奖记录请求:userId={},prizeGrade={},current={},pageSize={}",
|
||||
userId, prizeGrade, current, pageSize);
|
||||
|
||||
// 调用服务获取中奖记录列表
|
||||
PageResponse<PredictRecord> result = predictRecordService.getWinningRecordsForAdmin(userId, prizeGrade, current, pageSize);
|
||||
|
||||
log.info("管理员获取双色球中奖记录完成,总记录数:{}", result.getTotal());
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("管理员获取双色球中奖记录失败:{}", e.getMessage(), e);
|
||||
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "管理员获取双色球中奖记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取近10期开奖期号
|
||||
* @return 近10期开奖期号列表
|
||||
@@ -1804,4 +1854,51 @@ public class BallAnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员获取所有双色球推测记录(支持分页和用户ID筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param current 当前页码,默认为1
|
||||
* @param pageSize 每页大小,默认为10
|
||||
* @param request HTTP请求
|
||||
* @return 分页的双色球预测记录
|
||||
*/
|
||||
@GetMapping("/admin/all-records")
|
||||
@Operation(summary = "管理员获取所有推测记录", description = "管理员获取所有双色球推测记录,支持分页和根据用户ID、中奖等级筛选")
|
||||
public ApiResponse<PageResponse<PredictRecord>> getAllRecordsForAdmin(
|
||||
@Parameter(description = "用户ID(可选),用于筛选指定用户的记录")
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@Parameter(description = "中奖等级(可选),例如:一等奖、二等奖、未中奖")
|
||||
@RequestParam(value = "predictResult", required = false) String predictResult,
|
||||
@Parameter(description = "当前页码,从1开始,默认为1")
|
||||
@RequestParam(value = "current", defaultValue = "1") Integer current,
|
||||
@Parameter(description = "每页大小,默认为10")
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// 验证管理员权限
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
|
||||
}
|
||||
|
||||
if (!userService.isAdmin(loginUser)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限访问,仅管理员可用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("管理员获取所有双色球推测记录,userId={},predictResult={},current={},pageSize={}",
|
||||
userId, predictResult, current, pageSize);
|
||||
|
||||
// 调用Service层方法
|
||||
PageResponse<PredictRecord> result = predictRecordService.getAllRecordsForAdmin(userId, predictResult, current, pageSize);
|
||||
|
||||
log.info("管理员获取双色球推测记录完成,总记录数:{}", result.getTotal());
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("管理员获取双色球推测记录失败:{}", e.getMessage(), e);
|
||||
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取推测记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import com.xy.xyaicpzs.common.requset.FirstBallPredictionRequest;
|
||||
import com.xy.xyaicpzs.common.requset.FollowBackBallPredictionRequest;
|
||||
import com.xy.xyaicpzs.common.requset.FollowerBallPredictionRequest;
|
||||
import com.xy.xyaicpzs.common.response.ApiResponse;
|
||||
import com.xy.xyaicpzs.domain.entity.DltDrawRecord;
|
||||
import com.xy.xyaicpzs.domain.entity.DltPredictRecord;
|
||||
import com.xy.xyaicpzs.domain.entity.User;
|
||||
import com.xy.xyaicpzs.domain.vo.BallCombinationAnalysisVO;
|
||||
import com.xy.xyaicpzs.domain.vo.BallPersistenceAnalysisVO;
|
||||
import com.xy.xyaicpzs.domain.vo.FirstBallPredictionResultVO;
|
||||
@@ -20,8 +22,10 @@ import com.xy.xyaicpzs.dlt.FirstBallPredictor.FirstBallPredictionResult;
|
||||
import com.xy.xyaicpzs.dlt.FollowBackBallPredictor;
|
||||
import com.xy.xyaicpzs.dlt.FollowerBallPredictor;
|
||||
import com.xy.xyaicpzs.service.DltCombinationAnalysisService;
|
||||
import com.xy.xyaicpzs.service.DltDrawRecordService;
|
||||
import com.xy.xyaicpzs.service.DltPersistenceAnalysisService;
|
||||
import com.xy.xyaicpzs.service.DltPredictRecordService;
|
||||
import com.xy.xyaicpzs.service.UserService;
|
||||
import com.xy.xyaicpzs.util.UserAuthValidator;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -66,6 +70,41 @@ public class DltBallAnalysisController {
|
||||
@Autowired
|
||||
private UserAuthValidator userAuthValidator;
|
||||
|
||||
@Autowired
|
||||
private DltDrawRecordService dltDrawRecordService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 获取近期大乐透开奖信息
|
||||
* @param limit 获取条数,可选参数,默认10条
|
||||
* @return 近期大乐透开奖信息列表
|
||||
*/
|
||||
@GetMapping("/recent-draws")
|
||||
@Operation(summary = "获取近期大乐透开奖信息", description = "获取最近的大乐透开奖信息,默认返回10条,按开奖期号倒序排列")
|
||||
public ApiResponse<List<DltDrawRecord>> 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<DltDrawRecord> result = dltDrawRecordService.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());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/predict-first-ball")
|
||||
@Operation(summary = "前区首球预测", description = "根据输入的级别和号码,预测前区首球")
|
||||
public ApiResponse<FirstBallPredictionResultVO> predictFirstBall(@RequestBody FirstBallPredictionRequest request) {
|
||||
|
||||
@@ -300,18 +300,10 @@ public class DltPredictController {
|
||||
@GetMapping("/front-first-ball-hit-rate")
|
||||
@Operation(summary = "获取大乐透前区首球命中率统计", description = "统计用户的大乐透前区首球命中次数和命中率")
|
||||
public ApiResponse<RedBallHitRateVO> getFrontFirstBallHitRate(HttpServletRequest request) {
|
||||
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看");
|
||||
}
|
||||
|
||||
// 检查VIP权限
|
||||
Date now = new Date();
|
||||
if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("接收到获取大乐透前区首球命中率统计请求");
|
||||
|
||||
@@ -335,18 +327,10 @@ public class DltPredictController {
|
||||
@GetMapping("/front-ball-hit-rate")
|
||||
@Operation(summary = "获取大乐透前区球号命中率统计", description = "统计用户的大乐透前区球号命中总数和命中率")
|
||||
public ApiResponse<RedBallHitRateVO> getFrontBallHitRate(HttpServletRequest request) {
|
||||
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看");
|
||||
}
|
||||
|
||||
// 检查VIP权限
|
||||
Date now = new Date();
|
||||
if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("接收到获取大乐透前区球号命中率统计请求");
|
||||
|
||||
@@ -370,18 +354,10 @@ public class DltPredictController {
|
||||
@GetMapping("/back-first-ball-hit-rate")
|
||||
@Operation(summary = "获取大乐透后区首球命中率统计", description = "统计用户的大乐透后区首球命中次数和命中率")
|
||||
public ApiResponse<RedBallHitRateVO> getBackFirstBallHitRate(HttpServletRequest request) {
|
||||
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看");
|
||||
}
|
||||
|
||||
// 检查VIP权限
|
||||
Date now = new Date();
|
||||
if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("接收到获取大乐透后区首球命中率统计请求");
|
||||
|
||||
@@ -405,18 +381,10 @@ public class DltPredictController {
|
||||
@GetMapping("/back-ball-hit-rate")
|
||||
@Operation(summary = "获取大乐透后区球号命中率统计", description = "统计用户的大乐透后区球号命中次数和命中率")
|
||||
public ApiResponse<RedBallHitRateVO> getBackBallHitRate(HttpServletRequest request) {
|
||||
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看");
|
||||
}
|
||||
|
||||
// 检查VIP权限
|
||||
Date now = new Date();
|
||||
if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("接收到获取大乐透后区球号命中率统计请求");
|
||||
|
||||
@@ -432,5 +400,100 @@ public class DltPredictController {
|
||||
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取大乐透后区球号命中率统计失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员获取所有大乐透推测记录(支持分页和用户ID筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param current 当前页码,默认为1
|
||||
* @param pageSize 每页大小,默认为10
|
||||
* @param request HTTP请求
|
||||
* @return 分页的大乐透预测记录
|
||||
*/
|
||||
@GetMapping("/admin/all-records")
|
||||
@Operation(summary = "管理员获取所有推测记录", description = "管理员获取所有大乐透推测记录,支持分页和根据用户ID、中奖等级筛选")
|
||||
public ApiResponse<PageResponse<DltPredictRecord>> getAllRecordsForAdmin(
|
||||
@Parameter(description = "用户ID(可选),用于筛选指定用户的记录")
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@Parameter(description = "中奖等级(可选),例如:一等奖、二等奖、未中奖")
|
||||
@RequestParam(value = "predictResult", required = false) String predictResult,
|
||||
@Parameter(description = "当前页码,从1开始,默认为1")
|
||||
@RequestParam(value = "current", defaultValue = "1") Integer current,
|
||||
@Parameter(description = "每页大小,默认为10")
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// 验证管理员权限
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
|
||||
}
|
||||
|
||||
if (!userService.isAdmin(loginUser)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限访问,仅管理员可用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("管理员获取所有大乐透推测记录,userId={},predictResult={},current={},pageSize={}",
|
||||
userId, predictResult, current, pageSize);
|
||||
|
||||
// 调用Service层方法
|
||||
PageResponse<DltPredictRecord> result = dltPredictRecordService.getAllRecordsForAdmin(userId, predictResult, current, pageSize);
|
||||
|
||||
log.info("管理员获取大乐透推测记录完成,总记录数:{}", result.getTotal());
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("管理员获取大乐透推测记录失败:{}", e.getMessage(), e);
|
||||
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取推测记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员获取大乐透中奖记录统计(返回每一条中奖记录)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param prizeGrade 奖项等级(可选)
|
||||
* @param current 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @param request HTTP请求
|
||||
* @return 分页的中奖记录列表
|
||||
*/
|
||||
@GetMapping("/admin/prize-statistics")
|
||||
@Operation(summary = "管理员获取中奖记录明细", description = "管理员获取所有用户的大乐透中奖记录明细,支持根据用户ID和奖项等级筛选")
|
||||
public ApiResponse<PageResponse<DltPredictRecord>> getAdminDltPrizeStatistics(
|
||||
@Parameter(description = "用户ID(可选)")
|
||||
@RequestParam(value = "userId", required = false) Long userId,
|
||||
@Parameter(description = "奖项等级(可选),例如:一等奖、二等奖")
|
||||
@RequestParam(value = "prizeGrade", required = false) String prizeGrade,
|
||||
@Parameter(description = "当前页码,从1开始,默认为1")
|
||||
@RequestParam(value = "current", defaultValue = "1") Integer current,
|
||||
@Parameter(description = "每页大小,默认为10")
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// 验证管理员权限
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
if (loginUser == null) {
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
|
||||
}
|
||||
|
||||
if (!userService.isAdmin(loginUser)) {
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限访问,仅管理员可用");
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("管理员获取大乐透中奖记录请求:userId={},prizeGrade={},current={},pageSize={}",
|
||||
userId, prizeGrade, current, pageSize);
|
||||
|
||||
// 调用服务获取中奖记录列表
|
||||
PageResponse<DltPredictRecord> result = dltPredictRecordService.getWinningRecordsForAdmin(userId, prizeGrade, current, pageSize);
|
||||
|
||||
log.info("管理员获取大乐透中奖记录完成,总记录数:{}", result.getTotal());
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("管理员获取大乐透中奖记录失败:{}", e.getMessage(), e);
|
||||
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "管理员获取大乐透中奖记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package com.xy.xyaicpzs.controller;
|
||||
|
||||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.model.ObjectMetadata;
|
||||
import com.qcloud.cos.model.PutObjectRequest;
|
||||
import com.qcloud.cos.model.PutObjectResult;
|
||||
import com.xy.xyaicpzs.common.ErrorCode;
|
||||
import com.xy.xyaicpzs.common.ResultUtils;
|
||||
import com.xy.xyaicpzs.common.response.ApiResponse;
|
||||
import com.xy.xyaicpzs.config.CosConfig;
|
||||
import com.xy.xyaicpzs.exception.BusinessException;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 文件上传控制器
|
||||
*
|
||||
* @author xy
|
||||
*/
|
||||
@Tag(name = "文件上传", description = "腾讯云 COS 文件上传相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
@Slf4j
|
||||
public class FileUploadController {
|
||||
|
||||
@Resource
|
||||
private COSClient cosClient;
|
||||
|
||||
@Resource
|
||||
private CosConfig cosConfig;
|
||||
|
||||
/**
|
||||
* 上传图片到腾讯云 COS
|
||||
*
|
||||
* @param file 上传的图片文件(最大50MB)
|
||||
* @return 文件访问 URL
|
||||
*/
|
||||
@Operation(summary = "上传图片", description = "上传图片到腾讯云 COS,文件名使用UUID命名,最大支持50MB")
|
||||
@PostMapping("/upload")
|
||||
public ApiResponse<Map<String, String>> uploadImage(@RequestParam("file") MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取原始文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件名不能为空");
|
||||
}
|
||||
|
||||
// 验证文件类型(仅允许图片)
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("image/")) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持上传图片文件");
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
String extension = "";
|
||||
int lastDotIndex = originalFilename.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
extension = originalFilename.substring(lastDotIndex);
|
||||
}
|
||||
|
||||
// 使用 UUID 生成唯一文件名
|
||||
String fileName = UUID.randomUUID().toString() + extension;
|
||||
|
||||
// 设置对象元数据
|
||||
ObjectMetadata objectMetadata = new ObjectMetadata();
|
||||
objectMetadata.setContentLength(file.getSize());
|
||||
objectMetadata.setContentType(file.getContentType());
|
||||
|
||||
// 创建上传请求
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||
cosConfig.getBucketName(),
|
||||
fileName,
|
||||
file.getInputStream(),
|
||||
objectMetadata
|
||||
);
|
||||
|
||||
// 执行上传
|
||||
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
||||
log.info("图片上传成功,文件名: {}, RequestId: {}", fileName, putObjectResult.getRequestId());
|
||||
|
||||
// 生成文件访问 URL
|
||||
String fileUrl = cosConfig.getDomain() + "/" + fileName;
|
||||
|
||||
// 返回结果
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("fileName", fileName);
|
||||
result.put("fileUrl", fileUrl);
|
||||
result.put("originalFilename", originalFilename);
|
||||
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("图片上传失败", e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "图片上传失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传图片
|
||||
*
|
||||
* @param files 上传的图片文件列表(单个文件最大50MB,总请求最大100MB)
|
||||
* @return 文件访问 URL 列表
|
||||
*/
|
||||
@Operation(summary = "批量上传图片", description = "批量上传多张图片到腾讯云 COS,单个文件最大50MB")
|
||||
@PostMapping("/upload/batch")
|
||||
public ApiResponse<Map<String, Object>> uploadImages(@RequestParam("files") MultipartFile[] files) {
|
||||
if (files == null || files.length == 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件列表不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
java.util.List<Map<String, String>> fileList = new java.util.ArrayList<>();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (file.isEmpty()) {
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取原始文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null) {
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 验证文件类型(仅允许图片)
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("image/")) {
|
||||
log.warn("跳过非图片文件: {}", originalFilename);
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
String extension = "";
|
||||
int lastDotIndex = originalFilename.lastIndexOf(".");
|
||||
if (lastDotIndex > 0) {
|
||||
extension = originalFilename.substring(lastDotIndex);
|
||||
}
|
||||
|
||||
// 使用 UUID 生成唯一文件名
|
||||
String fileName = UUID.randomUUID().toString() + extension;
|
||||
|
||||
// 设置对象元数据
|
||||
ObjectMetadata objectMetadata = new ObjectMetadata();
|
||||
objectMetadata.setContentLength(file.getSize());
|
||||
objectMetadata.setContentType(file.getContentType());
|
||||
|
||||
// 创建上传请求
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||
cosConfig.getBucketName(),
|
||||
fileName,
|
||||
file.getInputStream(),
|
||||
objectMetadata
|
||||
);
|
||||
|
||||
// 执行上传
|
||||
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
||||
log.info("图片上传成功,文件名: {}, RequestId: {}", fileName, putObjectResult.getRequestId());
|
||||
|
||||
// 生成文件访问 URL
|
||||
String fileUrl = cosConfig.getDomain() + "/" + fileName;
|
||||
|
||||
// 添加到结果列表
|
||||
Map<String, String> fileInfo = new HashMap<>();
|
||||
fileInfo.put("fileName", fileName);
|
||||
fileInfo.put("fileUrl", fileUrl);
|
||||
fileInfo.put("originalFilename", originalFilename);
|
||||
fileList.add(fileInfo);
|
||||
|
||||
successCount++;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("单个图片上传失败:{}", file.getOriginalFilename(), e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("fileList", fileList);
|
||||
result.put("total", files.length);
|
||||
result.put("successCount", successCount);
|
||||
result.put("failCount", failCount);
|
||||
|
||||
return ResultUtils.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("批量图片上传失败", e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "批量图片上传失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.xy.xyaicpzs.controller;
|
||||
|
||||
import com.xy.xyaicpzs.common.response.ApiResponse;
|
||||
import com.xy.xyaicpzs.service.PageViewService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 页面访问量统计控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/pv")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "页面访问量统计", description = "PV统计相关接口")
|
||||
public class PageViewController {
|
||||
|
||||
private final PageViewService pageViewService;
|
||||
|
||||
@PostMapping("/record")
|
||||
@Operation(summary = "记录页面访问", description = "记录一次页面访问")
|
||||
public ApiResponse<String> recordPageView() {
|
||||
pageViewService.incrementPageView();
|
||||
return ApiResponse.success("记录成功");
|
||||
}
|
||||
|
||||
@GetMapping("/total")
|
||||
@Operation(summary = "获取总PV", description = "获取网站累计总访问量")
|
||||
public ApiResponse<Long> getTotalPageViews() {
|
||||
return ApiResponse.success(pageViewService.getTotalPageViews());
|
||||
}
|
||||
|
||||
@GetMapping("/today")
|
||||
@Operation(summary = "获取今日PV", description = "获取今日访问量")
|
||||
public ApiResponse<Long> getTodayPageViews() {
|
||||
return ApiResponse.success(pageViewService.getTodayPageViews());
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@Operation(summary = "根据天数获取PV统计", description = "获取近N天的PV统计(最大90天)")
|
||||
public ApiResponse<Map<String, Long>> getPageViewsByDays(
|
||||
@Parameter(description = "天数:7/30/90,默认7天,最大90天") @RequestParam(defaultValue = "7") int days) {
|
||||
return ApiResponse.success(pageViewService.getPageViewsByDays(days));
|
||||
}
|
||||
}
|
||||
@@ -363,6 +363,26 @@ public class UserController {
|
||||
if (userQueryRequest.getIsVip() != null) {
|
||||
queryWrapper.eq("isVip", userQueryRequest.getIsVip());
|
||||
}
|
||||
|
||||
// 套餐类别匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getVipType())) {
|
||||
queryWrapper.eq("vipType", userQueryRequest.getVipType());
|
||||
}
|
||||
|
||||
// 所在省市模糊匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getLocation())) {
|
||||
queryWrapper.like("location", userQueryRequest.getLocation());
|
||||
}
|
||||
|
||||
// 彩票偏好匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getPreference())) {
|
||||
queryWrapper.like("preference", userQueryRequest.getPreference());
|
||||
}
|
||||
|
||||
// 获客渠道匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getChannel())) {
|
||||
queryWrapper.like("channel", userQueryRequest.getChannel());
|
||||
}
|
||||
}
|
||||
|
||||
List<User> userList = userService.list(queryWrapper);
|
||||
@@ -424,6 +444,26 @@ public class UserController {
|
||||
if (userQueryRequest.getIsVip() != null) {
|
||||
queryWrapper.eq("isVip", userQueryRequest.getIsVip());
|
||||
}
|
||||
|
||||
// 套餐类别匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getVipType())) {
|
||||
queryWrapper.eq("vipType", userQueryRequest.getVipType());
|
||||
}
|
||||
|
||||
// 所在省市模糊匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getLocation())) {
|
||||
queryWrapper.like("location", userQueryRequest.getLocation());
|
||||
}
|
||||
|
||||
// 彩票偏好匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getPreference())) {
|
||||
queryWrapper.like("preference", userQueryRequest.getPreference());
|
||||
}
|
||||
|
||||
// 获客渠道匹配
|
||||
if (StringUtils.isNotBlank(userQueryRequest.getChannel())) {
|
||||
queryWrapper.like("channel", userQueryRequest.getChannel());
|
||||
}
|
||||
}
|
||||
|
||||
Page<User> userPage = userService.page(new Page<>(current, size), queryWrapper);
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -110,7 +111,7 @@ public class VipCodeController {
|
||||
/**
|
||||
* 获取一个可用的会员码
|
||||
*
|
||||
* @param vipExpireTime 会员有效月数(1或12)
|
||||
* @param vipExpireTime 会员有效月数
|
||||
* @return 可用的会员码
|
||||
*/
|
||||
@GetMapping("/available")
|
||||
@@ -128,8 +129,8 @@ public class VipCodeController {
|
||||
if (vipExpireTime == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数不能为空");
|
||||
}
|
||||
if (vipExpireTime != 1 && vipExpireTime != 12) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数只能是1或12");
|
||||
if (vipExpireTime <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数必须大于0");
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -295,4 +296,79 @@ public class VipCodeController {
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取会员码统计数量失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据月份获取已使用的会员码(分页)
|
||||
*
|
||||
* @param vipExpireTime 会员有效月数
|
||||
* @param current 当前页码,默认为1
|
||||
* @param pageSize 每页大小,默认为10
|
||||
* @param httpServletRequest Http请求
|
||||
* @return 分页的已使用会员码列表
|
||||
*/
|
||||
@GetMapping("/used/by-month")
|
||||
@Operation(summary = "根据月份获取已使用的会员码", description = "根据会员有效月数查询已使用的会员码,支持分页")
|
||||
public ApiResponse<Page<VipCodeVO>> getUsedVipCodesByMonth(
|
||||
@Parameter(description = "会员有效月数", required = true)
|
||||
@RequestParam("vipExpireTime") Integer vipExpireTime,
|
||||
@Parameter(description = "当前页码,默认为1", required = false)
|
||||
@RequestParam(value = "current", defaultValue = "1") Long current,
|
||||
@Parameter(description = "每页大小,默认为10", required = false)
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Long pageSize,
|
||||
HttpServletRequest httpServletRequest) {
|
||||
|
||||
// 权限校验
|
||||
User loginUser = userService.getLoginUser(httpServletRequest);
|
||||
if (!userService.isAdmin(loginUser)){
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限");
|
||||
}
|
||||
|
||||
// 参数校验
|
||||
if (vipExpireTime == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数不能为空");
|
||||
}
|
||||
if (vipExpireTime <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "会员有效月数必须大于0");
|
||||
}
|
||||
if (current <= 0) {
|
||||
current = 1L;
|
||||
}
|
||||
if (pageSize <= 0) {
|
||||
pageSize = 10L;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建查询条件:已使用且指定月份
|
||||
QueryWrapper<VipCode> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("isUse", 1) // 已使用
|
||||
.eq("vipExpireTime", vipExpireTime); // 指定月份
|
||||
|
||||
// 按使用时间倒序排序(最近使用的在前)
|
||||
queryWrapper.orderByDesc("updateTime");
|
||||
|
||||
// 执行分页查询
|
||||
Page<VipCode> vipCodePage = vipCodeService.page(new Page<>(current, pageSize), queryWrapper);
|
||||
|
||||
// 转换为VO对象
|
||||
List<VipCodeVO> vipCodeVOList = vipCodePage.getRecords().stream().map(vipCode -> {
|
||||
VipCodeVO vipCodeVO = new VipCodeVO();
|
||||
BeanUtils.copyProperties(vipCode, vipCodeVO);
|
||||
return vipCodeVO;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 创建VO分页对象,确保正确传递所有分页信息
|
||||
Page<VipCodeVO> vipCodeVOPage = new Page<>(vipCodePage.getCurrent(), vipCodePage.getSize(), vipCodePage.getTotal());
|
||||
vipCodeVOPage.setRecords(vipCodeVOList);
|
||||
// 手动设置pages值
|
||||
vipCodeVOPage.setPages(vipCodePage.getPages());
|
||||
|
||||
log.info("根据月份获取已使用的会员码完成,月份:{},页码:{},每页大小:{},返回{}条记录",
|
||||
vipExpireTime, current, pageSize, vipCodeVOList.size());
|
||||
return ResultUtils.success(vipCodeVOPage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("根据月份获取已使用的会员码失败:{}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取已使用的会员码失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class BackBallPredictor {
|
||||
* @param previousFrontBalls 上期前区5个号码
|
||||
* @param previousBackBalls 上期后区2个号码
|
||||
* @param nextBackBalls 下期后区2个号码
|
||||
* @return 预测的4个后区首球号码和筛选过程
|
||||
* @return 预测的5个后区首球号码和筛选过程
|
||||
*/
|
||||
public BackBallPredictionResult predictBackBallWithProcess(String level, List<Integer> nextFrontBalls, List<Integer> previousFrontBalls,
|
||||
List<Integer> previousBackBalls, List<Integer> nextBackBalls) {
|
||||
@@ -876,10 +876,10 @@ public class BackBallPredictor {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有候选球中筛选最终的4个球(带过程)
|
||||
* 从所有候选球中筛选最终的5个球(带过程)
|
||||
* @param allCandidateBalls 所有候选球
|
||||
* @param d6CoefficientMap D6表中的系数信息
|
||||
* @return 最终选出的4个球和筛选过程
|
||||
* @return 最终选出的5个球和筛选过程
|
||||
*/
|
||||
private BackBallPredictionResult selectFinal4BallsWithProcess(List<Integer> allCandidateBalls, Map<Integer, List<BallWithCoefficient>> d6CoefficientMap) {
|
||||
// 统计球号出现次数
|
||||
@@ -909,7 +909,7 @@ public class BackBallPredictor {
|
||||
int frequency = frequencyGroup.getKey();
|
||||
int ballsInGroup = frequencyGroup.getValue().size();
|
||||
|
||||
if (currentSelected + ballsInGroup <= 4) {
|
||||
if (currentSelected + ballsInGroup <= 5) {
|
||||
// 这个频率组的球全部入选
|
||||
currentSelected += ballsInGroup;
|
||||
minSelectedFrequency = frequency;
|
||||
@@ -935,12 +935,12 @@ public class BackBallPredictor {
|
||||
|
||||
// 按频率从高到低处理每个频率组
|
||||
for (Map.Entry<Integer, List<Integer>> frequencyGroup : frequencyGroups.entrySet()) {
|
||||
if (resultBalls.size() >= 4) {
|
||||
if (resultBalls.size() >= 5) {
|
||||
break;
|
||||
}
|
||||
|
||||
List<Integer> ballsInGroup = frequencyGroup.getValue();
|
||||
int needCount = Math.min(4 - resultBalls.size(), ballsInGroup.size());
|
||||
int needCount = Math.min(5 - resultBalls.size(), ballsInGroup.size());
|
||||
|
||||
if (ballsInGroup.size() <= needCount) {
|
||||
// 如果这个频率组的球数量 <= 需要的数量,全部加入
|
||||
@@ -975,7 +975,7 @@ public class BackBallPredictor {
|
||||
|
||||
// 生成筛选过程说明
|
||||
if (hasSecondarySelection) {
|
||||
processBuilder.append("无法直接筛选出前4个,其中");
|
||||
processBuilder.append("无法直接筛选出前5个,其中");
|
||||
Collections.sort(directSelected);
|
||||
String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append(directSelectedStr).append("直接入选,");
|
||||
@@ -991,7 +991,7 @@ public class BackBallPredictor {
|
||||
processBuilder.append("最终筛选出").append(finalSelectedStr).append(",");
|
||||
|
||||
String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append("组成前4个球号:").append(allResultStr).append("。");
|
||||
processBuilder.append("组成前5个球号:").append(allResultStr).append("。");
|
||||
|
||||
processBuilder.append("筛选步骤:").append(selectionSteps).append("。");
|
||||
if (!detailedCoefficientInfo.isEmpty()) {
|
||||
@@ -999,7 +999,7 @@ public class BackBallPredictor {
|
||||
}
|
||||
} else {
|
||||
String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append("直接筛选出前4个球号:").append(allResultStr).append("。");
|
||||
processBuilder.append("直接筛选出前5个球号:").append(allResultStr).append("。");
|
||||
}
|
||||
|
||||
// 无论是否有二次筛选,都要显示筛选步骤
|
||||
|
||||
@@ -83,7 +83,7 @@ public class FollowBackBallPredictor {
|
||||
* @param nextFrontBalls 下期前区5个号码
|
||||
* @param previousFrontBalls 上期前区5个号码
|
||||
* @param previousBackBalls 上期后区2个号码
|
||||
* @return 预测的3个后区随球号码和筛选过程
|
||||
* @return 预测的4个后区随球号码和筛选过程
|
||||
*/
|
||||
public FollowBackBallPredictionResult predictFollowBackBallWithProcess(String level, Integer backFirstBall, List<Integer> nextFrontBalls, List<Integer> previousFrontBalls,
|
||||
List<Integer> previousBackBalls) {
|
||||
@@ -141,6 +141,9 @@ public class FollowBackBallPredictor {
|
||||
// (6) 从百期排行表获取前2名
|
||||
List<Integer> top2FromHistoryTop100 = getTop2FromBackendHistoryTop100();
|
||||
allCandidateBalls.addAll(top2FromHistoryTop100);
|
||||
|
||||
// (7) 排除后区首球,确保随球不包含首球本身
|
||||
allCandidateBalls.removeIf(ball -> ball.equals(backFirstBall));
|
||||
|
||||
return selectFinal3BallsWithProcess(allCandidateBalls, d7CoefficientMap);
|
||||
}
|
||||
@@ -215,6 +218,9 @@ public class FollowBackBallPredictor {
|
||||
allCandidateBalls.add(ball);
|
||||
}
|
||||
|
||||
// (7) 排除后区首球,确保随球不包含首球本身
|
||||
allCandidateBalls.removeIf(ball -> ball.equals(backFirstBall));
|
||||
|
||||
// 最终筛选3个球
|
||||
return selectFinal3Balls(allCandidateBalls, d7CoefficientMap);
|
||||
}
|
||||
@@ -949,10 +955,10 @@ public class FollowBackBallPredictor {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有候选球中筛选最终的3个球(带过程)
|
||||
* 从所有候选球中筛选最终的4个球(带过程)
|
||||
* @param allCandidateBalls 所有候选球
|
||||
* @param d7CoefficientMap D7表中的系数信息
|
||||
* @return 最终选出的3个球和筛选过程
|
||||
* @return 最终选出的4个球和筛选过程
|
||||
*/
|
||||
private FollowBackBallPredictionResult selectFinal3BallsWithProcess(List<Integer> allCandidateBalls, Map<Integer, List<BallWithCoefficient>> d7CoefficientMap) {
|
||||
// 统计球号出现次数
|
||||
@@ -982,7 +988,7 @@ public class FollowBackBallPredictor {
|
||||
int frequency = frequencyGroup.getKey();
|
||||
int ballsInGroup = frequencyGroup.getValue().size();
|
||||
|
||||
if (currentSelected + ballsInGroup <= 3) {
|
||||
if (currentSelected + ballsInGroup <= 4) {
|
||||
// 这个频率组的球全部入选
|
||||
currentSelected += ballsInGroup;
|
||||
minSelectedFrequency = frequency;
|
||||
@@ -1008,12 +1014,12 @@ public class FollowBackBallPredictor {
|
||||
|
||||
// 按频率从高到低处理每个频率组
|
||||
for (Map.Entry<Integer, List<Integer>> frequencyGroup : frequencyGroups.entrySet()) {
|
||||
if (resultBalls.size() >= 3) {
|
||||
if (resultBalls.size() >= 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
List<Integer> ballsInGroup = frequencyGroup.getValue();
|
||||
int needCount = Math.min(3 - resultBalls.size(), ballsInGroup.size());
|
||||
int needCount = Math.min(4 - resultBalls.size(), ballsInGroup.size());
|
||||
|
||||
if (ballsInGroup.size() <= needCount) {
|
||||
// 如果这个频率组的球数量 <= 需要的数量,全部加入
|
||||
@@ -1048,7 +1054,7 @@ public class FollowBackBallPredictor {
|
||||
|
||||
// 生成筛选过程说明
|
||||
if (hasSecondarySelection) {
|
||||
processBuilder.append("无法直接筛选出前3个,其中");
|
||||
processBuilder.append("无法直接筛选出前4个,其中");
|
||||
Collections.sort(directSelected);
|
||||
String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append(directSelectedStr).append("直接入选,");
|
||||
@@ -1064,7 +1070,7 @@ public class FollowBackBallPredictor {
|
||||
processBuilder.append("最终筛选出").append(finalSelectedStr).append(",");
|
||||
|
||||
String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append("组成前3个球号:").append(allResultStr).append("。");
|
||||
processBuilder.append("组成前4个球号:").append(allResultStr).append("。");
|
||||
|
||||
processBuilder.append("筛选步骤:").append(selectionSteps).append("。");
|
||||
if (!detailedCoefficientInfo.isEmpty()) {
|
||||
@@ -1072,7 +1078,7 @@ public class FollowBackBallPredictor {
|
||||
}
|
||||
} else {
|
||||
String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append("直接筛选出前3个球号:").append(allResultStr).append("。");
|
||||
processBuilder.append("直接筛选出前4个球号:").append(allResultStr).append("。");
|
||||
}
|
||||
|
||||
// 无论是否有二次筛选,都要显示筛选步骤
|
||||
|
||||
@@ -927,10 +927,10 @@ public class FollowerBallPredictor {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有候选球中筛选最终的10个球(带过程)
|
||||
* 从所有候选球中筛选最终的12个球(带过程)
|
||||
* @param allCandidateBalls 所有候选球
|
||||
* @param d5CoefficientMap D5表中的系数信息
|
||||
* @return 最终选出的10个球和筛选过程
|
||||
* @return 最终选出的12个球和筛选过程
|
||||
*/
|
||||
private FollowerBallPredictionResult selectFinal10BallsWithProcess(List<Integer> allCandidateBalls, Map<Integer, List<BallWithCoefficient>> d5CoefficientMap) {
|
||||
// 统计球号出现次数
|
||||
@@ -960,7 +960,7 @@ public class FollowerBallPredictor {
|
||||
int frequency = frequencyGroup.getKey();
|
||||
int ballsInGroup = frequencyGroup.getValue().size();
|
||||
|
||||
if (currentSelected + ballsInGroup <= 10) {
|
||||
if (currentSelected + ballsInGroup <= 12) {
|
||||
// 这个频率组的球全部入选
|
||||
currentSelected += ballsInGroup;
|
||||
minSelectedFrequency = frequency;
|
||||
@@ -986,12 +986,12 @@ public class FollowerBallPredictor {
|
||||
|
||||
// 按频率从高到低处理每个频率组
|
||||
for (Map.Entry<Integer, List<Integer>> frequencyGroup : frequencyGroups.entrySet()) {
|
||||
if (resultBalls.size() >= 10) {
|
||||
if (resultBalls.size() >= 12) {
|
||||
break;
|
||||
}
|
||||
|
||||
List<Integer> ballsInGroup = frequencyGroup.getValue();
|
||||
int needCount = Math.min(10 - resultBalls.size(), ballsInGroup.size());
|
||||
int needCount = Math.min(12 - resultBalls.size(), ballsInGroup.size());
|
||||
|
||||
if (ballsInGroup.size() <= needCount) {
|
||||
// 如果这个频率组的球数量 <= 需要的数量,全部加入
|
||||
@@ -1026,7 +1026,7 @@ public class FollowerBallPredictor {
|
||||
|
||||
// 生成筛选过程说明
|
||||
if (hasSecondarySelection) {
|
||||
processBuilder.append("无法直接筛选出前10个,其中");
|
||||
processBuilder.append("无法直接筛选出前12个,其中");
|
||||
Collections.sort(directSelected);
|
||||
String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append(directSelectedStr).append("直接入选,");
|
||||
@@ -1042,7 +1042,7 @@ public class FollowerBallPredictor {
|
||||
processBuilder.append("最终筛选出").append(finalSelectedStr).append(",");
|
||||
|
||||
String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append("组成前10个球号:").append(allResultStr).append("。");
|
||||
processBuilder.append("组成前12个球号:").append(allResultStr).append("。");
|
||||
|
||||
processBuilder.append("筛选步骤:").append(selectionSteps).append("。");
|
||||
if (!detailedCoefficientInfo.isEmpty()) {
|
||||
@@ -1050,7 +1050,7 @@ public class FollowerBallPredictor {
|
||||
}
|
||||
} else {
|
||||
String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", "));
|
||||
processBuilder.append("直接筛选出前10个球号:").append(allResultStr).append("。");
|
||||
processBuilder.append("直接筛选出前12个球号:").append(allResultStr).append("。");
|
||||
}
|
||||
|
||||
// 无论是否有二次筛选,都要显示筛选步骤
|
||||
|
||||
@@ -62,6 +62,26 @@ public class UserAddRequest implements Serializable {
|
||||
* 会员到期时间
|
||||
*/
|
||||
private Date vipExpire;
|
||||
|
||||
/**
|
||||
* 套餐类别(体验会员/月度会员/年度会员)
|
||||
*/
|
||||
private String vipType;
|
||||
|
||||
/**
|
||||
* 所在省市
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 彩票偏好(双色球/大乐透等)
|
||||
*/
|
||||
private String preference;
|
||||
|
||||
/**
|
||||
* 获客渠道
|
||||
*/
|
||||
private String channel;
|
||||
|
||||
/**
|
||||
* 状态:0-正常,1-封禁
|
||||
|
||||
@@ -51,6 +51,26 @@ public class UserQueryRequest extends PageRequest implements Serializable {
|
||||
* 是否会员:0-非会员,1-会员
|
||||
*/
|
||||
private Integer isVip;
|
||||
|
||||
/**
|
||||
* 套餐类别(体验会员/月度会员/年度会员)
|
||||
*/
|
||||
private String vipType;
|
||||
|
||||
/**
|
||||
* 所在省市
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 彩票偏好(双色球/大乐透等)
|
||||
*/
|
||||
private String preference;
|
||||
|
||||
/**
|
||||
* 获客渠道
|
||||
*/
|
||||
private String channel;
|
||||
|
||||
/**
|
||||
* 状态:0-正常,1-封禁
|
||||
|
||||
@@ -67,6 +67,26 @@ public class UserUpdateRequest implements Serializable {
|
||||
* 会员到期时间
|
||||
*/
|
||||
private Date vipExpire;
|
||||
|
||||
/**
|
||||
* 套餐类别(体验会员/月度会员/年度会员)
|
||||
*/
|
||||
private String vipType;
|
||||
|
||||
/**
|
||||
* 所在省市
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 彩票偏好(双色球/大乐透等)
|
||||
*/
|
||||
private String preference;
|
||||
|
||||
/**
|
||||
* 获客渠道
|
||||
*/
|
||||
private String channel;
|
||||
|
||||
/**
|
||||
* 状态:0-正常,1-封禁
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 公告管理表
|
||||
* @TableName announcement
|
||||
*/
|
||||
@TableName(value ="announcement")
|
||||
@Data
|
||||
public class Announcement {
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 公告标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 公告详情内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发布人ID
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long publisherId;
|
||||
|
||||
/**
|
||||
* 发布人名称
|
||||
*/
|
||||
private String publisherName;
|
||||
|
||||
/**
|
||||
* 公告状态:0-草稿,1-已发布,2-已下架
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 优先级:0-普通,1-置顶
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 浏览次数
|
||||
*/
|
||||
private Integer viewCount;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除:0-未删除,1-已删除
|
||||
*/
|
||||
private Integer isDelete;
|
||||
}
|
||||
@@ -63,4 +63,9 @@ public class DltDrawRecord {
|
||||
* 后区2
|
||||
*/
|
||||
private Integer backBall2;
|
||||
|
||||
/**
|
||||
* 奖池(单位:亿元)
|
||||
*/
|
||||
private Double prizePool;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ 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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -17,16 +19,19 @@ public class DltPredictRecord {
|
||||
* 唯一标识符
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 开奖期号
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long drawId;
|
||||
|
||||
/**
|
||||
@@ -88,5 +93,6 @@ public class DltPredictRecord {
|
||||
/**
|
||||
* 奖金
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long bonus;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.xy.xyaicpzs.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -16,6 +18,7 @@ public class LotteryDraws {
|
||||
* 开奖期号
|
||||
*/
|
||||
@TableId
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long drawId;
|
||||
|
||||
/**
|
||||
@@ -57,4 +60,14 @@ public class LotteryDraws {
|
||||
* 蓝球
|
||||
*/
|
||||
private Integer blueBall;
|
||||
|
||||
/**
|
||||
* 奖池(单位:亿元)
|
||||
*/
|
||||
private Double prizePool;
|
||||
|
||||
/**
|
||||
* 是否处于特别规定期间:0-否,1-是
|
||||
*/
|
||||
private Integer isSpecialPeriod;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ 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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -17,11 +19,13 @@ public class OperationHistory {
|
||||
* 唯一标识符
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 操作用户ID
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ 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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -17,16 +19,19 @@ public class PredictRecord {
|
||||
* 唯一标识符
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 开奖期号
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long drawId;
|
||||
|
||||
/**
|
||||
@@ -87,6 +92,7 @@ public class PredictRecord {
|
||||
/**
|
||||
* 奖金
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long bonus;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ 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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -17,6 +19,7 @@ public class User {
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
@@ -64,6 +67,26 @@ public class User {
|
||||
*/
|
||||
private Date vipExpire;
|
||||
|
||||
/**
|
||||
* 套餐类别(体验会员/月度会员/年度会员)
|
||||
*/
|
||||
private String vipType;
|
||||
|
||||
/**
|
||||
* 所在省市
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 彩票偏好(双色球/大乐透等)
|
||||
*/
|
||||
private String preference;
|
||||
|
||||
/**
|
||||
* 获客渠道
|
||||
*/
|
||||
private String channel;
|
||||
|
||||
/**
|
||||
* 状态:0-正常,1-封禁
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,8 @@ 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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -17,6 +19,7 @@ public class VipCode {
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
@@ -42,6 +45,7 @@ public class VipCode {
|
||||
/**
|
||||
* 创建的用户id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long createdUserId;
|
||||
|
||||
/**
|
||||
@@ -53,6 +57,7 @@ public class VipCode {
|
||||
/**
|
||||
* 使用的用户id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long usedUserId;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ 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 com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -17,11 +19,13 @@ public class VipExchangeRecord {
|
||||
* 唯一标识符
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
@@ -37,6 +41,7 @@ public class VipExchangeRecord {
|
||||
/**
|
||||
* 订单编号
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long orderNo;
|
||||
|
||||
/**
|
||||
|
||||
74
src/main/java/com/xy/xyaicpzs/domain/vo/AnnouncementVO.java
Normal file
74
src/main/java/com/xy/xyaicpzs/domain/vo/AnnouncementVO.java
Normal file
@@ -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 AnnouncementVO {
|
||||
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
@Schema(description = "唯一标识符")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 公告标题
|
||||
*/
|
||||
@Schema(description = "公告标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 公告详情内容
|
||||
*/
|
||||
@Schema(description = "公告详情内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发布人ID
|
||||
*/
|
||||
@Schema(description = "发布人ID")
|
||||
private Long publisherId;
|
||||
|
||||
/**
|
||||
* 发布人名称
|
||||
*/
|
||||
@Schema(description = "发布人名称")
|
||||
private String publisherName;
|
||||
|
||||
/**
|
||||
* 公告状态:0-草稿,1-已发布,2-已下架
|
||||
*/
|
||||
@Schema(description = "公告状态:0-草稿,1-已发布,2-已下架")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 优先级:0-普通,1-置顶
|
||||
*/
|
||||
@Schema(description = "优先级:0-普通,1-置顶")
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 浏览次数
|
||||
*/
|
||||
@Schema(description = "浏览次数")
|
||||
private Integer viewCount;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Schema(description = "更新时间")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -60,6 +60,26 @@ public class UserVO implements Serializable {
|
||||
* 会员到期时间
|
||||
*/
|
||||
private Date vipExpire;
|
||||
|
||||
/**
|
||||
* 套餐类别(体验会员/月度会员/年度会员)
|
||||
*/
|
||||
private String vipType;
|
||||
|
||||
/**
|
||||
* 所在省市
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 彩票偏好(双色球/大乐透等)
|
||||
*/
|
||||
private String preference;
|
||||
|
||||
/**
|
||||
* 获客渠道
|
||||
*/
|
||||
private String channel;
|
||||
|
||||
/**
|
||||
* 状态:0-正常,1-封禁
|
||||
|
||||
18
src/main/java/com/xy/xyaicpzs/mapper/AnnouncementMapper.java
Normal file
18
src/main/java/com/xy/xyaicpzs/mapper/AnnouncementMapper.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.xy.xyaicpzs.mapper;
|
||||
|
||||
import com.xy.xyaicpzs.domain.entity.Announcement;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @author XY003
|
||||
* @description 针对表【announcement(公告管理表)】的数据库操作Mapper
|
||||
* @createDate 2025-11-21 17:52:54
|
||||
* @Entity com.xy.xyaicpzs.domain.entity.Announcement
|
||||
*/
|
||||
public interface AnnouncementMapper extends BaseMapper<Announcement> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xy.xyaicpzs.service;
|
||||
|
||||
import com.xy.xyaicpzs.domain.entity.Announcement;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @author XY003
|
||||
* @description 针对表【announcement(公告管理表)】的数据库操作Service
|
||||
* @createDate 2025-11-21 17:52:54
|
||||
*/
|
||||
public interface AnnouncementService extends IService<Announcement> {
|
||||
|
||||
}
|
||||
@@ -57,4 +57,12 @@ public interface DltDataAnalysisService {
|
||||
* @return 后区球号命中率统计信息
|
||||
*/
|
||||
RedBallHitRateVO getBackBallHitRate(Long userId);
|
||||
/**
|
||||
* 管理员获取大乐透奖金统计(支持用户ID和奖项筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param prizeGrade 奖项等级(可选)
|
||||
* @return 奖金统计信息
|
||||
*/
|
||||
PrizeEstimateVO getAllUsersDltPrizeStatistics(Long userId, String prizeGrade);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.xy.xyaicpzs.service;
|
||||
import com.xy.xyaicpzs.domain.entity.DltDrawRecord;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author XY003
|
||||
* @description 针对表【dlt_draw_record(大乐透开奖信息表)】的数据库操作Service
|
||||
@@ -10,4 +12,11 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
*/
|
||||
public interface DltDrawRecordService extends IService<DltDrawRecord> {
|
||||
|
||||
/**
|
||||
* 获取近期大乐透开奖信息
|
||||
* @param limit 获取条数,默认10条
|
||||
* @return 开奖信息列表,按开奖期号倒序排列
|
||||
*/
|
||||
List<DltDrawRecord> getRecentDraws(Integer limit);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.xy.xyaicpzs.service;
|
||||
|
||||
import com.xy.xyaicpzs.domain.entity.DltPredictRecord;
|
||||
import com.xy.xyaicpzs.common.response.PageResponse;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -40,4 +41,24 @@ public interface DltPredictRecordService extends IService<DltPredictRecord> {
|
||||
*/
|
||||
Long getDltPredictRecordsCountByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 管理员获取所有大乐透推测记录(支持分页和用户ID筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param predictResult 中奖等级(可选)
|
||||
* @param current 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 分页的大乐透预测记录
|
||||
*/
|
||||
PageResponse<DltPredictRecord> getAllRecordsForAdmin(Long userId, String predictResult, Integer current, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 管理员获取所有中奖记录(支持分页和筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param prizeGrade 奖项等级(可选)
|
||||
* @param current 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 分页的中奖记录
|
||||
*/
|
||||
PageResponse<DltPredictRecord> getWinningRecordsForAdmin(Long userId, String prizeGrade, Integer current, Integer pageSize);
|
||||
|
||||
}
|
||||
|
||||
92
src/main/java/com/xy/xyaicpzs/service/PageViewService.java
Normal file
92
src/main/java/com/xy/xyaicpzs/service/PageViewService.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.xy.xyaicpzs.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 页面访问量统计服务
|
||||
* 使用Redis实现PV统计
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PageViewService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
// Redis Key前缀
|
||||
private static final String PV_TOTAL_KEY = "pv:total"; // 总PV
|
||||
private static final String PV_DAILY_KEY = "pv:daily:"; // 每日PV前缀
|
||||
private static final int MAX_DAYS = 90; // 最大保存天数
|
||||
|
||||
/**
|
||||
* 增加页面访问量
|
||||
*/
|
||||
public void incrementPageView() {
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
|
||||
|
||||
// 增加总PV
|
||||
redisTemplate.opsForValue().increment(PV_TOTAL_KEY);
|
||||
|
||||
// 增加今日总PV
|
||||
String dailyKey = PV_DAILY_KEY + today;
|
||||
redisTemplate.opsForValue().increment(dailyKey);
|
||||
// 设置过期时间为90天
|
||||
redisTemplate.expire(dailyKey, MAX_DAYS, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总PV
|
||||
*/
|
||||
public Long getTotalPageViews() {
|
||||
Object value = redisTemplate.opsForValue().get(PV_TOTAL_KEY);
|
||||
return value != null ? Long.parseLong(value.toString()) : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日总PV
|
||||
*/
|
||||
public Long getTodayPageViews() {
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
|
||||
Object value = redisTemplate.opsForValue().get(PV_DAILY_KEY + today);
|
||||
return value != null ? Long.parseLong(value.toString()) : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期的PV
|
||||
* @param date 日期,格式:yyyy-MM-dd
|
||||
*/
|
||||
public Long getDailyPageViews(String date) {
|
||||
Object value = redisTemplate.opsForValue().get(PV_DAILY_KEY + date);
|
||||
return value != null ? Long.parseLong(value.toString()) : 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据日期范围获取PV统计(近7天/30天/90天)
|
||||
* @param days 天数,最大90天
|
||||
* @return 日期和对应PV的Map,按日期倒序排列
|
||||
*/
|
||||
public Map<String, Long> getPageViewsByDays(int days) {
|
||||
// 限制最大天数为90天
|
||||
if (days > MAX_DAYS) {
|
||||
days = MAX_DAYS;
|
||||
}
|
||||
|
||||
Map<String, Long> result = new LinkedHashMap<>();
|
||||
LocalDate today = LocalDate.now();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
|
||||
|
||||
for (int i = 0; i < days; i++) {
|
||||
String date = today.minusDays(i).format(formatter);
|
||||
Long pv = getDailyPageViews(date);
|
||||
result.put(date, pv);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.xy.xyaicpzs.service;
|
||||
|
||||
import com.xy.xyaicpzs.domain.entity.PredictRecord;
|
||||
import com.xy.xyaicpzs.common.response.PageResponse;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -47,4 +48,24 @@ public interface PredictRecordService extends IService<PredictRecord> {
|
||||
*/
|
||||
Long getPredictRecordsCountByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 管理员获取所有推测记录(支持分页和用户ID筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param predictResult 中奖等级(可选)
|
||||
* @param current 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 分页的预测记录
|
||||
*/
|
||||
PageResponse<PredictRecord> getAllRecordsForAdmin(Long userId, String predictResult, Integer current, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 管理员获取所有中奖记录(支持分页和筛选)
|
||||
* @param userId 用户ID(可选)
|
||||
* @param prizeGrade 奖项等级(可选)
|
||||
* @param current 当前页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 分页的中奖记录
|
||||
*/
|
||||
PageResponse<PredictRecord> getWinningRecordsForAdmin(Long userId, String prizeGrade, Integer current, Integer pageSize);
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public interface VipCodeService extends IService<VipCode> {
|
||||
|
||||
/**
|
||||
* 获取一个可用的会员码
|
||||
* @param vipExpireTime 会员有效月数(1或12)
|
||||
* @param vipExpireTime 会员有效月数
|
||||
* @param createdUserId 创建人ID
|
||||
* @param createdUserName 创建人名称
|
||||
* @return 可用的会员码,如果没有则返回null
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xy.xyaicpzs.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xy.xyaicpzs.domain.entity.Announcement;
|
||||
import com.xy.xyaicpzs.service.AnnouncementService;
|
||||
import com.xy.xyaicpzs.mapper.AnnouncementMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author XY003
|
||||
* @description 针对表【announcement(公告管理表)】的数据库操作Service实现
|
||||
* @createDate 2025-11-21 17:52:54
|
||||
*/
|
||||
@Service
|
||||
public class AnnouncementServiceImpl extends ServiceImpl<AnnouncementMapper, Announcement>
|
||||
implements AnnouncementService{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ 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 com.xy.xyaicpzs.util.SsqPrizeCalculator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -19,6 +21,7 @@ import java.util.List;
|
||||
/**
|
||||
* 数据分析服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DataAnalysisServiceImpl implements DataAnalysisService {
|
||||
|
||||
@@ -82,37 +85,83 @@ public class DataAnalysisServiceImpl implements DataAnalysisService {
|
||||
|
||||
@Override
|
||||
public int processPendingPredictions() {
|
||||
log.info("开始处理双色球待开奖预测记录");
|
||||
|
||||
// 查询所有待开奖的预测记录
|
||||
QueryWrapper<PredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("predictStatus", "待开奖")
|
||||
.eq("type", "ssq");
|
||||
List<PredictRecord> pendingRecords = predictRecordMapper.selectList(queryWrapper);
|
||||
|
||||
log.info("找到{}条待开奖的双色球预测记录", pendingRecords.size());
|
||||
|
||||
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("已中奖");
|
||||
try {
|
||||
// 查询对应期号的开奖结果
|
||||
LotteryDraws draws = lotteryDrawsMapper.selectById(record.getDrawId());
|
||||
|
||||
if (draws != null) {
|
||||
log.debug("处理预测记录ID:{},期号:{},奖池:{}亿元,特别规定期间:{}",
|
||||
record.getId(), record.getDrawId(), draws.getPrizePool(),
|
||||
draws.getIsSpecialPeriod());
|
||||
|
||||
// 构建预测号码数组 [红球6个, 蓝球1个]
|
||||
Integer[] predictNumbers = {
|
||||
record.getRedBall1(),
|
||||
record.getRedBall2(),
|
||||
record.getRedBall3(),
|
||||
record.getRedBall4(),
|
||||
record.getRedBall5(),
|
||||
record.getRedBall6(),
|
||||
record.getBlueBall()
|
||||
};
|
||||
|
||||
// 构建开奖号码数组 [红球6个, 蓝球1个]
|
||||
Integer[] drawNumbers = {
|
||||
draws.getRedBall1(),
|
||||
draws.getRedBall2(),
|
||||
draws.getRedBall3(),
|
||||
draws.getRedBall4(),
|
||||
draws.getRedBall5(),
|
||||
draws.getRedBall6(),
|
||||
draws.getBlueBall()
|
||||
};
|
||||
|
||||
// 获取奖池金额和特别规定状态
|
||||
Double prizePool = draws.getPrizePool() != null ? draws.getPrizePool() : 0.0;
|
||||
boolean isSpecialPeriod = draws.getIsSpecialPeriod() != null && draws.getIsSpecialPeriod() == 1;
|
||||
|
||||
// 使用SsqPrizeCalculator计算中奖结果
|
||||
SsqPrizeCalculator.PrizeResult prizeResult = SsqPrizeCalculator.calculatePrize(
|
||||
predictNumbers, drawNumbers, prizePool, isSpecialPeriod);
|
||||
|
||||
// 更新预测记录
|
||||
if(prizeResult.getPrizeLevel().equals("未中奖")){
|
||||
record.setPredictStatus("未中奖");
|
||||
}else{
|
||||
record.setPredictStatus("已中奖");
|
||||
}
|
||||
|
||||
record.setPredictResult(prizeResult.getPrizeLevel());
|
||||
record.setBonus(prizeResult.getBonus());
|
||||
predictRecordMapper.updateById(record);
|
||||
|
||||
log.debug("预测记录ID:{} 处理完成,中奖等级:{},奖金:{}元(奖池:{}亿元,特别规定:{})",
|
||||
record.getId(), prizeResult.getPrizeLevel(), prizeResult.getBonus(),
|
||||
prizePool, isSpecialPeriod);
|
||||
|
||||
processedCount++;
|
||||
} else {
|
||||
log.debug("未找到期号{}的开奖记录", record.getDrawId());
|
||||
}
|
||||
|
||||
// 更新预测记录
|
||||
|
||||
record.setPredictResult(result);
|
||||
record.setBonus(bonus);
|
||||
predictRecordMapper.updateById(record);
|
||||
|
||||
processedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("处理预测记录ID:{} 时发生错误:{}", record.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("双色球待开奖预测记录处理完成,共处理{}条记录", processedCount);
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
@@ -157,68 +206,4 @@ public class DataAnalysisServiceImpl implements DataAnalysisService {
|
||||
queryWrapper.eq("type", "ssq");
|
||||
return predictRecordMapper.selectCount(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算预测结果
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,8 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
DltDrawRecord drawRecord = dltDrawRecordMapper.selectOne(drawQueryWrapper);
|
||||
|
||||
if (drawRecord != null) {
|
||||
log.debug("处理预测记录ID:{},期号:{}", record.getId(), record.getDrawId());
|
||||
log.debug("处理预测记录ID:{},期号:{},奖池:{}亿元",
|
||||
record.getId(), record.getDrawId(), drawRecord.getPrizePool());
|
||||
|
||||
// 构建预测号码数组 [前区5个号码, 后区2个号码]
|
||||
Integer[] predictNumbers = {
|
||||
@@ -85,8 +86,12 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
drawRecord.getBackBall2()
|
||||
};
|
||||
|
||||
// 使用DltPrizeCalculator计算中奖结果
|
||||
DltPrizeCalculator.PrizeResult prizeResult = DltPrizeCalculator.calculatePrize(predictNumbers, drawNumbers);
|
||||
// 获取奖池金额,如果为null则默认为0
|
||||
Double prizePool = drawRecord.getPrizePool() != null ? drawRecord.getPrizePool() : 0.0;
|
||||
|
||||
// 使用DltPrizeCalculator计算中奖结果,传入奖池金额
|
||||
DltPrizeCalculator.PrizeResult prizeResult = DltPrizeCalculator.calculatePrize(
|
||||
predictNumbers, drawNumbers, prizePool);
|
||||
|
||||
// 更新预测记录
|
||||
// 根据中奖结果设置状态
|
||||
@@ -100,8 +105,8 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
|
||||
dltPredictRecordMapper.updateById(record);
|
||||
|
||||
log.debug("预测记录ID:{} 处理完成,中奖等级:{},奖金:{}",
|
||||
record.getId(), prizeResult.getPrizeLevel(), prizeResult.getBonus());
|
||||
log.debug("预测记录ID:{} 处理完成,中奖等级:{},奖金:{}元(奖池:{}亿元)",
|
||||
record.getId(), prizeResult.getPrizeLevel(), prizeResult.getBonus(), prizePool);
|
||||
|
||||
processedCount++;
|
||||
} else {
|
||||
@@ -147,7 +152,8 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
for (DltPredictRecord record : predictRecords) {
|
||||
if ("待开奖".equals(record.getPredictStatus())) {
|
||||
pendingCount++;
|
||||
} else if ("已开奖".equals(record.getPredictStatus())) {
|
||||
} else {
|
||||
// 已开奖的记录(包括已开奖、已中奖、未中奖等状态)
|
||||
drawnCount++;
|
||||
// 检查是否中奖(除了"未中奖"都算中奖)
|
||||
if (!"未中奖".equals(record.getPredictResult()) &&
|
||||
@@ -158,7 +164,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算命中率
|
||||
// 计算命中率:命中次数 ÷ (总次数 - 待开奖次数) = 命中次数 ÷ 已开奖次数
|
||||
BigDecimal hitRate = drawnCount > 0 ?
|
||||
BigDecimal.valueOf(hitCount).divide(BigDecimal.valueOf(drawnCount), 4, RoundingMode.HALF_UP) :
|
||||
BigDecimal.ZERO;
|
||||
@@ -184,7 +190,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("userId", userId);
|
||||
// 排除待开奖状态
|
||||
queryWrapper.eq("predictStatus", "已开奖");
|
||||
queryWrapper.ne("predictStatus", "待开奖");
|
||||
// 如果指定了预测记录ID,则只查询该记录
|
||||
if (predictId != null) {
|
||||
queryWrapper.eq("id", predictId);
|
||||
@@ -198,7 +204,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
Map<String, BigDecimal> totalBonusByPrizeLevel = new HashMap<>();
|
||||
|
||||
// 初始化大乐透的所有等级
|
||||
String[] prizeLevels = {"一等奖", "二等奖", "三等奖", "四等奖", "五等奖", "六等奖", "七等奖", "八等奖", "九等奖", "未中奖"};
|
||||
String[] prizeLevels = {"一等奖", "二等奖", "三等奖", "四等奖", "五等奖", "六等奖", "七等奖", "未中奖"};
|
||||
for (String level : prizeLevels) {
|
||||
countByPrizeLevel.put(level, 0);
|
||||
totalBonusByPrizeLevel.put(level, BigDecimal.ZERO);
|
||||
@@ -269,7 +275,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
// 查询用户的所有预测记录(除了"待开奖"状态的)
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("userId", userId)
|
||||
.eq("predictStatus", "已开奖");
|
||||
.ne("predictStatus", "待开奖");
|
||||
|
||||
List<DltPredictRecord> predictRecords = dltPredictRecordMapper.selectList(queryWrapper);
|
||||
|
||||
@@ -323,7 +329,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
// 查询用户的所有预测记录(除了"待开奖"状态的)
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("userId", userId)
|
||||
.eq("predictStatus", "已开奖");
|
||||
.ne("predictStatus", "待开奖");
|
||||
|
||||
List<DltPredictRecord> predictRecords = dltPredictRecordMapper.selectList(queryWrapper);
|
||||
|
||||
@@ -384,7 +390,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
// 查询用户的所有预测记录(除了"待开奖"状态的)
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("userId", userId)
|
||||
.eq("predictStatus", "已开奖");
|
||||
.ne("predictStatus", "待开奖");
|
||||
|
||||
List<DltPredictRecord> predictRecords = dltPredictRecordMapper.selectList(queryWrapper);
|
||||
|
||||
@@ -435,7 +441,7 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
// 查询用户的所有预测记录(除了"待开奖"状态的)
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("userId", userId)
|
||||
.eq("predictStatus", "已开奖");
|
||||
.ne("predictStatus", "待开奖");
|
||||
|
||||
List<DltPredictRecord> predictRecords = dltPredictRecordMapper.selectList(queryWrapper);
|
||||
|
||||
@@ -482,4 +488,125 @@ public class DltDataAnalysisServiceImpl implements DltDataAnalysisService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrizeEstimateVO getAllUsersDltPrizeStatistics(Long userId, String prizeGrade) {
|
||||
log.info("管理员开始获取所有用户的大乐透奖金统计,userId={},prizeGrade={}", userId, prizeGrade);
|
||||
|
||||
// 查询条件
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
// 排除待开奖状态
|
||||
queryWrapper.ne("predictStatus", "待开奖");
|
||||
|
||||
// 如果提供了用户ID,添加筛选条件
|
||||
if (userId != null) {
|
||||
queryWrapper.eq("userId", userId);
|
||||
}
|
||||
|
||||
// 如果提供了奖项等级,添加筛选条件
|
||||
if (prizeGrade != null && !prizeGrade.trim().isEmpty()) {
|
||||
queryWrapper.eq("predictResult", prizeGrade);
|
||||
}
|
||||
|
||||
// 查询符合条件的大乐透预测记录
|
||||
List<DltPredictRecord> records = dltPredictRecordMapper.selectList(queryWrapper);
|
||||
|
||||
// 按中奖等级分组统计
|
||||
Map<String, Integer> countByPrizeLevel = new HashMap<>();
|
||||
Map<String, BigDecimal> totalBonusByPrizeLevel = new HashMap<>();
|
||||
|
||||
// 初始化大乐透的所有等级
|
||||
String[] prizeLevels = {"一等奖", "二等奖", "三等奖", "四等奖", "五等奖", "六等奖", "七等奖", "未中奖"};
|
||||
for (String level : prizeLevels) {
|
||||
countByPrizeLevel.put(level, 0);
|
||||
totalBonusByPrizeLevel.put(level, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
// 统计各等级数量和奖金
|
||||
for (DltPredictRecord record : records) {
|
||||
String prizeLevel = record.getPredictResult();
|
||||
if (prizeLevel == null || prizeLevel.trim().isEmpty()) {
|
||||
prizeLevel = "未中奖";
|
||||
}
|
||||
|
||||
// 如果只查询特定奖项,且当前记录不是该奖项(理论上SQL已经过滤,这里双重保险)
|
||||
if (prizeGrade != null && !prizeGrade.trim().isEmpty() && !prizeGrade.equals(prizeLevel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 确保prizeLevel在预定义的等级中,或者动态添加
|
||||
if (!countByPrizeLevel.containsKey(prizeLevel)) {
|
||||
countByPrizeLevel.put(prizeLevel, 0);
|
||||
totalBonusByPrizeLevel.put(prizeLevel, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
// 更新计数
|
||||
countByPrizeLevel.put(prizeLevel, countByPrizeLevel.get(prizeLevel) + 1);
|
||||
|
||||
// 累计奖金
|
||||
if (record.getBonus() != null) {
|
||||
BigDecimal bonus = new BigDecimal(record.getBonus().toString());
|
||||
totalBonusByPrizeLevel.put(prizeLevel,
|
||||
totalBonusByPrizeLevel.get(prizeLevel).add(bonus));
|
||||
}
|
||||
}
|
||||
|
||||
// 构建返回结果
|
||||
List<PrizeEstimateVO.PrizeDetailItem> prizeDetails = new ArrayList<>();
|
||||
|
||||
// 如果指定了奖项,只返回该奖项的统计
|
||||
if (prizeGrade != null && !prizeGrade.trim().isEmpty()) {
|
||||
if (countByPrizeLevel.containsKey(prizeGrade)) {
|
||||
int count = countByPrizeLevel.get(prizeGrade);
|
||||
BigDecimal totalBonus = totalBonusByPrizeLevel.get(prizeGrade);
|
||||
BigDecimal singlePrize = BigDecimal.ZERO;
|
||||
if (count > 0 && totalBonus.compareTo(BigDecimal.ZERO) > 0) {
|
||||
singlePrize = totalBonus.divide(new BigDecimal(count), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
prizeDetails.add(PrizeEstimateVO.PrizeDetailItem.builder()
|
||||
.prizeLevel(prizeGrade)
|
||||
.winningCount(count)
|
||||
.singlePrize(singlePrize)
|
||||
.subtotal(totalBonus)
|
||||
.build());
|
||||
}
|
||||
} else {
|
||||
// 否则返回所有等级的统计
|
||||
for (String level : prizeLevels) {
|
||||
// 跳过没有记录的等级
|
||||
if (!countByPrizeLevel.containsKey(level) || countByPrizeLevel.get(level) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int count = countByPrizeLevel.get(level);
|
||||
BigDecimal totalBonus = totalBonusByPrizeLevel.get(level);
|
||||
BigDecimal singlePrize = BigDecimal.ZERO;
|
||||
if (count > 0 && totalBonus.compareTo(BigDecimal.ZERO) > 0) {
|
||||
singlePrize = totalBonus.divide(new BigDecimal(count), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
prizeDetails.add(PrizeEstimateVO.PrizeDetailItem.builder()
|
||||
.prizeLevel(level)
|
||||
.winningCount(count)
|
||||
.singlePrize(singlePrize)
|
||||
.subtotal(totalBonus)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总奖金
|
||||
BigDecimal totalPrize = prizeDetails.stream()
|
||||
.map(PrizeEstimateVO.PrizeDetailItem::getSubtotal)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
PrizeEstimateVO result = PrizeEstimateVO.builder()
|
||||
.totalPrize(totalPrize)
|
||||
.prizeDetails(prizeDetails)
|
||||
.build();
|
||||
|
||||
log.info("管理员获取大乐透奖金统计完成,总奖金{},明细数量:{}", totalPrize, prizeDetails.size());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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.DltDrawRecord;
|
||||
import com.xy.xyaicpzs.service.DltDrawRecordService;
|
||||
import com.xy.xyaicpzs.mapper.DltDrawRecordMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author XY003
|
||||
* @description 针对表【dlt_draw_record(大乐透开奖信息表)】的数据库操作Service实现
|
||||
@@ -15,6 +18,15 @@ import org.springframework.stereotype.Service;
|
||||
public class DltDrawRecordServiceImpl extends ServiceImpl<DltDrawRecordMapper, DltDrawRecord>
|
||||
implements DltDrawRecordService{
|
||||
|
||||
@Override
|
||||
public List<DltDrawRecord> getRecentDraws(Integer limit) {
|
||||
if (limit == null || limit <= 0) {
|
||||
limit = 10;
|
||||
}
|
||||
QueryWrapper<DltDrawRecord> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.orderByDesc("draw_id").last("LIMIT " + limit);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.xy.xyaicpzs.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.xy.xyaicpzs.domain.entity.DltPredictRecord;
|
||||
import com.xy.xyaicpzs.service.DltPredictRecordService;
|
||||
import com.xy.xyaicpzs.mapper.DltPredictRecordMapper;
|
||||
import com.xy.xyaicpzs.common.response.PageResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -68,8 +71,95 @@ public class DltPredictRecordServiceImpl extends ServiceImpl<DltPredictRecordMap
|
||||
return count(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResponse<DltPredictRecord> getAllRecordsForAdmin(Long userId, String predictResult, Integer current, Integer pageSize) {
|
||||
// 参数校验
|
||||
if (current < 1) {
|
||||
current = 1;
|
||||
}
|
||||
if (pageSize < 1) {
|
||||
pageSize = 10;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100; // 限制最大每页100条
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
// 如果提供了用户ID,添加筛选条件
|
||||
if (userId != null) {
|
||||
queryWrapper.eq("userId", userId);
|
||||
}
|
||||
|
||||
// 如果提供了中奖等级,添加筛选条件
|
||||
if (StringUtils.isNotBlank(predictResult)) {
|
||||
queryWrapper.eq("predictResult", predictResult);
|
||||
}
|
||||
|
||||
// 按预测时间降序排序
|
||||
queryWrapper.orderByDesc("predictTime");
|
||||
|
||||
// 执行分页查询
|
||||
Page<DltPredictRecord> page = new Page<>(current, pageSize);
|
||||
Page<DltPredictRecord> resultPage = page(page, queryWrapper);
|
||||
|
||||
// 构建分页响应对象
|
||||
return PageResponse.of(
|
||||
resultPage.getRecords(),
|
||||
resultPage.getTotal(),
|
||||
(int) resultPage.getCurrent(),
|
||||
(int) resultPage.getSize()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResponse<DltPredictRecord> getWinningRecordsForAdmin(Long userId, String prizeGrade, Integer current, Integer pageSize) {
|
||||
// 参数校验
|
||||
if (current < 1) {
|
||||
current = 1;
|
||||
}
|
||||
if (pageSize < 1) {
|
||||
pageSize = 10;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<DltPredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
// 排除待开奖和未中奖的记录,只保留中奖记录
|
||||
queryWrapper.ne("predictStatus", "待开奖");
|
||||
// 注意:数据库中未中奖可能存为"未中奖"或空,视具体逻辑而定,这里假设是"未中奖"
|
||||
// 同时为了保险,也可以检查bonus > 0,但有些小奖可能bonus未设置?通常会有bonus。
|
||||
// 这里使用predictResult不为"未中奖"且不为空
|
||||
queryWrapper.isNotNull("predictResult");
|
||||
|
||||
// 如果提供了用户ID
|
||||
if (userId != null) {
|
||||
queryWrapper.eq("userId", userId);
|
||||
}
|
||||
|
||||
// 如果提供了具体奖项等级
|
||||
if (StringUtils.isNotBlank(prizeGrade)) {
|
||||
queryWrapper.eq("predictResult", prizeGrade);
|
||||
}
|
||||
|
||||
// 按预测时间降序排序
|
||||
queryWrapper.orderByDesc("predictTime");
|
||||
|
||||
// 执行分页查询
|
||||
Page<DltPredictRecord> page = new Page<>(current, pageSize);
|
||||
Page<DltPredictRecord> resultPage = page(page, queryWrapper);
|
||||
|
||||
// 构建分页响应对象
|
||||
return PageResponse.of(
|
||||
resultPage.getRecords(),
|
||||
resultPage.getTotal(),
|
||||
(int) resultPage.getCurrent(),
|
||||
(int) resultPage.getSize()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.xy.xyaicpzs.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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 com.xy.xyaicpzs.common.response.PageResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -75,4 +78,92 @@ public class PredictRecordServiceImpl extends ServiceImpl<PredictRecordMapper, P
|
||||
queryWrapper.eq("userId", userId);
|
||||
return count(queryWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResponse<PredictRecord> getAllRecordsForAdmin(Long userId, String predictResult, Integer current, Integer pageSize) {
|
||||
// 参数校验
|
||||
if (current < 1) {
|
||||
current = 1;
|
||||
}
|
||||
if (pageSize < 1) {
|
||||
pageSize = 10;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100; // 限制最大每页100条
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<PredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
// 如果提供了用户ID,添加筛选条件
|
||||
if (userId != null) {
|
||||
queryWrapper.eq("userId", userId);
|
||||
}
|
||||
|
||||
// 如果提供了中奖等级,添加筛选条件
|
||||
if (StringUtils.isNotBlank(predictResult)) {
|
||||
queryWrapper.eq("predictResult", predictResult);
|
||||
}
|
||||
|
||||
// 按预测时间降序排序
|
||||
queryWrapper.orderByDesc("predictTime");
|
||||
|
||||
// 执行分页查询
|
||||
Page<PredictRecord> page = new Page<>(current, pageSize);
|
||||
Page<PredictRecord> resultPage = page(page, queryWrapper);
|
||||
|
||||
// 构建分页响应对象
|
||||
return PageResponse.of(
|
||||
resultPage.getRecords(),
|
||||
resultPage.getTotal(),
|
||||
(int) resultPage.getCurrent(),
|
||||
(int) resultPage.getSize()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResponse<PredictRecord> getWinningRecordsForAdmin(Long userId, String prizeGrade, Integer current, Integer pageSize) {
|
||||
// 参数校验
|
||||
if (current < 1) {
|
||||
current = 1;
|
||||
}
|
||||
if (pageSize < 1) {
|
||||
pageSize = 10;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<PredictRecord> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
// 排除待开奖和未中奖的记录
|
||||
queryWrapper.ne("predictStatus", "待开奖");
|
||||
queryWrapper.isNotNull("predictResult");
|
||||
|
||||
// 如果提供了用户ID
|
||||
if (userId != null) {
|
||||
queryWrapper.eq("userId", userId);
|
||||
}
|
||||
|
||||
// 如果提供了具体奖项等级
|
||||
if (StringUtils.isNotBlank(prizeGrade)) {
|
||||
queryWrapper.eq("predictResult", prizeGrade);
|
||||
}
|
||||
|
||||
// 按预测时间降序排序
|
||||
queryWrapper.orderByDesc("predictTime");
|
||||
|
||||
// 执行分页查询
|
||||
Page<PredictRecord> page = new Page<>(current, pageSize);
|
||||
Page<PredictRecord> resultPage = page(page, queryWrapper);
|
||||
|
||||
// 构建分页响应对象
|
||||
return PageResponse.of(
|
||||
resultPage.getRecords(),
|
||||
resultPage.getTotal(),
|
||||
(int) resultPage.getCurrent(),
|
||||
(int) resultPage.getSize()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
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.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
import com.xy.xyaicpzs.service.SmsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -19,24 +20,30 @@ import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 短信服务实现类
|
||||
* 短信服务实现类(腾讯云SMS)
|
||||
*/
|
||||
@Service
|
||||
public class SmsServiceImpl implements SmsService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SmsServiceImpl.class);
|
||||
|
||||
@Value("${aliyun.sms.sign-name:西安精彩数据服务社}")
|
||||
@Value("${tencent.sms.secret-id}")
|
||||
private String secretId;
|
||||
|
||||
@Value("${tencent.sms.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${tencent.sms.sdk-app-id}")
|
||||
private String sdkAppId;
|
||||
|
||||
@Value("${tencent.sms.template-id}")
|
||||
private String templateId;
|
||||
|
||||
@Value("${tencent.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;
|
||||
@Value("${tencent.sms.region:ap-guangzhou}")
|
||||
private String region;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@@ -51,15 +58,11 @@ public class SmsServiceImpl implements SmsService {
|
||||
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);
|
||||
private SmsClient createSmsClient() {
|
||||
Credential cred = new Credential(secretId, secretKey);
|
||||
return new SmsClient(cred, region);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,37 +101,66 @@ public class SmsServiceImpl implements SmsService {
|
||||
// 生成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 {
|
||||
// 创建腾讯云短信客户端
|
||||
SmsClient client = createSmsClient();
|
||||
|
||||
// 构建短信请求
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
|
||||
// 短信应用ID
|
||||
req.setSmsSdkAppId(sdkAppId);
|
||||
|
||||
// 短信签名内容
|
||||
req.setSignName(signName);
|
||||
|
||||
// 模板ID
|
||||
req.setTemplateId(templateId);
|
||||
|
||||
// 模板参数:验证码
|
||||
String[] templateParamSet = {verificationCode};
|
||||
req.setTemplateParamSet(templateParamSet);
|
||||
|
||||
// 下发手机号码,采用E.164标准,+[国家或地区码][手机号]
|
||||
// 例如:+8613711112222,其中前面有一个+号,86为国家码,13711112222为手机号
|
||||
String[] phoneNumberSet = {"+86" + phoneNumber};
|
||||
req.setPhoneNumberSet(phoneNumberSet);
|
||||
|
||||
// 发送短信
|
||||
client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||
logger.info("短信验证码发送成功,手机号: {}", phoneNumber);
|
||||
SendSmsResponse resp = client.SendSms(req);
|
||||
|
||||
// 将验证码保存到Redis,设置过期时间
|
||||
String codeKey = SMS_CODE_PREFIX + phoneNumber;
|
||||
redisTemplate.opsForValue().set(codeKey, verificationCode, SMS_CODE_EXPIRE, TimeUnit.MINUTES);
|
||||
|
||||
// 增加当天发送次数,并设置过期时间为当天结束
|
||||
if (count == null) {
|
||||
count = 0;
|
||||
// 检查发送结果
|
||||
if (resp.getSendStatusSet() != null && resp.getSendStatusSet().length > 0) {
|
||||
String code = resp.getSendStatusSet()[0].getCode();
|
||||
if ("Ok".equals(code)) {
|
||||
logger.info("短信验证码发送成功,手机号: {}, SerialNo: {}", phoneNumber, resp.getSendStatusSet()[0].getSerialNo());
|
||||
|
||||
// 将验证码保存到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;
|
||||
} else {
|
||||
logger.error("短信发送失败, 手机号: {}, Code: {}, Message: {}",
|
||||
phoneNumber, code, resp.getSendStatusSet()[0].getMessage());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
logger.error("短信发送失败, 手机号: {}, 响应为空", phoneNumber);
|
||||
return false;
|
||||
}
|
||||
redisTemplate.opsForValue().set(countKey, count + 1, getSecondsUntilEndOfDay(), TimeUnit.SECONDS);
|
||||
|
||||
return true;
|
||||
} catch (TeaException error) {
|
||||
logger.error("短信发送失败, 手机号: {}, 错误信息: {}, 诊断信息: {}",
|
||||
phoneNumber, error.getMessage(), error.getData().get("Recommend"));
|
||||
} catch (TencentCloudSDKException e) {
|
||||
logger.error("腾讯云短信发送异常, 手机号: {}, 错误信息: {}", phoneNumber, e.getMessage(), e);
|
||||
return false;
|
||||
} catch (Exception error) {
|
||||
logger.error("短信发送异常, 手机号: {}", phoneNumber, error);
|
||||
} catch (Exception e) {
|
||||
logger.error("短信发送异常, 手机号: {}", phoneNumber, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ 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.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -41,6 +43,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public long userRegister(String userAccount, String userName, String userPassword, String checkPassword) {
|
||||
@@ -122,8 +127,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
|
||||
}
|
||||
|
||||
// 3. 记录用户的登录态
|
||||
// 3. 生成唯一token并存储到Redis(实现单终端登录)
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
String redisKey = UserConstant.REDIS_USER_LOGIN_TOKEN_PREFIX + user.getId();
|
||||
|
||||
// 将token存储到Redis,如果之前有token会被覆盖(实现单终端登录)
|
||||
redisTemplate.opsForValue().set(redisKey, token, UserConstant.USER_LOGIN_TOKEN_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
// 4. 将token和用户信息存储到Session
|
||||
request.getSession().setAttribute(UserConstant.USER_LOGIN_TOKEN, token);
|
||||
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
|
||||
|
||||
log.info("用户登录成功,用户ID:{},生成token:{}", user.getId(), token);
|
||||
return getSafetyUser(user);
|
||||
}
|
||||
|
||||
@@ -141,6 +156,27 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
if (currentUser == null || currentUser.getId() == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
|
||||
}
|
||||
|
||||
// 验证token是否有效(单终端登录验证)
|
||||
String sessionToken = (String) request.getSession().getAttribute(UserConstant.USER_LOGIN_TOKEN);
|
||||
if (sessionToken == null) {
|
||||
log.warn("Session中不存在token,用户ID:{}", currentUser.getId());
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "登录已失效,请重新登录");
|
||||
}
|
||||
|
||||
// 从Redis获取该用户的有效token
|
||||
String redisKey = UserConstant.REDIS_USER_LOGIN_TOKEN_PREFIX + currentUser.getId();
|
||||
String redisToken = (String) redisTemplate.opsForValue().get(redisKey);
|
||||
|
||||
// 如果Redis中没有token,或者Session中的token与Redis中的不一致,说明在其他设备登录了
|
||||
if (redisToken == null || !redisToken.equals(sessionToken)) {
|
||||
log.warn("Token验证失败,用户ID:{},Session token:{},Redis token:{}",
|
||||
currentUser.getId(), sessionToken, redisToken);
|
||||
// 清除Session
|
||||
request.getSession().invalidate();
|
||||
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "账号在其他设备登录,当前会话已失效");
|
||||
}
|
||||
|
||||
// 从数据库查询(追求性能的话可以注释,直接走缓存)
|
||||
long userId = currentUser.getId();
|
||||
currentUser = this.getById(userId);
|
||||
@@ -157,11 +193,24 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
*/
|
||||
@Override
|
||||
public boolean userLogout(HttpServletRequest request) {
|
||||
if (request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE) == null) {
|
||||
Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
|
||||
if (userObj == null) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
|
||||
}
|
||||
// 移除登录态
|
||||
|
||||
User user = (User) userObj;
|
||||
Long userId = user.getId();
|
||||
|
||||
// 清除Redis中的token
|
||||
if (userId != null) {
|
||||
String redisKey = UserConstant.REDIS_USER_LOGIN_TOKEN_PREFIX + userId;
|
||||
redisTemplate.delete(redisKey);
|
||||
log.info("用户注销,清除Redis token,用户ID:{}", userId);
|
||||
}
|
||||
|
||||
// 移除Session中的登录态和token
|
||||
request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE);
|
||||
request.getSession().removeAttribute(UserConstant.USER_LOGIN_TOKEN);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -269,9 +318,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
user.setUserName(userName);
|
||||
user.setCreateTime(new Date());
|
||||
user.setUpdateTime(new Date());
|
||||
// 设置为VIP用户,有效期10天
|
||||
// 设置为VIP用户,有效期30天
|
||||
user.setIsVip(0);
|
||||
Date vipExpireDate = new Date(System.currentTimeMillis() + 10L * 24 * 60 * 60 * 1000);
|
||||
Date vipExpireDate = new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000);
|
||||
user.setVipExpire(vipExpireDate);
|
||||
|
||||
boolean saveResult = this.save(user);
|
||||
@@ -319,8 +368,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号未注册");
|
||||
}
|
||||
|
||||
// 3. 记录用户的登录态
|
||||
// 3. 生成唯一token并存储到Redis(实现单终端登录)
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
String redisKey = UserConstant.REDIS_USER_LOGIN_TOKEN_PREFIX + user.getId();
|
||||
|
||||
// 将token存储到Redis,如果之前有token会被覆盖(实现单终端登录)
|
||||
redisTemplate.opsForValue().set(redisKey, token, UserConstant.USER_LOGIN_TOKEN_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
// 4. 将token和用户信息存储到Session
|
||||
request.getSession().setAttribute(UserConstant.USER_LOGIN_TOKEN, token);
|
||||
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
|
||||
|
||||
log.info("用户手机号登录成功,用户ID:{},生成token:{}", user.getId(), token);
|
||||
return getSafetyUser(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,15 +68,22 @@ public class VipCodeServiceImpl extends ServiceImpl<VipCodeMapper, VipCode> impl
|
||||
|
||||
// 4. 根据会员码的vipExpireTime判断会员类型
|
||||
String memberType;
|
||||
String vipType; // 用户表的vipType字段
|
||||
int orderAmount;
|
||||
if (vipCode.getVipExpireTime() == 1) {
|
||||
int expireMonths = vipCode.getVipExpireTime();
|
||||
if (expireMonths < 12) {
|
||||
memberType = "月度会员";
|
||||
orderAmount = 10;
|
||||
} else if (vipCode.getVipExpireTime() == 12) {
|
||||
vipType = "月度会员";
|
||||
orderAmount = 10 * expireMonths;
|
||||
} else if (expireMonths >= 12) {
|
||||
memberType = "年度会员";
|
||||
orderAmount = 100;
|
||||
vipType = "年度会员";
|
||||
orderAmount = 100 * (expireMonths / 12);
|
||||
} else {
|
||||
throw new IllegalArgumentException("无效的会员有效期:" + vipCode.getVipExpireTime());
|
||||
// 支持自定义月份,按比例计算订单金额(10元/月)
|
||||
memberType = expireMonths + "个月会员";
|
||||
vipType = "月度会员"; // 默认归类为月度会员
|
||||
orderAmount = 10 * expireMonths;
|
||||
}
|
||||
|
||||
// 5. 先在vip_exchange_record表插入兑换记录
|
||||
@@ -107,6 +114,7 @@ public class VipCodeServiceImpl extends ServiceImpl<VipCodeMapper, VipCode> impl
|
||||
updateUser.setId(userId);
|
||||
updateUser.setIsVip(1); // 设置为会员
|
||||
updateUser.setVipExpire(newVipExpire);
|
||||
updateUser.setVipType(vipType); // 设置会员类型
|
||||
updateUser.setUpdateTime(new Date());
|
||||
|
||||
int userUpdateResult = userMapper.updateById(updateUser);
|
||||
@@ -115,6 +123,8 @@ public class VipCodeServiceImpl extends ServiceImpl<VipCodeMapper, VipCode> impl
|
||||
throw new RuntimeException("更新用户会员状态失败");
|
||||
}
|
||||
|
||||
log.info("会员码激活成功,用户ID:{},会员类型:{},vipType:{},新的到期时间:{}", userId, memberType, vipType, newVipExpire);
|
||||
|
||||
// 8. 标记会员码为已使用,并记录使用人信息和使用时间
|
||||
Date now = new Date();
|
||||
VipCode updateVipCode = new VipCode();
|
||||
@@ -175,8 +185,8 @@ public class VipCodeServiceImpl extends ServiceImpl<VipCodeMapper, VipCode> impl
|
||||
throw new IllegalArgumentException("生成数量必须大于0");
|
||||
}
|
||||
|
||||
if (vipExpireTime != 1 && vipExpireTime != 12) {
|
||||
throw new IllegalArgumentException("会员有效月数只能是1或12");
|
||||
if (vipExpireTime <= 0) {
|
||||
throw new IllegalArgumentException("会员有效月数必须大于0");
|
||||
}
|
||||
|
||||
if (numCodes > 1000) {
|
||||
@@ -228,8 +238,8 @@ public class VipCodeServiceImpl extends ServiceImpl<VipCodeMapper, VipCode> impl
|
||||
public String getAvailableVipCode(int vipExpireTime, Long createdUserId, String createdUserName) {
|
||||
log.info("查找可用会员码,有效月数:{},创建人ID:{}", vipExpireTime, createdUserId);
|
||||
|
||||
if (vipExpireTime != 1 && vipExpireTime != 12) {
|
||||
throw new IllegalArgumentException("会员有效月数只能是1或12");
|
||||
if (vipExpireTime <= 0) {
|
||||
throw new IllegalArgumentException("会员有效月数必须大于0");
|
||||
}
|
||||
|
||||
QueryWrapper<VipCode> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
@@ -734,6 +734,10 @@ public class DltDataImporter {
|
||||
|
||||
// I列: 后区2
|
||||
entity.setBackBall2(getCellIntegerValue(row.getCell(8)));
|
||||
|
||||
// J列: 奖池(单位:亿元,直接存储)
|
||||
Double prizePool = getCellNumericValue(row.getCell(9));
|
||||
entity.setPrizePool(prizePool != null ? prizePool : 0.0);
|
||||
|
||||
// 验证必要字段
|
||||
if (entity.getFrontBall1() != null && entity.getFrontBall2() != null &&
|
||||
@@ -741,11 +745,11 @@ public class DltDataImporter {
|
||||
entity.getFrontBall5() != null && entity.getBackBall1() != null &&
|
||||
entity.getBackBall2() != null) {
|
||||
dataList.add(entity);
|
||||
log.debug("添加大乐透开奖记录:期号{},日期{},前区{}-{}-{}-{}-{},后区{}-{}",
|
||||
log.debug("添加大乐透开奖记录:期号{},日期{},前区{}-{}-{}-{}-{},后区{}-{},奖池{}亿",
|
||||
entity.getDrawId(), entity.getDrawDate(),
|
||||
entity.getFrontBall1(), entity.getFrontBall2(), entity.getFrontBall3(),
|
||||
entity.getFrontBall4(), entity.getFrontBall5(),
|
||||
entity.getBackBall1(), entity.getBackBall2());
|
||||
entity.getBackBall1(), entity.getBackBall2(), entity.getPrizePool());
|
||||
} else {
|
||||
log.warn("第{}行数据不完整,跳过", i + 1);
|
||||
}
|
||||
@@ -825,6 +829,10 @@ public class DltDataImporter {
|
||||
|
||||
// I列: 后区2
|
||||
entity.setBackBall2(getCellIntegerValue(row.getCell(8)));
|
||||
|
||||
// J列: 奖池(单位:亿元,直接存储)
|
||||
Double prizePool = getCellNumericValue(row.getCell(9));
|
||||
entity.setPrizePool(prizePool != null ? prizePool : 0.0);
|
||||
|
||||
// 验证必要字段
|
||||
if (entity.getFrontBall1() != null && entity.getFrontBall2() != null &&
|
||||
@@ -833,11 +841,11 @@ public class DltDataImporter {
|
||||
entity.getBackBall2() != null) {
|
||||
dataList.add(entity);
|
||||
newCount++;
|
||||
log.debug("添加新大乐透开奖记录:期号{},日期{},前区{}-{}-{}-{}-{},后区{}-{}",
|
||||
log.debug("添加新大乐透开奖记录:期号{},日期{},前区{}-{}-{}-{}-{},后区{}-{},奖池{}亿",
|
||||
entity.getDrawId(), entity.getDrawDate(),
|
||||
entity.getFrontBall1(), entity.getFrontBall2(), entity.getFrontBall3(),
|
||||
entity.getFrontBall4(), entity.getFrontBall5(),
|
||||
entity.getBackBall1(), entity.getBackBall2());
|
||||
entity.getBackBall1(), entity.getBackBall2(), entity.getPrizePool());
|
||||
} else {
|
||||
log.warn("第{}行数据不完整,跳过", i + 1);
|
||||
}
|
||||
@@ -955,6 +963,58 @@ public class DltDataImporter {
|
||||
return numericValue != null ? numericValue.intValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单元格的Long值(用于奖池等大数值)
|
||||
*/
|
||||
private Long getCellLongValue(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 {
|
||||
return Long.parseLong(strValue);
|
||||
} catch (NumberFormatException e) {
|
||||
try {
|
||||
// 尝试解析为double再转long
|
||||
value = Double.parseDouble(strValue);
|
||||
} catch (NumberFormatException ex) {
|
||||
log.warn("无法解析单元格Long值:{}", strValue);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FORMULA:
|
||||
try {
|
||||
value = cell.getNumericCellValue();
|
||||
} catch (Exception e) {
|
||||
log.warn("无法获取公式单元格的Long值:{}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case BLANK:
|
||||
return null;
|
||||
default:
|
||||
log.warn("不支持的单元格类型:{}", cell.getCellType());
|
||||
return null;
|
||||
}
|
||||
|
||||
return (long) value;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("读取单元格Long值失败:{}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单元格的Date值
|
||||
*/
|
||||
|
||||
@@ -41,9 +41,10 @@ public class DltPrizeCalculator {
|
||||
*
|
||||
* @param predictNumbers 预测号码数组,格式:[前区5个号码, 后区2个号码]
|
||||
* @param drawNumbers 开奖号码数组,格式:[前区5个号码, 后区2个号码]
|
||||
* @param prizePool 奖池金额(单位:亿元),用于判断奖金档位
|
||||
* @return 中奖结果
|
||||
*/
|
||||
public static PrizeResult calculatePrize(Integer[] predictNumbers, Integer[] drawNumbers) {
|
||||
public static PrizeResult calculatePrize(Integer[] predictNumbers, Integer[] drawNumbers, Double prizePool) {
|
||||
if (predictNumbers == null || drawNumbers == null) {
|
||||
throw new IllegalArgumentException("预测号码和开奖号码不能为空");
|
||||
}
|
||||
@@ -64,14 +65,25 @@ public class DltPrizeCalculator {
|
||||
// 计算后区命中个数
|
||||
int backMatches = calculateMatches(predictBack, drawBack);
|
||||
|
||||
log.debug("前区命中{}个,后区命中{}个", frontMatches, backMatches);
|
||||
log.debug("前区命中{}个,后区命中{}个,奖池金额:{}亿元", frontMatches, backMatches, prizePool);
|
||||
|
||||
// 根据中奖规则判断奖级
|
||||
String prizeLevel = determinePrizeLevel(frontMatches, backMatches);
|
||||
Long bonus = calculateBonus(prizeLevel);
|
||||
Long bonus = calculateBonus(prizeLevel, prizePool);
|
||||
|
||||
return new PrizeResult(prizeLevel, bonus, frontMatches, backMatches);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算大乐透中奖结果(兼容旧版本,默认奖池为0)
|
||||
*
|
||||
* @param predictNumbers 预测号码数组,格式:[前区5个号码, 后区2个号码]
|
||||
* @param drawNumbers 开奖号码数组,格式:[前区5个号码, 后区2个号码]
|
||||
* @return 中奖结果
|
||||
*/
|
||||
public static PrizeResult calculatePrize(Integer[] predictNumbers, Integer[] drawNumbers) {
|
||||
return calculatePrize(predictNumbers, drawNumbers, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个号码数组的匹配个数
|
||||
@@ -91,63 +103,93 @@ public class DltPrizeCalculator {
|
||||
|
||||
/**
|
||||
* 根据前区和后区命中个数判断中奖等级
|
||||
*
|
||||
* 中奖规则:
|
||||
* 一等奖:5+2(前区5个+后区2个)
|
||||
* 二等奖:5+1(前区5个+后区1个)
|
||||
* 三等奖:5+0 或 4+2(前区5个 或 前区4个+后区2个)
|
||||
* 四等奖:4+1(前区4个+后区1个)
|
||||
* 五等奖:4+0 或 3+2(前区4个 或 前区3个+后区2个)
|
||||
* 六等奖:3+1 或 2+2(前区3个+后区1个 或 前区2个+后区2个)
|
||||
* 七等奖:3+0 或 1+2 或 2+1 或 0+2(前区3个 或 前区1个+后区2个 或 前区2个+后区1个 或 后区2个)
|
||||
*/
|
||||
private static String determinePrizeLevel(int frontMatches, int backMatches) {
|
||||
// 根据大乐透中奖规则图片判断
|
||||
if (frontMatches == 5 && backMatches == 2) {
|
||||
return "一等奖";
|
||||
} else if (frontMatches == 5 && backMatches == 1) {
|
||||
return "二等奖";
|
||||
} else if (frontMatches == 5 && backMatches == 0) {
|
||||
} else if ((frontMatches == 5 && backMatches == 0) ||
|
||||
(frontMatches == 4 && backMatches == 2)) {
|
||||
return "三等奖";
|
||||
} else if (frontMatches == 4 && backMatches == 2) {
|
||||
return "四等奖";
|
||||
} else if (frontMatches == 4 && backMatches == 1) {
|
||||
return "四等奖";
|
||||
} else if ((frontMatches == 4 && backMatches == 0) ||
|
||||
(frontMatches == 3 && backMatches == 2)) {
|
||||
return "五等奖";
|
||||
} else if (frontMatches == 4 && backMatches == 0) {
|
||||
return "六等奖";
|
||||
} else if (frontMatches == 3 && backMatches == 2) {
|
||||
return "七等奖";
|
||||
} else if ((frontMatches == 3 && backMatches == 1) ||
|
||||
(frontMatches == 2 && backMatches == 2)) {
|
||||
return "八等奖";
|
||||
return "六等奖";
|
||||
} else if ((frontMatches == 3 && backMatches == 0) ||
|
||||
(frontMatches == 2 && backMatches == 1) ||
|
||||
(frontMatches == 1 && backMatches == 2) ||
|
||||
(frontMatches == 2 && backMatches == 1) ||
|
||||
(frontMatches == 0 && backMatches == 2)) {
|
||||
return "九等奖";
|
||||
return "七等奖";
|
||||
} else {
|
||||
return "未中奖";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据中奖等级计算奖金(基本投注2元)
|
||||
* 根据中奖等级和奖池金额计算奖金
|
||||
*
|
||||
* 奖金规则(基本投注2元):
|
||||
* - 奖池≥8亿元时为高奖金档
|
||||
* - 奖池<8亿元时为低奖金档
|
||||
*
|
||||
* 一等奖:浮动奖金,500万封顶(不区分奖池)
|
||||
* 二等奖:浮动奖金,500万封顶(不区分奖池)
|
||||
* 三等奖:奖池≥8亿 6666元,奖池<8亿 5000元
|
||||
* 四等奖:奖池≥8亿 380元,奖池<8亿 300元
|
||||
* 五等奖:奖池≥8亿 200元,奖池<8亿 150元
|
||||
* 六等奖:奖池≥8亿 18元,奖池<8亿 15元
|
||||
* 七等奖:奖池≥8亿 7元,奖池<8亿 5元
|
||||
*
|
||||
* @param prizeLevel 中奖等级
|
||||
* @param prizePool 奖池金额(单位:亿元)
|
||||
* @return 奖金(元)
|
||||
*/
|
||||
private static Long calculateBonus(String prizeLevel) {
|
||||
private static Long calculateBonus(String prizeLevel, Double prizePool) {
|
||||
// 判断奖池档位:8亿元
|
||||
boolean isHighPrizePool = (prizePool != null && prizePool >= 8.0);
|
||||
|
||||
switch (prizeLevel) {
|
||||
case "一等奖":
|
||||
return 10000000L; // 1000万元(浮动奖金,这里设置为最高奖金)
|
||||
return 5000000L; // 500万元封顶(浮动奖金)
|
||||
case "二等奖":
|
||||
return 5000000L; // 500万元(浮动奖金)
|
||||
return 5000000L; // 500万元封顶(浮动奖金)
|
||||
case "三等奖":
|
||||
return 10000L; // 1万元
|
||||
return isHighPrizePool ? 6666L : 5000L;
|
||||
case "四等奖":
|
||||
return 3000L; // 3000元
|
||||
return isHighPrizePool ? 380L : 300L;
|
||||
case "五等奖":
|
||||
return 300L; // 300元
|
||||
return isHighPrizePool ? 200L : 150L;
|
||||
case "六等奖":
|
||||
return 200L; // 200元
|
||||
return isHighPrizePool ? 18L : 15L;
|
||||
case "七等奖":
|
||||
return 100L; // 100元
|
||||
case "八等奖":
|
||||
return 15L; // 15元
|
||||
case "九等奖":
|
||||
return 5L; // 5元
|
||||
return isHighPrizePool ? 7L : 5L;
|
||||
default:
|
||||
return 0L; // 未中奖
|
||||
return 0L; // 未中奖
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据中奖等级计算奖金(兼容旧版本,默认奖池为0)
|
||||
* @deprecated 请使用 calculateBonus(String prizeLevel, Double prizePool)
|
||||
*/
|
||||
@Deprecated
|
||||
private static Long calculateBonus(String prizeLevel) {
|
||||
return calculateBonus(prizeLevel, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证号码格式是否正确
|
||||
|
||||
@@ -32,6 +32,7 @@ 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.util.SsqPrizeCalculator;
|
||||
import com.xy.xyaicpzs.service.T3Service;
|
||||
import com.xy.xyaicpzs.service.T4Service;
|
||||
import com.xy.xyaicpzs.service.T5Service;
|
||||
@@ -48,6 +49,7 @@ 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.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
@@ -1229,6 +1231,22 @@ public class ExcelDataImporter {
|
||||
|
||||
// I列: 蓝球
|
||||
entity.setBlueBall(getCellIntegerValue(row.getCell(8)));
|
||||
|
||||
// J列: 奖池(单位:亿元,直接存储)
|
||||
Double prizePool = getCellNumericValue(row.getCell(9));
|
||||
entity.setPrizePool(prizePool != null ? prizePool : 0.0);
|
||||
|
||||
// 判断特别规定状态(需要查询上一期的状态)
|
||||
// 由于是覆盖导入,按顺序处理,可以从已添加的列表中获取上一期状态
|
||||
boolean wasInSpecialPeriod = false;
|
||||
if (!dataList.isEmpty()) {
|
||||
// 获取上一条记录的特别规定状态
|
||||
LotteryDraws previousRecord = dataList.get(dataList.size() - 1);
|
||||
wasInSpecialPeriod = previousRecord.getIsSpecialPeriod() != null && previousRecord.getIsSpecialPeriod() == 1;
|
||||
}
|
||||
boolean isSpecialPeriod = SsqPrizeCalculator.shouldExecuteSpecialRule(
|
||||
entity.getPrizePool(), wasInSpecialPeriod);
|
||||
entity.setIsSpecialPeriod(isSpecialPeriod ? 1 : 0);
|
||||
|
||||
// 验证必要字段
|
||||
if (entity.getRedBall1() != null && entity.getRedBall2() != null &&
|
||||
@@ -1236,11 +1254,11 @@ public class ExcelDataImporter {
|
||||
entity.getRedBall5() != null && entity.getRedBall6() != null &&
|
||||
entity.getBlueBall() != null) {
|
||||
dataList.add(entity);
|
||||
log.debug("添加开奖记录:期号{},日期{},红球{}-{}-{}-{}-{}-{},蓝球{}",
|
||||
log.debug("添加开奖记录:期号{},日期{},红球{}-{}-{}-{}-{}-{},蓝球{},奖池{}亿",
|
||||
entity.getDrawId(), entity.getDrawDate(),
|
||||
entity.getRedBall1(), entity.getRedBall2(), entity.getRedBall3(),
|
||||
entity.getRedBall4(), entity.getRedBall5(), entity.getRedBall6(),
|
||||
entity.getBlueBall());
|
||||
entity.getBlueBall(), entity.getPrizePool());
|
||||
} else {
|
||||
log.warn("第{}行数据不完整,跳过", i + 1);
|
||||
}
|
||||
@@ -1416,6 +1434,30 @@ public class ExcelDataImporter {
|
||||
|
||||
// I列: 蓝球
|
||||
entity.setBlueBall(getCellIntegerValue(row.getCell(8)));
|
||||
|
||||
// J列: 奖池(单位:亿元,直接存储)
|
||||
Double prizePool = getCellNumericValue(row.getCell(9));
|
||||
entity.setPrizePool(prizePool != null ? prizePool : 0.0);
|
||||
|
||||
// 判断特别规定状态(需要查询上一期的状态)
|
||||
// 追加导入时,需要查询数据库中最新一期的状态
|
||||
boolean wasInSpecialPeriod = false;
|
||||
if (dataList.isEmpty()) {
|
||||
// 第一条记录,查询数据库中最新一期的状态
|
||||
QueryWrapper<LotteryDraws> lastRecordQuery = new QueryWrapper<>();
|
||||
lastRecordQuery.orderByDesc("drawId").last("LIMIT 1");
|
||||
LotteryDraws lastRecord = lotteryDrawsMapper.selectOne(lastRecordQuery);
|
||||
if (lastRecord != null) {
|
||||
wasInSpecialPeriod = lastRecord.getIsSpecialPeriod() != null && lastRecord.getIsSpecialPeriod() == 1;
|
||||
}
|
||||
} else {
|
||||
// 从已添加的列表中获取上一期状态
|
||||
LotteryDraws previousRecord = dataList.get(dataList.size() - 1);
|
||||
wasInSpecialPeriod = previousRecord.getIsSpecialPeriod() != null && previousRecord.getIsSpecialPeriod() == 1;
|
||||
}
|
||||
boolean isSpecialPeriod = SsqPrizeCalculator.shouldExecuteSpecialRule(
|
||||
entity.getPrizePool(), wasInSpecialPeriod);
|
||||
entity.setIsSpecialPeriod(isSpecialPeriod ? 1 : 0);
|
||||
|
||||
// 验证必要字段
|
||||
if (entity.getRedBall1() != null && entity.getRedBall2() != null &&
|
||||
@@ -1424,11 +1466,11 @@ public class ExcelDataImporter {
|
||||
entity.getBlueBall() != null) {
|
||||
dataList.add(entity);
|
||||
newCount++;
|
||||
log.debug("添加新开奖记录:期号{},日期{},红球{}-{}-{}-{}-{}-{},蓝球{}",
|
||||
log.debug("添加新开奖记录:期号{},日期{},红球{}-{}-{}-{}-{}-{},蓝球{},奖池{}亿",
|
||||
entity.getDrawId(), entity.getDrawDate(),
|
||||
entity.getRedBall1(), entity.getRedBall2(), entity.getRedBall3(),
|
||||
entity.getRedBall4(), entity.getRedBall5(), entity.getRedBall6(),
|
||||
entity.getBlueBall());
|
||||
entity.getBlueBall(), entity.getPrizePool());
|
||||
} else {
|
||||
log.warn("第{}行数据不完整,跳过", i + 1);
|
||||
}
|
||||
|
||||
117
src/main/java/com/xy/xyaicpzs/util/SsqPrizeCalculator.java
Normal file
117
src/main/java/com/xy/xyaicpzs/util/SsqPrizeCalculator.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.xy.xyaicpzs.util;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 双色球奖金计算器
|
||||
* 根据新规则计算中奖等级和奖金
|
||||
*/
|
||||
public class SsqPrizeCalculator {
|
||||
|
||||
// 特别规定启动阈值:15亿(数据库存储单位为亿)
|
||||
private static final double SPECIAL_RULE_START_THRESHOLD = 15.0;
|
||||
|
||||
// 特别规定停止阈值:3亿(数据库存储单位为亿)
|
||||
private static final double SPECIAL_RULE_STOP_THRESHOLD = 3.0;
|
||||
|
||||
/**
|
||||
* 中奖结果
|
||||
*/
|
||||
@Data
|
||||
public static class PrizeResult {
|
||||
private String prizeLevel; // 中奖等级
|
||||
private Long bonus; // 奖金(元)
|
||||
|
||||
public PrizeResult(String prizeLevel, Long bonus) {
|
||||
this.prizeLevel = prizeLevel;
|
||||
this.bonus = bonus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算中奖结果
|
||||
*
|
||||
* @param predictNumbers 预测号码 [红球1-6, 蓝球]
|
||||
* @param drawNumbers 开奖号码 [红球1-6, 蓝球]
|
||||
* @param prizePool 奖池金额(单位:亿元)
|
||||
* @param isSpecialPeriod 是否处于特别规定期间
|
||||
* @return 中奖结果
|
||||
*/
|
||||
public static PrizeResult calculatePrize(Integer[] predictNumbers, Integer[] drawNumbers,
|
||||
Double prizePool, boolean isSpecialPeriod) {
|
||||
// 比较红球匹配数
|
||||
int redMatches = 0;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 6; j++) {
|
||||
if (predictNumbers[i] != null && predictNumbers[i].equals(drawNumbers[j])) {
|
||||
redMatches++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 比较蓝球是否匹配
|
||||
boolean blueMatch = predictNumbers[6] != null && predictNumbers[6].equals(drawNumbers[6]);
|
||||
|
||||
// 根据匹配情况判断奖项和奖金
|
||||
String prizeLevel;
|
||||
Long bonus;
|
||||
|
||||
if (redMatches == 6 && blueMatch) {
|
||||
// 一等奖:6红+1蓝
|
||||
prizeLevel = "一等奖";
|
||||
bonus = 5_000_000L; // 500万封顶
|
||||
} else if (redMatches == 6) {
|
||||
// 二等奖:6红
|
||||
prizeLevel = "二等奖";
|
||||
bonus = 5_000_000L; // 500万封顶
|
||||
} else if (redMatches == 5 && blueMatch) {
|
||||
// 三等奖:5红+1蓝
|
||||
prizeLevel = "三等奖";
|
||||
bonus = 3_000L;
|
||||
} else if (redMatches == 5 || (redMatches == 4 && blueMatch)) {
|
||||
// 四等奖:5红 或 4红+1蓝
|
||||
prizeLevel = "四等奖";
|
||||
bonus = 200L;
|
||||
} else if (redMatches == 4 || (redMatches == 3 && blueMatch)) {
|
||||
// 五等奖:4红 或 3红+1蓝
|
||||
prizeLevel = "五等奖";
|
||||
bonus = 10L;
|
||||
} else if (blueMatch) {
|
||||
// 六等奖:1蓝
|
||||
prizeLevel = "六等奖";
|
||||
bonus = 5L;
|
||||
} else if (redMatches == 3 && isSpecialPeriod) {
|
||||
// 福运奖:3红(仅在特别规定期间)
|
||||
prizeLevel = "福运奖";
|
||||
bonus = 5L;
|
||||
} else {
|
||||
// 未中奖
|
||||
prizeLevel = "未中奖";
|
||||
bonus = 0L;
|
||||
}
|
||||
|
||||
return new PrizeResult(prizeLevel, bonus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该执行特别规定
|
||||
*
|
||||
* @param currentPrizePool 当前期奖池金额(单位:亿元)
|
||||
* @param previousIsSpecialPeriod 上一期是否处于特别规定期间
|
||||
* @return 当前期是否应该执行特别规定
|
||||
*/
|
||||
public static boolean shouldExecuteSpecialRule(Double currentPrizePool, boolean previousIsSpecialPeriod) {
|
||||
if (currentPrizePool == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果上一期不在特别规定期间,检查是否达到启动阈值
|
||||
if (!previousIsSpecialPeriod) {
|
||||
return currentPrizePool >= SPECIAL_RULE_START_THRESHOLD;
|
||||
}
|
||||
|
||||
// 如果上一期在特别规定期间,检查是否低于停止阈值
|
||||
return currentPrizePool >= SPECIAL_RULE_STOP_THRESHOLD;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
spring:
|
||||
application:
|
||||
name: xy-ai-cpzs
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 50MB
|
||||
max-request-size: 100MB
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/cpzs
|
||||
username: cpzs_root
|
||||
password: cpzs_123456
|
||||
# username: root
|
||||
# password: root
|
||||
# username: cpzs
|
||||
# password: cpzs_123456
|
||||
username: root
|
||||
password: root
|
||||
# datasource:
|
||||
# driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# url: jdbc:mysql://47.117.22.239:3306/cpzs?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&failOverReadOnly=false&connectTimeout=10000&socketTimeout=30000
|
||||
@@ -28,7 +34,7 @@ spring:
|
||||
# database: 0
|
||||
data:
|
||||
redis:
|
||||
host: 47.117.22.239
|
||||
host: 124.222.15.61
|
||||
port: 6379
|
||||
database: 0
|
||||
password: cpzs_123456
|
||||
@@ -78,8 +84,21 @@ aliyun:
|
||||
access-key-id: LTAI5tR18rXPYazi3y8kAuep
|
||||
access-key-secret: KZ1aKZOupilVc332SXE1g1DfKsqHPu
|
||||
gateway-url: wss://nls-gateway-cn-shanghai.aliyuncs.com/ws/v1
|
||||
|
||||
# 腾讯云短信服务配置
|
||||
tencent:
|
||||
sms:
|
||||
sign-name: 西安精彩数据服务社
|
||||
template-code: SMS_489840017
|
||||
access-key-id: LTAI5tR18rXPYazi3y8kAuep
|
||||
access-key-secret: KZ1aKZOupilVc332SXE1g1DfKsqHPu
|
||||
secret-id: AKIDTuCIBrJXZVEl7FBtOfjaGj283lNSRDnA
|
||||
secret-key: EG7mFXdWJwMcfU8ZptmSvCHBtPm02sX0
|
||||
sdk-app-id: 1401068120
|
||||
template-id: 2598660
|
||||
sign-name: 西安溢彩数智科技有限公司
|
||||
region: ap-beijing
|
||||
|
||||
# 腾讯云COS配置
|
||||
cos:
|
||||
secret-id: AKIDTuCIBrJXZVEl7FBtOfjaGj283lNSRDnA
|
||||
secret-key: EG7mFXdWJwMcfU8ZptmSvCHBtPm02sX0
|
||||
bucket-name: yicaishuzhi-1326058838
|
||||
region: ap-beijing
|
||||
domain: https://yicaishuzhi-1326058838.cos.ap-beijing.myqcloud.com
|
||||
27
src/main/resources/generator/mapper/AnnouncementMapper.xml
Normal file
27
src/main/resources/generator/mapper/AnnouncementMapper.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.xy.xyaicpzs.mapper.AnnouncementMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.xy.xyaicpzs.domain.entity.Announcement">
|
||||
<id property="id" column="id" />
|
||||
<result property="title" column="title" />
|
||||
<result property="content" column="content" />
|
||||
<result property="announcementTime" column="announcementTime" />
|
||||
<result property="publisherId" column="publisherId" />
|
||||
<result property="publisherName" column="publisherName" />
|
||||
<result property="status" column="status" />
|
||||
<result property="priority" column="priority" />
|
||||
<result property="viewCount" column="viewCount" />
|
||||
<result property="createTime" column="createTime" />
|
||||
<result property="updateTime" column="updateTime" />
|
||||
<result property="isDelete" column="isDelete" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,title,content,announcementTime,publisherId,publisherName,
|
||||
status,priority,viewCount,createTime,updateTime,
|
||||
isDelete
|
||||
</sql>
|
||||
</mapper>
|
||||
@@ -15,13 +15,14 @@
|
||||
<result property="frontBall5" column="frontBall5" />
|
||||
<result property="backBall1" column="backBall1" />
|
||||
<result property="backBall2" column="backBall2" />
|
||||
<result property="prizePool" column="prizePool" />
|
||||
<result property="createTime" column="createTime" />
|
||||
<result property="updateTime" column="updateTime" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,drawId,drawDate,frontBall1,frontBall2,frontBall3,
|
||||
frontBall4,frontBall5,backBall1,backBall2,createTime,
|
||||
frontBall4,frontBall5,backBall1,backBall2,prizePool,createTime,
|
||||
updateTime
|
||||
</sql>
|
||||
</mapper>
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
<result property="redBall5" column="redBall5" />
|
||||
<result property="redBall6" column="redBall6" />
|
||||
<result property="blueBall" column="blueBall" />
|
||||
<result property="prizePool" column="prizePool" />
|
||||
<result property="isSpecialPeriod" column="is_special_period" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
drawId,drawDate,redBall1,redBall2,redBall3,redBall4,
|
||||
redBall5,redBall6,blueBall
|
||||
redBall5,redBall6,blueBall,prizePool,is_special_period
|
||||
</sql>
|
||||
</mapper>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.xy.xyaicpzs;
|
||||
|
||||
import com.xy.xyaicpzs.common.response.ApiResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* ApiResponse测试类
|
||||
*/
|
||||
public class ApiResponseTest {
|
||||
|
||||
@Test
|
||||
public void testSuccessResponse() {
|
||||
String data = "测试数据";
|
||||
ApiResponse<String> response = ApiResponse.success(data);
|
||||
|
||||
assertEquals(0, response.getCode());
|
||||
assertTrue(response.isSuccess());
|
||||
assertEquals("操作成功", response.getMessage());
|
||||
assertEquals(data, response.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorResponse() {
|
||||
String errorMessage = "错误信息";
|
||||
ApiResponse<String> response = ApiResponse.error(errorMessage);
|
||||
|
||||
assertEquals(1, response.getCode());
|
||||
assertFalse(response.isSuccess());
|
||||
assertEquals(errorMessage, response.getMessage());
|
||||
assertNull(response.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorResponseWithCode() {
|
||||
Integer errorCode = 500;
|
||||
String errorMessage = "系统错误";
|
||||
ApiResponse<String> response = ApiResponse.error(errorCode, errorMessage);
|
||||
|
||||
assertEquals(errorCode, response.getCode());
|
||||
assertFalse(response.isSuccess());
|
||||
assertEquals(errorMessage, response.getMessage());
|
||||
assertNull(response.getData());
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package com.xy.xyaicpzs;
|
||||
|
||||
import com.xy.xyaicpzs.util.DltPrizeCalculator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 大乐透中奖规则计算器测试类
|
||||
*/
|
||||
public class DltPrizeCalculatorTest {
|
||||
|
||||
@Test
|
||||
public void testFirstPrize() {
|
||||
// 一等奖:前区5+后区2
|
||||
Integer[] predict = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw = {1, 2, 3, 4, 5, 6, 7};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw);
|
||||
|
||||
assertEquals("一等奖", result.getPrizeLevel());
|
||||
assertEquals(5, result.getFrontMatches());
|
||||
assertEquals(2, result.getBackMatches());
|
||||
assertEquals(10000000L, result.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondPrize() {
|
||||
// 二等奖:前区5+后区1
|
||||
Integer[] predict = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw = {1, 2, 3, 4, 5, 6, 8};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw);
|
||||
|
||||
assertEquals("二等奖", result.getPrizeLevel());
|
||||
assertEquals(5, result.getFrontMatches());
|
||||
assertEquals(1, result.getBackMatches());
|
||||
assertEquals(5000000L, result.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThirdPrize() {
|
||||
// 三等奖:前区5+后区0
|
||||
Integer[] predict = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw = {1, 2, 3, 4, 5, 8, 9};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw);
|
||||
|
||||
assertEquals("三等奖", result.getPrizeLevel());
|
||||
assertEquals(5, result.getFrontMatches());
|
||||
assertEquals(0, result.getBackMatches());
|
||||
assertEquals(10000L, result.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFourthPrize() {
|
||||
// 四等奖:前区4+后区2
|
||||
Integer[] predict = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw = {1, 2, 3, 4, 8, 6, 7};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw);
|
||||
|
||||
assertEquals("四等奖", result.getPrizeLevel());
|
||||
assertEquals(4, result.getFrontMatches());
|
||||
assertEquals(2, result.getBackMatches());
|
||||
assertEquals(3000L, result.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEighthPrize() {
|
||||
// 八等奖:前区3+后区1 或 前区2+后区2
|
||||
Integer[] predict1 = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw1 = {1, 2, 3, 8, 9, 6, 10};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result1 = DltPrizeCalculator.calculatePrize(predict1, draw1);
|
||||
assertEquals("八等奖", result1.getPrizeLevel());
|
||||
assertEquals(15L, result1.getBonus());
|
||||
|
||||
// 前区2+后区2
|
||||
Integer[] predict2 = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw2 = {1, 2, 8, 9, 10, 6, 7};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result2 = DltPrizeCalculator.calculatePrize(predict2, draw2);
|
||||
assertEquals("八等奖", result2.getPrizeLevel());
|
||||
assertEquals(15L, result2.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNinthPrize() {
|
||||
// 九等奖:前区3+后区0
|
||||
Integer[] predict = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw = {1, 2, 3, 8, 9, 10, 11};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw);
|
||||
|
||||
assertEquals("九等奖", result.getPrizeLevel());
|
||||
assertEquals(3, result.getFrontMatches());
|
||||
assertEquals(0, result.getBackMatches());
|
||||
assertEquals(5L, result.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoPrize() {
|
||||
// 未中奖
|
||||
Integer[] predict = {1, 2, 3, 4, 5, 6, 7};
|
||||
Integer[] draw = {8, 9, 10, 11, 12, 8, 9};
|
||||
|
||||
DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw);
|
||||
|
||||
assertEquals("未中奖", result.getPrizeLevel());
|
||||
assertEquals(0, result.getFrontMatches());
|
||||
assertEquals(0, result.getBackMatches());
|
||||
assertEquals(0L, result.getBonus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateNumbers() {
|
||||
// 有效号码
|
||||
Integer[] validNumbers = {1, 2, 3, 4, 5, 6, 7};
|
||||
assertTrue(DltPrizeCalculator.validateNumbers(validNumbers));
|
||||
|
||||
// 无效号码 - 长度不对
|
||||
Integer[] invalidLength = {1, 2, 3, 4, 5, 6};
|
||||
assertFalse(DltPrizeCalculator.validateNumbers(invalidLength));
|
||||
|
||||
// 无效号码 - 前区号码超出范围
|
||||
Integer[] invalidFront = {1, 2, 3, 4, 36, 6, 7};
|
||||
assertFalse(DltPrizeCalculator.validateNumbers(invalidFront));
|
||||
|
||||
// 无效号码 - 后区号码超出范围
|
||||
Integer[] invalidBack = {1, 2, 3, 4, 5, 6, 13};
|
||||
assertFalse(DltPrizeCalculator.validateNumbers(invalidBack));
|
||||
|
||||
// 无效号码 - 前区重复
|
||||
Integer[] duplicateFront = {1, 2, 3, 4, 1, 6, 7};
|
||||
assertFalse(DltPrizeCalculator.validateNumbers(duplicateFront));
|
||||
|
||||
// 无效号码 - 后区重复
|
||||
Integer[] duplicateBack = {1, 2, 3, 4, 5, 6, 6};
|
||||
assertFalse(DltPrizeCalculator.validateNumbers(duplicateBack));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatNumbers() {
|
||||
Integer[] numbers = {1, 2, 3, 4, 5, 6, 7};
|
||||
String formatted = DltPrizeCalculator.formatNumbers(numbers);
|
||||
assertEquals("前区: 01 02 03 04 05 | 后区: 06 07", formatted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.xy.xyaicpzs;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class XyAiCpzsApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
229
test.txt
229
test.txt
@@ -1,229 +0,0 @@
|
||||
{
|
||||
"code": 0,
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"results": [
|
||||
{
|
||||
"ballNumber": 14,
|
||||
"frequency": 8,
|
||||
"coefficientSum": 186.64000000000001,
|
||||
"top100Ranking": 6,
|
||||
"historyRanking": 1
|
||||
},
|
||||
{
|
||||
"ballNumber": 7,
|
||||
"frequency": 7,
|
||||
"coefficientSum": 174.87,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 10
|
||||
},
|
||||
{
|
||||
"ballNumber": 26,
|
||||
"frequency": 7,
|
||||
"coefficientSum": 150.24,
|
||||
"top100Ranking": 31,
|
||||
"historyRanking": 2
|
||||
},
|
||||
{
|
||||
"ballNumber": 2,
|
||||
"frequency": 6,
|
||||
"coefficientSum": 146.03,
|
||||
"top100Ranking": 5,
|
||||
"historyRanking": 12
|
||||
},
|
||||
{
|
||||
"ballNumber": 18,
|
||||
"frequency": 6,
|
||||
"coefficientSum": 172.67000000000002,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 8
|
||||
},
|
||||
{
|
||||
"ballNumber": 20,
|
||||
"frequency": 6,
|
||||
"coefficientSum": 144.49,
|
||||
"top100Ranking": 23,
|
||||
"historyRanking": 7
|
||||
},
|
||||
{
|
||||
"ballNumber": 22,
|
||||
"frequency": 6,
|
||||
"coefficientSum": 125.7,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 3
|
||||
},
|
||||
{
|
||||
"ballNumber": 4,
|
||||
"frequency": 5,
|
||||
"coefficientSum": 114.42999999999999,
|
||||
"top100Ranking": 19,
|
||||
"historyRanking": 22
|
||||
},
|
||||
{
|
||||
"ballNumber": 6,
|
||||
"frequency": 5,
|
||||
"coefficientSum": 142.57,
|
||||
"top100Ranking": 1,
|
||||
"historyRanking": 6
|
||||
},
|
||||
{
|
||||
"ballNumber": 8,
|
||||
"frequency": 5,
|
||||
"coefficientSum": 109.75999999999999,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 13
|
||||
},
|
||||
{
|
||||
"ballNumber": 17,
|
||||
"frequency": 5,
|
||||
"coefficientSum": 144.9,
|
||||
"top100Ranking": 6,
|
||||
"historyRanking": 4
|
||||
},
|
||||
{
|
||||
"ballNumber": 27,
|
||||
"frequency": 5,
|
||||
"coefficientSum": 122.24000000000001,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 11
|
||||
},
|
||||
{
|
||||
"ballNumber": 30,
|
||||
"frequency": 5,
|
||||
"coefficientSum": 123.61,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 19
|
||||
},
|
||||
{
|
||||
"ballNumber": 1,
|
||||
"frequency": 4,
|
||||
"coefficientSum": 87.6,
|
||||
"top100Ranking": 33,
|
||||
"historyRanking": 4
|
||||
},
|
||||
{
|
||||
"ballNumber": 10,
|
||||
"frequency": 4,
|
||||
"coefficientSum": 87.45,
|
||||
"top100Ranking": 2,
|
||||
"historyRanking": 14
|
||||
},
|
||||
{
|
||||
"ballNumber": 12,
|
||||
"frequency": 4,
|
||||
"coefficientSum": 107.33,
|
||||
"top100Ranking": 23,
|
||||
"historyRanking": 18
|
||||
},
|
||||
{
|
||||
"ballNumber": 15,
|
||||
"frequency": 4,
|
||||
"coefficientSum": 85.14,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 25
|
||||
},
|
||||
{
|
||||
"ballNumber": 5,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 57.349999999999994,
|
||||
"top100Ranking": 30,
|
||||
"historyRanking": 21
|
||||
},
|
||||
{
|
||||
"ballNumber": 9,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 83.07000000000001,
|
||||
"top100Ranking": 19,
|
||||
"historyRanking": 16
|
||||
},
|
||||
{
|
||||
"ballNumber": 13,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 81.9,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 19
|
||||
},
|
||||
{
|
||||
"ballNumber": 19,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 80.5,
|
||||
"top100Ranking": 19,
|
||||
"historyRanking": 15
|
||||
},
|
||||
{
|
||||
"ballNumber": 24,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 58.43,
|
||||
"top100Ranking": 26,
|
||||
"historyRanking": 31
|
||||
},
|
||||
{
|
||||
"ballNumber": 25,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 58.9,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 23
|
||||
},
|
||||
{
|
||||
"ballNumber": 32,
|
||||
"frequency": 3,
|
||||
"coefficientSum": 83.3,
|
||||
"top100Ranking": 28,
|
||||
"historyRanking": 8
|
||||
},
|
||||
{
|
||||
"ballNumber": 3,
|
||||
"frequency": 2,
|
||||
"coefficientSum": 52.97,
|
||||
"top100Ranking": 8,
|
||||
"historyRanking": 17
|
||||
},
|
||||
{
|
||||
"ballNumber": 11,
|
||||
"frequency": 2,
|
||||
"coefficientSum": 55.769999999999996,
|
||||
"top100Ranking": 19,
|
||||
"historyRanking": 23
|
||||
},
|
||||
{
|
||||
"ballNumber": 16,
|
||||
"frequency": 2,
|
||||
"coefficientSum": 54.83,
|
||||
"top100Ranking": 3,
|
||||
"historyRanking": 25
|
||||
},
|
||||
{
|
||||
"ballNumber": 23,
|
||||
"frequency": 1,
|
||||
"coefficientSum": 28.23,
|
||||
"top100Ranking": 23,
|
||||
"historyRanking": 27
|
||||
},
|
||||
{
|
||||
"ballNumber": 31,
|
||||
"frequency": 1,
|
||||
"coefficientSum": 4.53,
|
||||
"top100Ranking": 31,
|
||||
"historyRanking": 29
|
||||
},
|
||||
{
|
||||
"ballNumber": 33,
|
||||
"frequency": 1,
|
||||
"coefficientSum": 27.3,
|
||||
"top100Ranking": 3,
|
||||
"historyRanking": 33
|
||||
}
|
||||
],
|
||||
"strategy": "H",
|
||||
"redBalls": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"blueBall": 7
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
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
|
||||
}
|
||||
355
会员码激活API使用说明.md
355
会员码激活API使用说明.md
@@ -1,355 +0,0 @@
|
||||
# 会员码管理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个月的会员时长。
|
||||
195
双色球奖金规则更新说明.md
Normal file
195
双色球奖金规则更新说明.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 双色球奖金规则更新说明
|
||||
|
||||
## 更新概述
|
||||
根据新的双色球开奖规则,系统已完成以下更新:
|
||||
|
||||
## 1. 数据库变更
|
||||
|
||||
### 新增字段
|
||||
在 `lottery_draws` 表中新增字段:
|
||||
- `is_special_period` (TINYINT): 标识该期是否处于特别规定期间(0-否,1-是)
|
||||
- `prize_pool` (BIGINT): 奖池金额(元),默认值为0
|
||||
|
||||
### SQL脚本
|
||||
- `sql/add_special_rule_status.sql`: 添加特别规定期间标识字段
|
||||
- `sql/add_prize_pool_column.sql`: 添加奖池字段
|
||||
|
||||
## 2. 新增工具类
|
||||
|
||||
### SsqPrizeCalculator.java
|
||||
位置:`src/main/java/com/xy/xyaicpzs/util/SsqPrizeCalculator.java`
|
||||
|
||||
主要功能:
|
||||
- 根据新规则计算中奖等级和奖金
|
||||
- 判断是否应该执行特别规定(福运奖期间)
|
||||
|
||||
关键方法:
|
||||
```java
|
||||
// 计算中奖结果
|
||||
public static PrizeResult calculatePrize(
|
||||
Integer[] predictNumbers, // 预测号码 [红球1-6, 蓝球]
|
||||
Integer[] drawNumbers, // 开奖号码 [红球1-6, 蓝球]
|
||||
Long prizePool, // 奖池金额(元)
|
||||
boolean isSpecialPeriod // 是否处于特别规定期间
|
||||
)
|
||||
|
||||
// 判断是否应该执行特别规定
|
||||
public static boolean shouldExecuteSpecialRule(
|
||||
Double currentPrizePool, // 当前期奖池金额(单位:亿元)
|
||||
boolean previousIsSpecialPeriod // 上一期是否处于特别规定期间
|
||||
)
|
||||
```
|
||||
|
||||
## 3. 新的中奖规则
|
||||
|
||||
### 奖项设置(7个等级)
|
||||
|
||||
| 奖项 | 中奖条件 | 奖金 |
|
||||
|------|---------|------|
|
||||
| 一等奖 | 6红+1蓝 | 500万封顶(浮动) |
|
||||
| 二等奖 | 6红 | 500万封顶(浮动) |
|
||||
| 三等奖 | 5红+1蓝 | 3000元(固定) |
|
||||
| 四等奖 | 5红 或 4红+1蓝 | 200元(固定) |
|
||||
| 五等奖 | 4红 或 3红+1蓝 | 10元(固定) |
|
||||
| 六等奖 | 1蓝 | 5元(固定) |
|
||||
| 福运奖 | 3红(仅特别规定期间) | 5元(固定) |
|
||||
|
||||
### 特别规定(福运奖)执行规则
|
||||
|
||||
**启动条件:**
|
||||
- 当奖池资金 ≥ 15亿元时开始执行特别规定
|
||||
|
||||
**停止条件:**
|
||||
- 执行特别规定后,某期开奖后奖池资金 < 3亿元时停止执行
|
||||
|
||||
**福运奖:**
|
||||
- 仅在特别规定期间有效
|
||||
- 中奖条件:投注号码与开奖号码中的任意3个红球相同
|
||||
- 奖金:5元
|
||||
|
||||
**重要说明:**
|
||||
特别规定状态需要跨期追踪,不能仅根据当期奖池判断。系统会记录每期的 `is_special_period` 状态,并根据上一期状态和当期奖池金额来判断当期是否执行特别规定。
|
||||
|
||||
## 4. 代码更新
|
||||
|
||||
### 4.1 实体类更新
|
||||
**LotteryDraws.java**
|
||||
- 新增 `prizePool` 字段(Double类型,单位:亿元)
|
||||
- 新增 `isSpecialPeriod` 字段(Integer类型,0或1)
|
||||
|
||||
### 4.2 Mapper XML更新
|
||||
**LotteryDrawsMapper.xml**
|
||||
- 在 `Base_Column_List` 中添加 `prize_pool` 和 `is_special_period` 字段
|
||||
- 更新所有相关的 SQL 语句
|
||||
|
||||
### 4.3 服务层更新
|
||||
**DataAnalysisServiceImpl.java**
|
||||
- 使用 `SsqPrizeCalculator` 替代旧的计算逻辑
|
||||
- 在 `processPendingPredictions()` 方法中:
|
||||
- 获取开奖记录的奖池金额和特别规定状态
|
||||
- 调用 `SsqPrizeCalculator.calculatePrize()` 计算中奖结果
|
||||
- 更新预测记录的中奖状态、等级和奖金
|
||||
- 删除旧的 `calculatePredictResult()` 和 `calculateBonus()` 方法
|
||||
|
||||
### 4.4 Excel导入更新
|
||||
**ExcelDataImporter.java**
|
||||
- `importT10Data()` 方法:
|
||||
- 读取J列(索引9)的奖池金额
|
||||
- 根据上一期状态计算当期的特别规定状态
|
||||
- 设置 `isSpecialPeriod` 字段
|
||||
- `appendT10Data()` 方法:
|
||||
- 同样支持奖池和特别规定状态的计算
|
||||
- 追加导入时会查询数据库中最新一期的状态
|
||||
|
||||
## 5. 使用说明
|
||||
|
||||
### 5.1 数据库迁移
|
||||
执行以下SQL脚本:
|
||||
```bash
|
||||
# 添加特别规定期间标识字段
|
||||
mysql -u用户名 -p数据库名 < sql/add_special_rule_status.sql
|
||||
|
||||
# 修改奖池字段类型(从BIGINT改为DECIMAL,单位从元改为亿元)
|
||||
mysql -u用户名 -p数据库名 < sql/update_prize_pool_to_decimal.sql
|
||||
```
|
||||
|
||||
### 5.2 Excel导入格式
|
||||
T10工作表(双色球开奖数据)列结构:
|
||||
- A列:开奖期号
|
||||
- B列:开奖日期
|
||||
- C-H列:红球1-6
|
||||
- I列:蓝球
|
||||
- **J列:奖池金额(单位:亿元,如 12.00、13.33)** ← 新增
|
||||
|
||||
D1工作表(大乐透开奖数据)列结构:
|
||||
- A列:开奖期号
|
||||
- B列:开奖日期
|
||||
- C-G列:前区球1-5
|
||||
- H-I列:后区球1-2
|
||||
- **J列:奖池金额(单位:亿元,如 8.50、10.25)** ← 新增
|
||||
|
||||
**重要说明**:
|
||||
- Excel中奖池以**亿元**为单位(如 12.00 表示12亿元)
|
||||
- 系统导入时会自动转换为**元**(12.00亿 → 1200000000元)
|
||||
- 判断规则时使用元为单位:
|
||||
- 双色球:15亿 = 1500000000元,3亿 = 300000000元
|
||||
- 大乐透:8亿 = 800000000元
|
||||
|
||||
### 5.3 API接口
|
||||
现有接口无需修改,系统会自动:
|
||||
1. 导入数据时计算特别规定状态
|
||||
2. 处理待开奖预测时使用新规则计算奖金
|
||||
3. 返回结果中包含福运奖等级(如果适用)
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
1. **奖池金额单位变更**:
|
||||
- Excel中:以"亿元"为单位(如14.01表示14.01亿元)
|
||||
- 数据库中:以"亿元"为单位存储(DECIMAL(10,2)类型)
|
||||
- 判断阈值:15亿元启动,3亿元停止
|
||||
- **重要**:如果之前已经导入过数据(以元为单位),需要执行 `sql/update_prize_pool_to_decimal.sql` 来转换数据类型
|
||||
|
||||
2. **历史数据处理**:
|
||||
- 如果需要重新计算历史预测记录,需要确保 `lottery_draws` 表中的 `is_special_period` 字段已正确设置
|
||||
- 可以通过重新导入Excel数据来自动计算历史期次的特别规定状态
|
||||
|
||||
3. **特别规定状态的连续性**:
|
||||
- 系统会自动追踪特别规定状态的变化
|
||||
- 导入数据时必须按期号顺序导入,以确保状态计算正确
|
||||
|
||||
4. **数据精度**:
|
||||
- 奖池金额使用 DECIMAL(10,2) 类型,最多支持10位数字,小数点后2位
|
||||
- 可以精确表示到0.01亿元(即100万元)
|
||||
|
||||
## 7. 测试建议
|
||||
|
||||
1. 测试特别规定启动:导入奖池≥15亿的数据,验证 `is_special_period` 为1
|
||||
2. 测试特别规定停止:在特别规定期间导入奖池<3亿的数据,验证 `is_special_period` 变为0
|
||||
3. 测试福运奖:在特别规定期间,预测3个红球匹配,验证能获得福运奖(5元)
|
||||
4. 测试非特别规定期间:验证3个红球匹配时不会获得福运奖
|
||||
|
||||
## 8. 文件清单
|
||||
|
||||
### 新增文件
|
||||
- `src/main/java/com/xy/xyaicpzs/util/SsqPrizeCalculator.java`
|
||||
- `sql/add_special_rule_status.sql`
|
||||
- `sql/update_prize_pool_to_decimal.sql`
|
||||
- `双色球奖金规则更新说明.md`(本文件)
|
||||
|
||||
### 修改文件
|
||||
- `src/main/java/com/xy/xyaicpzs/domain/entity/LotteryDraws.java`
|
||||
- `src/main/resources/generator/mapper/LotteryDrawsMapper.xml`
|
||||
- `src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java`
|
||||
- `src/main/java/com/xy/xyaicpzs/util/ExcelDataImporter.java`
|
||||
|
||||
## 9. 完成状态
|
||||
|
||||
✅ 数据库字段添加
|
||||
✅ 实体类更新
|
||||
✅ Mapper XML更新
|
||||
✅ 奖金计算器实现
|
||||
✅ 服务层更新
|
||||
✅ Excel导入逻辑更新
|
||||
✅ 代码编译通过
|
||||
|
||||
**所有代码已完成并通过编译检查,可以部署使用。**
|
||||
132
大乐透奖金规则更新说明.md
Normal file
132
大乐透奖金规则更新说明.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 大乐透奖金规则更新说明
|
||||
|
||||
## 更新概述
|
||||
大乐透奖池字段单位从"元"改为"亿元",以便与Excel导入数据保持一致。
|
||||
|
||||
## 1. 数据库变更
|
||||
|
||||
### 字段类型修改
|
||||
- **字段名**:`prizePool`
|
||||
- **旧类型**:`BIGINT`(单位:元)
|
||||
- **新类型**:`DECIMAL(10,2)`(单位:亿元)
|
||||
- **表名**:`dlt_draw_record`
|
||||
|
||||
### SQL脚本
|
||||
- `sql/update_prize_pool_to_decimal.sql`: 修改奖池字段类型
|
||||
|
||||
## 2. 代码变更
|
||||
|
||||
### 2.1 实体类更新
|
||||
**DltDrawRecord.java**
|
||||
- `prizePool` 字段从 `Long` 改为 `Double`
|
||||
- 单位从"元"改为"亿元"
|
||||
|
||||
### 2.2 工具类更新
|
||||
**DltPrizeCalculator.java**
|
||||
- `calculatePrize()` 方法参数 `prizePool` 从 `Long` 改为 `Double`
|
||||
- 奖池阈值从 `800000000L`(8亿元)改为 `8.0`(8亿)
|
||||
- 注释更新为"单位:亿元"
|
||||
|
||||
### 2.3 导入逻辑更新
|
||||
**DltDataImporter.java**
|
||||
- `importD1Data()` 方法:删除 `* 100000000` 转换,直接存储亿元值
|
||||
- `appendD1Data()` 方法:删除 `* 100000000` 转换,直接存储亿元值
|
||||
- 日志输出改为"亿元"
|
||||
|
||||
### 2.4 服务层更新
|
||||
**DltDataAnalysisServiceImpl.java**
|
||||
- `processPendingDltPredictions()` 方法中 `prizePool` 类型从 `Long` 改为 `Double`
|
||||
- 日志输出改为"亿元"
|
||||
|
||||
## 3. 奖金规则
|
||||
|
||||
### 奖池阈值
|
||||
- **高奖金档**:奖池 ≥ 8亿元
|
||||
- **低奖金档**:奖池 < 8亿元
|
||||
|
||||
### 奖金标准
|
||||
|
||||
| 奖项 | 中奖条件 | 奖池≥8亿 | 奖池<8亿 |
|
||||
|------|---------|---------|---------|
|
||||
| 一等奖 | 5+2 | 500万封顶(浮动) | 500万封顶(浮动) |
|
||||
| 二等奖 | 5+1 | 500万封顶(浮动) | 500万封顶(浮动) |
|
||||
| 三等奖 | 5+0 或 4+2 | 6666元 | 5000元 |
|
||||
| 四等奖 | 4+1 | 380元 | 300元 |
|
||||
| 五等奖 | 4+0 或 3+2 | 200元 | 150元 |
|
||||
| 六等奖 | 3+1 或 2+2 | 18元 | 15元 |
|
||||
| 七等奖 | 3+0 或 1+2 或 2+1 或 0+2 | 7元 | 5元 |
|
||||
|
||||
## 4. 使用说明
|
||||
|
||||
### 4.1 数据库迁移
|
||||
执行SQL脚本修改字段类型:
|
||||
```bash
|
||||
mysql -u用户名 -p数据库名 < sql/update_prize_pool_to_decimal.sql
|
||||
```
|
||||
|
||||
### 4.2 Excel导入格式
|
||||
D1工作表(大乐透开奖数据)列结构:
|
||||
- A列:开奖期号
|
||||
- B列:开奖日期
|
||||
- C-G列:前区球1-5
|
||||
- H-I列:后区球1-2
|
||||
- **J列:奖池金额(单位:亿元,如 8.50、10.25)**
|
||||
|
||||
**示例**:
|
||||
- Excel中填写:`8.50` 表示 8.50亿元
|
||||
- 数据库存储:`8.50`
|
||||
- 判断逻辑:`8.50 >= 8.0` → 使用高奖金档
|
||||
|
||||
### 4.3 API接口
|
||||
现有接口无需修改,系统会自动:
|
||||
1. 导入数据时直接存储亿元值
|
||||
2. 处理待开奖预测时根据奖池判断奖金档位
|
||||
3. 返回结果中奖池单位为亿元
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
1. **奖池金额单位变更**:
|
||||
- Excel中:以"亿元"为单位(如8.50表示8.50亿元)
|
||||
- 数据库中:以"亿元"为单位存储(DECIMAL(10,2)类型)
|
||||
- 判断阈值:8亿元
|
||||
- **重要**:如果之前已经导入过数据(以元为单位),需要执行 `sql/update_prize_pool_to_decimal.sql` 来转换数据类型
|
||||
|
||||
2. **数据精度**:
|
||||
- 奖池金额使用 DECIMAL(10,2) 类型,最多支持10位数字,小数点后2位
|
||||
- 可以精确表示到0.01亿元(即100万元)
|
||||
|
||||
3. **向后兼容**:
|
||||
- `DltPrizeCalculator.calculatePrize()` 提供了无奖池参数的重载方法,默认奖池为0
|
||||
- 旧代码可以继续使用,但建议更新为传入奖池参数的版本
|
||||
|
||||
## 6. 测试建议
|
||||
|
||||
1. 测试高奖金档:导入奖池≥8亿的数据,验证奖金计算正确
|
||||
2. 测试低奖金档:导入奖池<8亿的数据,验证奖金计算正确
|
||||
3. 测试边界值:导入奖池=8.00的数据,验证使用高奖金档
|
||||
4. 测试空值:验证奖池为空时默认为0.0
|
||||
|
||||
## 7. 文件清单
|
||||
|
||||
### 修改文件
|
||||
- `src/main/java/com/xy/xyaicpzs/domain/entity/DltDrawRecord.java`
|
||||
- `src/main/java/com/xy/xyaicpzs/util/DltPrizeCalculator.java`
|
||||
- `src/main/java/com/xy/xyaicpzs/util/DltDataImporter.java`
|
||||
- `src/main/java/com/xy/xyaicpzs/service/impl/DltDataAnalysisServiceImpl.java`
|
||||
|
||||
### 新增文件
|
||||
- `大乐透奖金规则更新说明.md`(本文件)
|
||||
|
||||
### SQL脚本
|
||||
- `sql/update_prize_pool_to_decimal.sql`(同时更新双色球和大乐透)
|
||||
|
||||
## 8. 完成状态
|
||||
|
||||
✅ 数据库字段类型修改
|
||||
✅ 实体类更新
|
||||
✅ 工具类更新
|
||||
✅ 导入逻辑更新
|
||||
✅ 服务层更新
|
||||
✅ 代码编译通过
|
||||
|
||||
**所有代码已完成并通过编译检查,可以部署使用。**
|
||||
471
大乐透数据导入使用说明.md
471
大乐透数据导入使用说明.md
@@ -1,471 +0,0 @@
|
||||
# 大乐透数据导入使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍如何使用大乐透数据导入功能,将包含D1工作表的Excel文件数据导入到`dlt_draw_record`数据库表中。
|
||||
|
||||
## 文件结构
|
||||
|
||||
### 新增文件
|
||||
|
||||
1. **DltDataImporter.java** - 大乐透数据导入工具类
|
||||
- 位置:`src/main/java/com/xy/xyaicpzs/util/DltDataImporter.java`
|
||||
- 功能:解析Excel文件中的D1工作表,将数据导入到数据库
|
||||
|
||||
2. **DltTestRunner.java** - 测试运行器
|
||||
- 位置:`src/main/java/com/xy/xyaicpzs/util/DltTestRunner.java`
|
||||
- 功能:提供手动测试导入功能
|
||||
|
||||
3. **DltImportController.java** - 导入控制器
|
||||
- 位置:`src/main/java/com/xy/xyaicpzs/controller/DltImportController.java`
|
||||
- 功能:提供RESTful API接口
|
||||
|
||||
## Excel文件格式要求
|
||||
|
||||
### D1工作表结构(开奖数据)
|
||||
|
||||
Excel文件必须包含名为"D1"的工作表,数据格式如下:
|
||||
|
||||
| 列 | 字段名 | 数据类型 | 描述 | 示例 |
|
||||
|----|--------|----------|------|------|
|
||||
| A | 开奖期号 | 字符串 | 大乐透期号 | 07001 |
|
||||
| B | 开奖日期 | 日期 | 开奖日期 | 2007-05-30 |
|
||||
| C | 前区1 | 整数 | 前区第1个号码 | 22 |
|
||||
| D | 前区2 | 整数 | 前区第2个号码 | 24 |
|
||||
| E | 前区3 | 整数 | 前区第3个号码 | 29 |
|
||||
| F | 前区4 | 整数 | 前区第4个号码 | 31 |
|
||||
| G | 前区5 | 整数 | 前区第5个号码 | 33 |
|
||||
| H | 后区1 | 整数 | 后区第1个号码 | 4 |
|
||||
| I | 后区2 | 整数 | 后区第2个号码 | 11 |
|
||||
|
||||
### 数据要求
|
||||
|
||||
- 第一行为标题行,从第二行开始为数据行
|
||||
- 开奖期号必须唯一,不能为空
|
||||
- 开奖日期支持多种格式:yyyy-MM-dd、yyyy/MM/dd、yyyy年MM月dd日等
|
||||
- 前区号码范围:1-35
|
||||
- 后区号码范围:1-12
|
||||
- 所有球号字段都不能为空
|
||||
|
||||
### D3工作表结构(前区历史数据)
|
||||
|
||||
Excel文件必须包含名为"D3"的工作表,数据格式如下:
|
||||
|
||||
| 列 | 字段名 | 数据类型 | 描述 | 示例 |
|
||||
|----|--------|----------|------|------|
|
||||
| A | 球号 | 整数 | 前区球号 | 1 |
|
||||
| B | 出现频次 | 整数 | 全部历史出现频次 | 410 |
|
||||
| C | 出现频率% | 小数 | 出现频率百分比 | 14.84 |
|
||||
| D | 平均隐现期 | 整数 | 平均隐现期次数 | 6 |
|
||||
| E | 最长隐现期 | 整数 | 最长隐现期次数 | 34 |
|
||||
| F | 最多连出期 | 整数 | 最多连出期次数 | 4 |
|
||||
| G | 活跃系数 | 小数 | 活跃系数 | 101.47 |
|
||||
| H | 出现频次 | 整数 | 最近100期出现频次 | 12 |
|
||||
| I | 平均隐现期 | 小数 | 最近100期平均隐现期 | 8.33 |
|
||||
| J | 当前隐现期 | 整数 | 当前隐现期 | 1 |
|
||||
| K | 最多连出期 | 整数 | 最近100期最多连出期 | 1 |
|
||||
| L | 活跃系数 | 小数 | 最近100期活跃系数 | 2.97 |
|
||||
| M | 排位 | 整数 | 历史数据排位 | 1 |
|
||||
| N | 球号 | 整数 | 排行球号 | 29 |
|
||||
| O | 活跃系数 | 小数 | 排行活跃系数 | 117.56 |
|
||||
| P | 排位 | 整数 | 百期数据排位 | 1 |
|
||||
| Q | 球号 | 整数 | 百期排行球号 | 20 |
|
||||
| R | 活跃系数 | 小数 | 百期排行活跃系数 | 5.20 |
|
||||
|
||||
### 数据要求(D3工作表)
|
||||
|
||||
- 第一行为标题行,从第二行开始为数据行
|
||||
- 球号必须唯一,不能为空
|
||||
- 前区球号范围:1-35
|
||||
- 排位数据必须完整
|
||||
- 活跃系数支持小数
|
||||
|
||||
### D4工作表结构(后区历史数据)
|
||||
|
||||
Excel文件必须包含名为"D4"的工作表,数据格式与D3相同:
|
||||
|
||||
| 列 | 字段名 | 数据类型 | 描述 | 示例 |
|
||||
|----|--------|----------|------|------|
|
||||
| A | 球号 | 整数 | 后区球号 | 1 |
|
||||
| B | 出现频次 | 整数 | 全部历史出现频次 | 150 |
|
||||
| C | 出现频率% | 小数 | 出现频率百分比 | 12.50 |
|
||||
| D | 平均隐现期 | 整数 | 平均隐现期次数 | 8 |
|
||||
| E | 最长隐现期 | 整数 | 最长隐现期次数 | 25 |
|
||||
| F | 最多连出期 | 整数 | 最多连出期次数 | 3 |
|
||||
| G | 活跃系数 | 小数 | 活跃系数 | 95.20 |
|
||||
| H | 出现频次 | 整数 | 最近100期出现频次 | 8 |
|
||||
| I | 平均隐现期 | 小数 | 最近100期平均隐现期 | 12.50 |
|
||||
| J | 当前隐现期 | 整数 | 当前隐现期 | 2 |
|
||||
| K | 最多连出期 | 整数 | 最近100期最多连出期 | 2 |
|
||||
| L | 活跃系数 | 小数 | 最近100期活跃系数 | 4.00 |
|
||||
| M | 排位 | 整数 | 历史数据排位 | 1 |
|
||||
| N | 球号 | 整数 | 排行球号 | 12 |
|
||||
| O | 活跃系数 | 小数 | 排行活跃系数 | 105.50 |
|
||||
| P | 排位 | 整数 | 百期数据排位 | 1 |
|
||||
| Q | 球号 | 整数 | 百期排行球号 | 8 |
|
||||
| R | 活跃系数 | 小数 | 百期排行活跃系数 | 4.50 |
|
||||
|
||||
### 数据要求(D4工作表)
|
||||
|
||||
- 第一行为标题行,从第二行开始为数据行
|
||||
- 球号必须唯一,不能为空
|
||||
- 后区球号范围:1-12
|
||||
- 排位数据必须完整
|
||||
- 活跃系数支持小数
|
||||
|
||||
## API接口
|
||||
|
||||
### 1. 上传文件导入开奖数据
|
||||
|
||||
**接口地址:** `POST /dlt/upload-draw-data`
|
||||
|
||||
**描述:** 上传Excel文件并导入大乐透开奖数据(D1工作表,会清空现有数据)
|
||||
|
||||
**参数:**
|
||||
- `file`: MultipartFile类型,Excel文件(.xlsx格式)
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
**返回示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": "大乐透开奖数据导入成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 上传文件导入前区历史数据
|
||||
|
||||
**接口地址:** `POST /dlt/upload-frontend-history`
|
||||
|
||||
**描述:** 上传Excel文件并导入大乐透前区历史数据(D3工作表,会清空现有数据)
|
||||
|
||||
**参数:**
|
||||
- `file`: MultipartFile类型,Excel文件(.xlsx格式)
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
**返回示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": "大乐透前区历史数据导入成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 上传文件导入后区历史数据
|
||||
|
||||
**接口地址:** `POST /dlt/upload-backend-history`
|
||||
|
||||
**描述:** 上传Excel文件并导入大乐透后区历史数据(D4工作表,会清空现有数据)
|
||||
|
||||
**参数:**
|
||||
- `file`: MultipartFile类型,Excel文件(.xlsx格式)
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
**返回示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": "大乐透后区历史数据导入成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 追加导入开奖数据
|
||||
|
||||
**接口地址:** `POST /dlt/upload-append-draw`
|
||||
|
||||
**描述:** 上传Excel文件并追加导入大乐透开奖数据(D1工作表,不清空现有数据,跳过重复记录)
|
||||
|
||||
**参数:**
|
||||
- `file`: MultipartFile类型,Excel文件(.xlsx格式)
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
**返回示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"success": true,
|
||||
"message": "操作成功",
|
||||
"data": "大乐透开奖数据追加导入成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 文件路径导入开奖数据(测试用)
|
||||
|
||||
**接口地址:** `POST /dlt/import-draw-by-path`
|
||||
|
||||
**描述:** 根据服务器上的文件路径导入开奖数据
|
||||
|
||||
**参数:**
|
||||
- `filePath`: 字符串,服务器上的Excel文件路径
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
### 6. 文件路径导入前区历史数据(测试用)
|
||||
|
||||
**接口地址:** `POST /dlt/import-frontend-by-path`
|
||||
|
||||
**描述:** 根据服务器上的文件路径导入前区历史数据
|
||||
|
||||
**参数:**
|
||||
- `filePath`: 字符串,服务器上的Excel文件路径
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
### 7. 文件路径导入后区历史数据(测试用)
|
||||
|
||||
**接口地址:** `POST /dlt/import-backend-by-path`
|
||||
|
||||
**描述:** 根据服务器上的文件路径导入后区历史数据
|
||||
|
||||
**参数:**
|
||||
- `filePath`: 字符串,服务器上的Excel文件路径
|
||||
|
||||
**权限:** 需要管理员权限
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. 准备Excel文件
|
||||
|
||||
#### 开奖数据文件(D1工作表)
|
||||
1. 创建包含D1工作表的Excel文件(.xlsx格式)
|
||||
2. 按照上述D1格式要求填入大乐透开奖数据
|
||||
3. 确保数据完整性和格式正确性
|
||||
|
||||
#### 前区历史数据文件(D3工作表)
|
||||
1. 创建包含D3工作表的Excel文件(.xlsx格式)
|
||||
2. 按照上述D3格式要求填入前区历史数据
|
||||
3. 确保数据完整性和格式正确性
|
||||
|
||||
#### 后区历史数据文件(D4工作表)
|
||||
1. 创建包含D4工作表的Excel文件(.xlsx格式)
|
||||
2. 按照上述D4格式要求填入后区历史数据
|
||||
3. 确保数据完整性和格式正确性
|
||||
|
||||
### 2. 通过API导入
|
||||
|
||||
#### 方式一:文件上传导入开奖数据
|
||||
```bash
|
||||
curl -X POST \
|
||||
http://localhost:8080/dlt/upload-draw-data \
|
||||
-H 'Content-Type: multipart/form-data' \
|
||||
-F 'file=@/path/to/your/dlt_draw_data.xlsx'
|
||||
```
|
||||
|
||||
#### 方式二:文件上传导入前区历史数据
|
||||
```bash
|
||||
curl -X POST \
|
||||
http://localhost:8080/dlt/upload-frontend-history \
|
||||
-H 'Content-Type: multipart/form-data' \
|
||||
-F 'file=@/path/to/your/dlt_frontend_data.xlsx'
|
||||
```
|
||||
|
||||
#### 方式三:文件上传导入后区历史数据
|
||||
```bash
|
||||
curl -X POST \
|
||||
http://localhost:8080/dlt/upload-backend-history \
|
||||
-H 'Content-Type: multipart/form-data' \
|
||||
-F 'file=@/path/to/your/dlt_backend_data.xlsx'
|
||||
```
|
||||
|
||||
#### 方式四:服务器文件路径导入
|
||||
```bash
|
||||
# 导入开奖数据
|
||||
curl -X POST \
|
||||
'http://localhost:8080/dlt/import-draw-by-path?filePath=/path/to/server/file.xlsx'
|
||||
|
||||
# 导入前区历史数据
|
||||
curl -X POST \
|
||||
'http://localhost:8080/dlt/import-frontend-by-path?filePath=/path/to/server/file.xlsx'
|
||||
|
||||
# 导入后区历史数据
|
||||
curl -X POST \
|
||||
'http://localhost:8080/dlt/import-backend-by-path?filePath=/path/to/server/file.xlsx'
|
||||
```
|
||||
|
||||
### 3. 查看导入结果
|
||||
|
||||
- 开奖数据导入成功后,数据将保存到`dlt_draw_record`表中
|
||||
- 前区历史数据导入成功后,数据将保存到四个前区历史相关表中
|
||||
- 后区历史数据导入成功后,数据将保存到四个后区历史相关表中
|
||||
- 系统会记录操作历史,可通过操作历史接口查看详细信息
|
||||
- 查看应用日志了解详细的导入过程和统计信息
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### 1. dlt_draw_record表(开奖数据)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_draw_record` (
|
||||
`id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
`drawId` VARCHAR(50) NOT NULL COMMENT '开奖期号',
|
||||
`drawDate` DATE NOT NULL COMMENT '开奖日期',
|
||||
`frontBall1` INT NOT NULL COMMENT '前区1',
|
||||
`frontBall2` INT NOT NULL COMMENT '前区2',
|
||||
`frontBall3` INT NOT NULL COMMENT '前区3',
|
||||
`frontBall4` INT NOT NULL COMMENT '前区4',
|
||||
`frontBall5` INT NOT NULL COMMENT '前区5',
|
||||
`backBall1` INT NOT NULL COMMENT '后区1',
|
||||
`backBall2` INT NOT NULL COMMENT '后区2',
|
||||
`createTime` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '记录创建时间',
|
||||
`updateTime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL COMMENT '记录更新时间'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透开奖信息表';
|
||||
```
|
||||
|
||||
### 2. dlt_frontend_history_all表(前区全部历史数据)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_frontend_history_all` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ballNumber INT NOT NULL COMMENT '球号',
|
||||
frequencyCount INT NULL COMMENT '出现频次',
|
||||
frequencyPercentage FLOAT NULL COMMENT '出现频率%',
|
||||
averageHiddenAppear INT NULL COMMENT '平均隐现期(次)',
|
||||
maxHiddenInterval INT NULL COMMENT '最长隐现期(次)',
|
||||
maxConsecutive INT NULL COMMENT '最多连出期(次)',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区全部历史数据表';
|
||||
```
|
||||
|
||||
### 3. dlt_frontend_history_100表(前区最近100期数据)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_frontend_history_100` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ballNumber INT NOT NULL COMMENT '球号',
|
||||
frequencyCount INT NULL COMMENT '出现频次',
|
||||
averageHiddenAppear FLOAT NULL COMMENT '平均隐现期(次)',
|
||||
currentHiddenInterval INT NULL COMMENT '当前隐现期',
|
||||
maxConsecutive INT NULL COMMENT '最多连出期(次)',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区最近100期数据表';
|
||||
```
|
||||
|
||||
### 4. dlt_frontend_history_top表(前区历史数据排行)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_frontend_history_top` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ranking INT NULL COMMENT '排位',
|
||||
ballNumber INT NULL COMMENT '球号',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区历史数据排行表';
|
||||
```
|
||||
|
||||
### 5. dlt_frontend_history_top_100表(前区百期数据排行)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_frontend_history_top_100` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ranking INT NULL COMMENT '排位',
|
||||
ballNumber INT NULL COMMENT '球号',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区百期数据排行表';
|
||||
```
|
||||
|
||||
### 6. dlt_backend_history_all表(后区全部历史数据)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_backend_history_all` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ballNumber INT NOT NULL COMMENT '球号',
|
||||
frequencyCount INT NULL COMMENT '出现频次',
|
||||
frequencyPercentage FLOAT NULL COMMENT '出现频率%',
|
||||
averageHiddenAppear INT NULL COMMENT '平均隐现期(次)',
|
||||
maxHiddenInterval INT NULL COMMENT '最长隐现期(次)',
|
||||
maxConsecutive INT NULL COMMENT '最多连出期(次)',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区全部历史数据表';
|
||||
```
|
||||
|
||||
### 7. dlt_backend_history_100表(后区最近100期数据)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_backend_history_100` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ballNumber INT NOT NULL COMMENT '球号',
|
||||
frequencyCount INT NULL COMMENT '出现频次',
|
||||
averageHiddenAppear FLOAT NULL COMMENT '平均隐现期(次)',
|
||||
currentHiddenInterval INT NULL COMMENT '当前隐现期',
|
||||
maxConsecutive INT NULL COMMENT '最多连出期(次)',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区最近100期数据表';
|
||||
```
|
||||
|
||||
### 8. dlt_backend_history_top表(后区历史数据排行)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_backend_history_top` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ranking INT NULL COMMENT '排位',
|
||||
ballNumber INT NULL COMMENT '球号',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区历史数据排行表';
|
||||
```
|
||||
|
||||
### 9. dlt_backend_history_top_100表(后区百期数据排行)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `dlt_backend_history_top_100` (
|
||||
id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
|
||||
ranking INT NULL COMMENT '排位',
|
||||
ballNumber INT NULL COMMENT '球号',
|
||||
activeCoefficient FLOAT NULL COMMENT '活跃系数'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区百期数据排行表';
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误及解决方案
|
||||
|
||||
1. **文件格式错误**
|
||||
- 错误:`只支持.xlsx格式的Excel文件`
|
||||
- 解决:确保上传的是.xlsx格式的Excel文件
|
||||
|
||||
2. **工作表不存在**
|
||||
- 错误:`未找到D1工作表`
|
||||
- 解决:确保Excel文件包含名为"D1"的工作表
|
||||
|
||||
3. **数据不完整**
|
||||
- 错误:`第X行数据不完整,跳过`
|
||||
- 解决:检查对应行的数据,确保所有必需字段都有值
|
||||
|
||||
4. **日期格式错误**
|
||||
- 错误:`第X行开奖日期为空,跳过`
|
||||
- 解决:检查日期格式,支持yyyy-MM-dd、yyyy/MM/dd等格式
|
||||
|
||||
5. **权限不足**
|
||||
- 错误:`无权限`
|
||||
- 解决:确保使用管理员账号登录
|
||||
|
||||
## 日志信息
|
||||
|
||||
导入过程中会产生详细的日志信息:
|
||||
|
||||
- **INFO级别**:导入进度、成功统计
|
||||
- **WARN级别**:跳过的无效数据行
|
||||
- **ERROR级别**:导入失败的错误信息
|
||||
- **DEBUG级别**:每条记录的详细信息(需开启DEBUG日志)
|
||||
|
||||
## 性能说明
|
||||
|
||||
- 支持批量导入,使用MyBatis-Plus的`saveBatch`方法
|
||||
- 追加导入时会检查重复记录,避免数据重复
|
||||
- 大文件导入建议分批处理,避免内存溢出
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据备份**:导入前建议备份现有数据
|
||||
2. **权限控制**:所有导入接口都需要管理员权限
|
||||
3. **文件大小**:注意服务器文件上传大小限制
|
||||
4. **并发控制**:避免同时进行多个导入操作
|
||||
5. **操作记录**:所有导入操作都会记录到操作历史表中
|
||||
238
短信服务迁移说明.md
Normal file
238
短信服务迁移说明.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 短信服务迁移说明 - 从阿里云SMS到腾讯云SMS
|
||||
|
||||
## 迁移概述
|
||||
将短信服务从阿里云SMS迁移到腾讯云SMS,保持原有功能不变。
|
||||
|
||||
## 1. 代码变更
|
||||
|
||||
### 1.1 SmsServiceImpl.java
|
||||
**位置**:`src/main/java/com/xy/xyaicpzs/service/impl/SmsServiceImpl.java`
|
||||
|
||||
**主要变更**:
|
||||
- 移除阿里云SDK依赖(`com.aliyun.dysmsapi20170525`)
|
||||
- 引入腾讯云SDK(`com.tencentcloudapi.sms.v20210111`)
|
||||
- 修改客户端创建方式
|
||||
- 修改短信发送请求构建方式
|
||||
- 手机号格式改为E.164标准(+86前缀)
|
||||
|
||||
**关键代码**:
|
||||
```java
|
||||
// 创建腾讯云短信客户端
|
||||
private SmsClient createSmsClient() {
|
||||
Credential cred = new Credential(secretId, secretKey);
|
||||
return new SmsClient(cred, region);
|
||||
}
|
||||
|
||||
// 发送短信
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
req.setSmsSdkAppId(sdkAppId);
|
||||
req.setSignName(signName);
|
||||
req.setTemplateId(templateId);
|
||||
req.setTemplateParamSet(new String[]{verificationCode});
|
||||
req.setPhoneNumberSet(new String[]{"+86" + phoneNumber});
|
||||
```
|
||||
|
||||
### 1.2 配置文件更新
|
||||
**位置**:`src/main/resources/application.yml`
|
||||
|
||||
**旧配置(阿里云)**:
|
||||
```yaml
|
||||
aliyun:
|
||||
sms:
|
||||
sign-name: 西安精彩数据服务社
|
||||
template-code: SMS_489840017
|
||||
access-key-id: LTAI5tR18rXPYazi3y8kAuep
|
||||
access-key-secret: KZ1aKZOupilVc332SXE1g1DfKsqHPu
|
||||
```
|
||||
|
||||
**新配置(腾讯云)**:
|
||||
```yaml
|
||||
tencent:
|
||||
sms:
|
||||
secret-id: ${TENCENT_SMS_SECRET_ID:your-secret-id}
|
||||
secret-key: ${TENCENT_SMS_SECRET_KEY:your-secret-key}
|
||||
sdk-app-id: 1401068120
|
||||
template-id: 2598660
|
||||
sign-name: 西安溢彩数智科技有限公司
|
||||
region: ap-guangzhou
|
||||
```
|
||||
|
||||
## 2. 配置说明
|
||||
|
||||
### 2.1 必需配置项
|
||||
| 配置项 | 说明 | 示例值 |
|
||||
|--------|------|--------|
|
||||
| secret-id | 腾讯云SecretId | 从腾讯云控制台获取 |
|
||||
| secret-key | 腾讯云SecretKey | 从腾讯云控制台获取 |
|
||||
| sdk-app-id | 短信应用ID | 1401068120 |
|
||||
| template-id | 短信模板ID | 2598660 |
|
||||
| sign-name | 短信签名 | 西安溢彩数智科技有限公司 |
|
||||
| region | 地域 | ap-guangzhou(默认) |
|
||||
|
||||
### 2.2 环境变量配置(推荐)
|
||||
为了安全,建议使用环境变量配置敏感信息:
|
||||
|
||||
**Linux/Mac**:
|
||||
```bash
|
||||
export TENCENT_SMS_SECRET_ID=your-secret-id
|
||||
export TENCENT_SMS_SECRET_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```cmd
|
||||
set TENCENT_SMS_SECRET_ID=your-secret-id
|
||||
set TENCENT_SMS_SECRET_KEY=your-secret-key
|
||||
```
|
||||
|
||||
或者直接在 `application.yml` 中替换默认值。
|
||||
|
||||
## 3. Maven依赖
|
||||
|
||||
确保 `pom.xml` 中已添加腾讯云SMS SDK依赖:
|
||||
|
||||
```xml
|
||||
<!-- 腾讯云SMS SDK -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||
<version>3.1.xxx</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**注意**:可以移除阿里云SMS相关依赖:
|
||||
```xml
|
||||
<!-- 可以删除 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<version>xxx</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## 4. 功能保持不变
|
||||
|
||||
### 4.1 验证码生成
|
||||
- 仍然生成6位随机数字验证码
|
||||
- 验证码有效期:5分钟
|
||||
|
||||
### 4.2 发送限制
|
||||
- 每个手机号每天最多发送10次
|
||||
- 使用Redis存储发送次数和验证码
|
||||
|
||||
### 4.3 API接口
|
||||
- 接口路径不变:`POST /api/sms/sendCode`
|
||||
- 请求参数不变:`phoneNumber`
|
||||
- 响应格式不变
|
||||
|
||||
## 5. 腾讯云SMS模板要求
|
||||
|
||||
### 5.1 模板格式
|
||||
根据模板ID `2598660`,模板内容应该类似:
|
||||
```
|
||||
您的验证码是{1},请在5分钟内完成验证。
|
||||
```
|
||||
|
||||
### 5.2 模板参数
|
||||
代码中传递的参数:
|
||||
```java
|
||||
String[] templateParamSet = {verificationCode};
|
||||
```
|
||||
|
||||
**注意**:腾讯云模板参数是数组形式,按顺序对应模板中的 `{1}`, `{2}` 等占位符。
|
||||
|
||||
## 6. 手机号格式
|
||||
|
||||
### 6.1 阿里云格式
|
||||
```
|
||||
13868246742 // 直接11位手机号
|
||||
```
|
||||
|
||||
### 6.2 腾讯云格式(E.164标准)
|
||||
```
|
||||
+8613868246742 // 需要加 +86 前缀
|
||||
```
|
||||
|
||||
代码中已自动处理:
|
||||
```java
|
||||
String[] phoneNumberSet = {"+86" + phoneNumber};
|
||||
```
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
### 7.1 发送成功判断
|
||||
腾讯云返回的状态码为 `"Ok"` 表示成功:
|
||||
```java
|
||||
if ("Ok".equals(code)) {
|
||||
// 发送成功
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 常见错误码
|
||||
| 错误码 | 说明 | 处理方式 |
|
||||
|--------|------|---------|
|
||||
| FailedOperation.ContainSensitiveWord | 内容包含敏感词 | 检查模板内容 |
|
||||
| FailedOperation.SignatureIncorrectOrUnapproved | 签名未审核或不正确 | 检查签名配置 |
|
||||
| LimitExceeded.PhoneNumberDailyLimit | 手机号日发送量超限 | 已在代码中限制 |
|
||||
| InvalidParameterValue.TemplateParameterFormatError | 模板参数格式错误 | 检查参数数组 |
|
||||
|
||||
## 8. 测试建议
|
||||
|
||||
### 8.1 单元测试
|
||||
1. 测试验证码生成
|
||||
2. 测试发送次数限制
|
||||
3. 测试验证码验证
|
||||
|
||||
### 8.2 集成测试
|
||||
1. 发送验证码到真实手机号
|
||||
2. 验证短信内容格式
|
||||
3. 验证验证码有效期
|
||||
4. 验证每日发送次数限制
|
||||
|
||||
### 8.3 测试命令
|
||||
```bash
|
||||
curl -X POST http://localhost:8123/api/sms/sendCode \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "phoneNumber=13868246742"
|
||||
```
|
||||
|
||||
## 9. 注意事项
|
||||
|
||||
1. **配置敏感信息**:
|
||||
- 不要将 `secret-id` 和 `secret-key` 提交到代码仓库
|
||||
- 使用环境变量或配置中心管理
|
||||
|
||||
2. **模板审核**:
|
||||
- 确保腾讯云控制台中模板ID `2598660` 已审核通过
|
||||
- 确保签名"西安溢彩数智科技有限公司"已审核通过
|
||||
|
||||
3. **地域选择**:
|
||||
- 默认使用 `ap-guangzhou`(广州)
|
||||
- 可根据实际情况修改为其他地域
|
||||
|
||||
4. **费用**:
|
||||
- 腾讯云SMS按条计费
|
||||
- 建议在控制台设置费用告警
|
||||
|
||||
## 10. 回滚方案
|
||||
|
||||
如果需要回滚到阿里云SMS:
|
||||
|
||||
1. 恢复 `SmsServiceImpl.java` 的旧版本
|
||||
2. 恢复 `application.yml` 中的阿里云配置
|
||||
3. 确保阿里云SDK依赖存在
|
||||
4. 重启应用
|
||||
|
||||
## 11. 完成状态
|
||||
|
||||
✅ 代码迁移完成
|
||||
✅ 配置文件更新
|
||||
✅ 编译检查通过
|
||||
✅ 功能保持不变
|
||||
|
||||
**迁移已完成,可以部署使用!**
|
||||
|
||||
## 12. 相关文档
|
||||
|
||||
- [腾讯云SMS SDK文档](https://cloud.tencent.com/document/product/382/43199)
|
||||
- [腾讯云SMS API文档](https://cloud.tencent.com/document/api/382/52071)
|
||||
- [腾讯云SMS控制台](https://console.cloud.tencent.com/smsv2)
|
||||
712
阿里云SMS接口集成文档.md
Normal file
712
阿里云SMS接口集成文档.md
Normal file
@@ -0,0 +1,712 @@
|
||||
# 阿里云SMS短信服务集成文档
|
||||
|
||||
## 目录
|
||||
1. [概述](#概述)
|
||||
2. [Maven依赖](#maven依赖)
|
||||
3. [配置文件](#配置文件)
|
||||
4. [代码实现](#代码实现)
|
||||
5. [使用示例](#使用示例)
|
||||
6. [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文档提供阿里云短信服务(SMS)的完整集成方案,包括验证码发送、验证码校验、发送频率限制等功能。
|
||||
|
||||
### 功能特性
|
||||
- ✅ 发送6位数字验证码
|
||||
- ✅ 验证码5分钟有效期
|
||||
- ✅ 每个手机号每天最多发送10次
|
||||
- ✅ 使用Redis存储验证码和发送次数
|
||||
- ✅ 完整的错误处理和日志记录
|
||||
|
||||
---
|
||||
|
||||
## Maven依赖
|
||||
|
||||
### pom.xml
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- 阿里云短信SDK -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<version>2.0.24</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云核心SDK -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>tea-openapi</artifactId>
|
||||
<version>0.2.8</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云Tea工具 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>tea-util</artifactId>
|
||||
<version>0.2.21</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
### application.yml
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
password: # 如果有密码则填写
|
||||
|
||||
# 阿里云短信服务配置
|
||||
aliyun:
|
||||
sms:
|
||||
# 短信签名(需要在阿里云控制台申请)
|
||||
sign-name: 你的短信签名
|
||||
# 短信模板CODE(需要在阿里云控制台申请)
|
||||
template-code: SMS_xxxxxxxx
|
||||
# 阿里云AccessKey ID
|
||||
access-key-id: ${ALIYUN_ACCESS_KEY_ID:your-access-key-id}
|
||||
# 阿里云AccessKey Secret
|
||||
access-key-secret: ${ALIYUN_ACCESS_KEY_SECRET:your-access-key-secret}
|
||||
```
|
||||
|
||||
### 环境变量配置(推荐)
|
||||
|
||||
**Linux/Mac**:
|
||||
```bash
|
||||
export ALIYUN_ACCESS_KEY_ID=your-access-key-id
|
||||
export ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```cmd
|
||||
set ALIYUN_ACCESS_KEY_ID=your-access-key-id
|
||||
set ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码实现
|
||||
|
||||
### 1. Service接口
|
||||
|
||||
**SmsService.java**
|
||||
|
||||
```java
|
||||
package com.example.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);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Service实现类
|
||||
|
||||
**SmsServiceImpl.java**
|
||||
|
||||
```java
|
||||
package com.example.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.example.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}")
|
||||
private String templateCode;
|
||||
|
||||
@Value("${aliyun.sms.access-key-id}")
|
||||
private String accessKeyId;
|
||||
|
||||
@Value("${aliyun.sms.access-key-secret}")
|
||||
private String accessKeySecret;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> 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 {
|
||||
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 {
|
||||
// 1. 检查当天发送次数是否达到上限
|
||||
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;
|
||||
}
|
||||
|
||||
// 2. 生成6位随机验证码
|
||||
String verificationCode = generateVerificationCode();
|
||||
|
||||
// 3. 构建短信请求
|
||||
Client client = createSmsClient();
|
||||
SendSmsRequest sendSmsRequest = new SendSmsRequest()
|
||||
.setSignName(signName)
|
||||
.setTemplateCode(templateCode)
|
||||
.setPhoneNumbers(phoneNumber)
|
||||
.setTemplateParam("{\"code\":\"" + verificationCode + "\"}");
|
||||
|
||||
RuntimeOptions runtime = new RuntimeOptions();
|
||||
|
||||
try {
|
||||
// 4. 发送短信
|
||||
client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||
logger.info("短信验证码发送成功,手机号: {}", phoneNumber);
|
||||
|
||||
// 5. 将验证码保存到Redis,设置过期时间
|
||||
String codeKey = SMS_CODE_PREFIX + phoneNumber;
|
||||
redisTemplate.opsForValue().set(codeKey, verificationCode, SMS_CODE_EXPIRE, TimeUnit.MINUTES);
|
||||
|
||||
// 6. 增加当天发送次数,并设置过期时间为当天结束
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Controller控制器
|
||||
|
||||
**SmsController.java**
|
||||
|
||||
```java
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.common.ApiResponse;
|
||||
import com.example.common.ResultUtils;
|
||||
import com.example.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.*;
|
||||
|
||||
/**
|
||||
* 短信控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/sms")
|
||||
@Tag(name = "短信接口", description = "提供短信验证码相关功能")
|
||||
public class SmsController {
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param phoneNumber 手机号
|
||||
* @return 发送结果
|
||||
*/
|
||||
@PostMapping("/sendCode")
|
||||
@Operation(summary = "发送短信验证码", description = "向指定手机号发送验证码,每个手机号每天最多发送10次")
|
||||
public ApiResponse<Boolean> 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*
|
||||
* @param phoneNumber 手机号
|
||||
* @param code 验证码
|
||||
* @return 验证结果
|
||||
*/
|
||||
@PostMapping("/verifyCode")
|
||||
@Operation(summary = "验证短信验证码", description = "验证手机号和验证码是否匹配")
|
||||
public ApiResponse<Boolean> verifyCode(
|
||||
@Parameter(description = "手机号码", required = true)
|
||||
@RequestParam String phoneNumber,
|
||||
@Parameter(description = "验证码", required = true)
|
||||
@RequestParam String code) {
|
||||
boolean valid = smsService.verifyCode(phoneNumber, code);
|
||||
if (valid) {
|
||||
return ResultUtils.success(true, "验证成功");
|
||||
} else {
|
||||
return ResultUtils.error(40002, "验证码错误或已过期");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 通用响应类(可选)
|
||||
|
||||
**ApiResponse.java**
|
||||
|
||||
```java
|
||||
package com.example.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 通用API响应类
|
||||
*/
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public ApiResponse(int code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ResultUtils.java**
|
||||
|
||||
```java
|
||||
package com.example.common;
|
||||
|
||||
/**
|
||||
* 响应工具类
|
||||
*/
|
||||
public class ResultUtils {
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(200, "success", data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(T data, String message) {
|
||||
return new ApiResponse<>(200, message, data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
return new ApiResponse<>(code, message, null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 发送验证码
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/sms/sendCode \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "phoneNumber=13800138000"
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "验证码发送成功",
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 验证验证码
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/sms/verifyCode \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "phoneNumber=13800138000&code=123456"
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "验证成功",
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 在业务代码中使用
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
public void register(String phoneNumber, String code, String password) {
|
||||
// 1. 验证验证码
|
||||
if (!smsService.verifyCode(phoneNumber, code)) {
|
||||
throw new BusinessException("验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 2. 执行注册逻辑
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* 找回密码
|
||||
*/
|
||||
public void resetPassword(String phoneNumber, String code, String newPassword) {
|
||||
// 1. 验证验证码
|
||||
if (!smsService.verifyCode(phoneNumber, code)) {
|
||||
throw new BusinessException("验证码错误或已过期");
|
||||
}
|
||||
|
||||
// 2. 重置密码
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 如何申请阿里云短信服务?
|
||||
|
||||
1. 登录[阿里云控制台](https://www.aliyun.com/)
|
||||
2. 进入"短信服务"产品页面
|
||||
3. 申请短信签名(需要企业资质或个人认证)
|
||||
4. 申请短信模板(需要审核,一般1-2个工作日)
|
||||
5. 获取AccessKey ID和AccessKey Secret
|
||||
|
||||
### 2. 短信模板格式
|
||||
|
||||
**模板示例**:
|
||||
```
|
||||
您的验证码是${code},请在5分钟内完成验证。
|
||||
```
|
||||
|
||||
**模板变量**:
|
||||
- 使用 `${变量名}` 格式
|
||||
- 代码中传递JSON格式:`{"code":"123456"}`
|
||||
|
||||
### 3. 常见错误码
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|--------|------|---------|
|
||||
| isv.BUSINESS_LIMIT_CONTROL | 触发业务流控 | 降低发送频率 |
|
||||
| isv.MOBILE_NUMBER_ILLEGAL | 手机号码格式错误 | 检查手机号格式 |
|
||||
| isv.TEMPLATE_MISSING_PARAMETERS | 模板参数缺失 | 检查模板参数 |
|
||||
| isv.INVALID_PARAMETERS | 参数异常 | 检查所有参数 |
|
||||
| isv.AMOUNT_NOT_ENOUGH | 账户余额不足 | 充值 |
|
||||
| isv.TEMPLATE_PARAMS_ILLEGAL | 模板变量值非法 | 检查变量值格式 |
|
||||
|
||||
### 4. 如何修改验证码位数?
|
||||
|
||||
修改 `generateVerificationCode()` 方法中的循环次数:
|
||||
|
||||
```java
|
||||
private String generateVerificationCode() {
|
||||
Random random = new Random();
|
||||
StringBuilder code = new StringBuilder();
|
||||
// 修改这里的数字,比如改为4位验证码
|
||||
for (int i = 0; i < 4; i++) {
|
||||
code.append(random.nextInt(10));
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 如何修改验证码有效期?
|
||||
|
||||
修改常量 `SMS_CODE_EXPIRE`:
|
||||
|
||||
```java
|
||||
// 修改为10分钟
|
||||
private static final long SMS_CODE_EXPIRE = 10;
|
||||
```
|
||||
|
||||
### 6. 如何修改每日发送次数限制?
|
||||
|
||||
修改常量 `MAX_SMS_COUNT_PER_DAY`:
|
||||
|
||||
```java
|
||||
// 修改为5次
|
||||
private static final int MAX_SMS_COUNT_PER_DAY = 5;
|
||||
```
|
||||
|
||||
### 7. Redis连接失败怎么办?
|
||||
|
||||
确保Redis服务已启动:
|
||||
|
||||
```bash
|
||||
# Linux/Mac
|
||||
redis-server
|
||||
|
||||
# 检查Redis是否运行
|
||||
redis-cli ping
|
||||
# 应该返回 PONG
|
||||
```
|
||||
|
||||
### 8. 如何测试短信发送?
|
||||
|
||||
阿里云提供测试环境,可以使用测试手机号:
|
||||
|
||||
```java
|
||||
// 测试环境配置
|
||||
config.endpoint = "dysmsapi.aliyuncs.com"; // 正式环境
|
||||
// config.endpoint = "dysmsapi-test.aliyuncs.com"; // 测试环境(如果有)
|
||||
```
|
||||
|
||||
### 9. 生产环境安全建议
|
||||
|
||||
1. **不要硬编码密钥**:使用环境变量或配置中心
|
||||
2. **启用HTTPS**:保护API通信安全
|
||||
3. **添加图形验证码**:防止恶意刷短信
|
||||
4. **IP限流**:防止单个IP频繁请求
|
||||
5. **手机号验证**:验证手机号格式和归属地
|
||||
6. **监控告警**:设置短信发送量告警
|
||||
|
||||
### 10. 性能优化建议
|
||||
|
||||
1. **Redis连接池**:配置合理的连接池参数
|
||||
2. **异步发送**:使用 `@Async` 异步发送短信
|
||||
3. **批量发送**:如果需要群发,使用批量接口
|
||||
4. **缓存优化**:合理设置Redis过期时间
|
||||
|
||||
---
|
||||
|
||||
## 完整项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── java/
|
||||
│ │ └── com/
|
||||
│ │ └── example/
|
||||
│ │ ├── controller/
|
||||
│ │ │ └── SmsController.java
|
||||
│ │ ├── service/
|
||||
│ │ │ ├── SmsService.java
|
||||
│ │ │ └── impl/
|
||||
│ │ │ └── SmsServiceImpl.java
|
||||
│ │ ├── common/
|
||||
│ │ │ ├── ApiResponse.java
|
||||
│ │ │ └── ResultUtils.java
|
||||
│ │ └── Application.java
|
||||
│ └── resources/
|
||||
│ └── application.yml
|
||||
└── test/
|
||||
└── java/
|
||||
└── com/
|
||||
└── example/
|
||||
└── service/
|
||||
└── SmsServiceTest.java
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 单元测试示例
|
||||
|
||||
**SmsServiceTest.java**
|
||||
|
||||
```java
|
||||
package com.example.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
class SmsServiceTest {
|
||||
|
||||
@Autowired
|
||||
private SmsService smsService;
|
||||
|
||||
@Test
|
||||
void testSendVerificationCode() throws Exception {
|
||||
// 使用测试手机号
|
||||
String phoneNumber = "13800138000";
|
||||
boolean result = smsService.sendVerificationCode(phoneNumber);
|
||||
assertTrue(result, "验证码发送应该成功");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifyCode() throws Exception {
|
||||
String phoneNumber = "13800138000";
|
||||
|
||||
// 发送验证码
|
||||
smsService.sendVerificationCode(phoneNumber);
|
||||
|
||||
// 验证错误的验证码
|
||||
boolean result1 = smsService.verifyCode(phoneNumber, "000000");
|
||||
assertFalse(result1, "错误的验证码应该验证失败");
|
||||
|
||||
// 注意:无法测试正确的验证码,因为验证码是随机生成的
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
本文档提供了阿里云SMS短信服务的完整集成方案,包括:
|
||||
|
||||
✅ Maven依赖配置
|
||||
✅ 完整的代码实现
|
||||
✅ 配置文件示例
|
||||
✅ 使用示例和测试方法
|
||||
✅ 常见问题解答
|
||||
✅ 安全和性能优化建议
|
||||
|
||||
将此文档保存到你的项目中,以后需要集成阿里云SMS时可以直接参考使用。
|
||||
|
||||
---
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [阿里云短信服务官网](https://www.aliyun.com/product/sms)
|
||||
- [阿里云短信服务API文档](https://help.aliyun.com/document_detail/101414.html)
|
||||
- [阿里云短信服务SDK](https://help.aliyun.com/document_detail/215759.html)
|
||||
- [阿里云短信服务控制台](https://dysms.console.aliyun.com/)
|
||||
Reference in New Issue
Block a user