From ec597ffe2e74b473ac3f23b0af669f17171c8cc6 Mon Sep 17 00:00:00 2001
From: lihanqi <13868246742@163.com>
Date: Sat, 14 Feb 2026 12:15:01 +0800
Subject: [PATCH] =?UTF-8?q?update:=20=E6=8F=90=E4=BA=A4=E6=89=80=E6=9C=89?=
=?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=92=8C=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Excel导入使用说明.md | 484 ------------
VIP兑换记录查询API使用说明.md | 182 -----
pom.xml | 13 +-
sql/add_prize_pool_column.sql | 11 +
sql/add_special_rule_status.sql | 9 +
sql/ddl.sql | 37 +-
sql/update_prize_pool_to_decimal.sql | 14 +
sql/user_viptype_update.sql | 93 +++
src/main/java/com/xy/xyaicpzs/Main.java | 42 --
.../requset/AnnouncementAddRequest.java | 36 +
.../requset/AnnouncementQueryRequest.java | 54 ++
.../requset/AnnouncementUpdateRequest.java | 42 ++
.../com/xy/xyaicpzs/config/CosConfig.java | 63 ++
.../com/xy/xyaicpzs/config/JacksonConfig.java | 37 +
.../xyaicpzs/config/ObjectMapperConfig.java | 23 +-
.../xy/xyaicpzs/constant/UserConstant.java | 15 +
.../controller/AnnouncementController.java | 630 ++++++++++++++++
.../controller/BallAnalysisController.java | 97 +++
.../controller/DltBallAnalysisController.java | 39 +
.../controller/DltPredictController.java | 127 +++-
.../controller/FileUploadController.java | 210 ++++++
.../controller/PageViewController.java | 49 ++
.../xyaicpzs/controller/UserController.java | 40 +
.../controller/VipCodeController.java | 82 +-
.../xy/xyaicpzs/dlt/BackBallPredictor.java | 18 +-
.../xyaicpzs/dlt/FollowBackBallPredictor.java | 24 +-
.../xyaicpzs/dlt/FollowerBallPredictor.java | 16 +-
.../domain/dto/user/UserAddRequest.java | 20 +
.../domain/dto/user/UserQueryRequest.java | 20 +
.../domain/dto/user/UserUpdateRequest.java | 20 +
.../xyaicpzs/domain/entity/Announcement.java | 75 ++
.../xyaicpzs/domain/entity/DltDrawRecord.java | 5 +
.../domain/entity/DltPredictRecord.java | 6 +
.../xyaicpzs/domain/entity/LotteryDraws.java | 13 +
.../domain/entity/OperationHistory.java | 4 +
.../xyaicpzs/domain/entity/PredictRecord.java | 6 +
.../com/xy/xyaicpzs/domain/entity/User.java | 23 +
.../xy/xyaicpzs/domain/entity/VipCode.java | 5 +
.../domain/entity/VipExchangeRecord.java | 5 +
.../xy/xyaicpzs/domain/vo/AnnouncementVO.java | 74 ++
.../com/xy/xyaicpzs/domain/vo/UserVO.java | 20 +
.../xyaicpzs/mapper/AnnouncementMapper.java | 18 +
.../xyaicpzs/service/AnnouncementService.java | 13 +
.../service/DltDataAnalysisService.java | 8 +
.../service/DltDrawRecordService.java | 9 +
.../service/DltPredictRecordService.java | 21 +
.../xy/xyaicpzs/service/PageViewService.java | 92 +++
.../service/PredictRecordService.java | 21 +
.../xy/xyaicpzs/service/VipCodeService.java | 2 +-
.../service/impl/AnnouncementServiceImpl.java | 22 +
.../service/impl/DataAnalysisServiceImpl.java | 149 ++--
.../impl/DltDataAnalysisServiceImpl.java | 153 +++-
.../impl/DltDrawRecordServiceImpl.java | 12 +
.../impl/DltPredictRecordServiceImpl.java | 98 ++-
.../impl/PredictRecordServiceImpl.java | 93 ++-
.../xyaicpzs/service/impl/SmsServiceImpl.java | 126 ++--
.../service/impl/UserServiceImpl.java | 71 +-
.../service/impl/VipCodeServiceImpl.java | 28 +-
.../com/xy/xyaicpzs/util/DltDataImporter.java | 68 +-
.../xy/xyaicpzs/util/DltPrizeCalculator.java | 98 ++-
.../xy/xyaicpzs/util/ExcelDataImporter.java | 50 +-
.../xy/xyaicpzs/util/SsqPrizeCalculator.java | 117 +++
src/main/resources/application.yml | 37 +-
.../generator/mapper/AnnouncementMapper.xml | 27 +
.../generator/mapper/DltDrawRecordMapper.xml | 3 +-
.../generator/mapper/LotteryDrawsMapper.xml | 4 +-
.../java/com/xy/xyaicpzs/ApiResponseTest.java | 45 --
.../xy/xyaicpzs/DltPrizeCalculatorTest.java | 151 ----
.../xy/xyaicpzs/XyAiCpzsApplicationTests.java | 13 -
test.txt | 229 ------
userController.txt | 164 ----
会员码激活API使用说明.md | 355 ---------
双色球奖金规则更新说明.md | 195 +++++
大乐透奖金规则更新说明.md | 132 ++++
大乐透数据导入使用说明.md | 471 ------------
短信服务迁移说明.md | 238 ++++++
阿里云SMS接口集成文档.md | 712 ++++++++++++++++++
77 files changed, 4417 insertions(+), 2411 deletions(-)
delete mode 100644 Excel导入使用说明.md
delete mode 100644 VIP兑换记录查询API使用说明.md
create mode 100644 sql/add_prize_pool_column.sql
create mode 100644 sql/add_special_rule_status.sql
create mode 100644 sql/update_prize_pool_to_decimal.sql
create mode 100644 sql/user_viptype_update.sql
delete mode 100644 src/main/java/com/xy/xyaicpzs/Main.java
create mode 100644 src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementAddRequest.java
create mode 100644 src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementQueryRequest.java
create mode 100644 src/main/java/com/xy/xyaicpzs/common/requset/AnnouncementUpdateRequest.java
create mode 100644 src/main/java/com/xy/xyaicpzs/config/CosConfig.java
create mode 100644 src/main/java/com/xy/xyaicpzs/config/JacksonConfig.java
create mode 100644 src/main/java/com/xy/xyaicpzs/controller/AnnouncementController.java
create mode 100644 src/main/java/com/xy/xyaicpzs/controller/FileUploadController.java
create mode 100644 src/main/java/com/xy/xyaicpzs/controller/PageViewController.java
create mode 100644 src/main/java/com/xy/xyaicpzs/domain/entity/Announcement.java
create mode 100644 src/main/java/com/xy/xyaicpzs/domain/vo/AnnouncementVO.java
create mode 100644 src/main/java/com/xy/xyaicpzs/mapper/AnnouncementMapper.java
create mode 100644 src/main/java/com/xy/xyaicpzs/service/AnnouncementService.java
create mode 100644 src/main/java/com/xy/xyaicpzs/service/PageViewService.java
create mode 100644 src/main/java/com/xy/xyaicpzs/service/impl/AnnouncementServiceImpl.java
create mode 100644 src/main/java/com/xy/xyaicpzs/util/SsqPrizeCalculator.java
create mode 100644 src/main/resources/generator/mapper/AnnouncementMapper.xml
delete mode 100644 src/test/java/com/xy/xyaicpzs/ApiResponseTest.java
delete mode 100644 src/test/java/com/xy/xyaicpzs/DltPrizeCalculatorTest.java
delete mode 100644 src/test/java/com/xy/xyaicpzs/XyAiCpzsApplicationTests.java
delete mode 100644 test.txt
delete mode 100644 userController.txt
delete mode 100644 会员码激活API使用说明.md
create mode 100644 双色球奖金规则更新说明.md
create mode 100644 大乐透奖金规则更新说明.md
delete mode 100644 大乐透数据导入使用说明.md
create mode 100644 短信服务迁移说明.md
create mode 100644 阿里云SMS接口集成文档.md
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