update: 提交所有修改和新增功能代码

This commit is contained in:
lihanqi
2026-02-14 12:15:01 +08:00
parent dc59f393fa
commit ec597ffe2e
77 changed files with 4417 additions and 2411 deletions

View File

@@ -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-6Integer类型
- 蓝球Integer类型
**数据映射**
- A列开奖期号drawId
- B列开奖日期drawDate
- C列红球1redBall1
- D列红球2redBall2
- E列红球3redBall3
- F列红球4redBall4
- G列红球5redBall5
- H列红球6redBall6
- I列蓝球blueBall
**字段映射**
- drawId开奖期号Long类型主键
- drawDate开奖日期Date类型支持yyyy-MM-dd、yyyy/MM/dd等格式
- redBall1-redBall6红球1-6Integer类型
- 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导入接口。

View File

@@ -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
View File

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

View 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;

View File

@@ -0,0 +1,9 @@
-- 为双色球开奖记录表添加特别规定期间标识字段
-- 用于标识该期是否处于特别规定期间(福运奖期间)
ALTER TABLE lottery_draws ADD COLUMN is_special_period TINYINT DEFAULT 0 COMMENT '是否处于特别规定期间0-否1-是';
-- 更新说明:
-- 特别规定启动条件:当奖池资金 >= 15亿元时开始执行
-- 特别规定停止条件:执行特别规定后,某期开奖后奖池资金 < 3亿元时停止
-- 在特别规定期间3个红球匹配可获得福运奖5元

View File

@@ -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`

View 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';

View 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;

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View 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);
}
}

View 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);
};
}
}

View File

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

View File

@@ -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虚拟用户

View File

@@ -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成功更新公告%sID%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成功删除公告%sID%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%sID%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());
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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) {

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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));
}
}

View File

@@ -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);

View File

@@ -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());
}
}
}

View File

@@ -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("");
}
// 无论是否有二次筛选,都要显示筛选步骤

View File

@@ -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("");
}
// 无论是否有二次筛选,都要显示筛选步骤

View File

@@ -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("");
}
// 无论是否有二次筛选,都要显示筛选步骤

View File

@@ -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-封禁

View File

@@ -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-封禁

View File

@@ -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-封禁

View File

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

View File

@@ -63,4 +63,9 @@ public class DltDrawRecord {
* 后区2
*/
private Integer backBall2;
/**
* 奖池(单位:亿元)
*/
private Double prizePool;
}

View File

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

View File

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

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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-封禁
*/

View File

@@ -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;
/**

View File

@@ -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;
/**

View 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;
}

View File

@@ -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-封禁

View 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> {
}

View File

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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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);
}

View File

@@ -30,7 +30,7 @@ public interface VipCodeService extends IService<VipCode> {
/**
* 获取一个可用的会员码
* @param vipExpireTime 会员有效月数1或12
* @param vipExpireTime 会员有效月数
* @param createdUserId 创建人ID
* @param createdUserName 创建人名称
* @return 可用的会员码如果没有则返回null

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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()
);
}
}

View File

@@ -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()
);
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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<>();

View File

@@ -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值
*/

View File

@@ -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);
}
/**
* 验证号码格式是否正确

View File

@@ -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);
}

View 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;
}
}

View File

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

View 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>

View File

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

View File

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

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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
View File

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

View File

@@ -1,164 +0,0 @@
2 签署 JWT
扣子的 JWT 生成方式及部分参数定义沿用业界统一流程规范,但 JWT 中 Header 和 Payload 部分由扣子平台自行定义。
在 JWTJSON Web Tokens的流程中通常使用私钥来签署signtoken。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
}

View File

@@ -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个月的会员时长。

View 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.0013.33** 新增
D1工作表大乐透开奖数据列结构
- A列开奖期号
- B列开奖日期
- C-G列前区球1-5
- H-I列后区球1-2
- **J列奖池金额单位亿元 8.5010.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导入逻辑更新
代码编译通过
**所有代码已完成并通过编译检查,可以部署使用。**

View 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.5010.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. 完成状态
数据库字段类型修改
实体类更新
工具类更新
导入逻辑更新
服务层更新
代码编译通过
**所有代码已完成并通过编译检查,可以部署使用。**

View File

@@ -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
View 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)

View 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/)