diff --git a/Excel导入使用说明.md b/Excel导入使用说明.md
deleted file mode 100644
index d0f52af..0000000
--- a/Excel导入使用说明.md
+++ /dev/null
@@ -1,484 +0,0 @@
-# Excel数据导入功能使用说明
-
-## 功能概述
-
-本系统提供了Excel数据导入功能,可以将包含T1、T2、T3、T4、T5、T6、T7、T8、T10和T11工作表的Excel文件数据导入到十四个数据库表中:
-
-### 红球数据(T1 Sheet):
-- `history_all` - 红球全部历史数据表
-- `history_100` - 红球最近100期数据表
-- `history_top` - 红球历史数据排行表
-- `history_top_100` - 红球100期数据排行表
-
-### 蓝球数据(T2 Sheet):
-- `blue_history_all` - 蓝球全部历史数据表
-- `blue_history_100` - 蓝球最近100期数据表
-- `blue_history_top` - 蓝球历史数据排行表
-- `blue_history_top_100` - 蓝球100期数据排行表
-
-### 红球线系数数据(T3 Sheet):
-- `t3` - 红球组红球的线系数表
-
-### 蓝球组红球线系数数据(T4 Sheet):
-- `t4` - 蓝球组红球的线系数表
-
-### 蓝球组蓝球线系数数据(T5 Sheet):
-- `t5` - 蓝球组蓝球的线系数表
-
-### 红球组蓝球线系数数据(T6 Sheet):
-- `t6` - 红球组蓝球的线系数表
-
-### 红球组红球面系数数据(T7 Sheet):
-- `t7` - 红球组红球的面系数表
-
-### 红球组蓝球面系数数据(T8 Sheet):
-- `t8` - 红球组蓝球的面系数表
-
-### 彩票开奖信息数据(T10 Sheet):
-- `lottery_draws` - 彩票开奖信息表
-
-### 蓝球组红球面系数数据(T11 Sheet):
-- `t11` - 蓝球组红球的面系数表
-
-## Excel文件格式要求
-
-### 文件要求
-- **文件格式**:必须是`.xlsx`格式
-- **工作表名称**:必须包含名为`T1`、`T2`、`T3`、`T4`、`T5`、`T6`、`T7`、`T8`、`T10`和`T11`的工作表
-- **编码**:支持中文
-
-### 数据结构要求
-
-**T1工作表(红球数据)**的数据结构如下:
-
-| 列位置 | 列名 | 对应表 | 字段说明 |
-|--------|------|--------|----------|
-| A-G | 全部历史数据 | history_all | 球号、出现频次、出现频率%、平均隐现期次、最长隐现期次、最多连出期次、点系数 |
-| H-M | 最近100期数据 | history_100 | 出现频次、出现频率%、平均隐现期、当前隐现期、最多连出期次、点系数 |
-| N-P | 历史数据排行 | history_top | 排行、球号、点系数 |
-| Q-S | 100期数据排行 | history_top_100 | 排行、球号、点系数 |
-
-**T2工作表(蓝球数据)**的数据结构如下:
-
-| 列位置 | 列名 | 对应表 | 字段说明 |
-|--------|------|--------|----------|
-| A-G | 全部历史数据 | blue_history_all | 球号、出现频次、出现频率%、平均隐现期次、最长隐现期次、最多连出期次、点系数 |
-| H-M | 最近100期数据 | blue_history_100 | 出现频次、出现频率%、平均隐现期、当前隐现期、最多连出期次、点系数 |
-| N-P | 历史数据排行 | blue_history_top | 排行、球号、点系数 |
-| Q-S | 100期数据排行 | blue_history_top_100 | 排行、球号、点系数 |
-
-**T3工作表(红球线系数数据)**的数据结构如下:
-
-| 列位置 | 数据组织 | 对应表 | 字段说明 |
-|--------|----------|--------|----------|
-| C | 1号组线系数 | t3 | 主球=1,从球=1-33,线系数=C列 |
-| F | 2号组线系数 | t3 | 主球=2,从球=1-33,线系数=F列 |
-| I | 3号组线系数 | t3 | 主球=3,从球=1-33,线系数=I列 |
-| ... | 依此类推 | t3 | 每三列为一组,组号即主球号 |
-
-**说明**:T3工作表每三列为一组数据,每组有33行数据,从球号固定为1-33(行号),线系数在C、F、I、L...列。
-
-**T4工作表(蓝球组红球线系数数据)**的数据结构如下:
-
-| 列位置 | 数据组织 | 对应表 | 字段说明 |
-|--------|----------|--------|----------|
-| C | 蓝球1号线系数 | t4 | 主球=1,从球=1-33,线系数=C列 |
-| F | 蓝球2号线系数 | t4 | 主球=2,从球=1-33,线系数=F列 |
-| I | 蓝球3号线系数 | t4 | 主球=3,从球=1-33,线系数=I列 |
-| ... | 依此类推 | t4 | 每三列为一组,最多16组 |
-
-**说明**:T4工作表每三列为一组数据,每组有33行数据,蓝球号码1-16(主球),红球号码1-33(从球,行号),线系数在C、F、I、L...列。
-
-**T5工作表(蓝球组蓝球线系数数据)**的数据结构如下:
-
-| 列位置 | 数据组织 | 对应表 | 字段说明 |
-|--------|----------|--------|----------|
-| C | 蓝球1号线系数 | t5 | 主球=1,从球=1-16,线系数=C列 |
-| F | 蓝球2号线系数 | t5 | 主球=2,从球=1-16,线系数=F列 |
-| I | 蓝球3号线系数 | t5 | 主球=3,从球=1-16,线系数=I列 |
-| ... | 依此类推 | t5 | 每三列为一组,最多16组 |
-
-**说明**:T5工作表每三列为一组数据,每组有16行数据,蓝球号码1-16(主球和从球),线系数在C、F、I、L...列。
-
-**T6工作表(红球组蓝球线系数数据)**的数据结构如下:
-
-| 列位置 | 数据组织 | 对应表 | 字段说明 |
-|--------|----------|--------|----------|
-| C | 红球1号线系数 | t6 | 主球=1,从球=1-16,线系数=C列 |
-| F | 红球2号线系数 | t6 | 主球=2,从球=1-16,线系数=F列 |
-| I | 红球3号线系数 | t6 | 主球=3,从球=1-16,线系数=I列 |
-| ... | 依此类推 | t6 | 每三列为一组,最多33组 |
-
-**说明**:T6工作表每三列为一组数据,每组有16行数据,红球号码1-33(主球),蓝球号码1-16(从球,行号),线系数在C、F、I、L...列。
-
-**T7工作表(红球组红球面系数数据)**的数据结构如下:
-
-| 列位置 | 数据组织 | 对应表 | 字段说明 |
-|--------|----------|--------|----------|
-| C | 红球1号面系数 | t7 | 主球=1,从球=2-33,面系数=C列 |
-| F | 红球2号面系数 | t7 | 主球=2,从球=1,3-33,面系数=F列 |
-| I | 红球3号面系数 | t7 | 主球=3,从球=1-2,4-33,面系数=I列 |
-| ... | 依此类推 | t7 | 每三列为一组,最多33组 |
-
-**说明**:T7工作表每三列为一组数据,每组有33行数据,红球号码1-33(主球和从球),面系数在C、F、I、L...列。**特殊处理**:排除自己和自己组合的情况。
-
-**Excel数据结构**:
-- **第1行**:标题行(1号组、面系数、2号组、面系数...)
-- **第2行**:从球1号的数据(对应所有主球的面系数)
-- **第3行**:从球2号的数据(对应所有主球的面系数)
-- **...**
-- **第34行**:从球33号的数据(对应所有主球的面系数)
-
-**处理逻辑**:
-- **1号组(主球1)**:
- - 球号列:B列,面系数列:C列
- - 读取所有行,从B列获取从球号,从C列获取面系数
- - 排除对角线(主球1=从球1的情况)
-- **2号组(主球2)**:
- - 球号列:E列,面系数列:F列
- - 读取所有行,从E列获取从球号,从F列获取面系数
- - 排除对角线(主球2=从球2的情况)
-- **依此类推...**
-
-**关键改进**:
-- **动态读取球号**:从Excel的球号列(B、E、H...)读取实际球号,不依赖行号
-- **完整数据覆盖**:读取到Excel的最后一行,确保包含33号球数据
-- **不排除对角线**:读取到什么数据就插入什么数据,包括主球=从球的情况
-- **最终生成**:33×33=1089条记录
-
-### 具体列映射
-
-#### 红球表映射(T1 Sheet)
-
-**history_all 表 (列A-G)**
-- A列:球号 (ballNumber)
-- B列:出现频次 (frequencyCount)
-- C列:出现频率% (frequencyPercentage)
-- D列:平均隐现期次 (averageInterval)
-- E列:最长隐现期次 (maxHiddenInterval)
-- F列:最多连出期次 (maxConsecutiveCount)
-- G列:点系数 (pointCoefficient)
-
-**history_100 表 (列H-M,球号使用A列)**
-- A列:球号 (ballNumber)
-- H列:出现频次 (frequencyCount)
-- J列:平均隐现期 (averageInterval)
-- K列:当前隐现期 (nowInterval)
-- L列:最多连出期次 (maxConsecutiveCount)
-- M列:点系数 (pointCoefficient)
-
-**history_top 表 (列N-P)**
-- N列:排行 (no)
-- O列:球号 (ballNumber)
-- P列:点系数 (pointCoefficient)
-
-**history_top_100 表 (列Q-S)**
-- Q列:排行 (no)
-- R列:球号 (ballNumber)
-- S列:点系数 (pointCoefficient)
-
-#### 蓝球表映射(T2 Sheet)
-
-**blue_history_all 表 (列A-G)**
-- A列:球号 (ballNumber)
-- B列:出现频次 (frequencyCount)
-- C列:出现频率% (frequencyPercentage)
-- D列:平均隐现期次 (averageInterval)
-- E列:最长隐现期次 (maxHiddenInterval)
-- F列:最多连出期次 (maxConsecutiveCount)
-- G列:点系数 (pointCoefficient)
-
-**blue_history_100 表 (列H-M,球号使用A列)**
-- A列:球号 (ballNumber)
-- H列:出现频次 (frequencyCount)
-- J列:平均隐现期 (averageInterval)
-- K列:当前隐现期 (nowInterval)
-- L列:最多连出期次 (maxConsecutiveCount)
-- M列:点系数 (pointCoefficient)
-
-**blue_history_top 表 (列N-P)**
-- N列:排行 (no)
-- O列:球号 (ballNumber)
-- P列:点系数 (pointCoefficient)
-
-**blue_history_top_100 表 (列Q-S)**
-- Q列:排行 (no)
-- R列:球号 (ballNumber)
-- S列:点系数 (pointCoefficient)
-
-#### T3表映射(T3 Sheet)
-
-**t3 表(红球线系数数据)**
-- 数据组织:每三列为一组,每组33行数据
-- 红球号码范围:1-33(主球和从球都是)
-- 线系数位置:C、F、I、L...列
-
-**数据映射**:
-- C列:红球1号线系数(主球=1,从球=1-33,线系数=C列)
-- F列:红球2号线系数(主球=2,从球=1-33,线系数=F列)
-- I列:红球3号线系数(主球=3,从球=1-33,线系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:主红球号码(1-33)
-- slaveBallNumber:从红球号码(固定1-33,对应行号)
-- lineCoefficient:线系数(每组第三列,保留两位小数)
-
-#### T4表映射(T4 Sheet)
-
-**t4 表(蓝球组红球的线系数)**
-- 数据组织:每三列为一组,每组33行数据
-- 蓝球号码范围:1-16(主球)
-- 红球号码范围:1-33(从球,对应行号)
-- 线系数位置:C、F、I、L...列
-
-**数据映射**:
-- C列:蓝球1号线系数(主球=1,从球=1-33,线系数=C列)
-- F列:蓝球2号线系数(主球=2,从球=1-33,线系数=F列)
-- I列:蓝球3号线系数(主球=3,从球=1-33,线系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:蓝球号码(1-16)
-- slaveBallNumber:红球号码(固定1-33,对应行号)
-- lineCoefficient:线系数(每组第三列,保留两位小数)
-
-#### T5表映射(T5 Sheet)
-
-**t5 表(蓝球组蓝球的线系数)**
-- 数据组织:每三列为一组,每组16行数据
-- 蓝球号码范围:1-16(主球和从球都是)
-- 线系数位置:C、F、I、L...列
-
-**数据映射**:
-- C列:蓝球1号线系数(主球=1,从球=1-16,线系数=C列)
-- F列:蓝球2号线系数(主球=2,从球=1-16,线系数=F列)
-- I列:蓝球3号线系数(主球=3,从球=1-16,线系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:主蓝球号码(1-16)
-- slaveBallNumber:从蓝球号码(固定1-16,对应行号)
-- lineCoefficient:线系数(每组第三列,保留两位小数)
-
-#### T6表映射(T6 Sheet)
-
-**t6 表(红球组蓝球的线系数)**
-- 数据组织:每三列为一组,每组16行数据
-- 红球号码范围:1-33(主球)
-- 蓝球号码范围:1-16(从球)
-- 线系数位置:C、F、I、L...列
-
-**数据映射**:
-- C列:红球1号线系数(主球=1,从球=1-16,线系数=C列)
-- F列:红球2号线系数(主球=2,从球=1-16,线系数=F列)
-- I列:红球3号线系数(主球=3,从球=1-16,线系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:主红球号码(1-33)
-- slaveBallNumber:从蓝球号码(固定1-16,对应行号)
-- lineCoefficient:线系数(每组第三列,保留两位小数)
-
-#### T7表映射(T7 Sheet)
-
-**t7 表(红球组红球的面系数)**
-- 数据组织:每三列为一组,每组33行数据
-- 红球号码范围:1-33(主球和从球都是)
-- 面系数位置:C、F、I、L...列
-- 特殊处理:读取到什么数据就插入什么数据,包括对角线
-
-**数据映射**:
-- C列:红球1号面系数(主球=1,从球=1-33,面系数=C列)
-- F列:红球2号面系数(主球=2,从球=1-33,面系数=F列)
-- I列:红球3号面系数(主球=3,从球=1-33,面系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:主红球号码(1-33)
-- slaveBallNumber:从红球号码(1-33,包括与主球相同的号码)
-- faceCoefficient:面系数(每组第三列,保留两位小数)
-
-#### T8表映射(T8 Sheet)
-
-**t8 表(红球组蓝球的面系数)**
-- 数据组织:每三列为一组,每组16行数据
-- 红球号码范围:1-33(主球)
-- 蓝球号码范围:1-16(从球)
-- 面系数位置:C、F、I、L...列
-
-**数据映射**:
-- C列:红球1号面系数(主球=1,从球=1-16,面系数=C列)
-- F列:红球2号面系数(主球=2,从球=1-16,面系数=F列)
-- I列:红球3号面系数(主球=3,从球=1-16,面系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:主红球号码(1-33)
-- slaveBallNumber:从蓝球号码(固定1-16,对应行号)
-- faceCoefficient:面系数(每组第三列,保留两位小数)
-
-#### T10表映射(T10 Sheet)
-
-**lottery_draws 表(彩票开奖信息)**
-- 数据组织:标准表格结构,每行一条开奖记录
-- 开奖期号:Long类型主键
-- 开奖日期:Date类型,支持多种格式
-- 红球1-6:Integer类型
-- 蓝球:Integer类型
-
-**数据映射**:
-- A列:开奖期号(drawId)
-- B列:开奖日期(drawDate)
-- C列:红球1(redBall1)
-- D列:红球2(redBall2)
-- E列:红球3(redBall3)
-- F列:红球4(redBall4)
-- G列:红球5(redBall5)
-- H列:红球6(redBall6)
-- I列:蓝球(blueBall)
-
-**字段映射**:
-- drawId:开奖期号(Long类型,主键)
-- drawDate:开奖日期(Date类型,支持yyyy-MM-dd、yyyy/MM/dd等格式)
-- redBall1-redBall6:红球1-6(Integer类型)
-- blueBall:蓝球(Integer类型)
-
-**数据特性**:
-- 所有字段均为必填项
-- 开奖期号为主键,不能重复
-- 日期格式自动识别和转换
-- 数据完整性验证
-
-#### T11表映射(T11 Sheet)
-
-**t11 表(蓝球组红球的面系数)**
-- 数据组织:每三列为一组,每组33行数据
-- 蓝球号码范围:1-16(主球)
-- 红球号码范围:1-33(从球)
-- 面系数位置:C、F、I、L...列
-
-**数据映射**:
-- C列:蓝球1号面系数(主球=1,从球=1-33,面系数=C列)
-- F列:蓝球2号面系数(主球=2,从球=1-33,面系数=F列)
-- I列:蓝球3号面系数(主球=3,从球=1-33,面系数=I列)
-- 依此类推...
-
-**字段映射**:
-- masterBallNumber:主蓝球号码(1-16)
-- slaveBallNumber:从红球号码(固定1-33,对应行号)
-- faceCoefficient:面系数(每组第三列,保留两位小数)
-
-## 使用方法
-
-### 1. API接口方式
-
-#### 1.1 文件上传导入
-```http
-POST /api/excel/upload
-Content-Type: multipart/form-data
-
-参数:
-- file: Excel文件 (.xlsx格式)
-```
-
-#### 1.2 文件路径导入
-```http
-POST /api/excel/import-by-path
-Content-Type: application/x-www-form-urlencoded
-
-参数:
-- filePath: Excel文件的完整路径 (例如: D:/data/kaifa1.xlsx)
-```
-
-#### 1.3 获取导入说明
-```http
-GET /api/excel/import-info
-```
-
-### 2. 程序调用方式
-
-```java
-@Autowired
-private ExcelImportService excelImportService;
-
-// 方式1:通过文件路径导入
-String result = excelImportService.importExcelFileByPath("D:/data/kaifa1.xlsx");
-
-// 方式2:通过MultipartFile导入
-String result = excelImportService.importExcelFile(multipartFile);
-```
-
-### 3. 测试方式
-
-运行测试类:
-```java
-// 运行 ExcelImportTest 类中的测试方法
-@Test
-public void testImportExcelByPath() {
- // 修改文件路径为实际路径
- String filePath = "D:/code/xy-ai-cpzs/kaifa1.xlsx";
- String result = excelImportService.importExcelFileByPath(filePath);
- System.out.println("导入结果:" + result);
-}
-```
-
-## 注意事项
-
-1. **数据清空**:每次导入前会清空现有数据,请谨慎操作
-2. **数据验证**:系统会验证数据的完整性,球号为空的记录会被跳过
-3. **错误处理**:导入过程中如有错误会回滚操作并返回错误信息
-4. **日志记录**:导入过程会记录详细日志,便于问题排查
-5. **文件大小**:建议文件大小不超过10MB
-6. **并发限制**:避免同时进行多个导入操作
-
-## 常见问题
-
-### Q1: 提示"未找到T1/T2/T3/T4/T5/T6/T7/T8/T10/T11工作表"
-**A**: 请检查Excel文件是否包含名为"T1"(红球数据)、"T2"(蓝球数据)、"T3"(红球线系数)、"T4"(蓝球组红球线系数)、"T5"(蓝球组蓝球线系数)、"T6"(红球组蓝球线系数)、"T7"(红球组红球面系数)、"T8"(红球组蓝球面系数)、"T10"(彩票开奖信息)和"T11"(蓝球组红球面系数)的工作表,注意区分大小写。如果缺少某个工作表,系统会跳过该部分数据并显示警告。
-
-### Q2: 导入后数据不完整
-**A**: 请检查Excel数据格式是否正确,确保数值类型的列包含有效数字。
-
-### Q3: 导入失败提示文件格式错误
-**A**: 请确保文件是.xlsx格式,不支持.xls格式。
-
-### Q4: 如何查看导入日志
-**A**: 导入过程中的日志会输出到控制台,可以通过查看应用日志了解详细信息。
-
-### Q5: 报错"Cannot get a NUMERIC value from a STRING cell"
-**A**: 这个错误已经修复。系统现在能够自动处理字符串类型的数值单元格,会尝试将字符串转换为数值。
-
-### Q6: Excel中有公式单元格怎么办
-**A**: 系统支持公式单元格,会自动读取公式计算后的结果值。
-
-### Q7: 单元格为空怎么处理
-**A**: 空白单元格会被自动跳过,对应的字段值会设为null。
-
-## 数据类型支持
-
-系统支持以下类型的Excel单元格:
-- **数值类型** - 直接读取数值
-- **字符串类型** - 尝试转换为数值(如果包含数字)
-- **公式类型** - 读取公式计算结果
-- **空白类型** - 设为null
-- **其他类型** - 会记录警告日志并设为null
-
-## 数据精度处理
-
-- **浮点数字段**:自动保留两位小数(使用四舍五入)
-- **整数字段**:直接转换为整数(去除小数部分)
-- **特殊值处理**:NaN和无穷大值会被设为null
-
-示例:
-- `123.456789` → `123.46`
-- `12.1` → `12.10`
-- `5` → `5.00`
-
-## Swagger文档
-
-启动应用后,可以通过以下地址访问API文档:
-- Swagger UI: http://localhost:8123/api/swagger-ui.html
-- Knife4j UI: http://localhost:8123/api/doc.html
-
-在文档中可以直接测试Excel导入接口。
\ No newline at end of file
diff --git a/VIP兑换记录查询API使用说明.md b/VIP兑换记录查询API使用说明.md
deleted file mode 100644
index 2d03d6c..0000000
--- a/VIP兑换记录查询API使用说明.md
+++ /dev/null
@@ -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. 分页查询采用内存分页,大数据量时建议使用数据库分页优化
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 4bf350a..7f88913 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,12 @@
3.1.1281
+
+
+ com.qcloud
+ cos_api
+ 5.6.155
+
com.alibaba.nls
@@ -83,7 +89,12 @@
commons-lang3
3.12.0
-
+
+
+ com.tencentcloudapi
+ tencentcloud-sdk-java
+ 3.1.1412
+
org.springframework.boot
diff --git a/sql/add_prize_pool_column.sql b/sql/add_prize_pool_column.sql
new file mode 100644
index 0000000..4d04be3
--- /dev/null
+++ b/sql/add_prize_pool_column.sql
@@ -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;
diff --git a/sql/add_special_rule_status.sql b/sql/add_special_rule_status.sql
new file mode 100644
index 0000000..b44bb18
--- /dev/null
+++ b/sql/add_special_rule_status.sql
@@ -0,0 +1,9 @@
+-- 为双色球开奖记录表添加特别规定期间标识字段
+-- 用于标识该期是否处于特别规定期间(福运奖期间)
+
+ALTER TABLE lottery_draws ADD COLUMN is_special_period TINYINT DEFAULT 0 COMMENT '是否处于特别规定期间:0-否,1-是';
+
+-- 更新说明:
+-- 特别规定启动条件:当奖池资金 >= 15亿元时开始执行
+-- 特别规定停止条件:执行特别规定后,某期开奖后奖池资金 < 3亿元时停止
+-- 在特别规定期间,3个红球匹配可获得福运奖(5元)
diff --git a/sql/ddl.sql b/sql/ddl.sql
index 91071d1..db189f8 100644
--- a/sql/ddl.sql
+++ b/sql/ddl.sql
@@ -166,7 +166,8 @@ CREATE TABLE IF NOT EXISTS `lottery_draws` (
`redBall4` INT NOT NULL COMMENT '红4',
`redBall5` INT NOT NULL COMMENT '红5',
`redBall6` INT NOT NULL COMMENT '红6',
- `blueBall` INT NOT NULL COMMENT '蓝球'
+ `blueBall` INT NOT NULL COMMENT '蓝球',
+ `prizePool` BIGINT NULL COMMENT '奖池'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT = '彩票开奖信息表';
@@ -183,6 +184,10 @@ create table if not exists user
userPassword varchar(512) not null comment '密码',
isVip int default 0 not null comment '是否会员:0-非会员,1-会员',
vipExpire datetime null comment '会员到期时间',
+ vipType varchar(50) default '体验会员' null comment '套餐类别(体验会员/月度会员/年度会员)',
+ location varchar(100) null comment '所在省市',
+ preference varchar(100) null comment '彩票偏好(双色球/大乐透等)',
+ channel varchar(100) null comment '获客渠道',
# vipNum int not null comment '会员编号',
`status` tinyint DEFAULT '0' COMMENT '状态0正常1不正常',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
@@ -273,7 +278,8 @@ CREATE TABLE IF NOT EXISTS `dlt_draw_record` (
`frontBall4` INT NOT NULL COMMENT '前区4',
`frontBall5` INT NOT NULL COMMENT '前区5',
`backBall1` INT NOT NULL COMMENT '后区1',
- `backBall2` INT NOT NULL COMMENT '后区2'
+ `backBall2` INT NOT NULL COMMENT '后区2',
+ `prizePool` BIGINT NULL COMMENT '奖池'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透开奖信息表';
@@ -438,3 +444,30 @@ CREATE TABLE IF NOT EXISTS `dlt_predict_record` (
`bonus` BIGINT default 0 NOT NULL COMMENT '奖金'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透推测记录表';
+
+
+-- 公告管理表
+CREATE TABLE IF NOT EXISTS `announcement` (
+ `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY,
+ `title` VARCHAR(200) NOT NULL COMMENT '公告标题',
+ `content` TEXT NOT NULL COMMENT '公告详情内容',
+ `publisherId` BIGINT NOT NULL COMMENT '发布人ID',
+ `publisherName` VARCHAR(256) NOT NULL COMMENT '发布人名称',
+ `status` TINYINT DEFAULT 1 NOT NULL COMMENT '公告状态:0-草稿,1-已发布,2-已下架',
+ `priority` TINYINT DEFAULT 0 NOT NULL COMMENT '优先级:0-普通,1-置顶',
+ `viewCount` INT DEFAULT 0 NOT NULL COMMENT '浏览次数',
+ `createTime` DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
+ `updateTime` DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `isDelete` TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除:0-未删除,1-已删除'
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4 COMMENT = '公告管理表';
+
+-- 为已存在的user表添加新字段
+ALTER TABLE `user`
+ ADD COLUMN `vipType` VARCHAR(50) DEFAULT '体验会员' NULL COMMENT '套餐类别(体验会员/月度会员/年度会员)' AFTER `vipExpire`,
+ ADD COLUMN `location` VARCHAR(100) NULL COMMENT '所在省市' AFTER `vipType`,
+ ADD COLUMN `preference` VARCHAR(100) NULL COMMENT '彩票偏好(双色球/大乐透等)' AFTER `location`,
+ ADD COLUMN `channel` VARCHAR(100) NULL COMMENT '获客渠道' AFTER `preference`;
+
+ALTER TABLE `user`
+ ADD COLUMN `vipType` VARCHAR(50) DEFAULT '体验会员' NULL COMMENT '套餐类别(体验会员/月度会员/年度会员)' AFTER `vipExpire`
diff --git a/sql/update_prize_pool_to_decimal.sql b/sql/update_prize_pool_to_decimal.sql
new file mode 100644
index 0000000..a78b9f1
--- /dev/null
+++ b/sql/update_prize_pool_to_decimal.sql
@@ -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';
diff --git a/sql/user_viptype_update.sql b/sql/user_viptype_update.sql
new file mode 100644
index 0000000..2954fcf
--- /dev/null
+++ b/sql/user_viptype_update.sql
@@ -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;
+
diff --git a/src/main/java/com/xy/xyaicpzs/Main.java b/src/main/java/com/xy/xyaicpzs/Main.java
deleted file mode 100644
index 5cccccd..0000000
--- a/src/main/java/com/xy/xyaicpzs/Main.java
+++ /dev/null
@@ -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 result = application.streamCall(param);
- result.blockingForEach(data -> {
- System.out.printf("%s\n",
- data.getOutput().getText());
-// System.out.println("session_id: " + data.getOutput().getSessionId());
- });
- }
- public static void main(String[] args) {
- try {
- streamCall();
- } catch (ApiException | NoApiKeyException | InputRequiredException e) {
- System.out.printf("Exception: %s", e.getMessage());
- System.out.println("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code");
- }
- System.exit(0);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementAddRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementAddRequest.java
new file mode 100644
index 0000000..05bdbe0
--- /dev/null
+++ b/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementAddRequest.java
@@ -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;
+}
diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementQueryRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementQueryRequest.java
new file mode 100644
index 0000000..e8e5b21
--- /dev/null
+++ b/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementQueryRequest.java
@@ -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;
+}
diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementUpdateRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementUpdateRequest.java
new file mode 100644
index 0000000..141f6ac
--- /dev/null
+++ b/src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementUpdateRequest.java
@@ -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;
+}
diff --git a/src/main/java/com/xy/xyaicpzs/config/CosConfig.java b/src/main/java/com/xy/xyaicpzs/config/CosConfig.java
new file mode 100644
index 0000000..48563ee
--- /dev/null
+++ b/src/main/java/com/xy/xyaicpzs/config/CosConfig.java
@@ -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);
+ }
+}
diff --git a/src/main/java/com/xy/xyaicpzs/config/JacksonConfig.java b/src/main/java/com/xy/xyaicpzs/config/JacksonConfig.java
new file mode 100644
index 0000000..ee8ce11
--- /dev/null
+++ b/src/main/java/com/xy/xyaicpzs/config/JacksonConfig.java
@@ -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);
+ };
+ }
+}
+
diff --git a/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java b/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java
index a4242f3..0859ebe 100644
--- a/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java
+++ b/src/main/java/com/xy/xyaicpzs/config/ObjectMapperConfig.java
@@ -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;
}
}
\ No newline at end of file
diff --git a/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java b/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java
index 9db1298..1b18a1f 100644
--- a/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java
+++ b/src/main/java/com/xy/xyaicpzs/constant/UserConstant.java
@@ -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(虚拟用户)
diff --git a/src/main/java/com/xy/xyaicpzs/controller/AnnouncementController.java b/src/main/java/com/xy/xyaicpzs/controller/AnnouncementController.java
new file mode 100644
index 0000000..f75ac65
--- /dev/null
+++ b/src/main/java/com/xy/xyaicpzs/controller/AnnouncementController.java
@@ -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 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 updateAnnouncement(@RequestBody AnnouncementUpdateRequest request,
+ HttpServletRequest httpServletRequest) {
+ // 权限校验
+ User loginUser = userService.getLoginUser(httpServletRequest);
+ if (!userService.isAdmin(loginUser)) {
+ return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "无权限");
+ }
+
+ Long userId = loginUser.getId();
+ String userName = loginUser.getUserName();
+
+ // 参数校验
+ if (request == null || request.getId() == null) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
+ }
+ if (request.getTitle() != null && request.getTitle().length() > 200) {
+ throw new BusinessException(ErrorCode.PARAMS_ERROR, "公告标题不能超过200个字符");
+ }
+
+ try {
+ // 检查公告是否存在
+ Announcement existingAnnouncement = announcementService.getById(request.getId());
+ if (existingAnnouncement == null) {
+ throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "公告不存在");
+ }
+
+ Announcement announcement = new Announcement();
+ BeanUtils.copyProperties(request, announcement);
+ boolean result = announcementService.updateById(announcement);
+
+ if (!result) {
+ throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告失败");
+ }
+
+ // 记录操作历史 - 成功
+ String resultMessage = String.format("%s成功更新公告:%s(ID:%d)",
+ userName, existingAnnouncement.getTitle(), request.getId());
+ operationHistoryService.recordOperation(userId, "更新公告", 1, "成功", resultMessage);
+
+ log.info("管理员{}更新公告成功,公告ID:{}", userName, request.getId());
+ return ResultUtils.success(true);
+
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("更新公告失败:{}", e.getMessage(), e);
+
+ // 记录操作历史 - 失败
+ String resultMessage = String.format("%s更新公告失败:%s,公告ID:%d",
+ userName, e.getMessage(), request.getId());
+ operationHistoryService.recordOperation(userId, "更新公告", 1, "失败", resultMessage);
+
+ throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 删除公告(逻辑删除)
+ *
+ * @param id 公告ID
+ * @param httpServletRequest Http请求
+ * @return 删除结果
+ */
+ @DeleteMapping("/delete/{id}")
+ @Operation(summary = "删除公告", description = "管理员删除公告(逻辑删除)")
+ public ApiResponse 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 updateWrapper = new UpdateWrapper<>();
+ updateWrapper.eq("id", id);
+ updateWrapper.set("isDelete", 1);
+ boolean result = announcementService.update(updateWrapper);
+
+ if (!result) {
+ throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除公告失败");
+ }
+
+ // 记录操作历史 - 成功
+ String resultMessage = String.format("%s成功删除公告:%s(ID:%d)",
+ userName, announcement.getTitle(), id);
+ operationHistoryService.recordOperation(userId, "删除公告", 1, "成功", resultMessage);
+
+ log.info("管理员{}删除公告成功,公告ID:{}", userName, id);
+ return ResultUtils.success(true);
+
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("删除公告失败:{}", e.getMessage(), e);
+
+ // 记录操作历史 - 失败
+ String resultMessage = String.format("%s删除公告失败:%s,公告ID:%d",
+ userName, e.getMessage(), id);
+ operationHistoryService.recordOperation(userId, "删除公告", 1, "失败", resultMessage);
+
+ throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除公告失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 分页获取公告列表
+ *
+ * @param queryRequest 查询请求
+ * @param httpServletRequest Http请求
+ * @return 分页公告列表
+ */
+ @GetMapping("/list/page")
+ @Operation(summary = "分页获取公告列表", description = "分页获取公告列表,支持多条件查询")
+ public ApiResponse> 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 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 announcementPage = announcementService.page(new Page<>(current, pageSize), queryWrapper);
+
+ // 转换为VO对象
+ List announcementVOList = announcementPage.getRecords().stream().map(announcement -> {
+ AnnouncementVO announcementVO = new AnnouncementVO();
+ BeanUtils.copyProperties(announcement, announcementVO);
+ return announcementVO;
+ }).collect(Collectors.toList());
+
+ // 创建VO分页对象
+ Page 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 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 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 updateWrapper = new UpdateWrapper<>();
+ updateWrapper.eq("id", id);
+ updateWrapper.set("status", status);
+ boolean result = announcementService.update(updateWrapper);
+
+ if (!result) {
+ throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告状态失败");
+ }
+
+ // 记录操作历史 - 成功
+ String statusText = status == 0 ? "草稿" : (status == 1 ? "已发布" : "已下架");
+ String resultMessage = String.format("%s成功将公告状态更新为%s:%s(ID:%d)",
+ userName, statusText, announcement.getTitle(), id);
+ operationHistoryService.recordOperation(userId, "更新公告状态", 1, "成功", resultMessage);
+
+ log.info("管理员{}更新公告状态成功,公告ID:{},新状态:{}", userName, id, statusText);
+ return ResultUtils.success(true);
+
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("更新公告状态失败:{}", e.getMessage(), e);
+
+ // 记录操作历史 - 失败
+ String resultMessage = String.format("%s更新公告状态失败:%s,公告ID:%d",
+ userName, e.getMessage(), id);
+ operationHistoryService.recordOperation(userId, "更新公告状态", 1, "失败", resultMessage);
+
+ throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新公告状态失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 增加公告浏览次数
+ *
+ * @param id 公告ID
+ * @return 更新结果
+ */
+ @PutMapping("/view/{id}")
+ @Operation(summary = "增加浏览次数", description = "用户查看公告时增加浏览次数")
+ public ApiResponse 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 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