Bladeren bron

first commit

虞江增 2 weken geleden
commit
92fa9e11b5
100 gewijzigde bestanden met toevoegingen van 15775 en 0 verwijderingen
  1. 33 0
      .gitignore
  2. 421 0
      API_DOCUMENTATION.md
  3. 180 0
      README.md
  4. 166 0
      Spring Boot升级说明.md
  5. 333 0
      VIP充值记录功能说明.md
  6. 27 0
      book-admin/.gitignore
  7. 251 0
      book-admin/README.md
  8. 29 0
      book-admin/package.json
  9. 72 0
      book-admin/pom.xml
  10. 14 0
      book-admin/src/main/java/com/yu/bookadmin/BookAdminApplication.java
  11. 36 0
      book-admin/src/main/resources/static/index.html
  12. 763 0
      book-admin/src/main/resources/static/pages/audiobooks.html
  13. 684 0
      book-admin/src/main/resources/static/pages/banners.html
  14. 677 0
      book-admin/src/main/resources/static/pages/books.html
  15. 199 0
      book-admin/src/main/resources/static/pages/login.html
  16. 644 0
      book-admin/src/main/resources/static/pages/rankings.html
  17. 184 0
      book-admin/src/main/resources/static/pages/test-backend.html
  18. 192 0
      book-admin/src/main/resources/static/pages/test-connection.html
  19. 721 0
      book-admin/src/main/resources/static/pages/users.html
  20. 474 0
      book-admin/src/main/resources/static/utils/api.js
  21. 13 0
      book-admin/src/test/java/com/yu/bookadmin/BookAdminApplicationTests.java
  22. 69 0
      book-admin/一键修复登录问题.bat
  23. 67 0
      book-admin/一键修复登录问题.sh
  24. 289 0
      book-admin/使用说明.md
  25. 265 0
      book-admin/启动指南.md
  26. 427 0
      book-admin/完整功能说明.md
  27. 279 0
      book-admin/实现总结.md
  28. 128 0
      book-admin/快速修复登录问题.md
  29. 106 0
      book-admin/快速启动.md
  30. 203 0
      book-admin/快速启动指南.md
  31. 233 0
      book-admin/登录问题排查指南.md
  32. 208 0
      book-admin/登录问题解决方案.md
  33. 196 0
      book-admin/项目结构说明.md
  34. 103 0
      pom.xml
  35. 15 0
      src/main/java/com/yu/book/BookApplication.java
  36. 45 0
      src/main/java/com/yu/book/admin/config/AdminWebConfig.java
  37. 185 0
      src/main/java/com/yu/book/admin/controller/AdminAudiobookController.java
  38. 176 0
      src/main/java/com/yu/book/admin/controller/AdminBannerController.java
  39. 204 0
      src/main/java/com/yu/book/admin/controller/AdminBookController.java
  40. 194 0
      src/main/java/com/yu/book/admin/controller/AdminChapterController.java
  41. 68 0
      src/main/java/com/yu/book/admin/controller/AdminController.java
  42. 148 0
      src/main/java/com/yu/book/admin/controller/AdminRankingController.java
  43. 217 0
      src/main/java/com/yu/book/admin/controller/AdminUserController.java
  44. 32 0
      src/main/java/com/yu/book/admin/dto/AddBookToRankingDTO.java
  45. 42 0
      src/main/java/com/yu/book/admin/dto/AdminLoginDTO.java
  46. 87 0
      src/main/java/com/yu/book/admin/dto/AdminUserDTO.java
  47. 98 0
      src/main/java/com/yu/book/admin/dto/AudiobookManageDTO.java
  48. 99 0
      src/main/java/com/yu/book/admin/dto/BookManageDTO.java
  49. 53 0
      src/main/java/com/yu/book/admin/dto/RankingSortDTO.java
  50. 21 0
      src/main/java/com/yu/book/admin/dto/ResetPasswordDTO.java
  51. 67 0
      src/main/java/com/yu/book/admin/entity/AdminOperationLog.java
  52. 77 0
      src/main/java/com/yu/book/admin/entity/Banner.java
  53. 57 0
      src/main/java/com/yu/book/admin/entity/BannerGroup.java
  54. 33 0
      src/main/java/com/yu/book/admin/entity/RankingGroup.java
  55. 26 0
      src/main/java/com/yu/book/admin/entity/RankingItem.java
  56. 76 0
      src/main/java/com/yu/book/admin/interceptor/AdminInterceptor.java
  57. 23 0
      src/main/java/com/yu/book/admin/mapper/AdminOperationLogMapper.java
  58. 86 0
      src/main/java/com/yu/book/admin/mapper/BannerMapper.java
  59. 46 0
      src/main/java/com/yu/book/admin/mapper/RankingMapper.java
  60. 235 0
      src/main/java/com/yu/book/admin/service/AdminAudiobookService.java
  61. 158 0
      src/main/java/com/yu/book/admin/service/AdminBannerService.java
  62. 243 0
      src/main/java/com/yu/book/admin/service/AdminBookService.java
  63. 149 0
      src/main/java/com/yu/book/admin/service/AdminChapterService.java
  64. 215 0
      src/main/java/com/yu/book/admin/service/AdminRankingService.java
  65. 80 0
      src/main/java/com/yu/book/admin/service/AdminService.java
  66. 257 0
      src/main/java/com/yu/book/admin/service/AdminUserService.java
  67. 239 0
      src/main/java/com/yu/book/admin/util/AdminAccountUtil.java
  68. 74 0
      src/main/java/com/yu/book/admin/vo/AdminAudiobookVO.java
  69. 130 0
      src/main/java/com/yu/book/admin/vo/AdminBookVO.java
  70. 57 0
      src/main/java/com/yu/book/admin/vo/AdminLoginVO.java
  71. 92 0
      src/main/java/com/yu/book/admin/vo/AdminUserVO.java
  72. 90 0
      src/main/java/com/yu/book/admin/vo/RankingItemVO.java
  73. 75 0
      src/main/java/com/yu/book/common/PageResult.java
  74. 107 0
      src/main/java/com/yu/book/common/Result.java
  75. 50 0
      src/main/java/com/yu/book/config/ValidationConfig.java
  76. 169 0
      src/main/java/com/yu/book/controller/AudiobookBookshelfController.java
  77. 228 0
      src/main/java/com/yu/book/controller/AudiobookController.java
  78. 51 0
      src/main/java/com/yu/book/controller/BannerController.java
  79. 73 0
      src/main/java/com/yu/book/controller/BookChapterController.java
  80. 297 0
      src/main/java/com/yu/book/controller/BookController.java
  81. 148 0
      src/main/java/com/yu/book/controller/BookshelfController.java
  82. 76 0
      src/main/java/com/yu/book/controller/BrowsingHistoryController.java
  83. 73 0
      src/main/java/com/yu/book/controller/CategoryController.java
  84. 83 0
      src/main/java/com/yu/book/controller/CommentController.java
  85. 102 0
      src/main/java/com/yu/book/controller/FeedbackController.java
  86. 174 0
      src/main/java/com/yu/book/controller/MessageController.java
  87. 59 0
      src/main/java/com/yu/book/controller/RankingController.java
  88. 78 0
      src/main/java/com/yu/book/controller/SearchHistoryController.java
  89. 123 0
      src/main/java/com/yu/book/controller/UserController.java
  90. 195 0
      src/main/java/com/yu/book/controller/VipController.java
  91. 67 0
      src/main/java/com/yu/book/demos/web/BasicController.java
  92. 44 0
      src/main/java/com/yu/book/demos/web/PathVariableController.java
  93. 43 0
      src/main/java/com/yu/book/demos/web/User.java
  94. 136 0
      src/main/java/com/yu/book/domain/Audiobook.java
  95. 91 0
      src/main/java/com/yu/book/domain/AudiobookChapter.java
  96. 104 0
      src/main/java/com/yu/book/domain/Book.java
  97. 86 0
      src/main/java/com/yu/book/domain/BookChapter.java
  98. 60 0
      src/main/java/com/yu/book/domain/BookComment.java
  99. 50 0
      src/main/java/com/yu/book/domain/BrowsingHistory.java
  100. 53 0
      src/main/java/com/yu/book/domain/Category.java

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

+ 421 - 0
API_DOCUMENTATION.md

@@ -0,0 +1,421 @@
+# API 接口文档
+
+## 基础信息
+
+- **基础URL**: `http://localhost:8080`
+- **数据格式**: JSON
+- **字符编码**: UTF-8
+
+## 统一响应格式
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {}
+}
+```
+
+### 状态码说明
+
+- `200`: 操作成功
+- `400`: 参数验证失败
+- `500`: 服务器错误
+
+---
+
+## 1. 用户相关接口
+
+### 1.1 用户注册
+
+**POST** `/api/user/register`
+
+**请求体:**
+
+```json
+{
+  "username": "testuser",
+  "password": "123456",
+  "nickname": "测试用户",
+  "phone": "13800138000",
+  "email": "test@example.com"
+}
+```
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "注册成功",
+  "data": {
+    "id": 1,
+    "username": "testuser",
+    "nickname": "测试用户",
+    "phone": "13800138000",
+    "email": "test@example.com",
+    "isVip": false,
+    "status": 1
+  }
+}
+```
+
+### 1.2 用户登录
+
+**POST** `/api/user/login`
+
+**请求体:**
+
+```json
+{
+  "username": "testuser",
+  "password": "123456"
+}
+```
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "登录成功",
+  "data": {
+    "user": {
+      "id": 1,
+      "username": "testuser",
+      "nickname": "测试用户",
+      "isVip": false,
+      "status": 1
+    },
+    "token": "mock_token_1"
+  }
+}
+```
+
+### 1.3 查询用户信息
+
+**GET** `/api/user/{id}`
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {
+    "id": 1,
+    "username": "testuser",
+    "nickname": "测试用户",
+    "avatar": null,
+    "phone": "13800138000",
+    "email": "test@example.com",
+    "isVip": false,
+    "status": 1
+  }
+}
+```
+
+---
+
+## 2. 书籍相关接口
+
+### 2.1 分页查询书籍
+
+**GET** `/api/book/list`
+
+**请求参数:**
+
+| 参数 | 类型 | 必填 | 说明 | 默认值 |
+|------|------|------|------|--------|
+| page | Integer | 否 | 页码 | 1 |
+| size | Integer | 否 | 每页数量 | 10 |
+| keyword | String | 否 | 搜索关键词(书名、作者、简介) | - |
+| categoryId | Integer | 否 | 分类ID | - |
+| status | Integer | 否 | 状态(0-下架,1-上架) | - |
+| isVip | Boolean | 否 | 是否VIP专享 | - |
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {
+    "list": [
+      {
+        "id": 1,
+        "title": "西游记",
+        "author": "(明) 吴承恩",
+        "cover": "https://picsum.photos/seed/book1/200/300",
+        "brief": "《西游记》是中国古代第一部神魔题材的长篇章回小说...",
+        "price": 0.00,
+        "isFree": true,
+        "isVip": false,
+        "categoryId": 1,
+        "status": 1,
+        "viewCount": 0,
+        "likeCount": 0,
+        "readCount": 0
+      }
+    ],
+    "total": 100,
+    "page": 1,
+    "size": 10,
+    "totalPages": 10
+  }
+}
+```
+
+### 2.2 根据ID查询书籍详情
+
+**GET** `/api/book/{id}`
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {
+    "id": 1,
+    "title": "西游记",
+    "author": "(明) 吴承恩",
+    "cover": "https://picsum.photos/seed/book1/200/300",
+    "image": "https://picsum.photos/seed/book1/200/300",
+    "brief": "《西游记》是中国古代第一部神魔题材的长篇章回小说...",
+    "desc": "中国古代第一部浪漫主义章回体长篇神魔小说...",
+    "introduction": "本书《西游记》是中国古代第一部浪漫主义章回体长篇神魔小说...",
+    "price": 0.00,
+    "isFree": true,
+    "isVip": false,
+    "categoryId": 1,
+    "status": 1,
+    "viewCount": 0,
+    "likeCount": 0,
+    "readCount": 0
+  }
+}
+```
+
+### 2.3 创建书籍
+
+**POST** `/api/book`
+
+**请求体:**
+
+```json
+{
+  "title": "新书标题",
+  "author": "作者名称",
+  "cover": "https://example.com/cover.jpg",
+  "image": "https://example.com/image.jpg",
+  "brief": "简短简介",
+  "desc": "描述",
+  "introduction": "详细介绍",
+  "price": 29.90,
+  "isFree": false,
+  "isVip": false,
+  "categoryId": 1,
+  "status": 1
+}
+```
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "创建成功",
+  "data": {
+    "id": 10,
+    "title": "新书标题",
+    "author": "作者名称",
+    "status": 1
+  }
+}
+```
+
+### 2.4 更新书籍
+
+**PUT** `/api/book/{id}`
+
+**请求体:** 同创建书籍
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "更新成功",
+  "data": {
+    "id": 1,
+    "title": "更新后的书名",
+    "status": 1
+  }
+}
+```
+
+### 2.5 删除书籍
+
+**DELETE** `/api/book/{id}`
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "删除成功",
+  "data": null
+}
+```
+
+### 2.6 批量删除书籍
+
+**DELETE** `/api/book/batch`
+
+**请求体:**
+
+```json
+[1, 2, 3]
+```
+
+**响应示例:**
+
+```json
+{
+  "code": 200,
+  "message": "批量删除成功",
+  "data": null
+}
+```
+
+---
+
+## 使用示例
+
+### 使用 curl 命令
+
+```bash
+# 用户注册
+curl -X POST "http://localhost:8080/api/user/register" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "username": "testuser",
+    "password": "123456",
+    "nickname": "测试用户"
+  }'
+
+# 用户登录
+curl -X POST "http://localhost:8080/api/user/login" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "username": "testuser",
+    "password": "123456"
+  }'
+
+# 查询书籍列表
+curl -X GET "http://localhost:8080/api/book/list?page=1&size=10"
+
+# 查询书籍详情
+curl -X GET "http://localhost:8080/api/book/1"
+
+# 创建书籍
+curl -X POST "http://localhost:8080/api/book" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "title": "新书",
+    "author": "作者",
+    "status": 1
+  }'
+```
+
+### 使用 JavaScript (fetch)
+
+```javascript
+// 用户注册
+fetch('http://localhost:8080/api/user/register', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json'
+  },
+  body: JSON.stringify({
+    username: 'testuser',
+    password: '123456',
+    nickname: '测试用户'
+  })
+})
+  .then(response => response.json())
+  .then(data => console.log(data));
+
+// 用户登录
+fetch('http://localhost:8080/api/user/login', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json'
+  },
+  body: JSON.stringify({
+    username: 'testuser',
+    password: '123456'
+  })
+})
+  .then(response => response.json())
+  .then(data => console.log(data));
+
+// 查询书籍列表
+fetch('http://localhost:8080/api/book/list?page=1&size=10')
+  .then(response => response.json())
+  .then(data => console.log(data));
+```
+
+---
+
+## 注意事项
+
+1. 所有POST/PUT请求需要设置 `Content-Type: application/json` 头
+2. 密码在注册和登录时使用MD5加密(生产环境建议使用BCrypt)
+3. 已配置跨域支持,允许所有来源的请求
+4. 参数验证失败会返回详细的错误信息
+
+---
+
+## 数据库配置
+
+执行 `book/src/main/resources/db/schema.sql` 文件创建数据库和表结构。
+
+确保 `application.properties` 中的数据库配置正确:
+
+```properties
+spring.datasource.url=jdbc:mysql://localhost:3306/books_db?...
+spring.datasource.username=root
+spring.datasource.password=root
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 180 - 0
README.md

@@ -0,0 +1,180 @@
+# 图书阅读系统后端项目
+
+## 项目结构
+
+```
+book/
+├── src/
+│   └── main/
+│       ├── java/
+│       │   └── com/yu/book/
+│       │       ├── BookApplication.java          # 启动类
+│       │       ├── controller/                   # 控制层
+│       │       │   ├── UserController.java       # 用户控制器
+│       │       │   └── BookController.java       # 书籍控制器
+│       │       ├── service/                      # 服务层
+│       │       │   ├── UserService.java          # 用户服务
+│       │       │   └── BookService.java         # 书籍服务
+│       │       ├── mapper/                       # 数据访问层
+│       │       │   ├── UserMapper.java           # 用户Mapper接口
+│       │       │   ├── BookMapper.java           # 书籍Mapper接口
+│       │       │   └── CategoryMapper.java       # 分类Mapper接口
+│       │       ├── domain/                        # 实体类(领域模型)
+│       │       │   ├── User.java                 # 用户实体
+│       │       │   ├── Book.java                  # 书籍实体
+│       │       │   └── Category.java             # 分类实体
+│       │       ├── dto/                           # 数据传输对象
+│       │       │   ├── LoginDTO.java             # 登录DTO
+│       │       │   ├── RegisterDTO.java         # 注册DTO
+│       │       │   └── BookDTO.java              # 书籍DTO
+│       │       ├── vo/                            # 视图对象
+│       │       │   ├── UserVO.java               # 用户VO
+│       │       │   ├── BookVO.java               # 书籍VO
+│       │       │   └── LoginVO.java              # 登录VO
+│       │       ├── common/                        # 通用类
+│       │       │   ├── Result.java               # 统一响应结果
+│       │       │   └── PageResult.java           # 分页结果
+│       │       ├── util/                          # 工具类
+│       │       │   └── PasswordUtil.java         # 密码工具类
+│       │       └── exception/                     # 异常处理
+│       │           └── GlobalExceptionHandler.java # 全局异常处理器
+│       └── resources/
+│           ├── application.properties           # 配置文件
+│           ├── mapper/                            # MyBatis XML映射文件
+│           │   ├── UserMapper.xml
+│           │   ├── BookMapper.xml
+│           │   └── CategoryMapper.xml
+│           └── db/
+│               └── schema.sql                     # 数据库初始化脚本
+├── pom.xml                                        # Maven配置文件
+├── API_DOCUMENTATION.md                           # API接口文档
+└── README.md                                      # 项目说明文档
+```
+
+## 技术栈
+
+- **Spring Boot**: 2.7.18
+- **MyBatis**: 2.3.1
+- **MySQL**: 5.7+ / 8.0+
+- **Java**: 8
+- **Lombok**: 简化代码
+
+## 功能特性
+
+### 1. 用户管理
+- ✅ 用户注册
+- ✅ 用户登录
+- ✅ 用户信息查询
+
+### 2. 书籍管理
+- ✅ 分页查询书籍(支持关键词搜索、分类筛选、状态筛选)
+- ✅ 根据ID查询书籍详情
+- ✅ 创建书籍
+- ✅ 更新书籍
+- ✅ 删除书籍
+- ✅ 批量删除书籍
+
+## 快速开始
+
+### 1. 环境要求
+
+- JDK 8+
+- Maven 3.6+
+- MySQL 5.7+ / 8.0+
+
+### 2. 数据库配置
+
+1. 创建MySQL数据库,执行 `src/main/resources/db/schema.sql` 文件
+2. 修改 `src/main/resources/application.properties` 中的数据库连接信息:
+
+```properties
+spring.datasource.url=jdbc:mysql://localhost:3306/books_db?...
+spring.datasource.username=root
+spring.datasource.password=root
+```
+
+### 3. 启动项目
+
+```bash
+# 使用Maven启动
+mvn spring-boot:run
+
+# 或者打包后运行
+mvn clean package
+java -jar target/book-0.0.1-SNAPSHOT.jar
+```
+
+### 4. 访问接口
+
+项目启动后,访问地址:`http://localhost:8080`
+
+## API接口
+
+详细API文档请参考 [API_DOCUMENTATION.md](./API_DOCUMENTATION.md)
+
+### 主要接口
+
+- **用户注册**: `POST /api/user/register`
+- **用户登录**: `POST /api/user/login`
+- **查询用户**: `GET /api/user/{id}`
+- **分页查询书籍**: `GET /api/book/list`
+- **查询书籍详情**: `GET /api/book/{id}`
+- **创建书籍**: `POST /api/book`
+- **更新书籍**: `PUT /api/book/{id}`
+- **删除书籍**: `DELETE /api/book/{id}`
+- **批量删除书籍**: `DELETE /api/book/batch`
+
+## 包结构说明
+
+### controller(控制层)
+处理HTTP请求,调用Service层处理业务逻辑,返回响应结果。
+
+### service(服务层)
+实现业务逻辑,调用Mapper层进行数据操作。
+
+### mapper(数据访问层)
+MyBatis的Mapper接口,定义数据操作方法。
+
+### domain(领域模型)
+实体类,对应数据库表结构。
+
+### dto(数据传输对象)
+用于接收前端请求参数的对象。
+
+### vo(视图对象)
+用于返回给前端的数据对象。
+
+### common(通用类)
+统一的响应结果封装类。
+
+### util(工具类)
+通用工具方法。
+
+### exception(异常处理)
+全局异常处理器。
+
+## 开发规范
+
+1. **包命名规范**: 所有包名使用小写字母,多个单词使用点分隔
+2. **类命名规范**: 使用大驼峰命名(PascalCase)
+3. **方法命名规范**: 使用小驼峰命名(camelCase)
+4. **注释规范**: 所有类和方法都应有JavaDoc注释
+5. **异常处理**: 使用全局异常处理器统一处理异常
+6. **响应格式**: 统一使用Result类封装响应结果
+
+## 注意事项
+
+1. 密码加密使用MD5(简单示例),生产环境建议使用BCrypt
+2. Token目前是模拟实现,生产环境建议使用JWT
+3. 已配置跨域支持,允许所有来源的请求
+4. 数据库字段使用下划线命名,Java实体使用驼峰命名,MyBatis会自动映射
+
+## 后续扩展
+
+- [ ] JWT Token认证
+- [ ] 权限管理
+- [ ] 文件上传(书籍封面)
+- [ ] 缓存支持(Redis)
+- [ ] 日志记录
+- [ ] 单元测试
+

+ 166 - 0
Spring Boot升级说明.md

@@ -0,0 +1,166 @@
+# Spring Boot 升级说明
+
+## 🔴 问题
+
+**错误信息:**
+```
+java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(...) accessible: module java.base does not "opens java.lang" to unnamed module
+```
+
+**原因:**
+- 当前Java版本:Java 17
+- 当前Spring Boot版本:1.5.22.RELEASE(只支持Java 8)
+- Java 17引入了模块系统,与Spring Boot 1.5.x不兼容
+
+## ✅ 解决方案
+
+### 升级Spring Boot版本
+
+将Spring Boot从1.5.22.RELEASE升级到2.7.18(支持Java 17)
+
+## 🔧 已修改的文件
+
+### 1. pom.xml
+
+#### Spring Boot版本
+- 从 `1.5.22.RELEASE` 升级到 `2.7.18`
+
+#### Maven Compiler插件
+- 从 `3.8.1` 升级到 `3.11.0`
+- Java版本从 `7` 升级到 `17`
+
+#### MyBatis版本
+- 从 `1.3.2` 升级到 `2.3.1`
+
+#### MySQL驱动
+- 从 `mysql-connector-java 5.1.49` 升级到 `mysql-connector-j 8.0.33`
+- 驱动类从 `com.mysql.jdbc.Driver` 改为 `com.mysql.cj.jdbc.Driver`
+
+#### Spring Boot Maven插件
+- 移除了 `<skip>true</skip>` 配置
+
+### 2. AdminWebConfig.java
+
+- 从 `WebMvcConfigurerAdapter` 改为 `WebMvcConfigurer`
+- Spring Boot 2.0+版本中 `WebMvcConfigurerAdapter` 已废弃
+
+### 3. AdminInterceptor.java
+
+- 移除了 `postHandle` 和 `afterCompletion` 方法
+- Spring Boot 2.0+版本中这些方法有默认实现,不需要实现
+
+### 4. application.properties
+
+- MySQL驱动类从 `com.mysql.jdbc.Driver` 改为 `com.mysql.cj.jdbc.Driver`
+
+## 📝 升级后的变化
+
+### 1. 依赖版本
+
+| 组件 | 旧版本 | 新版本 |
+|------|--------|--------|
+| Spring Boot | 1.5.22.RELEASE | 2.7.18 |
+| MyBatis | 1.3.2 | 2.3.1 |
+| MySQL驱动 | 5.1.49 | 8.0.33 |
+| Java版本 | 7/8 | 17 |
+
+### 2. API变化
+
+#### WebMvcConfigurer
+- Spring Boot 1.5.x:使用 `WebMvcConfigurerAdapter`
+- Spring Boot 2.0+:直接实现 `WebMvcConfigurer`
+
+#### HandlerInterceptor
+- Spring Boot 1.5.x:需要实现所有方法
+- Spring Boot 2.0+:方法有默认实现,只需实现需要的方法
+
+#### MySQL驱动
+- 旧版本:`com.mysql.jdbc.Driver`
+- 新版本:`com.mysql.cj.jdbc.Driver`
+
+## 🚀 启动步骤
+
+### 1. 清理并重新编译
+
+```bash
+cd book
+mvn clean compile
+```
+
+### 2. 启动后端服务
+
+```bash
+mvn spring-boot:run
+```
+
+### 3. 验证服务
+
+访问:`http://localhost:8081/api/admin/login`
+
+## ⚠️ 注意事项
+
+### 1. 数据库连接
+
+- MySQL 8.0+ 需要使用新的驱动类 `com.mysql.cj.jdbc.Driver`
+- 如果使用MySQL 5.7,可以继续使用旧驱动,但建议升级
+
+### 2. 兼容性
+
+- Spring Boot 2.7.18 完全支持Java 17
+- 所有现有功能应该可以正常工作
+- 如果遇到问题,请查看启动日志
+
+### 3. 其他依赖
+
+- 其他依赖版本会自动由Spring Boot管理
+- 如果遇到依赖冲突,可能需要手动指定版本
+
+## 🔍 验证升级
+
+### 1. 检查启动日志
+
+启动后应该看到:
+```
+Started BookApplication
+Tomcat started on port(s): 8081
+```
+
+### 2. 测试API接口
+
+```bash
+# 测试登录接口
+curl -X POST http://localhost:8081/api/admin/login \
+  -H "Content-Type: application/json" \
+  -d '{"username":"admin","password":"admin123"}'
+```
+
+### 3. 测试前端
+
+- 访问:`http://localhost:8000/pages/login.html`
+- 使用管理员账号登录
+- 测试书籍管理功能
+
+## 📚 参考文档
+
+- [Spring Boot 2.7 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes)
+- [Migrating from Spring Boot 1.x to 2.x](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 333 - 0
VIP充值记录功能说明.md

@@ -0,0 +1,333 @@
+# VIP充值记录功能说明
+
+## 功能概述
+
+实现了VIP充值记录功能,当用户充值VIP后,会自动记录在数据库中,并且购买VIP的用户可以观看VIP书籍和听书。
+
+## 数据库设计
+
+### VIP充值记录表 (vip_records)
+
+创建数据库表:
+
+```bash
+# 执行数据库脚本
+mysql -u root -p books_db < book/src/main/resources/db/vip_records_schema.sql
+```
+
+表结构说明:
+- `id`: 记录ID(主键)
+- `user_id`: 用户ID
+- `vip_type`: VIP类型(month-月卡,quarter-季卡,year-年卡)
+- `vip_name`: VIP名称(月卡VIP、季卡VIP、年卡VIP)
+- `price`: 价格
+- `duration`: 时长(天数)
+- `start_time`: VIP开始时间
+- `expire_time`: VIP过期时间
+- `payment_method`: 支付方式(alipay-支付宝,wechat-微信,other-其他)
+- `payment_status`: 支付状态(0-待支付,1-已支付,2-已取消,3-已退款)
+- `order_no`: 订单号
+- `transaction_id`: 交易流水号
+- `remark`: 备注
+- `created_at`: 创建时间
+- `updated_at`: 更新时间
+
+## 后端实现
+
+### 1. 实体类
+- `VipRecord.java` - VIP充值记录实体类
+- `PurchaseVipDTO.java` - 购买VIP DTO
+
+### 2. Mapper
+- `VipRecordMapper.java` - Mapper接口
+- `VipRecordMapper.xml` - MyBatis映射文件
+
+### 3. Service
+- `VipService.java` - VIP服务类
+  - `purchaseVip()` - 购买VIP(自动更新用户VIP状态)
+  - `confirmPayment()` - 确认支付(支付回调)
+  - `checkVipStatus()` - 检查用户VIP状态(检查是否过期)
+  - `getUserVipRecords()` - 获取用户VIP充值记录列表
+
+### 4. Controller
+- `VipController.java` - VIP控制器
+
+### 5. 权限验证
+- `BookService.java` - 修改了`getBookById()`方法,添加VIP权限检查
+- `AudiobookService.java` - 修改了`getChapterDetail()`方法,添加VIP权限检查
+
+## API接口
+
+### 1. 获取VIP套餐列表
+
+**接口地址**:`GET /api/vip/plans`
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "data": [
+    {
+      "type": "month",
+      "name": "月卡VIP",
+      "price": 30.00,
+      "duration": 30,
+      "desc": "适合短期阅读",
+      "benefits": ["无限阅读", "免费听书", "专属客服"]
+    },
+    {
+      "type": "quarter",
+      "name": "季卡VIP",
+      "price": 80.00,
+      "duration": 90,
+      "desc": "超值优惠",
+      "benefits": ["无限阅读", "免费听书", "专属客服", "优先更新"]
+    },
+    {
+      "type": "year",
+      "name": "年卡VIP",
+      "price": 288.00,
+      "duration": 365,
+      "desc": "最超值选择",
+      "benefits": ["无限阅读", "免费听书", "专属客服", "优先更新", "专属活动"]
+    }
+  ]
+}
+```
+
+### 2. 购买VIP
+
+**接口地址**:`POST /api/vip/purchase`
+
+**请求参数**:
+```json
+{
+  "userId": 1,
+  "vipType": "month",
+  "vipName": "月卡VIP",
+  "price": 30.00,
+  "paymentMethod": "wechat"
+}
+```
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "message": "购买成功",
+  "data": {
+    "id": 1,
+    "type": "月卡VIP",
+    "price": 30.00,
+    "activateTime": "2025-01-11 10:00:00",
+    "expireTime": "2025-02-10 10:00:00",
+    "orderNo": "VIP1234567890",
+    "paymentStatus": 1
+  }
+}
+```
+
+### 3. 确认支付(支付回调)
+
+**接口地址**:`POST /api/vip/confirm-payment`
+
+**请求参数**:
+- `recordId`: 记录ID(必填)
+- `transactionId`: 交易流水号(可选)
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "message": "支付确认成功"
+}
+```
+
+### 4. 检查用户VIP状态
+
+**接口地址**:`GET /api/vip/check`
+
+**请求参数**:
+- `userId`: 用户ID(必填)
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "data": {
+    "isVip": true
+  }
+}
+```
+
+### 5. 获取用户的VIP充值记录列表
+
+**接口地址**:`GET /api/vip/records`
+
+**请求参数**:
+- `userId`: 用户ID(必填)
+- `paymentStatus`: 支付状态(0-待支付,1-已支付,不传则查询全部)
+- `page`: 页码(默认1)
+- `size`: 每页数量(默认10)
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "data": {
+    "list": [
+      {
+        "id": 1,
+        "type": "月卡VIP",
+        "price": 30.00,
+        "activateTime": "2025-01-11 10:00:00",
+        "expireTime": "2025-02-10 10:00:00",
+        "paymentStatus": 1,
+        "orderNo": "VIP1234567890"
+      }
+    ],
+    "total": 10,
+    "page": 1,
+    "size": 10
+  }
+}
+```
+
+## 前端实现
+
+### 1. API函数
+
+在 `books/utils/api.js` 中添加了以下函数:
+- `getVipPlans()` - 获取VIP套餐列表
+- `purchaseVip(purchaseData)` - 购买VIP
+- `confirmVipPayment(recordId, transactionId)` - 确认支付
+- `checkVipStatus(userId)` - 检查用户VIP状态
+- `getVipRecords(userId, paymentStatus, page, size)` - 获取VIP充值记录列表
+- `getBookById(id, userId)` - 获取书籍详情(已更新,支持userId参数)
+- `getChapterDetail(chapterId, userId)` - 获取章节详情(已更新,支持userId参数)
+
+### 2. 页面
+
+- `books/pages/vip/vip.vue` - VIP购买页面
+  - 从后端获取VIP套餐列表
+  - 选择VIP套餐
+  - 跳转到支付页面
+
+- `books/pages/payment/payment.vue` - 支付页面
+  - 选择支付方式
+  - 调用购买VIP接口
+  - 支付成功后更新本地用户信息
+
+- `books/pages/vip-records/vip-records.vue` - VIP充值记录页面
+  - 显示用户的VIP充值记录列表
+  - 支持下拉刷新和上拉加载更多
+
+- `books/pages/book-detail/book-detail.vue` - 书籍详情页面
+  - 已更新,传递userId参数用于VIP权限检查
+
+## VIP权限验证
+
+### 1. 书籍权限验证
+
+当用户访问VIP书籍时:
+- 后端检查用户VIP状态
+- 如果用户不是VIP或VIP已过期,在`BookVO`中设置`requireVip = true`
+- 前端根据`requireVip`字段显示VIP提示
+
+### 2. 听书权限验证
+
+当用户播放非免费听书章节时:
+- 前端检查用户VIP状态
+- 如果用户不是VIP,显示VIP提示并跳转到VIP购买页面
+- 后端也会验证VIP权限,确保安全
+
+## VIP状态管理
+
+### 1. 自动续费
+
+如果用户已经是VIP且未过期,购买新的VIP时:
+- 新的VIP有效期从原VIP过期时间开始计算
+- 实现VIP续费功能
+
+### 2. 过期检查
+
+- `VipService.checkVipStatus()`方法会自动检查VIP是否过期
+- 如果VIP已过期,自动更新用户VIP状态为`isVip = false`
+
+### 3. VIP时长计算
+
+- 月卡VIP:30天
+- 季卡VIP:90天
+- 年卡VIP:365天
+
+## 使用说明
+
+### 1. 创建数据库表
+
+```bash
+mysql -u root -p books_db < book/src/main/resources/db/vip_records_schema.sql
+```
+
+### 2. 启动后端服务
+
+### 3. 测试VIP购买流程
+
+1. 用户打开VIP购买页面
+2. 选择VIP套餐(月卡/季卡/年卡)
+3. 跳转到支付页面
+4. 选择支付方式并确认支付
+5. 支付成功后,用户VIP状态自动更新
+6. 可以在VIP充值记录页面查看充值记录
+
+### 4. 测试VIP权限验证
+
+1. 非VIP用户访问VIP书籍时,会显示VIP提示
+2. 非VIP用户播放非免费听书章节时,会显示VIP提示
+3. VIP用户访问VIP内容时,正常访问
+
+## 注意事项
+
+1. **支付流程**:
+   - 当前实现为模拟支付(立即更新VIP状态)
+   - 实际项目中应该等待支付成功回调后再更新VIP状态
+   - 可以调用`confirmPayment()`方法在支付回调时更新状态
+
+2. **VIP过期处理**:
+   - VIP过期后会自动更新用户状态
+   - 建议定期执行任务检查并更新过期VIP状态
+
+3. **续费逻辑**:
+   - VIP未过期时购买新VIP,从过期时间开始计算
+   - VIP已过期时购买新VIP,从当前时间开始计算
+
+4. **权限验证**:
+   - 前端和后端都进行VIP权限验证
+   - 确保数据安全
+
+## 后续扩展
+
+### 1. 支付回调集成(可选)
+
+集成真实的支付平台(支付宝、微信支付):
+- 实现支付回调接口
+- 在支付成功后调用`confirmPayment()`方法
+
+### 2. VIP优惠券(可选)
+
+- 添加VIP优惠券功能
+- 购买VIP时可以使用优惠券
+
+### 3. VIP自动续费(可选)
+
+- 实现VIP自动续费功能
+- 在VIP即将过期时提醒用户续费
+
+### 4. VIP等级系统(可选)
+
+- 实现VIP等级系统(普通VIP、高级VIP、超级VIP)
+- 不同等级享受不同权益
+
+
+
+
+

+ 27 - 0
book-admin/.gitignore

@@ -0,0 +1,27 @@
+# Dependencies
+node_modules/
+package-lock.json
+yarn.lock
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+logs/
+*.log
+npm-debug.log*
+
+# Environment
+.env
+.env.local
+
+# Maven
+target/

+ 251 - 0
book-admin/README.md

@@ -0,0 +1,251 @@
+# 后台管理系统
+
+## 功能概述
+
+后台管理系统用于管理小程序中的书籍数据,包括:
+- 书籍的增删改查
+- 书籍的上架/下架
+- 书籍状态管理
+- 管理员登录(只有管理员可以登录)
+
+## 技术栈
+
+- 后端:Spring Boot + MyBatis + MySQL
+- 前端:HTML + Vue.js 3 + Fetch API
+
+## 项目结构
+
+```
+book-admin/
+├── src/main/resources/static/
+│   ├── pages/
+│   │   ├── login.html          # 登录页面
+│   │   └── books.html          # 书籍管理页面
+│   └── utils/
+│       └── api.js              # API接口文件
+├── package.json                # npm配置文件
+└── README.md                   # 说明文档
+```
+
+## 安装和运行
+
+### 🚀 快速启动
+
+#### 1. 数据库初始化
+
+执行数据库脚本创建管理员账号:
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/admin_schema.sql
+```
+
+#### 2. 启动后端服务
+
+```bash
+# 在book项目根目录执行
+cd ..
+mvn spring-boot:run
+
+# 或者使用IDE启动 BookApplication.java
+```
+
+**验证后端服务:**
+- 打开浏览器访问:`http://localhost:8081/api/admin/login`
+- 如果能看到响应,说明服务已启动
+
+#### 3. 启动前端开发服务器
+
+```bash
+# 在book-admin目录执行
+cd book-admin
+
+# 首次使用需要安装依赖
+npm install
+
+# 启动开发服务器(自动打开浏览器)
+npm run dev
+```
+
+#### 4. 登录
+
+默认管理员账号:
+- 用户名:`admin`
+- 密码:`admin123`
+
+## 功能说明
+
+### 1. 管理员登录
+
+- 只有 `role` 为 `admin` 的用户可以登录
+- 普通用户不能登录后台管理系统
+- Token保存在localStorage中
+
+### 2. 书籍管理
+
+#### 查询功能
+- 支持按书名、作者搜索
+- 支持按状态筛选(上架/下架)
+- 支持按分类筛选
+- 支持分页查询
+
+#### 添加书籍
+- 点击"添加书籍"按钮
+- 填写书籍信息(书名、作者、封面、简介等)
+- 设置书籍状态(上架/下架)
+- 选择分类
+
+#### 编辑书籍
+- 点击书籍列表中的"编辑"按钮
+- 修改书籍信息
+- 保存更改
+
+#### 删除书籍
+- 单个删除:点击"删除"按钮
+- 批量删除:选中多个书籍后点击"批量删除"
+
+#### 上架/下架
+- 单个操作:点击"上架"或"下架"按钮
+- 批量操作:选中多个书籍后点击"批量上架"或"批量下架"
+
+### 3. 权限控制
+
+- 所有后台管理接口都需要在请求头中携带token
+- token格式:`admin_token_{userId}`
+- 如果token无效或过期,会返回401未授权错误
+
+## API接口
+
+### 管理员登录
+
+**POST** `/api/admin/login`
+
+请求体:
+```json
+{
+  "username": "admin",
+  "password": "admin123"
+}
+```
+
+响应:
+```json
+{
+  "code": 200,
+  "message": "登录成功",
+  "data": {
+    "id": 1,
+    "username": "admin",
+    "nickname": "管理员",
+    "role": "admin",
+    "token": "admin_token_1"
+  }
+}
+```
+
+### 书籍管理接口
+
+所有书籍管理接口都需要在请求头中携带token:
+```
+Authorization: admin_token_1
+```
+
+#### 分页查询书籍
+
+**GET** `/api/admin/book/list?page=1&size=10&keyword=&status=&categoryId=`
+
+#### 根据ID查询书籍
+
+**GET** `/api/admin/book/{id}`
+
+#### 创建书籍
+
+**POST** `/api/admin/book`
+
+#### 更新书籍
+
+**PUT** `/api/admin/book/{id}`
+
+#### 删除书籍
+
+**DELETE** `/api/admin/book/{id}`
+
+#### 上架书籍
+
+**PUT** `/api/admin/book/{id}/publish`
+
+#### 下架书籍
+
+**PUT** `/api/admin/book/{id}/unpublish`
+
+## 创建管理员账号
+
+### 方法1:使用SQL脚本
+
+执行 `admin_schema.sql` 脚本会自动创建默认管理员账号。
+
+### 方法2:手动创建
+
+```sql
+-- 创建管理员账号
+INSERT INTO `users` (`username`, `nickname`, `password`, `role`, `status`, `created_at`, `updated_at`)
+VALUES ('admin', '管理员', '0192023a7bbd73250516f069df18b500', 'admin', 1, NOW(), NOW());
+
+-- 密码:admin123(MD5加密后:0192023a7bbd73250516f069df18b500)
+```
+
+## 注意事项
+
+1. **密码加密**:系统使用MD5加密密码,默认管理员密码 `admin123` 的MD5值为 `0192023a7bbd73250516f069df18b500`
+2. **Token管理**:当前使用的是模拟token,实际项目中应该使用JWT Token
+3. **CORS配置**:后端已配置CORS,允许跨域请求
+4. **API地址**:前端API地址配置在 `utils/api.js` 中,默认是 `http://localhost:8081`
+5. **Vue.js版本**:前端使用Vue.js 3,通过CDN引入
+
+## 后续优化
+
+1. 使用JWT Token进行身份验证
+2. 添加token过期时间管理
+3. 添加操作日志记录
+4. 添加数据统计功能
+5. 添加用户管理功能
+6. 添加分类管理功能
+7. 添加图片上传功能
+8. 添加数据导出功能
+
+## 启动命令
+
+```bash
+# 1. 启动后端服务(在book项目根目录)
+mvn spring-boot:run
+
+# 2. 启动前端开发服务器(在book-admin目录)
+cd book-admin
+npm install  # 首次使用
+npm run dev  # 启动开发服务器
+```
+
+## 访问地址
+
+- 登录页面:`http://localhost:8000/pages/login.html`
+- 书籍管理页面:`http://localhost:8000/pages/books.html`(需要登录)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 29 - 0
book-admin/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "book-admin",
+  "version": "1.0.0",
+  "description": "图书管理系统后台管理界面",
+  "scripts": {
+    "dev": "http-server src/main/resources/static -p 8002 -o -c-1",
+    "start": "http-server src/main/resources/static -p 8002 -o -c-1",
+    "serve": "http-server src/main/resources/static -p 8002 -c-1"
+  },
+  "keywords": [
+    "book",
+    "admin",
+    "management"
+  ],
+  "author": "",
+  "license": "ISC",
+  "devDependencies": {
+    "http-server": "^14.1.1"
+  }
+}
+
+
+
+
+
+
+
+
+

+ 72 - 0
book-admin/pom.xml

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.yu</groupId>
+    <artifactId>book-admin</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>book-admin</name>
+    <description>book-admin</description>
+    <properties>
+        <java.version>8</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <spring-boot.version>3.0.2</spring-boot.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>17</source>
+                    <target>17</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <mainClass>com.yu.bookadmin.BookAdminApplication</mainClass>
+                    <skip>true</skip>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 14 - 0
book-admin/src/main/java/com/yu/bookadmin/BookAdminApplication.java

@@ -0,0 +1,14 @@
+package com.yu.bookadmin;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class BookAdminApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(BookAdminApplication.class, args);
+    }
+
+
+}

+ 36 - 0
book-admin/src/main/resources/static/index.html

@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统</title>
+    <script>
+        // 自动跳转到登录页面
+        window.location.href = 'pages/login.html';
+    </script>
+</head>
+<body>
+    <p>正在跳转到登录页面...</p>
+    <p>如果页面没有自动跳转,请<a href="pages/login.html">点击这里</a></p>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 763 - 0
book-admin/src/main/resources/static/pages/audiobooks.html

@@ -0,0 +1,763 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统 - 听书管理</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: #f5f5f5;
+        }
+        
+        .header {
+            background: white;
+            padding: 20px 30px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        
+        .header-title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        .header-actions {
+            display: flex;
+            gap: 10px;
+        }
+        
+        .btn {
+            padding: 8px 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: opacity 0.3s;
+        }
+        
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+        
+        .btn-danger {
+            background: #f56565;
+            color: white;
+        }
+        
+        .btn-success {
+            background: #48bb78;
+            color: white;
+        }
+        
+        .btn-warning {
+            background: #ed8936;
+            color: white;
+        }
+        
+        .btn:hover {
+            opacity: 0.9;
+        }
+        
+        .container {
+            max-width: 1200px;
+            margin: 20px auto;
+            padding: 0 20px;
+        }
+        
+        .nav-tabs {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 20px;
+        }
+        
+        .nav-tab {
+            padding: 10px 18px;
+            border-radius: 6px;
+            background: white;
+            color: #333;
+            text-decoration: none;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+            transition: all 0.3s;
+            font-size: 14px;
+        }
+        
+        .nav-tab:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 10px rgba(0,0,0,0.12);
+        }
+        
+        .nav-tab.active {
+            background: #667eea;
+            color: white;
+            box-shadow: 0 6px 14px rgba(102, 126, 234, 0.35);
+        }
+        
+        .toolbar {
+            background: white;
+            padding: 20px;
+            border-radius: 5px;
+            margin-bottom: 20px;
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+            align-items: center;
+        }
+        
+        .search-input {
+            flex: 1;
+            min-width: 220px;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-select {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .table-container {
+            background: white;
+            border-radius: 5px;
+            overflow: hidden;
+        }
+        
+        table {
+            width: 100%;
+            border-collapse: collapse;
+        }
+        
+        th, td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #eee;
+            vertical-align: middle;
+        }
+        
+        th {
+            background: #f8f9fa;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        tr:hover {
+            background: #f8f9fa;
+        }
+        
+        .status-badge {
+            padding: 4px 8px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: bold;
+        }
+        
+        .status-online {
+            background: #c6f6d5;
+            color: #22543d;
+        }
+        
+        .status-offline {
+            background: #fed7d7;
+            color: #742a2a;
+        }
+        
+        .tag {
+            display: inline-block;
+            padding: 2px 6px;
+            border-radius: 4px;
+            font-size: 12px;
+            margin-right: 4px;
+        }
+        
+        .tag-free {
+            background: #e6fffa;
+            color: #0b735a;
+        }
+        
+        .tag-vip {
+            background: #fee2e2;
+            color: #c53030;
+        }
+        
+        .action-buttons {
+            display: flex;
+            gap: 5px;
+        }
+        
+        .btn-sm {
+            padding: 4px 8px;
+            font-size: 12px;
+        }
+        
+        .stats {
+            font-size: 12px;
+            color: #777;
+            line-height: 1.4;
+        }
+        
+        .pagination {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            gap: 10px;
+            margin-top: 20px;
+            padding: 20px;
+        }
+        
+        .modal {
+            display: none;
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background: rgba(0, 0, 0, 0.5);
+            z-index: 1000;
+        }
+        
+        .modal.show {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+        
+        .modal-content {
+            background: white;
+            border-radius: 5px;
+            padding: 30px;
+            max-width: 700px;
+            width: 90%;
+            max-height: 80vh;
+            overflow-y: auto;
+        }
+        
+        .modal-header {
+            font-size: 20px;
+            font-weight: bold;
+            margin-bottom: 20px;
+        }
+        
+        .form-group {
+            margin-bottom: 15px;
+        }
+        
+        .form-label {
+            display: block;
+            margin-bottom: 5px;
+            color: #666;
+            font-size: 14px;
+        }
+        
+        .form-input, .form-textarea {
+            width: 100%;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-textarea {
+            min-height: 100px;
+            resize: vertical;
+        }
+        
+        .grid-two {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+            gap: 15px;
+        }
+        
+        .modal-actions {
+            display: flex;
+            justify-content: flex-end;
+            gap: 10px;
+            margin-top: 20px;
+        }
+        
+        .loading {
+            text-align: center;
+            padding: 20px;
+            color: #666;
+        }
+        
+        .empty {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+        }
+        
+        img {
+            max-width: 60px;
+            max-height: 80px;
+            object-fit: cover;
+            border-radius: 4px;
+        }
+    </style>
+</head>
+<body>
+    <div id="app">
+        <div class="header">
+            <div class="header-title">听书管理</div>
+            <div class="header-actions">
+                <button class="btn btn-primary" @click="showAddModal">添加听书</button>
+                <button class="btn btn-danger" @click="logout">退出登录</button>
+            </div>
+        </div>
+        
+        <div class="container">
+            <div class="nav-tabs">
+                <a class="nav-tab" href="books.html">电子书管理</a>
+                <a class="nav-tab active" href="audiobooks.html">听书管理</a>
+                <a class="nav-tab" href="rankings.html">排行榜管理</a>
+                <a class="nav-tab" href="banners.html">轮播图管理</a>
+                <a class="nav-tab" href="users.html">用户管理</a>
+            </div>
+            
+            <div class="toolbar">
+                <input type="text" class="search-input" v-model="searchKeyword" placeholder="搜索书名、作者、主播..." @input="handleSearch">
+                <select class="form-select" v-model="filterStatus" @change="loadAudiobooks" style="width: 140px;">
+                    <option value="">全部状态</option>
+                    <option value="1">上架</option>
+                    <option value="0">下架</option>
+                </select>
+                <select class="form-select" v-model="filterVip" @change="loadAudiobooks" style="width: 140px;">
+                    <option value="">全部类型</option>
+                    <option value="vip">仅VIP</option>
+                    <option value="free">仅免费</option>
+                </select>
+                <select class="form-select" v-model="filterCategory" @change="loadAudiobooks" style="width: 160px;">
+                    <option value="">全部分类</option>
+                    <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+                </select>
+                <button class="btn btn-primary" @click="loadAudiobooks">查询</button>
+                <button class="btn btn-danger" @click="batchDelete" :disabled="selectedAudiobooks.length === 0">批量删除</button>
+                <button class="btn btn-success" @click="batchPublish" :disabled="selectedAudiobooks.length === 0">批量上架</button>
+                <button class="btn btn-warning" @click="batchUnpublish" :disabled="selectedAudiobooks.length === 0">批量下架</button>
+            </div>
+            
+            <div class="table-container">
+                <table>
+                    <thead>
+                        <tr>
+                            <th><input type="checkbox" @change="toggleSelectAll" v-model="selectAll"></th>
+                            <th>ID</th>
+                            <th>封面</th>
+                            <th>书名 / 主播</th>
+                            <th>分类</th>
+                            <th>标签</th>
+                            <th>统计</th>
+                            <th>状态</th>
+                            <th>操作</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="loading">
+                            <td colspan="9" class="loading">加载中...</td>
+                        </tr>
+                        <tr v-else-if="audiobooks.length === 0">
+                            <td colspan="9" class="empty">暂无数据</td>
+                        </tr>
+                        <tr v-else v-for="item in audiobooks" :key="item.id">
+                            <td><input type="checkbox" :value="item.id" v-model="selectedAudiobooks"></td>
+                            <td>{{ item.id }}</td>
+                            <td>
+                                <img :src="item.image || item.cover" alt="" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'60\' height=\'80\'%3E%3Crect width=\'60\' height=\'80\' fill=\'%23ddd\'/%3E%3Ctext x=\'50%25\' y=\'50%25\' text-anchor=\'middle\' dy=\'.3em\' fill=\'%23999\'%3E无图%3C/text%3E%3C/svg%3E'">
+                            </td>
+                            <td>
+                                <div>{{ item.title }}</div>
+                                <div class="stats">作者:{{ item.author || '未知' }}</div>
+                                <div class="stats">主播:{{ item.narrator || '未知' }}</div>
+                            </td>
+                            <td>{{ item.categoryName || '-' }}</td>
+                            <td>
+                                <span v-if="item.isFree" class="tag tag-free">免费</span>
+                                <span v-if="item.isVip" class="tag tag-vip">VIP</span>
+                                <div class="stats" v-if="item.chapterCount">
+                                    章节:{{ item.chapterCount }} 个
+                                </div>
+                                <div class="stats" v-if="item.totalDurationText">
+                                    时长:{{ item.totalDurationText }}
+                                </div>
+                            </td>
+                            <td>
+                                <div class="stats">浏览:{{ item.viewCount || 0 }}</div>
+                                <div class="stats">播放:{{ item.playCount || 0 }}</div>
+                                <div class="stats">点赞:{{ item.likeCount || 0 }}</div>
+                            </td>
+                            <td>
+                                <span :class="['status-badge', item.status === 1 ? 'status-online' : 'status-offline']">
+                                    {{ item.status === 1 ? '上架' : '下架' }}
+                                </span>
+                            </td>
+                            <td>
+                                <div class="action-buttons">
+                                    <button class="btn btn-primary btn-sm" @click="editAudiobook(item)">编辑</button>
+                                    <button v-if="item.status === 1" class="btn btn-warning btn-sm" @click="unpublishAudiobook(item.id)">下架</button>
+                                    <button v-else class="btn btn-success btn-sm" @click="publishAudiobook(item.id)">上架</button>
+                                    <button class="btn btn-danger btn-sm" @click="deleteAudiobook(item.id)">删除</button>
+                                </div>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+            
+            <div class="pagination">
+                <button class="btn" @click="prevPage" :disabled="page === 1">上一页</button>
+                <span>第 {{ page }} 页,共 {{ totalPages }} 页,共 {{ total }} 条</span>
+                <button class="btn" @click="nextPage" :disabled="page >= totalPages">下一页</button>
+            </div>
+        </div>
+        
+        <div class="modal" :class="{ show: showModal }" @click.self="closeModal">
+            <div class="modal-content">
+                <div class="modal-header">{{ editingAudiobook.id ? '编辑听书' : '添加听书' }}</div>
+                <form @submit.prevent="saveAudiobook">
+                    <div class="grid-two">
+                        <div class="form-group">
+                            <label class="form-label">书名 *</label>
+                            <input type="text" class="form-input" v-model="editingAudiobook.title" required>
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">作者</label>
+                            <input type="text" class="form-input" v-model="editingAudiobook.author">
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">主播</label>
+                            <input type="text" class="form-input" v-model="editingAudiobook.narrator">
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">分类</label>
+                            <select class="form-select" v-model="editingAudiobook.categoryId">
+                                <option value="">请选择分类</option>
+                                <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+                            </select>
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">状态 *</label>
+                            <select class="form-select" v-model="editingAudiobook.status" required>
+                                <option value="1">上架</option>
+                                <option value="0">下架</option>
+                            </select>
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">是否免费</label>
+                            <select class="form-select" v-model="editingAudiobook.isFree">
+                                <option :value="false">否</option>
+                                <option :value="true">是</option>
+                            </select>
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">是否VIP专享</label>
+                            <select class="form-select" v-model="editingAudiobook.isVip">
+                                <option :value="false">否</option>
+                                <option :value="true">是</option>
+                            </select>
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">章节数</label>
+                            <input type="number" class="form-input" v-model.number="editingAudiobook.chapterCount" min="0">
+                        </div>
+                        <div class="form-group">
+                            <label class="form-label">总时长(秒)</label>
+                            <input type="number" class="form-input" v-model.number="editingAudiobook.totalDuration" min="0">
+                        </div>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label class="form-label">封面URL</label>
+                        <input type="text" class="form-input" v-model="editingAudiobook.cover">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">图片URL</label>
+                        <input type="text" class="form-input" v-model="editingAudiobook.image">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">简介</label>
+                        <textarea class="form-textarea" v-model="editingAudiobook.brief"></textarea>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">描述</label>
+                        <textarea class="form-textarea" v-model="editingAudiobook.desc"></textarea>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">详细介绍</label>
+                        <textarea class="form-textarea" v-model="editingAudiobook.introduction"></textarea>
+                    </div>
+                    
+                    <div class="modal-actions">
+                        <button type="button" class="btn" @click="closeModal">取消</button>
+                        <button type="submit" class="btn btn-primary">保存</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+    
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <script type="module">
+        import {
+            getAdminAudiobooks,
+            createAdminAudiobook,
+            updateAdminAudiobook,
+            deleteAdminAudiobook,
+            deleteAdminAudiobooks,
+            publishAdminAudiobook,
+            unpublishAdminAudiobook,
+            publishAdminAudiobooks,
+            unpublishAdminAudiobooks,
+            getAllCategories
+        } from '../utils/api.js'
+        
+        const { createApp } = Vue
+        
+        createApp({
+            data() {
+                return {
+                    token: localStorage.getItem('admin_token'),
+                    audiobooks: [],
+                    categories: [],
+                    loading: false,
+                    page: 1,
+                    pageSize: 10,
+                    total: 0,
+                    searchKeyword: '',
+                    filterStatus: '',
+                    filterVip: '',
+                    filterCategory: '',
+                    selectedAudiobooks: [],
+                    selectAll: false,
+                    showModal: false,
+                    editingAudiobook: this.getEmptyAudiobook(),
+                    searchTimer: null
+                }
+            },
+            computed: {
+                totalPages() {
+                    return Math.max(1, Math.ceil(this.total / this.pageSize))
+                }
+            },
+            mounted() {
+                if (!this.token) {
+                    window.location.href = 'login.html'
+                    return
+                }
+                this.loadCategories()
+                this.loadAudiobooks()
+            },
+            methods: {
+                getEmptyAudiobook() {
+                    return {
+                        id: null,
+                        title: '',
+                        author: '',
+                        narrator: '',
+                        cover: '',
+                        image: '',
+                        brief: '',
+                        desc: '',
+                        introduction: '',
+                        categoryId: null,
+                        status: 1,
+                        isFree: false,
+                        isVip: false,
+                        chapterCount: 0,
+                        totalDuration: 0
+                    }
+                },
+                async loadCategories() {
+                    try {
+                        const res = await getAllCategories('')
+                        if (res && res.code === 200) {
+                            this.categories = res.data || []
+                        }
+                    } catch (err) {
+                        console.error('加载分类失败:', err)
+                    }
+                },
+                async loadAudiobooks() {
+                    this.loading = true
+                    try {
+                        let vipFilter = this.filterVip
+                        let isVipParam
+                        let isFreeParam
+                        if (vipFilter === 'vip') {
+                            isVipParam = true
+                        } else if (vipFilter === 'free') {
+                            isFreeParam = true
+                        }
+                        
+                        const params = {
+                            page: this.page,
+                            size: this.pageSize,
+                            keyword: this.searchKeyword,
+                            categoryId: this.filterCategory || undefined,
+                            status: this.filterStatus === '' ? undefined : Number(this.filterStatus),
+                            isVip: isVipParam,
+                            isFree: isFreeParam
+                        }
+                        const res = await getAdminAudiobooks(params, this.token)
+                        if (res.code === 200 && res.data) {
+                            this.audiobooks = res.data.list || []
+                            this.total = res.data.total || 0
+                        }
+                    } catch (err) {
+                        alert(err.message || '加载听书失败')
+                    } finally {
+                        this.loading = false
+                    }
+                },
+                handleSearch() {
+                    clearTimeout(this.searchTimer)
+                    this.searchTimer = setTimeout(() => {
+                        this.page = 1
+                        this.loadAudiobooks()
+                    }, 500)
+                },
+                prevPage() {
+                    if (this.page > 1) {
+                        this.page--
+                        this.loadAudiobooks()
+                    }
+                },
+                nextPage() {
+                    if (this.page < this.totalPages) {
+                        this.page++
+                        this.loadAudiobooks()
+                    }
+                },
+                toggleSelectAll() {
+                    if (this.selectAll) {
+                        this.selectedAudiobooks = this.audiobooks.map(item => item.id)
+                    } else {
+                        this.selectedAudiobooks = []
+                    }
+                },
+                showAddModal() {
+                    this.editingAudiobook = this.getEmptyAudiobook()
+                    this.showModal = true
+                },
+                editAudiobook(item) {
+                    this.editingAudiobook = { ...item }
+                    if (this.editingAudiobook.isFree === null || this.editingAudiobook.isFree === undefined) {
+                        this.editingAudiobook.isFree = false
+                    }
+                    if (this.editingAudiobook.isVip === null || this.editingAudiobook.isVip === undefined) {
+                        this.editingAudiobook.isVip = false
+                    }
+                    this.showModal = true
+                },
+                closeModal() {
+                    this.showModal = false
+                },
+                async saveAudiobook() {
+                    try {
+                        const payload = { ...this.editingAudiobook }
+                        if (payload.id) {
+                            await updateAdminAudiobook(payload.id, payload, this.token)
+                            alert('更新成功')
+                        } else {
+                            await createAdminAudiobook(payload, this.token)
+                            alert('创建成功')
+                        }
+                        this.closeModal()
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '保存失败')
+                    }
+                },
+                async deleteAudiobook(id) {
+                    if (!confirm('确定要删除这条听书吗?')) return
+                    try {
+                        await deleteAdminAudiobook(id, this.token)
+                        alert('删除成功')
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '删除失败')
+                    }
+                },
+                async batchDelete() {
+                    if (this.selectedAudiobooks.length === 0) return
+                    if (!confirm(`确定要删除选中的 ${this.selectedAudiobooks.length} 条听书吗?`)) return
+                    try {
+                        await deleteAdminAudiobooks(this.selectedAudiobooks, this.token)
+                        alert('批量删除成功')
+                        this.selectedAudiobooks = []
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '批量删除失败')
+                    }
+                },
+                async publishAudiobook(id) {
+                    try {
+                        await publishAdminAudiobook(id, this.token)
+                        alert('上架成功')
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '上架失败')
+                    }
+                },
+                async unpublishAudiobook(id) {
+                    try {
+                        await unpublishAdminAudiobook(id, this.token)
+                        alert('下架成功')
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '下架失败')
+                    }
+                },
+                async batchPublish() {
+                    if (this.selectedAudiobooks.length === 0) return
+                    try {
+                        await publishAdminAudiobooks(this.selectedAudiobooks, this.token)
+                        alert('批量上架成功')
+                        this.selectedAudiobooks = []
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '批量上架失败')
+                    }
+                },
+                async batchUnpublish() {
+                    if (this.selectedAudiobooks.length === 0) return
+                    try {
+                        await unpublishAdminAudiobooks(this.selectedAudiobooks, this.token)
+                        alert('批量下架成功')
+                        this.selectedAudiobooks = []
+                        this.loadAudiobooks()
+                    } catch (err) {
+                        alert(err.message || '批量下架失败')
+                    }
+                },
+                logout() {
+                    if (confirm('确定要退出登录吗?')) {
+                        localStorage.removeItem('admin_token')
+                        localStorage.removeItem('admin_user')
+                        window.location.href = 'login.html'
+                    }
+                }
+            },
+            watch: {
+                selectedAudiobooks() {
+                    this.selectAll = this.selectedAudiobooks.length === this.audiobooks.length && this.audiobooks.length > 0
+                }
+            }
+        }).mount('#app')
+    </script>
+</body>
+</html>
+
+

+ 684 - 0
book-admin/src/main/resources/static/pages/banners.html

@@ -0,0 +1,684 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统 - 轮播图管理</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: #f5f5f5;
+        }
+        
+        .header {
+            background: white;
+            padding: 20px 30px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        
+        .header-title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        .header-actions {
+            display: flex;
+            gap: 10px;
+        }
+        
+        .btn {
+            padding: 8px 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: opacity 0.3s;
+        }
+        
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+        
+        .btn-danger {
+            background: #f56565;
+            color: white;
+        }
+        
+        .btn-success {
+            background: #48bb78;
+            color: white;
+        }
+        
+        .btn-warning {
+            background: #ed8936;
+            color: white;
+        }
+        
+        .btn:hover {
+            opacity: 0.9;
+        }
+        
+        .btn:disabled {
+            opacity: 0.5;
+            cursor: not-allowed;
+        }
+        
+        .container {
+            max-width: 1400px;
+            margin: 20px auto;
+            padding: 0 20px;
+        }
+        
+        .nav-tabs {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 20px;
+            background: white;
+            padding: 10px;
+            border-radius: 8px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+        }
+        
+        .nav-tab {
+            padding: 12px 24px;
+            border-radius: 6px;
+            background: white;
+            color: #333;
+            text-decoration: none;
+            cursor: pointer;
+            transition: all 0.3s;
+            font-size: 15px;
+            font-weight: 500;
+            border: 2px solid transparent;
+        }
+        
+        .nav-tab:hover {
+            background: #f8f9fa;
+        }
+        
+        .nav-tab.active {
+            background: #667eea;
+            color: white;
+            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+        }
+        
+        .card {
+            background: white;
+            padding: 20px;
+            border-radius: 5px;
+            margin-bottom: 20px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+        }
+        
+        .card h3 {
+            margin-bottom: 15px;
+            color: #333;
+        }
+        
+        .row {
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+            margin-bottom: 15px;
+            align-items: center;
+        }
+        
+        .form-input, .form-select {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-input {
+            min-width: 200px;
+        }
+        
+        .form-select {
+            min-width: 150px;
+        }
+        
+        .table-container {
+            background: white;
+            border-radius: 5px;
+            overflow-x: auto;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+        }
+        
+        table {
+            width: 100%;
+            border-collapse: collapse;
+        }
+        
+        th, td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #eee;
+        }
+        
+        th {
+            background: #f8f9fa;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        tr:hover {
+            background: #f8f9fa;
+        }
+        
+        .badge {
+            padding: 4px 8px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: bold;
+        }
+        
+        .badge.on {
+            background: #c6f6d5;
+            color: #22543d;
+        }
+        
+        .badge.off {
+            background: #fed7d7;
+            color: #742a2a;
+        }
+        
+        .note {
+            color: #6b7280;
+            font-size: 12px;
+            margin-top: 10px;
+        }
+        
+        .action-buttons {
+            display: flex;
+            gap: 5px;
+        }
+        
+        .btn-sm {
+            padding: 4px 8px;
+            font-size: 12px;
+        }
+        
+        .loading {
+            text-align: center;
+            padding: 20px;
+            color: #666;
+        }
+        
+        .empty {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+        }
+        
+        .channel-tabs {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 15px;
+        }
+        
+        .channel-tab {
+            padding: 8px 16px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            background: white;
+            cursor: pointer;
+            transition: all 0.3s;
+        }
+        
+        .channel-tab:hover {
+            background: #f8f9fa;
+        }
+        
+        .channel-tab.active {
+            background: #667eea;
+            color: white;
+            border-color: #667eea;
+        }
+        
+        .preview-img {
+            max-width: 200px;
+            max-height: 100px;
+            margin-top: 10px;
+            border-radius: 5px;
+            border: 1px solid #ddd;
+        }
+        
+        .link-text {
+            display: inline-block;
+            max-width: 300px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            vertical-align: middle;
+        }
+        
+        th:nth-child(4), td:nth-child(4) {
+            max-width: 350px;
+        }
+    </style>
+</head>
+<body>
+    <div id="app">
+        <div class="header">
+            <div class="header-title">轮播图管理</div>
+            <div class="header-actions">
+                <button class="btn btn-danger" @click="logout">退出登录</button>
+            </div>
+        </div>
+        
+        <div class="container">
+            <div class="nav-tabs">
+                <a class="nav-tab" href="books.html">电子书管理</a>
+                <a class="nav-tab" href="audiobooks.html">听书管理</a>
+                <a class="nav-tab" href="rankings.html">排行榜管理</a>
+                <a class="nav-tab active" href="banners.html">轮播图管理</a>
+                <a class="nav-tab" href="users.html">用户管理</a>
+            </div>
+            
+            <!-- 分组管理 -->
+            <div class="card">
+                <h3>轮播图分组管理</h3>
+                <div class="channel-tabs">
+                    <div class="channel-tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">全部</div>
+                    <div class="channel-tab" :class="{ active: currentTab === 'ebook' }" @click="switchTab('ebook')">电子书</div>
+                    <div class="channel-tab" :class="{ active: currentTab === 'audio' }" @click="switchTab('audio')">听书</div>
+                </div>
+                <div class="row">
+                    <select class="form-select" v-model="selectedGroupId" @change="loadBanners" style="width: 300px;">
+                        <option value="">请选择分组</option>
+                        <option v-for="group in filteredGroups" :key="group.id" :value="group.id">
+                            {{ group.name }} ({{ group.code }}) - {{ group.status === 1 ? '启用' : '停用' }}
+                        </option>
+                    </select>
+                    <button class="btn btn-primary" @click="loadGroups">刷新分组</button>
+                </div>
+                <div class="row">
+                    <input type="text" class="form-input" v-model="newGroup.name" placeholder="分组名称,如 首页轮播" style="width: 200px;">
+                    <input type="text" class="form-input" v-model="newGroup.code" placeholder="分组编码,如 home_banner" style="width: 200px;">
+                    <input type="number" class="form-input" v-model.number="newGroup.sort" placeholder="排序" style="width: 100px;">
+                    <select class="form-select" v-model.number="newGroup.status" style="width: 100px;">
+                        <option :value="1">启用</option>
+                        <option :value="0">停用</option>
+                    </select>
+                    <button class="btn btn-success" @click="createGroup">新增分组</button>
+                    <button class="btn btn-danger" @click="deleteGroup" :disabled="!selectedGroupId">删除分组</button>
+                </div>
+                <div class="note">提示:推荐使用频道前缀区分,如电子书使用 ebook_*,听书使用 audio_*;小程序首页可分别读取对应分组 code。</div>
+            </div>
+            
+            <!-- 轮播图管理 -->
+            <div class="card" v-if="selectedGroupId">
+                <h3>{{ isEditing ? '编辑轮播图' : '轮播图管理' }}</h3>
+                <div class="row">
+                    <input type="text" class="form-input" v-model="newBanner.title" placeholder="标题" style="flex: 1;">
+                    <input type="text" class="form-input" v-model="newBanner.image" placeholder="图片URL" style="flex: 2;">
+                    <input type="text" class="form-input" v-model="newBanner.link" placeholder="外链URL(当跳转类型为外链时必填)" style="flex: 2;">
+                </div>
+                <div class="row">
+                    <select class="form-select" v-model="newBanner.targetType" style="width: 150px;">
+                        <option value="">无跳转</option>
+                        <option value="book">书籍</option>
+                        <option value="audiobook">有声书</option>
+                        <option value="url">外链</option>
+                    </select>
+                    <input type="number" class="form-input" v-model.number="newBanner.targetId" placeholder="目标ID(书籍/有声书)" style="width: 150px;">
+                    <input type="number" class="form-input" v-model.number="newBanner.sort" placeholder="排序" style="width: 100px;">
+                    <select class="form-select" v-model.number="newBanner.status" style="width: 100px;">
+                        <option :value="1">启用</option>
+                        <option :value="0">停用</option>
+                    </select>
+                    <button class="btn btn-success" v-if="!isEditing" @click="createBanner">新增轮播图</button>
+                    <button class="btn btn-primary" v-if="isEditing" @click="updateBannerItem">保存修改</button>
+                    <button class="btn btn-warning" v-if="isEditing" @click="cancelEdit">取消编辑</button>
+                </div>
+                <div class="note" style="margin-top: 10px;">
+                    <strong>跳转设置说明:</strong><br>
+                    1. 选择"书籍"或"有声书"时,需要填写对应的目标ID<br>
+                    2. 选择"外链"时,需要填写完整的外链URL(如:https://example.com)<br>
+                    3. 选择"无跳转"时,点击轮播图不会有任何反应
+                </div>
+                <img v-if="newBanner.image" :src="newBanner.image" alt="预览" class="preview-img" @error="handleImageError">
+            </div>
+            
+            <!-- 轮播图列表 -->
+            <div class="table-container" v-if="selectedGroupId">
+                <table>
+                    <thead>
+                        <tr>
+                            <th>ID</th>
+                            <th>标题</th>
+                            <th>图片</th>
+                            <th>跳转</th>
+                            <th>排序</th>
+                            <th>状态</th>
+                            <th>操作</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="loading">
+                            <td colspan="7" class="loading">加载中...</td>
+                        </tr>
+                        <tr v-else-if="banners.length === 0">
+                            <td colspan="7" class="empty">暂无数据</td>
+                        </tr>
+                        <tr v-else v-for="banner in banners" :key="banner.id">
+                            <td>{{ banner.id }}</td>
+                            <td>{{ banner.title || '-' }}</td>
+                            <td><a :href="banner.image" target="_blank" style="color: #667eea;">预览</a></td>
+                            <td>
+                                <span v-if="banner.targetType === 'book'">书籍 #{{ banner.targetId }}</span>
+                                <span v-else-if="banner.targetType === 'audiobook'">有声书 #{{ banner.targetId }}</span>
+                                <span v-else-if="banner.targetType === 'url'">
+                                    外链 🔗 
+                                    <a :href="banner.link" target="_blank" :title="banner.link" style="color: #667eea; font-size: 12px; text-decoration: none;" class="link-text">
+                                        {{ banner.link }}
+                                    </a>
+                                </span>
+                                <span v-else>-</span>
+                            </td>
+                            <td>{{ banner.sort }}</td>
+                            <td>
+                                <span :class="['badge', banner.status === 1 ? 'on' : 'off']">
+                                    {{ banner.status === 1 ? '启用' : '停用' }}
+                                </span>
+                            </td>
+                            <td>
+                                <div class="action-buttons">
+                                    <button class="btn btn-warning btn-sm" @click="editBannerItem(banner)">编辑</button>
+                                    <button class="btn btn-warning btn-sm" @click="toggleBannerStatus(banner)">
+                                        {{ banner.status === 1 ? '停用' : '启用' }}
+                                    </button>
+                                    <button class="btn btn-danger btn-sm" @click="deleteBannerItem(banner.id)">删除</button>
+                                </div>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+    
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <script type="module">
+        import { 
+            getBannerGroups, createBannerGroup, deleteBannerGroup,
+            getBanners, createBanner, updateBanner, deleteBanner
+        } from '../utils/api.js'
+        
+        const { createApp } = Vue
+        
+        const app = createApp({
+            data() {
+                return {
+                    token: localStorage.getItem('admin_token'),
+                    currentTab: 'all',
+                    groups: [],
+                    selectedGroupId: '',
+                    banners: [],
+                    loading: false,
+                    newGroup: {
+                        name: '',
+                        code: '',
+                        sort: 0,
+                        status: 1
+                    },
+                    newBanner: {
+                        title: '',
+                        image: '',
+                        link: '',
+                        targetType: '',
+                        targetId: null,
+                        sort: 0,
+                        status: 1
+                    },
+                    editingBanner: null,
+                    isEditing: false
+                }
+            },
+            computed: {
+                filteredGroups() {
+                    if (this.currentTab === 'ebook') {
+                        return this.groups.filter(g => (g.code || '').startsWith('ebook_'))
+                    } else if (this.currentTab === 'audio') {
+                        return this.groups.filter(g => (g.code || '').startsWith('audio_'))
+                    }
+                    return this.groups
+                }
+            },
+            mounted() {
+                if (!this.token) {
+                    alert('请先登录')
+                    window.location.href = 'login.html'
+                    return
+                }
+                this.loadGroups()
+            },
+            methods: {
+                switchTab(tab) {
+                    this.currentTab = tab
+                    if (this.filteredGroups.length > 0 && !this.selectedGroupId) {
+                        this.selectedGroupId = this.filteredGroups[0].id
+                        this.loadBanners()
+                    }
+                },
+                async loadGroups() {
+                    try {
+                        const res = await getBannerGroups(this.token)
+                        if (res && res.code === 200) {
+                            this.groups = res.data || []
+                            if (this.filteredGroups.length > 0 && !this.selectedGroupId) {
+                                this.selectedGroupId = this.filteredGroups[0].id
+                                this.loadBanners()
+                            }
+                        } else {
+                            alert('加载分组失败: ' + (res.message || res.msg || '未知错误'))
+                        }
+                    } catch (err) {
+                        console.error('加载分组失败:', err)
+                        alert('加载分组失败: ' + (err.message || '未知错误'))
+                    }
+                },
+                async createGroup() {
+                    if (!this.newGroup.name || !this.newGroup.code) {
+                        alert('请输入分组名称和编码')
+                        return
+                    }
+                    try {
+                        const code = this.newGroup.code
+                        await createBannerGroup(this.newGroup, this.token)
+                        alert('创建成功')
+                        if (code.startsWith('ebook_')) {
+                            this.currentTab = 'ebook'
+                        } else if (code.startsWith('audio_')) {
+                            this.currentTab = 'audio'
+                        }
+                        this.newGroup = { name: '', code: '', sort: 0, status: 1 }
+                        this.loadGroups()
+                    } catch (err) {
+                        alert(err.message || '创建失败')
+                    }
+                },
+                async deleteGroup() {
+                    if (!this.selectedGroupId) return
+                    if (!confirm('确定要删除该分组及其下的所有图片吗?')) return
+                    try {
+                        await deleteBannerGroup(this.selectedGroupId, this.token)
+                        alert('删除成功')
+                        this.selectedGroupId = ''
+                        this.banners = []
+                        this.loadGroups()
+                    } catch (err) {
+                        alert(err.message || '删除失败')
+                    }
+                },
+                async loadBanners() {
+                    if (!this.selectedGroupId) return
+                    this.loading = true
+                    try {
+                        const res = await getBanners(this.selectedGroupId, null, this.token)
+                        if (res && res.code === 200) {
+                            this.banners = res.data || []
+                        } else {
+                            alert('加载轮播图失败: ' + (res.message || res.msg || '未知错误'))
+                        }
+                    } catch (err) {
+                        console.error('加载轮播图失败:', err)
+                        alert('加载轮播图失败: ' + (err.message || '未知错误'))
+                    } finally {
+                        this.loading = false
+                    }
+                },
+                async createBanner() {
+                    if (!this.selectedGroupId) {
+                        alert('请先选择分组')
+                        return
+                    }
+                    if (!this.newBanner.image) {
+                        alert('请输入图片URL')
+                        return
+                    }
+                    // 验证跳转设置
+                    if (this.newBanner.targetType === 'url' && !this.newBanner.link) {
+                        alert('选择外链跳转时,必须填写外链URL')
+                        return
+                    }
+                    if ((this.newBanner.targetType === 'book' || this.newBanner.targetType === 'audiobook') && !this.newBanner.targetId) {
+                        alert('选择书籍或有声书跳转时,必须填写目标ID')
+                        return
+                    }
+                    try {
+                        const bannerData = {
+                            groupId: this.selectedGroupId,
+                            title: this.newBanner.title,
+                            image: this.newBanner.image,
+                            link: this.newBanner.link || null,
+                            targetType: this.newBanner.targetType || null,
+                            targetId: this.newBanner.targetId || null,
+                            sort: this.newBanner.sort,
+                            status: this.newBanner.status
+                        }
+                        await createBanner(bannerData, this.token)
+                        alert('创建成功')
+                        this.resetBannerForm()
+                        this.loadBanners()
+                    } catch (err) {
+                        alert(err.message || '创建失败')
+                    }
+                },
+                editBannerItem(banner) {
+                    this.isEditing = true
+                    this.editingBanner = banner
+                    this.newBanner = {
+                        title: banner.title || '',
+                        image: banner.image || '',
+                        link: banner.link || '',
+                        targetType: banner.targetType || '',
+                        targetId: banner.targetId || null,
+                        sort: banner.sort || 0,
+                        status: banner.status !== undefined ? banner.status : 1
+                    }
+                    // 滚动到表单位置
+                    this.$nextTick(() => {
+                        document.querySelector('.card h3').scrollIntoView({ behavior: 'smooth', block: 'start' })
+                    })
+                },
+                cancelEdit() {
+                    this.isEditing = false
+                    this.editingBanner = null
+                    this.resetBannerForm()
+                },
+                async updateBannerItem() {
+                    if (!this.editingBanner || !this.editingBanner.id) {
+                        alert('编辑数据错误')
+                        return
+                    }
+                    if (!this.newBanner.image) {
+                        alert('请输入图片URL')
+                        return
+                    }
+                    // 验证跳转设置
+                    if (this.newBanner.targetType === 'url' && !this.newBanner.link) {
+                        alert('选择外链跳转时,必须填写外链URL')
+                        return
+                    }
+                    if ((this.newBanner.targetType === 'book' || this.newBanner.targetType === 'audiobook') && !this.newBanner.targetId) {
+                        alert('选择书籍或有声书跳转时,必须填写目标ID')
+                        return
+                    }
+                    try {
+                        const bannerData = {
+                            title: this.newBanner.title,
+                            image: this.newBanner.image,
+                            link: this.newBanner.link || null,
+                            targetType: this.newBanner.targetType || null,
+                            targetId: this.newBanner.targetId || null,
+                            sort: this.newBanner.sort,
+                            status: this.newBanner.status
+                        }
+                        await updateBanner(this.editingBanner.id, bannerData, this.token)
+                        alert('更新成功')
+                        this.isEditing = false
+                        this.editingBanner = null
+                        this.resetBannerForm()
+                        this.loadBanners()
+                    } catch (err) {
+                        alert(err.message || '更新失败')
+                    }
+                },
+                resetBannerForm() {
+                    this.newBanner = { title: '', image: '', link: '', targetType: '', targetId: null, sort: 0, status: 1 }
+                },
+                async toggleBannerStatus(banner) {
+                    try {
+                        const newStatus = banner.status === 1 ? 0 : 1
+                        await updateBanner(banner.id, { status: newStatus }, this.token)
+                        alert('更新成功')
+                        this.loadBanners()
+                    } catch (err) {
+                        alert(err.message || '更新失败')
+                    }
+                },
+                async deleteBannerItem(id) {
+                    if (!confirm('确定要删除该轮播图吗?')) return
+                    try {
+                        await deleteBanner(id, this.token)
+                        alert('删除成功')
+                        this.loadBanners()
+                    } catch (err) {
+                        alert(err.message || '删除失败')
+                    }
+                },
+                handleImageError(event) {
+                    event.target.style.display = 'none'
+                },
+                logout() {
+                    if (confirm('确定要退出登录吗?')) {
+                        localStorage.removeItem('admin_token')
+                        localStorage.removeItem('admin_user')
+                        window.location.href = 'login.html'
+                    }
+                }
+            }
+        })
+        
+        app.mount('#app')
+    </script>
+</body>
+</html>
+
+
+
+
+

+ 677 - 0
book-admin/src/main/resources/static/pages/books.html

@@ -0,0 +1,677 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统 - 书籍管理</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: #f5f5f5;
+        }
+        
+        .header {
+            background: white;
+            padding: 20px 30px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        
+        .header-title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        .header-actions {
+            display: flex;
+            gap: 10px;
+        }
+        
+        .btn {
+            padding: 8px 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: opacity 0.3s;
+        }
+        
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+        
+        .btn-danger {
+            background: #f56565;
+            color: white;
+        }
+        
+        .btn-success {
+            background: #48bb78;
+            color: white;
+        }
+        
+        .btn-warning {
+            background: #ed8936;
+            color: white;
+        }
+        
+        .btn:hover {
+            opacity: 0.9;
+        }
+        
+        .container {
+            max-width: 1200px;
+            margin: 20px auto;
+            padding: 0 20px;
+        }
+        
+        .nav-tabs {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 20px;
+        }
+        
+        .nav-tab {
+            padding: 10px 18px;
+            border-radius: 6px;
+            background: white;
+            color: #333;
+            text-decoration: none;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+            transition: all 0.3s;
+            font-size: 14px;
+        }
+        
+        .nav-tab:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 10px rgba(0,0,0,0.12);
+        }
+        
+        .nav-tab.active {
+            background: #667eea;
+            color: white;
+            box-shadow: 0 6px 14px rgba(102, 126, 234, 0.35);
+        }
+        
+        .toolbar {
+            background: white;
+            padding: 20px;
+            border-radius: 5px;
+            margin-bottom: 20px;
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+            align-items: center;
+        }
+        
+        .search-input {
+            flex: 1;
+            min-width: 200px;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-select {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .table-container {
+            background: white;
+            border-radius: 5px;
+            overflow: hidden;
+        }
+        
+        table {
+            width: 100%;
+            border-collapse: collapse;
+        }
+        
+        th, td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #eee;
+        }
+        
+        th {
+            background: #f8f9fa;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        tr:hover {
+            background: #f8f9fa;
+        }
+        
+        .status-badge {
+            padding: 4px 8px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: bold;
+        }
+        
+        .status-online {
+            background: #c6f6d5;
+            color: #22543d;
+        }
+        
+        .status-offline {
+            background: #fed7d7;
+            color: #742a2a;
+        }
+        
+        .action-buttons {
+            display: flex;
+            gap: 5px;
+        }
+        
+        .btn-sm {
+            padding: 4px 8px;
+            font-size: 12px;
+        }
+        
+        .pagination {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            gap: 10px;
+            margin-top: 20px;
+            padding: 20px;
+        }
+        
+        .modal {
+            display: none;
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background: rgba(0, 0, 0, 0.5);
+            z-index: 1000;
+        }
+        
+        .modal.show {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+        
+        .modal-content {
+            background: white;
+            border-radius: 5px;
+            padding: 30px;
+            max-width: 600px;
+            width: 90%;
+            max-height: 80vh;
+            overflow-y: auto;
+        }
+        
+        .modal-header {
+            font-size: 20px;
+            font-weight: bold;
+            margin-bottom: 20px;
+        }
+        
+        .form-group {
+            margin-bottom: 15px;
+        }
+        
+        .form-label {
+            display: block;
+            margin-bottom: 5px;
+            color: #666;
+            font-size: 14px;
+        }
+        
+        .form-input, .form-textarea {
+            width: 100%;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-textarea {
+            min-height: 100px;
+            resize: vertical;
+        }
+        
+        .modal-actions {
+            display: flex;
+            justify-content: flex-end;
+            gap: 10px;
+            margin-top: 20px;
+        }
+        
+        .loading {
+            text-align: center;
+            padding: 20px;
+            color: #666;
+        }
+        
+        .empty {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+        }
+        
+        img {
+            max-width: 50px;
+            max-height: 70px;
+            object-fit: cover;
+        }
+    </style>
+</head>
+<body>
+    <div id="app">
+        <div class="header">
+            <div class="header-title">书籍管理</div>
+            <div class="header-actions">
+                <button class="btn btn-primary" @click="showAddModal">添加书籍</button>
+                <button class="btn btn-danger" @click="logout">退出登录</button>
+            </div>
+        </div>
+        
+        <div class="container">
+            <div class="nav-tabs">
+                <a class="nav-tab active" href="books.html">电子书管理</a>
+                <a class="nav-tab" href="audiobooks.html">听书管理</a>
+                <a class="nav-tab" href="rankings.html">排行榜管理</a>
+                <a class="nav-tab" href="banners.html">轮播图管理</a>
+                <a class="nav-tab" href="users.html">用户管理</a>
+            </div>
+            
+            <div class="toolbar">
+                <input type="text" class="search-input" v-model="searchKeyword" placeholder="搜索书名、作者..." @input="handleSearch">
+                <select class="form-select" v-model="filterStatus" @change="loadBooks" style="width: 150px;">
+                    <option value="">全部状态</option>
+                    <option value="1">上架</option>
+                    <option value="0">下架</option>
+                </select>
+                <select class="form-select" v-model="filterCategory" @change="loadBooks" style="width: 150px;">
+                    <option value="">全部分类</option>
+                    <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+                </select>
+                <button class="btn btn-primary" @click="loadBooks">查询</button>
+                <button class="btn btn-danger" @click="batchDelete" :disabled="selectedBooks.length === 0">批量删除</button>
+                <button class="btn btn-success" @click="batchPublish" :disabled="selectedBooks.length === 0">批量上架</button>
+                <button class="btn btn-warning" @click="batchUnpublish" :disabled="selectedBooks.length === 0">批量下架</button>
+            </div>
+            
+            <div class="table-container">
+                <table>
+                    <thead>
+                        <tr>
+                            <th><input type="checkbox" @change="toggleSelectAll" v-model="selectAll"></th>
+                            <th>ID</th>
+                            <th>封面</th>
+                            <th>书名</th>
+                            <th>作者</th>
+                            <th>分类</th>
+                            <th>价格</th>
+                            <th>状态</th>
+                            <th>操作</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="loading">
+                            <td colspan="9" class="loading">加载中...</td>
+                        </tr>
+                        <tr v-else-if="books.length === 0">
+                            <td colspan="9" class="empty">暂无数据</td>
+                        </tr>
+                        <tr v-else v-for="book in books" :key="book.id">
+                            <td><input type="checkbox" :value="book.id" v-model="selectedBooks"></td>
+                            <td>{{ book.id }}</td>
+                            <td><img :src="book.image || book.cover" alt="" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'50\' height=\'70\'%3E%3Crect width=\'50\' height=\'70\' fill=\'%23ddd\'/%3E%3Ctext x=\'50%25\' y=\'50%25\' text-anchor=\'middle\' dy=\'.3em\' fill=\'%23999\'%3E无图%3C/text%3E%3C/svg%3E'"></td>
+                            <td>{{ book.title }}</td>
+                            <td>{{ book.author }}</td>
+                            <td>{{ book.categoryName || '-' }}</td>
+                            <td>{{ book.price }}元</td>
+                            <td>
+                                <span :class="['status-badge', book.status === 1 ? 'status-online' : 'status-offline']">
+                                    {{ book.status === 1 ? '上架' : '下架' }}
+                                </span>
+                            </td>
+                            <td>
+                                <div class="action-buttons">
+                                    <button class="btn btn-primary btn-sm" @click="editBook(book)">编辑</button>
+                                    <button v-if="book.status === 1" class="btn btn-warning btn-sm" @click="unpublishBook(book.id)">下架</button>
+                                    <button v-else class="btn btn-success btn-sm" @click="publishBook(book.id)">上架</button>
+                                    <button class="btn btn-danger btn-sm" @click="deleteBook(book.id)">删除</button>
+                                </div>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+            
+            <div class="pagination">
+                <button class="btn" @click="prevPage" :disabled="page === 1">上一页</button>
+                <span>第 {{ page }} 页,共 {{ totalPages }} 页,共 {{ total }} 条</span>
+                <button class="btn" @click="nextPage" :disabled="page >= totalPages">下一页</button>
+            </div>
+        </div>
+        
+        <!-- 编辑/添加书籍模态框 -->
+        <div class="modal" :class="{ show: showModal }" @click.self="closeModal">
+            <div class="modal-content">
+                <div class="modal-header">{{ editingBook.id ? '编辑书籍' : '添加书籍' }}</div>
+                <form @submit.prevent="saveBook">
+                    <div class="form-group">
+                        <label class="form-label">书名 *</label>
+                        <input type="text" class="form-input" v-model="editingBook.title" required>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">作者</label>
+                        <input type="text" class="form-input" v-model="editingBook.author">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">封面URL</label>
+                        <input type="text" class="form-input" v-model="editingBook.cover">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">图片URL</label>
+                        <input type="text" class="form-input" v-model="editingBook.image">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">简介</label>
+                        <textarea class="form-textarea" v-model="editingBook.brief"></textarea>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">描述</label>
+                        <textarea class="form-textarea" v-model="editingBook.desc"></textarea>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">详细介绍</label>
+                        <textarea class="form-textarea" v-model="editingBook.introduction"></textarea>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">价格</label>
+                        <input type="number" class="form-input" v-model.number="editingBook.price" step="0.01" min="0">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">是否免费</label>
+                        <select class="form-select" v-model="editingBook.isFree">
+                            <option :value="false">否</option>
+                            <option :value="true">是</option>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">是否VIP</label>
+                        <select class="form-select" v-model="editingBook.isVip">
+                            <option :value="false">否</option>
+                            <option :value="true">是</option>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">分类</label>
+                        <select class="form-select" v-model="editingBook.categoryId">
+                            <option value="">请选择分类</option>
+                            <option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">状态 *</label>
+                        <select class="form-select" v-model="editingBook.status" required>
+                            <option value="1">上架</option>
+                            <option value="0">下架</option>
+                        </select>
+                    </div>
+                    <div class="modal-actions">
+                        <button type="button" class="btn" @click="closeModal">取消</button>
+                        <button type="submit" class="btn btn-primary">保存</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+    
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <script type="module">
+        import { getAdminBooks, createAdminBook, updateAdminBook, deleteAdminBook, deleteAdminBooks, publishAdminBook, unpublishAdminBook, publishAdminBooks, unpublishAdminBooks, getAllCategories } from '../utils/api.js'
+        
+        const { createApp } = Vue
+        
+        createApp({
+            data() {
+                return {
+                    token: localStorage.getItem('admin_token'),
+                    books: [],
+                    categories: [],
+                    loading: false,
+                    page: 1,
+                    pageSize: 10,
+                    total: 0,
+                    searchKeyword: '',
+                    filterStatus: '',
+                    filterCategory: '',
+                    selectedBooks: [],
+                    selectAll: false,
+                    showModal: false,
+                    editingBook: {
+                        id: null,
+                        title: '',
+                        author: '',
+                        cover: '',
+                        image: '',
+                        brief: '',
+                        desc: '',
+                        introduction: '',
+                        price: 0,
+                        isFree: false,
+                        isVip: false,
+                        categoryId: null,
+                        status: 1
+                    },
+                    searchTimer: null
+                }
+            },
+            computed: {
+                totalPages() {
+                    return Math.ceil(this.total / this.pageSize)
+                }
+            },
+            mounted() {
+                if (!this.token) {
+                    window.location.href = 'login.html'
+                    return
+                }
+                this.loadCategories()
+                this.loadBooks()
+            },
+            methods: {
+                async loadCategories() {
+                    try {
+                        const res = await getAllCategories('')
+                        if (res && res.code === 200) {
+                            this.categories = res.data || []
+                        }
+                    } catch (err) {
+                        console.error('加载分类失败:', err)
+                    }
+                },
+                async loadBooks() {
+                    this.loading = true
+                    try {
+                        const params = {
+                            page: this.page,
+                            size: this.pageSize,
+                            keyword: this.searchKeyword,
+                            status: this.filterStatus || undefined,
+                            categoryId: this.filterCategory || undefined
+                        }
+                        const res = await getAdminBooks(params, this.token)
+                        if (res.code === 200 && res.data) {
+                            this.books = res.data.list || []
+                            this.total = res.data.total || 0
+                        }
+                    } catch (err) {
+                        alert(err.message || '加载书籍失败')
+                    } finally {
+                        this.loading = false
+                    }
+                },
+                handleSearch() {
+                    clearTimeout(this.searchTimer)
+                    this.searchTimer = setTimeout(() => {
+                        this.page = 1
+                        this.loadBooks()
+                    }, 500)
+                },
+                prevPage() {
+                    if (this.page > 1) {
+                        this.page--
+                        this.loadBooks()
+                    }
+                },
+                nextPage() {
+                    if (this.page < this.totalPages) {
+                        this.page++
+                        this.loadBooks()
+                    }
+                },
+                toggleSelectAll() {
+                    if (this.selectAll) {
+                        this.selectedBooks = this.books.map(b => b.id)
+                    } else {
+                        this.selectedBooks = []
+                    }
+                },
+                showAddModal() {
+                    this.editingBook = {
+                        id: null,
+                        title: '',
+                        author: '',
+                        cover: '',
+                        image: '',
+                        brief: '',
+                        desc: '',
+                        introduction: '',
+                        price: 0,
+                        isFree: false,
+                        isVip: false,
+                        categoryId: null,
+                        status: 1
+                    }
+                    this.showModal = true
+                },
+                editBook(book) {
+                    this.editingBook = { ...book }
+                    this.showModal = true
+                },
+                closeModal() {
+                    this.showModal = false
+                },
+                async saveBook() {
+                    try {
+                        if (this.editingBook.id) {
+                            await updateAdminBook(this.editingBook.id, this.editingBook, this.token)
+                            alert('更新成功')
+                        } else {
+                            await createAdminBook(this.editingBook, this.token)
+                            alert('创建成功')
+                        }
+                        this.closeModal()
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '保存失败')
+                    }
+                },
+                async deleteBook(id) {
+                    if (!confirm('确定要删除这本书吗?')) return
+                    try {
+                        await deleteAdminBook(id, this.token)
+                        alert('删除成功')
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '删除失败')
+                    }
+                },
+                async batchDelete() {
+                    if (this.selectedBooks.length === 0) return
+                    if (!confirm(`确定要删除选中的 ${this.selectedBooks.length} 本书吗?`)) return
+                    try {
+                        await deleteAdminBooks(this.selectedBooks, this.token)
+                        alert('批量删除成功')
+                        this.selectedBooks = []
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '批量删除失败')
+                    }
+                },
+                async publishBook(id) {
+                    try {
+                        await publishAdminBook(id, this.token)
+                        alert('上架成功')
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '上架失败')
+                    }
+                },
+                async unpublishBook(id) {
+                    try {
+                        await unpublishAdminBook(id, this.token)
+                        alert('下架成功')
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '下架失败')
+                    }
+                },
+                async batchPublish() {
+                    if (this.selectedBooks.length === 0) return
+                    try {
+                        await publishAdminBooks(this.selectedBooks, this.token)
+                        alert('批量上架成功')
+                        this.selectedBooks = []
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '批量上架失败')
+                    }
+                },
+                async batchUnpublish() {
+                    if (this.selectedBooks.length === 0) return
+                    try {
+                        await unpublishAdminBooks(this.selectedBooks, this.token)
+                        alert('批量下架成功')
+                        this.selectedBooks = []
+                        this.loadBooks()
+                    } catch (err) {
+                        alert(err.message || '批量下架失败')
+                    }
+                },
+                logout() {
+                    if (confirm('确定要退出登录吗?')) {
+                        localStorage.removeItem('admin_token')
+                        localStorage.removeItem('admin_user')
+                        window.location.href = 'login.html'
+                    }
+                }
+            },
+            watch: {
+                selectedBooks() {
+                    this.selectAll = this.selectedBooks.length === this.books.length && this.books.length > 0
+                }
+            }
+        }).mount('#app')
+    </script>
+</body>
+</html>
+
+
+
+

+ 199 - 0
book-admin/src/main/resources/static/pages/login.html

@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统 - 登录</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            padding: 20px;
+        }
+        
+        .login-container {
+            background: white;
+            border-radius: 10px;
+            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+            padding: 40px;
+            width: 100%;
+            max-width: 400px;
+        }
+        
+        .login-title {
+            font-size: 28px;
+            font-weight: bold;
+            text-align: center;
+            margin-bottom: 30px;
+            color: #333;
+        }
+        
+        .form-group {
+            margin-bottom: 20px;
+        }
+        
+        .form-label {
+            display: block;
+            margin-bottom: 8px;
+            color: #666;
+            font-size: 14px;
+        }
+        
+        .form-input {
+            width: 100%;
+            padding: 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+            transition: border-color 0.3s;
+        }
+        
+        .form-input:focus {
+            outline: none;
+            border-color: #667eea;
+        }
+        
+        .login-btn {
+            width: 100%;
+            padding: 12px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            border: none;
+            border-radius: 5px;
+            font-size: 16px;
+            cursor: pointer;
+            transition: opacity 0.3s;
+            margin-top: 10px;
+        }
+        
+        .login-btn:hover {
+            opacity: 0.9;
+        }
+        
+        .login-btn:disabled {
+            opacity: 0.6;
+            cursor: not-allowed;
+        }
+        
+        .error-message {
+            color: #f56565;
+            font-size: 14px;
+            margin-top: 10px;
+            text-align: left;
+            white-space: pre-line;
+            background: #fee;
+            padding: 10px;
+            border-radius: 5px;
+            border: 1px solid #fcc;
+        }
+        
+        .loading {
+            text-align: center;
+            color: #666;
+            margin-top: 10px;
+        }
+    </style>
+</head>
+<body>
+    <div class="login-container">
+        <h1 class="login-title">后台管理系统</h1>
+        <form id="loginForm">
+            <div class="form-group">
+                <label class="form-label">用户名</label>
+                <input type="text" class="form-input" id="username" placeholder="请输入用户名" required>
+            </div>
+            <div class="form-group">
+                <label class="form-label">密码</label>
+                <input type="password" class="form-input" id="password" placeholder="请输入密码" required>
+            </div>
+            <button type="submit" class="login-btn" id="loginBtn">登录</button>
+            <div id="errorMessage" class="error-message" style="display: none;"></div>
+            <div id="loading" class="loading" style="display: none;">登录中...</div>
+        </form>
+    </div>
+    
+    <script type="module">
+        import { adminLogin } from '../utils/api.js'
+        
+        const loginForm = document.getElementById('loginForm')
+        const usernameInput = document.getElementById('username')
+        const passwordInput = document.getElementById('password')
+        const loginBtn = document.getElementById('loginBtn')
+        const errorMessage = document.getElementById('errorMessage')
+        const loading = document.getElementById('loading')
+        
+        loginForm.addEventListener('submit', async (e) => {
+            e.preventDefault()
+            
+            const username = usernameInput.value.trim()
+            const password = passwordInput.value.trim()
+            
+            if (!username || !password) {
+                showError('请输入用户名和密码')
+                return
+            }
+            
+            // 显示加载状态
+            loginBtn.disabled = true
+            loading.style.display = 'block'
+            errorMessage.style.display = 'none'
+            
+            try {
+                const res = await adminLogin(username, password)
+                
+                if (res.code === 200 && res.data) {
+                    // 保存token和用户信息
+                    localStorage.setItem('admin_token', res.data.token)
+                    localStorage.setItem('admin_user', JSON.stringify(res.data))
+                    
+                    // 跳转到书籍管理页面
+                    window.location.href = 'books.html'
+                } else {
+                    showError(res.message || '登录失败')
+                }
+            } catch (err) {
+                console.error('登录错误:', err)
+                // 显示详细的错误信息
+                let errorMsg = err.message || '登录失败,请检查网络连接'
+                // 如果是连接错误,提供更详细的提示
+                if (errorMsg.includes('无法连接到后端服务')) {
+                    errorMsg = errorMsg + '\n\n请确保:\n1. 后端服务已启动(运行 BookApplication.java)\n2. 后端服务运行在 http://localhost:8001\n3. 浏览器控制台查看详细错误信息'
+                }
+                showError(errorMsg)
+            } finally {
+                loginBtn.disabled = false
+                loading.style.display = 'none'
+            }
+        })
+        
+        function showError(message) {
+            errorMessage.textContent = message
+            errorMessage.style.display = 'block'
+        }
+        
+        // 检查是否已登录
+        const token = localStorage.getItem('admin_token')
+        if (token) {
+            window.location.href = 'books.html'
+        }
+    </script>
+</body>
+</html>
+
+
+
+
+
+
+
+

+ 644 - 0
book-admin/src/main/resources/static/pages/rankings.html

@@ -0,0 +1,644 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统 - 排行榜管理</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: #f5f5f5;
+        }
+        
+        .header {
+            background: white;
+            padding: 20px 30px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        
+        .header-title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        .header-actions {
+            display: flex;
+            gap: 10px;
+        }
+        
+        .btn {
+            padding: 8px 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: opacity 0.3s;
+        }
+        
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+        
+        .btn-success {
+            background: #48bb78;
+            color: white;
+        }
+        
+        .btn-danger {
+            background: #f56565;
+            color: white;
+        }
+        
+        .btn:hover {
+            opacity: 0.9;
+        }
+        
+        .container {
+            max-width: 1400px;
+            margin: 20px auto;
+            padding: 0 20px;
+        }
+        
+        .nav-tabs {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 20px;
+        }
+        
+        .nav-tab {
+            padding: 10px 18px;
+            border-radius: 6px;
+            background: white;
+            color: #333;
+            text-decoration: none;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+            transition: all 0.3s;
+            font-size: 14px;
+        }
+        
+        .nav-tab:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 10px rgba(0,0,0,0.12);
+        }
+        
+        .nav-tab.active {
+            background: #667eea;
+            color: white;
+        }
+        
+        .filter-bar {
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            margin-bottom: 20px;
+            display: flex;
+            gap: 15px;
+            align-items: center;
+            flex-wrap: wrap;
+        }
+        
+        .filter-item {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+        }
+        
+        .filter-item label {
+            font-size: 14px;
+            color: #666;
+            white-space: nowrap;
+        }
+        
+        .filter-item select {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+            min-width: 150px;
+        }
+        
+        .book-list {
+            background: white;
+            border-radius: 8px;
+            padding: 20px;
+        }
+        
+        .book-item {
+            display: flex;
+            align-items: center;
+            padding: 15px;
+            border: 1px solid #eee;
+            border-radius: 8px;
+            margin-bottom: 10px;
+            cursor: move;
+            transition: all 0.3s;
+            background: white;
+        }
+        
+        .book-item:hover {
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+            transform: translateY(-2px);
+        }
+        
+        .book-item.dragging {
+            opacity: 0.5;
+        }
+        
+        .drag-handle {
+            cursor: move;
+            color: #999;
+            font-size: 20px;
+            margin-right: 15px;
+            user-select: none;
+        }
+        
+        .rank-number {
+            width: 40px;
+            height: 40px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #667eea;
+            color: white;
+            border-radius: 50%;
+            font-weight: bold;
+            margin-right: 15px;
+            flex-shrink: 0;
+        }
+        
+        .book-cover {
+            width: 80px;
+            height: 110px;
+            object-fit: cover;
+            border-radius: 5px;
+            margin-right: 15px;
+            flex-shrink: 0;
+            background: #f0f0f0;
+        }
+        
+        .book-info {
+            flex: 1;
+            min-width: 0;
+        }
+        
+        .book-title {
+            font-size: 16px;
+            font-weight: bold;
+            color: #333;
+            margin-bottom: 5px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+        
+        .book-author {
+            font-size: 14px;
+            color: #666;
+            margin-bottom: 5px;
+        }
+        
+        .book-category {
+            font-size: 12px;
+            color: #999;
+        }
+        
+        .book-actions {
+            display: flex;
+            gap: 10px;
+            margin-left: 15px;
+        }
+        
+        .btn-small {
+            padding: 6px 12px;
+            font-size: 12px;
+        }
+        
+        .empty-state {
+            text-align: center;
+            padding: 60px 20px;
+            color: #999;
+        }
+        
+        .empty-state-icon {
+            font-size: 48px;
+            margin-bottom: 15px;
+        }
+        
+        .loading {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+        }
+        
+        .message {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            padding: 12px 20px;
+            border-radius: 5px;
+            color: white;
+            z-index: 1000;
+            animation: slideIn 0.3s;
+        }
+        
+        .message.success {
+            background: #48bb78;
+        }
+        
+        .message.error {
+            background: #f56565;
+        }
+        
+        @keyframes slideIn {
+            from {
+                transform: translateX(100%);
+                opacity: 0;
+            }
+            to {
+                transform: translateX(0);
+                opacity: 1;
+            }
+        }
+    </style>
+</head>
+<body>
+    <div class="header">
+        <div class="header-title">排行榜管理</div>
+        <div class="header-actions">
+            <button class="btn btn-success" onclick="saveSort()">保存排序</button>
+            <button class="btn btn-primary" onclick="showAddBookModal()">添加书籍</button>
+            <a href="books.html" class="btn btn-primary">返回书籍管理</a>
+        </div>
+    </div>
+
+    <div class="container">
+        <div class="nav-tabs">
+            <a href="books.html" class="nav-tab">电子书管理</a>
+            <a href="audiobooks.html" class="nav-tab">听书管理</a>
+            <a href="rankings.html" class="nav-tab active">排行榜管理</a>
+            <a href="banners.html" class="nav-tab">轮播图管理</a>
+            <a href="users.html" class="nav-tab">用户管理</a>
+        </div>
+
+        <div class="filter-bar">
+            <div class="filter-item">
+                <label>排行榜类型:</label>
+                <select id="rankingGroupSelect" onchange="loadRankingItems()">
+                    <option value="">请选择...</option>
+                </select>
+            </div>
+            <div class="filter-item">
+                <label>分类筛选:</label>
+                <select id="categorySelect" onchange="loadRankingItems()">
+                    <option value="">全部分类</option>
+                </select>
+            </div>
+            <div class="filter-item">
+                <label>状态:</label>
+                <select id="statusSelect" onchange="loadRankingItems()">
+                    <option value="1">启用</option>
+                    <option value="0">禁用</option>
+                    <option value="">全部</option>
+                </select>
+            </div>
+        </div>
+
+        <div class="book-list" id="bookList">
+            <div class="loading">请选择排行榜类型...</div>
+        </div>
+    </div>
+
+    <!-- 添加书籍模态框 -->
+    <div id="addBookModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center;">
+        <div style="background: white; padding: 30px; border-radius: 8px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto;">
+            <h2 style="margin-bottom: 20px;">添加书籍到排行榜</h2>
+            <div style="margin-bottom: 15px;">
+                <label>搜索书籍:</label>
+                <input type="text" id="bookSearchInput" placeholder="输入书名或作者" style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ddd; border-radius: 5px;">
+                <button class="btn btn-primary" onclick="searchBooks()" style="margin-top: 10px; width: 100%;">搜索</button>
+            </div>
+            <div id="searchResults" style="max-height: 400px; overflow-y: auto;"></div>
+            <div style="margin-top: 20px; text-align: right;">
+                <button class="btn" onclick="closeAddBookModal()" style="background: #ccc; color: #333;">取消</button>
+            </div>
+        </div>
+    </div>
+
+    <script type="module">
+        import { 
+            getRankingGroups, 
+            getRankingItemsWithBooks, 
+            batchUpdateRankingSort,
+            addBookToRanking,
+            removeBookFromRanking,
+            getAllCategories,
+            getAdminBooks
+        } from '../utils/api.js';
+
+        let token = localStorage.getItem('admin_token') || '';
+        let currentGroupId = null;
+        let currentCategoryId = null;
+        let currentStatus = 1;
+        let rankingItems = [];
+        let draggedElement = null;
+
+        // 初始化
+        async function init() {
+            if (!token) {
+                alert('请先登录');
+                window.location.href = 'login.html';
+                return;
+            }
+
+            await loadRankingGroups();
+            await loadCategories();
+        }
+
+        // 加载排行榜组
+        async function loadRankingGroups() {
+            try {
+                const res = await getRankingGroups(token);
+                if (res && res.code === 200 && res.data) {
+                    const select = document.getElementById('rankingGroupSelect');
+                    select.innerHTML = '<option value="">请选择...</option>';
+                    res.data.forEach(group => {
+                        const option = document.createElement('option');
+                        option.value = group.id;
+                        option.textContent = group.name;
+                        select.appendChild(option);
+                    });
+                }
+            } catch (err) {
+                showMessage('加载排行榜组失败: ' + err.message, 'error');
+            }
+        }
+
+        // 加载分类
+        async function loadCategories() {
+            try {
+                const res = await getAllCategories(token);
+                if (res && res.code === 200 && res.data) {
+                    const select = document.getElementById('categorySelect');
+                    res.data.forEach(category => {
+                        const option = document.createElement('option');
+                        option.value = category.id;
+                        option.textContent = category.name;
+                        select.appendChild(option);
+                    });
+                }
+            } catch (err) {
+                console.error('加载分类失败:', err);
+            }
+        }
+
+        // 加载排行榜项
+        window.loadRankingItems = async function() {
+            const groupSelect = document.getElementById('rankingGroupSelect');
+            const categorySelect = document.getElementById('categorySelect');
+            const statusSelect = document.getElementById('statusSelect');
+
+            currentGroupId = groupSelect.value ? parseInt(groupSelect.value) : null;
+            currentCategoryId = categorySelect.value ? parseInt(categorySelect.value) : null;
+            currentStatus = statusSelect.value ? parseInt(statusSelect.value) : null;
+
+            if (!currentGroupId) {
+                document.getElementById('bookList').innerHTML = '<div class="loading">请选择排行榜类型...</div>';
+                return;
+            }
+
+            document.getElementById('bookList').innerHTML = '<div class="loading">加载中...</div>';
+
+            try {
+                const res = await getRankingItemsWithBooks(currentGroupId, currentCategoryId, currentStatus, token);
+                if (res && res.code === 200 && res.data) {
+                    rankingItems = res.data;
+                    renderBookList();
+                } else {
+                    document.getElementById('bookList').innerHTML = '<div class="empty-state"><div class="empty-state-icon">📚</div><div>暂无数据</div></div>';
+                }
+            } catch (err) {
+                showMessage('加载失败: ' + err.message, 'error');
+                document.getElementById('bookList').innerHTML = '<div class="empty-state"><div class="empty-state-icon">❌</div><div>加载失败</div></div>';
+            }
+        };
+
+        // 渲染书籍列表
+        function renderBookList() {
+            const container = document.getElementById('bookList');
+            if (rankingItems.length === 0) {
+                container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📚</div><div>暂无书籍,点击"添加书籍"按钮添加</div></div>';
+                return;
+            }
+
+            container.innerHTML = rankingItems.map((item, index) => `
+                <div class="book-item" draggable="true" data-id="${item.id}" data-index="${index}"
+                     ondragstart="handleDragStart(event)" 
+                     ondragend="handleDragEnd(event)"
+                     ondragover="handleDragOver(event)"
+                     ondrop="handleDrop(event)">
+                    <div class="drag-handle">☰</div>
+                    <div class="rank-number">${index + 1}</div>
+                    <img class="book-cover" src="${item.bookCover || 'https://via.placeholder.com/80x110'}" alt="${item.bookTitle}" onerror="this.src='https://via.placeholder.com/80x110'">
+                    <div class="book-info">
+                        <div class="book-title">${item.bookTitle || '未知'}</div>
+                        <div class="book-author">${item.bookAuthor || '未知作者'}</div>
+                        <div class="book-category">${item.categoryName || '全部分类'}</div>
+                    </div>
+                    <div class="book-actions">
+                        <button class="btn btn-danger btn-small" onclick="removeBook(${item.id})">移除</button>
+                    </div>
+                </div>
+            `).join('');
+        }
+
+        // 拖拽处理
+        window.handleDragStart = function(e) {
+            draggedElement = e.target;
+            e.target.classList.add('dragging');
+            e.dataTransfer.effectAllowed = 'move';
+        };
+
+        window.handleDragEnd = function(e) {
+            e.target.classList.remove('dragging');
+            draggedElement = null;
+        };
+
+        window.handleDragOver = function(e) {
+            e.preventDefault();
+            e.dataTransfer.dropEffect = 'move';
+        };
+
+        window.handleDrop = function(e) {
+            e.preventDefault();
+            if (!draggedElement) return;
+
+            const targetItem = e.target.closest('.book-item');
+            if (!targetItem || targetItem === draggedElement) return;
+
+            const draggedIndex = parseInt(draggedElement.dataset.index);
+            const targetIndex = parseInt(targetItem.dataset.index);
+
+            // 更新数组
+            const [removed] = rankingItems.splice(draggedIndex, 1);
+            rankingItems.splice(targetIndex, 0, removed);
+
+            // 重新渲染
+            renderBookList();
+        };
+
+        // 保存排序
+        window.saveSort = async function() {
+            if (!currentGroupId) {
+                showMessage('请先选择排行榜类型', 'error');
+                return;
+            }
+
+            if (rankingItems.length === 0) {
+                showMessage('没有需要保存的数据', 'error');
+                return;
+            }
+
+            const sortData = {
+                groupId: currentGroupId,
+                categoryId: currentCategoryId,
+                items: rankingItems.map((item, index) => ({
+                    id: item.id,
+                    bookId: item.bookId,
+                    sort: index + 1
+                }))
+            };
+
+            try {
+                const res = await batchUpdateRankingSort(sortData, token);
+                if (res && res.code === 200) {
+                    showMessage('排序保存成功', 'success');
+                } else {
+                    showMessage('保存失败: ' + (res.message || '未知错误'), 'error');
+                }
+            } catch (err) {
+                showMessage('保存失败: ' + err.message, 'error');
+            }
+        };
+
+        // 移除书籍
+        window.removeBook = async function(itemId) {
+            if (!confirm('确定要移除此书籍吗?')) return;
+
+            try {
+                const res = await removeBookFromRanking(itemId, token);
+                if (res && res.code === 200) {
+                    showMessage('移除成功', 'success');
+                    await loadRankingItems();
+                } else {
+                    showMessage('移除失败: ' + (res.message || '未知错误'), 'error');
+                }
+            } catch (err) {
+                showMessage('移除失败: ' + err.message, 'error');
+            }
+        };
+
+        // 显示添加书籍模态框
+        window.showAddBookModal = function() {
+            if (!currentGroupId) {
+                showMessage('请先选择排行榜类型', 'error');
+                return;
+            }
+            document.getElementById('addBookModal').style.display = 'flex';
+        };
+
+        // 关闭添加书籍模态框
+        window.closeAddBookModal = function() {
+            document.getElementById('addBookModal').style.display = 'none';
+            document.getElementById('searchResults').innerHTML = '';
+            document.getElementById('bookSearchInput').value = '';
+        };
+
+        // 搜索书籍
+        window.searchBooks = async function() {
+            const keyword = document.getElementById('bookSearchInput').value.trim();
+            if (!keyword) {
+                showMessage('请输入搜索关键词', 'error');
+                return;
+            }
+
+            const resultsDiv = document.getElementById('searchResults');
+            resultsDiv.innerHTML = '<div class="loading">搜索中...</div>';
+
+            try {
+                const res = await getAdminBooks({ keyword: keyword, page: 1, size: 20, status: 1 }, token);
+                if (res && res.code === 200 && res.data && res.data.list) {
+                    const books = res.data.list;
+                    if (books.length === 0) {
+                        resultsDiv.innerHTML = '<div class="empty-state">未找到相关书籍</div>';
+                        return;
+                    }
+
+                    resultsDiv.innerHTML = books.map(book => `
+                        <div style="display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee;">
+                            <img src="${book.cover || book.image || 'https://via.placeholder.com/60x80'}" 
+                                 style="width: 60px; height: 80px; object-fit: cover; border-radius: 5px; margin-right: 15px;"
+                                 onerror="this.src='https://via.placeholder.com/60x80'">
+                            <div style="flex: 1;">
+                                <div style="font-weight: bold; margin-bottom: 5px;">${book.title}</div>
+                                <div style="font-size: 12px; color: #666;">${book.author || '未知作者'}</div>
+                            </div>
+                            <button class="btn btn-primary btn-small" onclick="addBookToRanking(${book.id})">添加</button>
+                        </div>
+                    `).join('');
+                } else {
+                    resultsDiv.innerHTML = '<div class="empty-state">搜索失败</div>';
+                }
+            } catch (err) {
+                showMessage('搜索失败: ' + err.message, 'error');
+                resultsDiv.innerHTML = '<div class="empty-state">搜索失败</div>';
+            }
+        };
+
+        // 添加书籍到排行榜
+        window.addBookToRanking = async function(bookId) {
+            try {
+                const res = await addBookToRanking(currentGroupId, bookId, currentCategoryId, token);
+                if (res && res.code === 200) {
+                    showMessage('添加成功', 'success');
+                    closeAddBookModal();
+                    await loadRankingItems();
+                } else {
+                    showMessage('添加失败: ' + (res.message || '未知错误'), 'error');
+                }
+            } catch (err) {
+                showMessage('添加失败: ' + err.message, 'error');
+            }
+        };
+
+        // 显示消息
+        function showMessage(message, type = 'success') {
+            const msgDiv = document.createElement('div');
+            msgDiv.className = `message ${type}`;
+            msgDiv.textContent = message;
+            document.body.appendChild(msgDiv);
+
+            setTimeout(() => {
+                msgDiv.remove();
+            }, 3000);
+        }
+
+        // 页面加载时初始化
+        init();
+    </script>
+</body>
+</html>
+
+
+
+

+ 184 - 0
book-admin/src/main/resources/static/pages/test-backend.html

@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后端连接测试</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            max-width: 800px;
+            margin: 50px auto;
+            padding: 20px;
+        }
+        .test-item {
+            margin: 20px 0;
+            padding: 15px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+        }
+        .success {
+            background: #d4edda;
+            border-color: #c3e6cb;
+            color: #155724;
+        }
+        .error {
+            background: #f8d7da;
+            border-color: #f5c6cb;
+            color: #721c24;
+        }
+        .info {
+            background: #d1ecf1;
+            border-color: #bee5eb;
+            color: #0c5460;
+        }
+        button {
+            padding: 10px 20px;
+            margin: 10px 5px;
+            cursor: pointer;
+            background: #007bff;
+            color: white;
+            border: none;
+            border-radius: 5px;
+        }
+        button:hover {
+            background: #0056b3;
+        }
+        pre {
+            background: #f4f4f4;
+            padding: 10px;
+            border-radius: 5px;
+            overflow-x: auto;
+        }
+    </style>
+</head>
+<body>
+    <h1>后端服务连接测试</h1>
+    
+    <div class="test-item info">
+        <h3>测试说明</h3>
+        <p>此页面用于测试后端服务是否正常运行。请按照以下步骤操作:</p>
+        <ol>
+            <li>确保后端服务已启动(运行 BookApplication.java)</li>
+            <li>点击下面的测试按钮</li>
+            <li>查看测试结果</li>
+        </ol>
+    </div>
+    
+    <button onclick="testConnection()">测试后端连接</button>
+    <button onclick="testLogin()">测试登录接口</button>
+    <button onclick="testCORS()">测试CORS配置</button>
+    
+    <div id="results"></div>
+    
+    <script>
+        const BASE_URL = 'http://localhost:8001';
+        
+        function addResult(title, content, type) {
+            const results = document.getElementById('results');
+            const div = document.createElement('div');
+            div.className = `test-item ${type}`;
+            div.innerHTML = `<h3>${title}</h3><pre>${content}</pre>`;
+            results.appendChild(div);
+        }
+        
+        function clearResults() {
+            document.getElementById('results').innerHTML = '';
+        }
+        
+        async function testConnection() {
+            clearResults();
+            addResult('测试1: 检查后端服务是否运行', '正在测试...', 'info');
+            
+            try {
+                // 测试1: 简单的连接测试
+                const response = await fetch(`${BASE_URL}/api/admin/login`, {
+                    method: 'OPTIONS',
+                    headers: {
+                        'Origin': window.location.origin
+                    }
+                });
+                
+                if (response.ok || response.status === 200 || response.status === 405) {
+                    addResult('测试1: 后端服务连接', `✓ 后端服务正在运行\n状态码: ${response.status}`, 'success');
+                } else {
+                    addResult('测试1: 后端服务连接', `✗ 连接失败\n状态码: ${response.status}`, 'error');
+                }
+            } catch (err) {
+                addResult('测试1: 后端服务连接', `✗ 无法连接到后端服务\n错误: ${err.message}\n\n请检查:\n1. 后端服务是否已启动\n2. 端口是否为8001\n3. 防火墙是否阻止了连接`, 'error');
+            }
+        }
+        
+        async function testCORS() {
+            clearResults();
+            addResult('测试2: 检查CORS配置', '正在测试...', 'info');
+            
+            try {
+                const response = await fetch(`${BASE_URL}/api/admin/login`, {
+                    method: 'OPTIONS',
+                    headers: {
+                        'Origin': window.location.origin,
+                        'Access-Control-Request-Method': 'POST',
+                        'Access-Control-Request-Headers': 'Content-Type'
+                    }
+                });
+                
+                const corsHeaders = {
+                    'Access-Control-Allow-Origin': response.headers.get('Access-Control-Allow-Origin'),
+                    'Access-Control-Allow-Methods': response.headers.get('Access-Control-Allow-Methods'),
+                    'Access-Control-Allow-Headers': response.headers.get('Access-Control-Allow-Headers'),
+                };
+                
+                if (corsHeaders['Access-Control-Allow-Origin']) {
+                    addResult('测试2: CORS配置', `✓ CORS配置正确\n${JSON.stringify(corsHeaders, null, 2)}`, 'success');
+                } else {
+                    addResult('测试2: CORS配置', `✗ CORS头未设置\n${JSON.stringify(corsHeaders, null, 2)}`, 'error');
+                }
+            } catch (err) {
+                addResult('测试2: CORS配置', `✗ 测试失败\n错误: ${err.message}`, 'error');
+            }
+        }
+        
+        async function testLogin() {
+            clearResults();
+            addResult('测试3: 测试登录接口', '正在测试...', 'info');
+            
+            try {
+                const response = await fetch(`${BASE_URL}/api/admin/login`, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'Origin': window.location.origin
+                    },
+                    body: JSON.stringify({
+                        username: 'admin',
+                        password: 'admin123'
+                    })
+                });
+                
+                const data = await response.json();
+                
+                if (response.ok && data.code === 200) {
+                    addResult('测试3: 登录接口', `✓ 登录成功\n${JSON.stringify(data, null, 2)}`, 'success');
+                } else {
+                    addResult('测试3: 登录接口', `✗ 登录失败\n状态码: ${response.status}\n响应: ${JSON.stringify(data, null, 2)}`, 'error');
+                }
+            } catch (err) {
+                addResult('测试3: 登录接口', `✗ 请求失败\n错误: ${err.message}\n\n可能的原因:\n1. 后端服务未启动\n2. CORS配置问题\n3. 网络连接问题`, 'error');
+            }
+        }
+    </script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+

+ 192 - 0
book-admin/src/main/resources/static/pages/test-connection.html

@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后端连接测试</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            max-width: 800px;
+            margin: 50px auto;
+            padding: 20px;
+        }
+        .test-item {
+            margin: 20px 0;
+            padding: 15px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+        }
+        .success {
+            background: #d4edda;
+            border-color: #c3e6cb;
+            color: #155724;
+        }
+        .error {
+            background: #f8d7da;
+            border-color: #f5c6cb;
+            color: #721c24;
+        }
+        .loading {
+            background: #d1ecf1;
+            border-color: #bee5eb;
+            color: #0c5460;
+        }
+        button {
+            padding: 10px 20px;
+            background: #007bff;
+            color: white;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            margin: 5px;
+        }
+        button:hover {
+            background: #0056b3;
+        }
+        pre {
+            background: #f4f4f4;
+            padding: 10px;
+            border-radius: 5px;
+            overflow-x: auto;
+        }
+    </style>
+</head>
+<body>
+    <h1>后端连接测试</h1>
+    <p>这个页面用于测试后端服务是否正常运行</p>
+    
+    <button onclick="testConnection()">测试连接</button>
+    <button onclick="testLogin()">测试登录</button>
+    <button onclick="testAll()">测试全部</button>
+    
+    <div id="results"></div>
+    
+    <script>
+        const BASE_URL = 'http://localhost:8081';
+        
+        async function testConnection() {
+            const resultsDiv = document.getElementById('results');
+            resultsDiv.innerHTML = '<div class="test-item loading">正在测试连接...</div>';
+            
+            try {
+                const response = await fetch(BASE_URL + '/api/admin/login', {
+                    method: 'OPTIONS'
+                });
+                
+                if (response.ok || response.status === 405) {
+                    resultsDiv.innerHTML = '<div class="test-item success">✅ 后端服务连接成功!<br>服务运行在: ' + BASE_URL + '</div>';
+                } else {
+                    resultsDiv.innerHTML = '<div class="test-item error">❌ 后端服务响应异常<br>状态码: ' + response.status + '</div>';
+                }
+            } catch (error) {
+                resultsDiv.innerHTML = '<div class="test-item error">❌ 无法连接到后端服务<br>错误: ' + error.message + '<br><br>请检查:<br>1. 后端服务是否启动<br>2. 端口是否为8081<br>3. 防火墙是否阻止</div>';
+            }
+        }
+        
+        async function testLogin() {
+            const resultsDiv = document.getElementById('results');
+            resultsDiv.innerHTML = '<div class="test-item loading">正在测试登录...</div>';
+            
+            try {
+                const response = await fetch(BASE_URL + '/api/admin/login', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({
+                        username: 'admin',
+                        password: 'admin123'
+                    })
+                });
+                
+                const data = await response.json();
+                
+                if (response.ok && data.code === 200) {
+                    resultsDiv.innerHTML = '<div class="test-item success">✅ 登录测试成功!<br><pre>' + JSON.stringify(data, null, 2) + '</pre></div>';
+                } else {
+                    resultsDiv.innerHTML = '<div class="test-item error">❌ 登录测试失败<br>状态码: ' + response.status + '<br>响应: <pre>' + JSON.stringify(data, null, 2) + '</pre></div>';
+                }
+            } catch (error) {
+                resultsDiv.innerHTML = '<div class="test-item error">❌ 登录测试失败<br>错误: ' + error.message + '</div>';
+            }
+        }
+        
+        async function testAll() {
+            const resultsDiv = document.getElementById('results');
+            resultsDiv.innerHTML = '<div class="test-item loading">正在测试所有功能...</div>';
+            
+            let html = '';
+            
+            // 测试1: 连接测试
+            html += '<h3>1. 连接测试</h3>';
+            try {
+                const response = await fetch(BASE_URL + '/api/admin/login', {
+                    method: 'OPTIONS'
+                });
+                if (response.ok || response.status === 405) {
+                    html += '<div class="test-item success">✅ 后端服务连接成功</div>';
+                } else {
+                    html += '<div class="test-item error">❌ 后端服务响应异常 (状态码: ' + response.status + ')</div>';
+                }
+            } catch (error) {
+                html += '<div class="test-item error">❌ 无法连接到后端服务: ' + error.message + '</div>';
+                resultsDiv.innerHTML = html;
+                return;
+            }
+            
+            // 测试2: 登录测试
+            html += '<h3>2. 登录测试</h3>';
+            try {
+                const response = await fetch(BASE_URL + '/api/admin/login', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({
+                        username: 'admin',
+                        password: 'admin123'
+                    })
+                });
+                
+                const data = await response.json();
+                
+                if (response.ok && data.code === 200) {
+                    html += '<div class="test-item success">✅ 登录测试成功<br><pre>' + JSON.stringify(data, null, 2) + '</pre></div>';
+                } else {
+                    html += '<div class="test-item error">❌ 登录测试失败<br>状态码: ' + response.status + '<br>响应: <pre>' + JSON.stringify(data, null, 2) + '</pre></div>';
+                }
+            } catch (error) {
+                html += '<div class="test-item error">❌ 登录测试失败: ' + error.message + '</div>';
+            }
+            
+            resultsDiv.innerHTML = html;
+        }
+        
+        // 页面加载时自动测试
+        window.onload = function() {
+            testConnection();
+        };
+    </script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 721 - 0
book-admin/src/main/resources/static/pages/users.html

@@ -0,0 +1,721 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>后台管理系统 - 用户管理</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: #f5f5f5;
+        }
+        
+        .header {
+            background: white;
+            padding: 20px 30px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+        
+        .header-title {
+            font-size: 24px;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        .header-actions {
+            display: flex;
+            gap: 10px;
+        }
+        
+        .btn {
+            padding: 8px 16px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: opacity 0.3s;
+        }
+        
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+        
+        .btn-danger {
+            background: #f56565;
+            color: white;
+        }
+        
+        .btn-success {
+            background: #48bb78;
+            color: white;
+        }
+        
+        .btn-warning {
+            background: #ed8936;
+            color: white;
+        }
+        
+        .btn:hover {
+            opacity: 0.9;
+        }
+        
+        .btn:disabled {
+            opacity: 0.5;
+            cursor: not-allowed;
+        }
+        
+        .container {
+            max-width: 1400px;
+            margin: 20px auto;
+            padding: 0 20px;
+        }
+        
+        .nav-tabs {
+            display: flex;
+            gap: 10px;
+            margin-bottom: 20px;
+        }
+        
+        .nav-tab {
+            padding: 10px 18px;
+            border-radius: 6px;
+            background: white;
+            color: #333;
+            text-decoration: none;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.08);
+            transition: all 0.3s;
+            font-size: 14px;
+        }
+        
+        .nav-tab:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 10px rgba(0,0,0,0.12);
+        }
+        
+        .nav-tab.active {
+            background: #667eea;
+            color: white;
+            box-shadow: 0 6px 14px rgba(102, 126, 234, 0.35);
+        }
+        
+        .toolbar {
+            background: white;
+            padding: 20px;
+            border-radius: 5px;
+            margin-bottom: 20px;
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+            align-items: center;
+        }
+        
+        .search-input {
+            flex: 1;
+            min-width: 200px;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-select {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .table-container {
+            background: white;
+            border-radius: 5px;
+            overflow-x: auto;
+        }
+        
+        table {
+            width: 100%;
+            border-collapse: collapse;
+            min-width: 1200px;
+        }
+        
+        th, td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #eee;
+        }
+        
+        th {
+            background: #f8f9fa;
+            font-weight: bold;
+            color: #333;
+        }
+        
+        tr:hover {
+            background: #f8f9fa;
+        }
+        
+        .status-badge {
+            padding: 4px 8px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: bold;
+        }
+        
+        .status-online {
+            background: #c6f6d5;
+            color: #22543d;
+        }
+        
+        .status-offline {
+            background: #fed7d7;
+            color: #742a2a;
+        }
+        
+        .vip-badge {
+            padding: 4px 8px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: bold;
+            background: #fef3c7;
+            color: #92400e;
+        }
+        
+        .action-buttons {
+            display: flex;
+            gap: 5px;
+            flex-wrap: wrap;
+        }
+        
+        .btn-sm {
+            padding: 4px 8px;
+            font-size: 12px;
+        }
+        
+        .pagination {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            gap: 10px;
+            margin-top: 20px;
+            padding: 20px;
+        }
+        
+        .modal {
+            display: none;
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background: rgba(0, 0, 0, 0.5);
+            z-index: 1000;
+        }
+        
+        .modal.show {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+        
+        .modal-content {
+            background: white;
+            border-radius: 5px;
+            padding: 30px;
+            max-width: 700px;
+            width: 90%;
+            max-height: 90vh;
+            overflow-y: auto;
+        }
+        
+        .modal-header {
+            font-size: 20px;
+            font-weight: bold;
+            margin-bottom: 20px;
+        }
+        
+        .form-group {
+            margin-bottom: 15px;
+        }
+        
+        .form-label {
+            display: block;
+            margin-bottom: 5px;
+            color: #666;
+            font-size: 14px;
+        }
+        
+        .form-input, .form-textarea {
+            width: 100%;
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+        
+        .form-textarea {
+            min-height: 80px;
+            resize: vertical;
+        }
+        
+        .modal-actions {
+            display: flex;
+            justify-content: flex-end;
+            gap: 10px;
+            margin-top: 20px;
+        }
+        
+        .loading {
+            text-align: center;
+            padding: 20px;
+            color: #666;
+        }
+        
+        .empty {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+        }
+        
+        .avatar {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            object-fit: cover;
+        }
+    </style>
+</head>
+<body>
+    <div id="app">
+        <div class="header">
+            <div class="header-title">用户管理</div>
+            <div class="header-actions">
+                <button class="btn btn-danger" @click="logout">退出登录</button>
+            </div>
+        </div>
+        
+        <div class="container">
+            <div class="nav-tabs">
+                <a class="nav-tab" href="books.html">电子书管理</a>
+                <a class="nav-tab" href="audiobooks.html">听书管理</a>
+                <a class="nav-tab" href="rankings.html">排行榜管理</a>
+                <a class="nav-tab" href="banners.html">轮播图管理</a>
+                <a class="nav-tab active" href="users.html">用户管理</a>
+            </div>
+            
+            <div class="toolbar">
+                <input type="text" class="search-input" v-model="searchKeyword" placeholder="搜索用户名、昵称、手机号、邮箱..." @input="handleSearch">
+                <select class="form-select" v-model="filterStatus" @change="loadUsers" style="width: 120px;">
+                    <option value="">全部状态</option>
+                    <option value="1">启用</option>
+                    <option value="0">禁用</option>
+                </select>
+                <select class="form-select" v-model="filterIsVip" @change="loadUsers" style="width: 120px;">
+                    <option value="">全部VIP</option>
+                    <option value="true">VIP用户</option>
+                    <option value="false">普通用户</option>
+                </select>
+                <select class="form-select" v-model="filterRole" @change="loadUsers" style="width: 120px;">
+                    <option value="">全部角色</option>
+                    <option value="admin">管理员</option>
+                    <option value="user">普通用户</option>
+                </select>
+                <button class="btn btn-primary" @click="loadUsers">查询</button>
+            </div>
+            
+            <div class="table-container">
+                <table>
+                    <thead>
+                        <tr>
+                            <th>ID</th>
+                            <th>头像</th>
+                            <th>用户名</th>
+                            <th>昵称</th>
+                            <th>手机号</th>
+                            <th>邮箱</th>
+                            <th>VIP</th>
+                            <th>角色</th>
+                            <th>状态</th>
+                            <th>注册时间</th>
+                            <th>操作</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="loading">
+                            <td colspan="11" class="loading">加载中...</td>
+                        </tr>
+                        <tr v-else-if="users.length === 0">
+                            <td colspan="11" class="empty">暂无数据</td>
+                        </tr>
+                        <tr v-else v-for="user in users" :key="user.id">
+                            <td>{{ user.id }}</td>
+                            <td>
+                                <img v-if="user.avatar" :src="user.avatar" alt="" class="avatar" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'40\' height=\'40\'%3E%3Ccircle cx=\'20\' cy=\'20\' r=\'20\' fill=\'%23ddd\'/%3E%3Ctext x=\'50%25\' y=\'50%25\' text-anchor=\'middle\' dy=\'.3em\' fill=\'%23999\' font-size=\'12\'%3E无%3C/text%3E%3C/svg%3E'">
+                                <span v-else style="color: #999;">-</span>
+                            </td>
+                            <td>{{ user.username }}</td>
+                            <td>{{ user.nickname || '-' }}</td>
+                            <td>{{ user.phone || '-' }}</td>
+                            <td>{{ user.email || '-' }}</td>
+                            <td>
+                                <span v-if="user.isVip" class="vip-badge">VIP</span>
+                                <span v-else style="color: #999;">-</span>
+                            </td>
+                            <td>{{ user.role === 'admin' ? '管理员' : '普通用户' }}</td>
+                            <td>
+                                <span :class="['status-badge', user.status === 1 ? 'status-online' : 'status-offline']">
+                                    {{ user.status === 1 ? '启用' : '禁用' }}
+                                </span>
+                            </td>
+                            <td>{{ formatDate(user.createdAt) }}</td>
+                            <td>
+                                <div class="action-buttons">
+                                    <button class="btn btn-primary btn-sm" @click="editUser(user)">编辑</button>
+                                    <button v-if="user.status === 1" class="btn btn-warning btn-sm" @click="disableUser(user.id)">禁用</button>
+                                    <button v-else class="btn btn-success btn-sm" @click="enableUser(user.id)">启用</button>
+                                    <button class="btn btn-danger btn-sm" @click="showResetPasswordModal(user)">重置密码</button>
+                                </div>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+            
+            <div class="pagination">
+                <button class="btn" @click="prevPage" :disabled="page === 1">上一页</button>
+                <span>第 {{ page }} 页,共 {{ totalPages }} 页,共 {{ total }} 条</span>
+                <button class="btn" @click="nextPage" :disabled="page >= totalPages">下一页</button>
+            </div>
+        </div>
+        
+        <!-- 编辑用户模态框 -->
+        <div class="modal" :class="{ show: showEditModal }" @click.self="closeEditModal">
+            <div class="modal-content">
+                <div class="modal-header">编辑用户</div>
+                <form @submit.prevent="saveUser">
+                    <div class="form-group">
+                        <label class="form-label">用户名 *</label>
+                        <input type="text" class="form-input" v-model="editingUser.username" required>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">昵称</label>
+                        <input type="text" class="form-input" v-model="editingUser.nickname">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">头像URL</label>
+                        <input type="text" class="form-input" v-model="editingUser.avatar">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">性别</label>
+                        <select class="form-select" v-model="editingUser.gender">
+                            <option value="">请选择</option>
+                            <option value="男">男</option>
+                            <option value="女">女</option>
+                            <option value="保密">保密</option>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">生日</label>
+                        <input type="date" class="form-input" v-model="editingUser.birthday">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">个人简介</label>
+                        <textarea class="form-textarea" v-model="editingUser.bio"></textarea>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">手机号</label>
+                        <input type="text" class="form-input" v-model="editingUser.phone">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">邮箱</label>
+                        <input type="email" class="form-input" v-model="editingUser.email">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">是否VIP</label>
+                        <select class="form-select" v-model="editingUser.isVip">
+                            <option :value="false">否</option>
+                            <option :value="true">是</option>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">VIP过期时间</label>
+                        <input type="datetime-local" class="form-input" v-model="editingUser.vipExpireTime">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">状态 *</label>
+                        <select class="form-select" v-model="editingUser.status" required>
+                            <option :value="1">启用</option>
+                            <option :value="0">禁用</option>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">角色 *</label>
+                        <select class="form-select" v-model="editingUser.role" required>
+                            <option value="user">普通用户</option>
+                            <option value="admin">管理员</option>
+                        </select>
+                    </div>
+                    <div class="modal-actions">
+                        <button type="button" class="btn" @click="closeEditModal">取消</button>
+                        <button type="submit" class="btn btn-primary">保存</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+        
+        <!-- 重置密码模态框 -->
+        <div class="modal" :class="{ show: showPasswordModal }" @click.self="closePasswordModal">
+            <div class="modal-content">
+                <div class="modal-header">重置密码</div>
+                <form @submit.prevent="resetPassword">
+                    <div class="form-group">
+                        <label class="form-label">用户名</label>
+                        <input type="text" class="form-input" :value="passwordUser.username" disabled>
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">新密码 *</label>
+                        <input type="password" class="form-input" v-model="newPassword" required placeholder="请输入新密码(6-20位)">
+                    </div>
+                    <div class="form-group">
+                        <label class="form-label">确认密码 *</label>
+                        <input type="password" class="form-input" v-model="confirmPassword" required placeholder="请再次输入新密码">
+                    </div>
+                    <div class="modal-actions">
+                        <button type="button" class="btn" @click="closePasswordModal">取消</button>
+                        <button type="submit" class="btn btn-primary">确定</button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+    
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <script type="module">
+        import { getAdminUsers, getAdminUserById, updateAdminUser, disableAdminUser, enableAdminUser, resetAdminUserPassword } from '../utils/api.js'
+        
+        const { createApp } = Vue
+        
+        createApp({
+            data() {
+                return {
+                    token: localStorage.getItem('admin_token'),
+                    users: [],
+                    loading: false,
+                    page: 1,
+                    pageSize: 10,
+                    total: 0,
+                    searchKeyword: '',
+                    filterStatus: '',
+                    filterIsVip: '',
+                    filterRole: '',
+                    showEditModal: false,
+                    showPasswordModal: false,
+                    editingUser: {
+                        id: null,
+                        username: '',
+                        nickname: '',
+                        avatar: '',
+                        gender: '',
+                        birthday: '',
+                        bio: '',
+                        phone: '',
+                        email: '',
+                        isVip: false,
+                        vipExpireTime: '',
+                        status: 1,
+                        role: 'user'
+                    },
+                    passwordUser: {
+                        id: null,
+                        username: ''
+                    },
+                    newPassword: '',
+                    confirmPassword: '',
+                    searchTimer: null
+                }
+            },
+            computed: {
+                totalPages() {
+                    return Math.ceil(this.total / this.pageSize)
+                }
+            },
+            mounted() {
+                if (!this.token) {
+                    window.location.href = 'login.html'
+                    return
+                }
+                this.loadUsers()
+            },
+            methods: {
+                async loadUsers() {
+                    this.loading = true
+                    try {
+                        const params = {
+                            page: this.page,
+                            size: this.pageSize,
+                            keyword: this.searchKeyword || undefined,
+                            status: this.filterStatus || undefined,
+                            isVip: this.filterIsVip === '' ? undefined : this.filterIsVip === 'true',
+                            role: this.filterRole || undefined
+                        }
+                        const res = await getAdminUsers(params, this.token)
+                        if (res.code === 200 && res.data) {
+                            this.users = res.data.list || []
+                            this.total = res.data.total || 0
+                        }
+                    } catch (err) {
+                        alert(err.message || '加载用户失败')
+                    } finally {
+                        this.loading = false
+                    }
+                },
+                handleSearch() {
+                    clearTimeout(this.searchTimer)
+                    this.searchTimer = setTimeout(() => {
+                        this.page = 1
+                        this.loadUsers()
+                    }, 500)
+                },
+                prevPage() {
+                    if (this.page > 1) {
+                        this.page--
+                        this.loadUsers()
+                    }
+                },
+                nextPage() {
+                    if (this.page < this.totalPages) {
+                        this.page++
+                        this.loadUsers()
+                    }
+                },
+                async editUser(user) {
+                    try {
+                        const res = await getAdminUserById(user.id, this.token)
+                        if (res.code === 200 && res.data) {
+                            this.editingUser = { ...res.data }
+                            // 格式化日期
+                            if (this.editingUser.birthday) {
+                                this.editingUser.birthday = this.editingUser.birthday.substring(0, 10)
+                            }
+                            if (this.editingUser.vipExpireTime) {
+                                const date = new Date(this.editingUser.vipExpireTime)
+                                this.editingUser.vipExpireTime = date.toISOString().substring(0, 16)
+                            }
+                            this.showEditModal = true
+                        }
+                    } catch (err) {
+                        alert(err.message || '加载用户信息失败')
+                    }
+                },
+                closeEditModal() {
+                    this.showEditModal = false
+                    this.editingUser = {
+                        id: null,
+                        username: '',
+                        nickname: '',
+                        avatar: '',
+                        gender: '',
+                        birthday: '',
+                        bio: '',
+                        phone: '',
+                        email: '',
+                        isVip: false,
+                        vipExpireTime: '',
+                        status: 1,
+                        role: 'user'
+                    }
+                },
+                async saveUser() {
+                    try {
+                        const userData = { ...this.editingUser }
+                        // 处理日期格式
+                        if (userData.vipExpireTime) {
+                            userData.vipExpireTime = new Date(userData.vipExpireTime).toISOString()
+                        }
+                        await updateAdminUser(this.editingUser.id, userData, this.token)
+                        alert('更新成功')
+                        this.closeEditModal()
+                        this.loadUsers()
+                    } catch (err) {
+                        alert(err.message || '更新失败')
+                    }
+                },
+                async disableUser(id) {
+                    if (!confirm('确定要禁用该用户吗?禁用后该用户将无法登录小程序。')) return
+                    try {
+                        await disableAdminUser(id, this.token)
+                        alert('禁用成功')
+                        this.loadUsers()
+                    } catch (err) {
+                        alert(err.message || '禁用失败')
+                    }
+                },
+                async enableUser(id) {
+                    try {
+                        await enableAdminUser(id, this.token)
+                        alert('启用成功')
+                        this.loadUsers()
+                    } catch (err) {
+                        alert(err.message || '启用失败')
+                    }
+                },
+                showResetPasswordModal(user) {
+                    this.passwordUser = { id: user.id, username: user.username }
+                    this.newPassword = ''
+                    this.confirmPassword = ''
+                    this.showPasswordModal = true
+                },
+                closePasswordModal() {
+                    this.showPasswordModal = false
+                    this.passwordUser = { id: null, username: '' }
+                    this.newPassword = ''
+                    this.confirmPassword = ''
+                },
+                async resetPassword() {
+                    if (!this.newPassword || this.newPassword.length < 6 || this.newPassword.length > 20) {
+                        alert('密码长度必须在6-20位之间')
+                        return
+                    }
+                    if (this.newPassword !== this.confirmPassword) {
+                        alert('两次输入的密码不一致')
+                        return
+                    }
+                    try {
+                        await resetAdminUserPassword(this.passwordUser.id, this.newPassword, this.token)
+                        alert('密码重置成功')
+                        this.closePasswordModal()
+                    } catch (err) {
+                        alert(err.message || '密码重置失败')
+                    }
+                },
+                formatDate(dateStr) {
+                    if (!dateStr) return '-'
+                    const date = new Date(dateStr)
+                    return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
+                },
+                logout() {
+                    if (confirm('确定要退出登录吗?')) {
+                        localStorage.removeItem('admin_token')
+                        localStorage.removeItem('admin_user')
+                        window.location.href = 'login.html'
+                    }
+                }
+            }
+        }).mount('#app')
+    </script>
+</body>
+</html>
+
+
+
+
+
+

+ 474 - 0
book-admin/src/main/resources/static/utils/api.js

@@ -0,0 +1,474 @@
+/**
+ * 后台管理系统API请求工具类
+ * 最后更新: 2025-01-11
+ */
+
+// API基础URL - 后端服务运行在8001端口
+const BASE_URL = 'http://localhost:8001'
+
+// 调试:输出BASE_URL(生产环境可删除)
+console.log('API Base URL:', BASE_URL)
+
+/**
+ * 统一请求方法
+ */
+function request(url, method = 'GET', data = {}, token = '') {
+	return new Promise((resolve, reject) => {
+		const headers = {
+			'Content-Type': 'application/json'
+		}
+		
+		// 如果有token,添加到请求头
+		if (token) {
+			headers['Authorization'] = token
+		}
+		
+		// 构建请求URL
+		let requestUrl = BASE_URL + url
+		console.log('请求URL:', requestUrl, '方法:', method) // 调试输出
+		if (method === 'GET' && data && Object.keys(data).length > 0) {
+			const queryParams = new URLSearchParams()
+			for (const key in data) {
+				if (data[key] !== undefined && data[key] !== null && data[key] !== '') {
+					queryParams.append(key, data[key])
+				}
+			}
+			const queryString = queryParams.toString()
+			if (queryString) {
+				requestUrl += (url.includes('?') ? '&' : '?') + queryString
+			}
+		}
+		
+		fetch(requestUrl, {
+			method: method,
+			headers: headers,
+			body: method !== 'GET' && data ? JSON.stringify(data) : undefined
+		})
+		.then(response => {
+			if (!response.ok) {
+				throw new Error(`HTTP error! status: ${response.status}`)
+			}
+			return response.json()
+		})
+		.then(res => {
+			console.log('API响应:', url, res)
+			if (res && res.code === 200) {
+				resolve(res)
+			} else {
+				reject(new Error(res.message || res.msg || '请求失败'))
+			}
+		})
+		.catch(err => {
+			console.error('API请求失败:', err)
+			// 更详细的错误信息
+			if (err.name === 'TypeError' && err.message.includes('Failed to fetch')) {
+				reject(new Error('无法连接到后端服务,请检查:\n1. 后端服务是否启动(端口8001)\n2. 后端服务地址是否正确\n3. 是否存在CORS问题'))
+			} else if (err.message.includes('HTTP error')) {
+				reject(new Error(`服务器错误: ${err.message}`))
+			} else {
+				reject(new Error(err.message || '网络请求失败,请检查后端服务'))
+			}
+		})
+	})
+}
+
+// ============================================
+// 管理员相关API
+// ============================================
+
+/**
+ * 管理员登录
+ */
+export function adminLogin(username, password) {
+	return request('/api/admin/login', 'POST', {
+		username: username,
+		password: password
+	})
+}
+
+// ============================================
+// 书籍管理API
+// ============================================
+
+/**
+ * 分页查询书籍
+ */
+export function getAdminBooks(params, token) {
+	return request('/api/admin/book/list', 'GET', params || {}, token)
+}
+
+/**
+ * 根据ID查询书籍
+ */
+export function getAdminBookById(id, token) {
+	return request(`/api/admin/book/${id}`, 'GET', {}, token)
+}
+
+/**
+ * 创建书籍
+ */
+export function createAdminBook(bookData, token) {
+	return request('/api/admin/book', 'POST', bookData, token)
+}
+
+/**
+ * 更新书籍
+ */
+export function updateAdminBook(id, bookData, token) {
+	return request(`/api/admin/book/${id}`, 'PUT', bookData, token)
+}
+
+/**
+ * 删除书籍
+ */
+export function deleteAdminBook(id, token) {
+	return request(`/api/admin/book/${id}`, 'DELETE', {}, token)
+}
+
+/**
+ * 批量删除书籍
+ */
+export function deleteAdminBooks(ids, token) {
+	return request('/api/admin/book/batch', 'DELETE', ids, token)
+}
+
+/**
+ * 上架书籍
+ */
+export function publishAdminBook(id, token) {
+	return request(`/api/admin/book/${id}/publish`, 'PUT', {}, token)
+}
+
+/**
+ * 下架书籍
+ */
+export function unpublishAdminBook(id, token) {
+	return request(`/api/admin/book/${id}/unpublish`, 'PUT', {}, token)
+}
+
+/**
+ * 批量上架书籍
+ */
+export function publishAdminBooks(ids, token) {
+	return request('/api/admin/book/batch/publish', 'PUT', { ids: ids }, token)
+}
+
+/**
+ * 批量下架书籍
+ */
+export function unpublishAdminBooks(ids, token) {
+	return request('/api/admin/book/batch/unpublish', 'PUT', { ids: ids }, token)
+}
+
+// ============================================
+// 听书管理API
+// ============================================
+
+/**
+ * 分页查询听书
+ */
+export function getAdminAudiobooks(params, token) {
+	return request('/api/admin/audiobook/list', 'GET', params || {}, token)
+}
+
+/**
+ * 根据ID查询听书
+ */
+export function getAdminAudiobookById(id, token) {
+	return request(`/api/admin/audiobook/${id}`, 'GET', {}, token)
+}
+
+/**
+ * 创建听书
+ */
+export function createAdminAudiobook(data, token) {
+	return request('/api/admin/audiobook', 'POST', data, token)
+}
+
+/**
+ * 更新听书
+ */
+export function updateAdminAudiobook(id, data, token) {
+	return request(`/api/admin/audiobook/${id}`, 'PUT', data, token)
+}
+
+/**
+ * 删除听书
+ */
+export function deleteAdminAudiobook(id, token) {
+	return request(`/api/admin/audiobook/${id}`, 'DELETE', {}, token)
+}
+
+/**
+ * 批量删除听书
+ */
+export function deleteAdminAudiobooks(ids, token) {
+	return request('/api/admin/audiobook/batch', 'DELETE', ids, token)
+}
+
+/**
+ * 上架听书
+ */
+export function publishAdminAudiobook(id, token) {
+	return request(`/api/admin/audiobook/${id}/publish`, 'PUT', {}, token)
+}
+
+/**
+ * 下架听书
+ */
+export function unpublishAdminAudiobook(id, token) {
+	return request(`/api/admin/audiobook/${id}/unpublish`, 'PUT', {}, token)
+}
+
+/**
+ * 批量上架听书
+ */
+export function publishAdminAudiobooks(ids, token) {
+	return request('/api/admin/audiobook/batch/publish', 'PUT', { ids: ids }, token)
+}
+
+/**
+ * 批量下架听书
+ */
+export function unpublishAdminAudiobooks(ids, token) {
+	return request('/api/admin/audiobook/batch/unpublish', 'PUT', { ids: ids }, token)
+}
+
+// ============================================
+// 分类相关API(用于下拉选择)
+// ============================================
+
+/**
+ * 获取所有分类(用于后台管理)
+ */
+export function getAllCategories(token) {
+	return request('/api/category/list', 'GET', {}, token || '')
+}
+
+// ============================================
+// 章节管理API
+// ============================================
+
+/**
+ * 根据书籍ID获取章节列表
+ */
+export function getAdminChapters(bookId, token) {
+	return request('/api/admin/chapter/list', 'GET', { bookId: bookId }, token)
+}
+
+/**
+ * 根据章节ID获取章节详情(包含内容)
+ */
+export function getAdminChapterById(id, token) {
+	return request(`/api/admin/chapter/${id}`, 'GET', {}, token)
+}
+
+/**
+ * 创建章节
+ */
+export function createAdminChapter(chapterData, token) {
+	return request('/api/admin/chapter/create', 'POST', chapterData, token)
+}
+
+/**
+ * 更新章节
+ */
+export function updateAdminChapter(chapterData, token) {
+	return request('/api/admin/chapter/update', 'PUT', chapterData, token)
+}
+
+/**
+ * 删除章节
+ */
+export function deleteAdminChapter(id, token) {
+	return request(`/api/admin/chapter/${id}`, 'DELETE', {}, token)
+}
+
+/**
+ * 批量删除章节
+ */
+export function deleteAdminChapters(chapterIds, token) {
+	return request('/api/admin/chapter/batch-delete', 'POST', { chapterIds: chapterIds }, token)
+}
+
+// ============================================
+// 轮播图管理API
+// ============================================
+
+/**
+ * 获取所有轮播图分组
+ */
+export function getBannerGroups(token) {
+	return request('/api/admin/banner/groups', 'GET', {}, token)
+}
+
+/**
+ * 创建轮播图分组
+ */
+export function createBannerGroup(groupData, token) {
+	return request('/api/admin/banner/groups', 'POST', groupData, token)
+}
+
+/**
+ * 更新轮播图分组
+ */
+export function updateBannerGroup(id, groupData, token) {
+	return request(`/api/admin/banner/groups/${id}`, 'PUT', groupData, token)
+}
+
+/**
+ * 删除轮播图分组
+ */
+export function deleteBannerGroup(id, token) {
+	return request(`/api/admin/banner/groups/${id}`, 'DELETE', {}, token)
+}
+
+/**
+ * 获取轮播图列表
+ */
+export function getBanners(groupId, status, token) {
+	const params = { groupId: groupId }
+	if (status !== undefined && status !== null) {
+		params.status = status
+	}
+	return request('/api/admin/banner/list', 'GET', params, token)
+}
+
+/**
+ * 创建轮播图
+ */
+export function createBanner(bannerData, token) {
+	return request('/api/admin/banner', 'POST', bannerData, token)
+}
+
+/**
+ * 更新轮播图
+ */
+export function updateBanner(id, bannerData, token) {
+	return request(`/api/admin/banner/${id}`, 'PUT', bannerData, token)
+}
+
+/**
+ * 删除轮播图
+ */
+export function deleteBanner(id, token) {
+	return request(`/api/admin/banner/${id}`, 'DELETE', {}, token)
+}
+
+// ============================================
+// 排行榜管理API
+// ============================================
+
+/**
+ * 获取所有排行榜组
+ */
+export function getRankingGroups(token) {
+	return request('/api/admin/ranking/groups', 'GET', {}, token)
+}
+
+/**
+ * 创建排行榜组
+ */
+export function createRankingGroup(groupData, token) {
+	return request('/api/admin/ranking/groups', 'POST', groupData, token)
+}
+
+/**
+ * 更新排行榜组
+ */
+export function updateRankingGroup(id, groupData, token) {
+	return request(`/api/admin/ranking/groups/${id}`, 'PUT', groupData, token)
+}
+
+/**
+ * 删除排行榜组
+ */
+export function deleteRankingGroup(id, token) {
+	return request(`/api/admin/ranking/groups/${id}`, 'DELETE', {}, token)
+}
+
+/**
+ * 获取排行榜项(包含书籍信息,支持按分类筛选)
+ */
+export function getRankingItemsWithBooks(groupId, categoryId, status, token) {
+	const params = { groupId: groupId }
+	if (categoryId !== undefined && categoryId !== null) {
+		params.categoryId = categoryId
+	}
+	if (status !== undefined && status !== null) {
+		params.status = status
+	}
+	return request('/api/admin/ranking/items/with-books', 'GET', params, token)
+}
+
+/**
+ * 批量更新排序
+ */
+export function batchUpdateRankingSort(sortData, token) {
+	return request('/api/admin/ranking/sort', 'POST', sortData, token)
+}
+
+/**
+ * 添加书籍到排行榜
+ */
+export function addBookToRanking(groupId, bookId, categoryId, token) {
+	const params = { groupId: groupId, bookId: bookId }
+	if (categoryId !== undefined && categoryId !== null) {
+		params.categoryId = categoryId
+	}
+	return request('/api/admin/ranking/items/add', 'POST', params, token)
+}
+
+/**
+ * 从排行榜移除书籍
+ */
+export function removeBookFromRanking(itemId, token) {
+	return request(`/api/admin/ranking/items/${itemId}/remove`, 'DELETE', {}, token)
+}
+
+// ============================================
+// 用户管理API
+// ============================================
+
+/**
+ * 分页查询用户
+ */
+export function getAdminUsers(params, token) {
+	return request('/api/admin/user/list', 'GET', params || {}, token)
+}
+
+/**
+ * 根据ID查询用户
+ */
+export function getAdminUserById(id, token) {
+	return request(`/api/admin/user/${id}`, 'GET', {}, token)
+}
+
+/**
+ * 更新用户信息
+ */
+export function updateAdminUser(id, userData, token) {
+	return request(`/api/admin/user/${id}`, 'PUT', userData, token)
+}
+
+/**
+ * 禁用用户
+ */
+export function disableAdminUser(id, token) {
+	return request(`/api/admin/user/${id}/disable`, 'PUT', {}, token)
+}
+
+/**
+ * 启用用户
+ */
+export function enableAdminUser(id, token) {
+	return request(`/api/admin/user/${id}/enable`, 'PUT', {}, token)
+}
+
+/**
+ * 重置用户密码
+ */
+export function resetAdminUserPassword(id, password, token) {
+	return request(`/api/admin/user/${id}/reset-password`, 'PUT', { password: password }, token)
+}
+

+ 13 - 0
book-admin/src/test/java/com/yu/bookadmin/BookAdminApplicationTests.java

@@ -0,0 +1,13 @@
+package com.yu.bookadmin;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class BookAdminApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}

+ 69 - 0
book-admin/一键修复登录问题.bat

@@ -0,0 +1,69 @@
+@echo off
+chcp 65001 >nul
+echo ============================================
+echo 一键修复登录问题
+echo ============================================
+echo.
+
+echo 请确保:
+echo 1. MySQL服务已启动
+echo 2. 数据库 books_db 已创建
+echo 3. 已知道MySQL root密码
+echo.
+
+set /p mysql_password=请输入MySQL root密码: 
+
+echo.
+echo 正在修复管理员账号...
+echo.
+
+cd /d %~dp0\..\book
+
+mysql -u root -p%mysql_password% books_db < src\main\resources\db\fix_admin_user.sql
+
+if %errorlevel% equ 0 (
+    echo.
+    echo ============================================
+    echo 修复完成!
+    echo ============================================
+    echo.
+    echo 默认管理员账号:
+    echo 用户名: admin
+    echo 密码: admin123
+    echo.
+    echo 现在可以重新登录了!
+) else (
+    echo.
+    echo ============================================
+    echo 修复失败!
+    echo ============================================
+    echo.
+    echo 请检查:
+    echo 1. MySQL服务是否启动
+    echo 2. 数据库 books_db 是否存在
+    echo 3. MySQL root密码是否正确
+    echo.
+    echo 或者手动执行SQL脚本:
+    echo mysql -u root -p books_db ^< src\main\resources\db\fix_admin_user.sql
+)
+
+pause
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 67 - 0
book-admin/一键修复登录问题.sh

@@ -0,0 +1,67 @@
+#!/bin/bash
+
+echo "============================================"
+echo "一键修复登录问题"
+echo "============================================"
+echo ""
+
+echo "请确保:"
+echo "1. MySQL服务已启动"
+echo "2. 数据库 books_db 已创建"
+echo "3. 已知道MySQL root密码"
+echo ""
+
+read -p "请输入MySQL root密码: " mysql_password
+
+echo ""
+echo "正在修复管理员账号..."
+echo ""
+
+cd "$(dirname "$0")/../book"
+
+mysql -u root -p"$mysql_password" books_db < src/main/resources/db/fix_admin_user.sql
+
+if [ $? -eq 0 ]; then
+    echo ""
+    echo "============================================"
+    echo "修复完成!"
+    echo "============================================"
+    echo ""
+    echo "默认管理员账号:"
+    echo "用户名: admin"
+    echo "密码: admin123"
+    echo ""
+    echo "现在可以重新登录了!"
+else
+    echo ""
+    echo "============================================"
+    echo "修复失败!"
+    echo "============================================"
+    echo ""
+    echo "请检查:"
+    echo "1. MySQL服务是否启动"
+    echo "2. 数据库 books_db 是否存在"
+    echo "3. MySQL root密码是否正确"
+    echo ""
+    echo "或者手动执行SQL脚本:"
+    echo "mysql -u root -p books_db < src/main/resources/db/fix_admin_user.sql"
+fi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 289 - 0
book-admin/使用说明.md

@@ -0,0 +1,289 @@
+# 后台管理系统使用说明
+
+## 📖 功能简介
+
+后台管理系统提供了完整的书籍管理功能,包括:
+- ✅ 管理员登录(只有管理员可以登录)
+- ✅ 书籍列表查询(支持搜索、筛选、分页)
+- ✅ 添加书籍
+- ✅ 编辑书籍
+- ✅ 删除书籍(单个/批量)
+- ✅ 上架/下架书籍(单个/批量)
+- ✅ 书籍状态管理
+
+## 🚀 快速开始
+
+### 1. 启动后端服务
+
+```bash
+cd book
+mvn spring-boot:run
+```
+
+### 2. 启动前端开发服务器
+
+```bash
+cd book-admin
+npm install  # 首次使用
+npm run dev  # 启动开发服务器
+```
+
+### 3. 登录系统
+
+访问:`http://localhost:8000/pages/login.html`
+
+- 用户名:`admin`
+- 密码:`admin123`
+
+## 🎯 功能使用
+
+### 1. 登录系统
+
+1. 打开登录页面
+2. 输入用户名和密码
+3. 点击"登录"按钮
+4. 登录成功后自动跳转到书籍管理页面
+
+### 2. 查看书籍列表
+
+1. 登录成功后进入书籍管理页面
+2. 可以看到书籍列表(分页显示)
+3. 支持搜索和筛选功能
+
+### 3. 搜索书籍
+
+1. 在搜索框中输入关键词(书名、作者)
+2. 系统会自动搜索(防抖处理,500ms延迟)
+3. 搜索结果实时更新
+
+### 4. 筛选书籍
+
+1. 选择状态筛选(上架/下架)
+2. 选择分类筛选
+3. 点击"查询"按钮或自动筛选
+
+### 5. 添加书籍
+
+1. 点击"添加书籍"按钮
+2. 填写书籍信息:
+   - 书名(必填)
+   - 作者
+   - 封面URL
+   - 图片URL
+   - 简介
+   - 描述
+   - 详细介绍
+   - 价格
+   - 是否免费
+   - 是否VIP
+   - 分类
+   - 状态(必填,上架/下架)
+3. 点击"保存"按钮
+4. 书籍创建成功后会刷新列表
+
+### 6. 编辑书籍
+
+1. 点击书籍列表中的"编辑"按钮
+2. 修改书籍信息
+3. 点击"保存"按钮
+4. 书籍更新成功后会刷新列表
+
+### 7. 删除书籍
+
+#### 单个删除
+1. 点击书籍列表中的"删除"按钮
+2. 确认删除
+3. 书籍删除成功后会刷新列表
+
+#### 批量删除
+1. 选中多个书籍(勾选复选框)
+2. 点击"批量删除"按钮
+3. 确认删除
+4. 书籍删除成功后会刷新列表
+
+### 8. 上架/下架书籍
+
+#### 单个操作
+1. 点击书籍列表中的"上架"或"下架"按钮
+2. 书籍状态更新成功后会刷新列表
+
+#### 批量操作
+1. 选中多个书籍(勾选复选框)
+2. 点击"批量上架"或"批量下架"按钮
+3. 书籍状态更新成功后会刷新列表
+
+### 9. 分页浏览
+
+1. 点击"上一页"或"下一页"按钮
+2. 查看当前页码和总页数
+3. 查看总记录数
+
+### 10. 退出登录
+
+1. 点击"退出登录"按钮
+2. 确认退出
+3. 清除登录信息,跳转到登录页面
+
+## 🔍 功能特性
+
+### 1. 搜索功能
+
+- 支持按书名、作者搜索
+- 实时搜索(防抖处理)
+- 搜索结果高亮显示
+
+### 2. 筛选功能
+
+- 支持按状态筛选(上架/下架)
+- 支持按分类筛选
+- 支持组合筛选
+
+### 3. 分页功能
+
+- 支持分页查询
+- 显示当前页码和总页数
+- 显示总记录数
+- 支持上一页/下一页导航
+
+### 4. 批量操作
+
+- 支持全选/取消全选
+- 支持批量删除
+- 支持批量上架
+- 支持批量下架
+
+### 5. 数据验证
+
+- 前端表单验证
+- 后端参数验证
+- 错误提示信息
+
+### 6. 状态管理
+
+- 书籍状态显示(上架/下架)
+- 状态颜色区分(绿色-上架,红色-下架)
+- 状态切换功能
+
+## 📊 数据统计
+
+### 书籍统计
+
+- 总记录数
+- 当前页码
+- 总页数
+- 每页记录数
+
+### 状态统计
+
+- 上架书籍数量
+- 下架书籍数量
+
+## 🔐 权限控制
+
+### 登录权限
+
+- 只有管理员(role='admin')可以登录
+- 普通用户(role='user')不能登录
+- 登录时验证用户角色和状态
+
+### 接口权限
+
+- 所有后台管理接口都需要token
+- Token验证通过后才能访问
+- Token无效时返回401错误
+
+## 💡 使用技巧
+
+### 1. 快速搜索
+
+- 输入关键词后等待500ms,系统自动搜索
+- 支持书名、作者搜索
+- 搜索结果实时更新
+
+### 2. 批量操作
+
+- 使用全选功能快速选中所有书籍
+- 使用批量操作功能快速处理多个书籍
+- 操作前会确认,防止误操作
+
+### 3. 状态管理
+
+- 上架书籍会显示在小程序中
+- 下架书籍不会显示在小程序中
+- 可以通过状态筛选查看上架/下架书籍
+
+### 4. 数据管理
+
+- 添加书籍时设置合适的分类
+- 设置合适的价格和VIP状态
+- 上传合适的封面图片
+- 填写详细的书籍介绍
+
+## 🆘 常见问题
+
+### 1. 无法登录
+
+**问题:** 提示"用户名或密码错误"
+
+**解决:**
+- 检查用户名和密码是否正确
+- 检查用户角色是否为admin
+- 检查用户状态是否为1(启用)
+
+### 2. 无法加载书籍列表
+
+**问题:** 书籍列表为空或加载失败
+
+**解决:**
+- 检查后端服务是否启动
+- 检查数据库连接是否正常
+- 检查token是否有效
+- 查看浏览器控制台错误信息
+
+### 3. 无法保存书籍
+
+**问题:** 保存书籍时提示错误
+
+**解决:**
+- 检查必填字段是否填写
+- 检查数据格式是否正确
+- 查看错误提示信息
+- 检查后端服务日志
+
+### 4. 无法删除书籍
+
+**问题:** 删除书籍时提示错误
+
+**解决:**
+- 检查书籍是否存在
+- 检查是否有权限删除
+- 查看错误提示信息
+- 检查后端服务日志
+
+## 📚 相关文档
+
+- `README.md` - 功能说明和API文档
+- `启动指南.md` - 详细启动步骤
+- `完整功能说明.md` - 完整功能说明
+- `快速启动.md` - 快速启动指南
+- `项目结构说明.md` - 项目结构说明
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 265 - 0
book-admin/启动指南.md

@@ -0,0 +1,265 @@
+# 后台管理系统启动指南
+
+## 📋 启动前准备
+
+### 1. 检查环境
+- ✅ Java 8 或以上版本
+- ✅ Maven 3.x
+- ✅ MySQL 5.7 或以上版本
+- ✅ Node.js 和 npm(用于启动前端服务器)
+- ✅ 浏览器(Chrome、Firefox、Edge等)
+
+### 2. 检查数据库
+- ✅ MySQL 服务已启动
+- ✅ 数据库 `books_db` 已创建
+- ✅ 数据库用户有权限访问
+
+## 🚀 启动步骤
+
+### 步骤1:初始化数据库
+
+#### 1.1 执行数据库脚本
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/admin_schema.sql
+```
+
+#### 1.2 验证管理员账号
+
+```sql
+-- 连接到MySQL
+mysql -u root -p books_db
+
+-- 查看管理员账号
+SELECT id, username, nickname, role, status FROM users WHERE username = 'admin';
+```
+
+**期望结果:**
+- 应该有一条记录
+- `username` = 'admin'
+- `role` = 'admin'
+- `status` = 1
+
+### 步骤2:启动后端服务
+
+#### 2.1 使用Maven启动(推荐)
+
+```bash
+# 在book项目根目录执行
+mvn spring-boot:run
+```
+
+#### 2.2 使用IDE启动
+
+1. 打开IDE(如IntelliJ IDEA、Eclipse等)
+2. 导入项目(Maven项目)
+3. 找到 `BookApplication.java` 文件
+4. 右键点击 → Run 'BookApplication'
+5. 等待服务启动完成
+
+#### 2.3 验证后端服务
+
+**查看启动日志:**
+- 应该看到 `Started BookApplication` 日志
+- 应该看到 `Tomcat started on port(s): 8081` 日志
+
+**测试服务:**
+- 打开浏览器,访问:`http://localhost:8081/api/admin/login`
+- 如果能看到响应(即使是错误响应),说明服务已启动
+
+### 步骤3:启动前端开发服务器
+
+#### 3.1 安装依赖(首次使用)
+
+```bash
+# 进入book-admin目录
+cd book-admin
+
+# 安装依赖
+npm install
+```
+
+#### 3.2 启动开发服务器
+
+```bash
+# 使用npm启动(推荐)
+npm run dev
+
+# 或者使用
+npm start
+```
+
+**功能:**
+- 启动本地服务器(端口8000)
+- 自动打开浏览器
+- 访问登录页面
+
+#### 3.3 验证前端服务
+
+**访问地址:**
+- 登录页面:`http://localhost:8000/pages/login.html`
+- 如果页面正常显示,说明前端服务已启动
+
+### 步骤4:登录系统
+
+#### 4.1 使用默认管理员账号登录
+
+- **用户名:** `admin`
+- **密码:** `admin123`
+
+#### 4.2 登录成功
+
+登录成功后,会自动跳转到书籍管理页面(`books.html`)
+
+## 🔍 问题排查
+
+### 问题1:后端服务无法启动
+
+**错误信息:** 端口被占用、数据库连接失败等
+
+**解决方法:**
+1. 检查端口8081是否被占用
+2. 检查数据库配置(`application.properties`)
+3. 检查MySQL服务是否启动
+4. 检查数据库 `books_db` 是否存在
+
+### 问题2:无法连接到后端服务
+
+**错误信息:** "Failed to fetch"、"无法连接到后端服务"
+
+**解决方法:**
+1. 检查后端服务是否启动
+2. 检查后端服务端口是否为8081
+3. 检查前端API地址配置(`utils/api.js`)
+4. 使用浏览器访问 `http://localhost:8081/api/admin/login` 测试
+
+### 问题3:登录失败
+
+**错误信息:** "用户名或密码错误"、"您不是管理员"
+
+**解决方法:**
+1. 检查管理员账号是否存在
+2. 检查管理员账号角色是否为 `admin`
+3. 检查管理员账号状态是否为 `1`(启用)
+4. 检查密码是否正确(默认密码:admin123)
+
+### 问题4:npm install 失败
+
+**错误信息:** 网络错误、权限错误等
+
+**解决方法:**
+1. 检查网络连接
+2. 使用国内镜像源:
+   ```bash
+   npm config set registry https://registry.npmmirror.com
+   npm install
+   ```
+3. 清除缓存:
+   ```bash
+   npm cache clean --force
+   npm install
+   ```
+
+### 问题5:端口被占用
+
+**错误信息:** `Port 8000 is already in use`
+
+**解决方法:**
+1. 修改端口:编辑 `package.json`,修改端口号
+2. 或者关闭占用端口的程序
+
+## 📝 快速启动命令
+
+### Windows
+
+```batch
+# 终端1:启动后端服务
+cd book
+mvn spring-boot:run
+
+# 终端2:启动前端服务器
+cd book-admin
+npm install  # 首次使用
+npm run dev  # 启动开发服务器
+```
+
+### Mac/Linux
+
+```bash
+# 终端1:启动后端服务
+cd book
+mvn spring-boot:run
+
+# 终端2:启动前端服务器
+cd book-admin
+npm install  # 首次使用
+npm run dev  # 启动开发服务器
+```
+
+## ✅ 启动检查清单
+
+- [ ] MySQL服务已启动
+- [ ] 数据库 `books_db` 已创建
+- [ ] 已执行 `admin_schema.sql` 脚本
+- [ ] 管理员账号已创建(用户名:admin,角色:admin)
+- [ ] 后端服务已启动(端口8081)
+- [ ] 后端服务启动日志正常
+- [ ] 前端依赖已安装(npm install)
+- [ ] 前端服务器已启动(端口8000)
+- [ ] 可以访问登录页面
+- [ ] 可以正常登录
+
+## 🎯 启动后的操作
+
+### 1. 测试登录
+- 使用管理员账号登录
+- 验证登录成功
+
+### 2. 测试功能
+- 查看书籍列表
+- 添加书籍
+- 编辑书籍
+- 删除书籍
+- 上架/下架书籍
+
+### 3. 查看日志
+- 后端服务日志
+- 浏览器控制台日志
+- 网络请求日志
+
+## 💡 提示
+
+1. **后端服务必须启动**:前端无法独立运行,必须依赖后端服务
+2. **使用npm启动**:推荐使用 `npm run dev` 启动前端服务器
+3. **检查端口**:确保8081和8000端口未被占用
+4. **查看日志**:启动时查看日志,及时发现错误
+5. **测试连接**:使用浏览器测试后端服务连接状态
+
+## 🆘 需要帮助?
+
+如果遇到问题,请:
+1. 查看错误日志
+2. 查看浏览器控制台
+3. 检查后端服务是否启动
+4. 检查数据库连接是否正常
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 427 - 0
book-admin/完整功能说明.md

@@ -0,0 +1,427 @@
+# 后台管理系统完整功能说明
+
+## 功能概述
+
+后台管理系统用于管理小程序中的书籍数据,实现了完整的书籍管理功能,包括:
+1. **管理员登录** - 只有管理员可以登录后台管理系统
+2. **书籍管理** - 书籍的增删改查
+3. **书籍状态管理** - 上架/下架操作
+4. **批量操作** - 批量删除、批量上架、批量下架
+
+## 后端实现
+
+### 1. 数据库设计
+
+#### 用户表添加role字段
+
+在 `users` 表中添加 `role` 字段,用于区分管理员和普通用户:
+
+```sql
+ALTER TABLE `users` 
+ADD COLUMN `role` VARCHAR(20) DEFAULT 'user' COMMENT '用户角色:admin-管理员,user-普通用户' AFTER `status`,
+ADD INDEX `idx_role` (`role`);
+```
+
+#### 创建默认管理员账号
+
+```sql
+INSERT INTO `users` (`username`, `nickname`, `password`, `role`, `status`, `created_at`, `updated_at`)
+SELECT 'admin', '管理员', '0192023a7bbd73250516f069df18b500', 'admin', 1, NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `users` WHERE `username` = 'admin');
+```
+
+默认管理员账号:
+- 用户名:`admin`
+- 密码:`admin123`(MD5加密后:`0192023a7bbd73250516f069df18b500`)
+
+### 2. 实体类
+
+#### User.java
+- 添加 `role` 字段(String类型)
+- 用于区分管理员和普通用户
+
+#### Book.java
+- 包含书籍的所有字段
+- 支持状态管理(status:0-下架,1-上架)
+
+### 3. DTO和VO
+
+#### AdminLoginDTO.java
+- 管理员登录DTO
+- 包含用户名和密码
+
+#### AdminLoginVO.java
+- 管理员登录VO
+- 包含用户信息和token
+
+#### BookManageDTO.java
+- 书籍管理DTO
+- 包含书籍的所有可编辑字段
+
+#### AdminBookVO.java
+- 后台管理书籍VO
+- 包含书籍的所有信息,包括分类名称
+
+### 4. 服务层
+
+#### AdminService.java
+- 管理员登录服务
+- 验证用户名和密码
+- 检查用户角色(只有admin可以登录)
+- 检查用户状态
+- 返回token
+
+#### AdminBookService.java
+- 书籍管理服务
+- 分页查询书籍(支持关键词、分类、状态筛选)
+- 根据ID查询书籍
+- 创建书籍
+- 更新书籍
+- 删除书籍(单个/批量)
+- 上架书籍(单个/批量)
+- 下架书籍(单个/批量)
+
+### 5. 控制器
+
+#### AdminController.java
+- 管理员登录接口
+- `POST /api/admin/login` - 管理员登录
+
+#### AdminBookController.java
+- 书籍管理接口
+- `GET /api/admin/book/list` - 分页查询书籍
+- `GET /api/admin/book/{id}` - 根据ID查询书籍
+- `POST /api/admin/book` - 创建书籍
+- `PUT /api/admin/book/{id}` - 更新书籍
+- `DELETE /api/admin/book/{id}` - 删除书籍
+- `DELETE /api/admin/book/batch` - 批量删除书籍
+- `PUT /api/admin/book/{id}/publish` - 上架书籍
+- `PUT /api/admin/book/{id}/unpublish` - 下架书籍
+- `PUT /api/admin/book/batch/publish` - 批量上架书籍
+- `PUT /api/admin/book/batch/unpublish` - 批量下架书籍
+
+### 6. 权限控制
+
+#### AdminInterceptor.java
+- 管理员权限拦截器
+- 拦截所有 `/api/admin/**` 路径的请求
+- 排除登录接口 `/api/admin/login`
+- 验证请求头中的token
+- 如果token无效,返回401未授权错误
+
+#### AdminWebConfig.java
+- Web配置类
+- 注册拦截器
+- 配置拦截路径和排除路径
+
+## 前端实现
+
+### 1. API接口
+
+#### api.js
+- 统一请求方法
+- 管理员登录API
+- 书籍管理API(增删改查、上架下架、批量操作)
+- 分类查询API
+
+### 2. 登录页面
+
+#### login.html
+- 管理员登录表单
+- 用户名和密码验证
+- 登录成功后保存token到localStorage
+- 跳转到书籍管理页面
+- 错误提示和加载状态
+
+### 3. 书籍管理页面
+
+#### books.html
+- 书籍列表展示
+- 搜索和筛选功能(关键词、状态、分类)
+- 分页功能
+- 添加书籍(模态框)
+- 编辑书籍(模态框)
+- 删除书籍(单个/批量)
+- 上架/下架书籍(单个/批量)
+- 退出登录
+
+## 功能特性
+
+### 1. 管理员登录
+
+#### 登录权限
+- 只有 `role` 为 `admin` 的用户可以登录
+- 普通用户(`role` 为 `user`)不能登录后台管理系统
+- 登录时会验证用户角色和状态
+
+#### Token管理
+- Token保存在localStorage中
+- Token格式:`admin_token_{userId}`
+- 所有后台管理接口都需要在请求头中携带token
+
+### 2. 书籍管理
+
+#### 查询功能
+- 支持按书名、作者搜索
+- 支持按状态筛选(上架/下架)
+- 支持按分类筛选
+- 支持分页查询
+- 支持查看所有状态的书籍(后台管理)
+
+#### 添加书籍
+- 填写书籍信息(书名、作者、封面、简介等)
+- 设置书籍价格、是否免费、是否VIP
+- 选择分类
+- 设置书籍状态(上架/下架)
+- 参数验证
+
+#### 编辑书籍
+- 修改书籍信息
+- 保留原有的统计信息(浏览次数、点赞数、阅读次数)
+- 参数验证
+
+#### 删除书籍
+- 单个删除:点击"删除"按钮,确认后删除
+- 批量删除:选中多个书籍后点击"批量删除",确认后删除
+
+#### 上架/下架
+- 单个操作:点击"上架"或"下架"按钮
+- 批量操作:选中多个书籍后点击"批量上架"或"批量下架"
+- 状态更新后自动刷新列表
+
+### 3. 权限控制
+
+#### 接口权限
+- 所有 `/api/admin/**` 接口都需要token
+- 登录接口 `/api/admin/login` 不需要token
+- 如果token无效,返回401未授权错误
+
+#### 前端权限
+- 登录页面会检查是否已登录,如果已登录会跳转到书籍管理页面
+- 书籍管理页面会检查token,如果token不存在会跳转到登录页面
+
+## 使用步骤
+
+### 1. 数据库初始化
+
+```bash
+mysql -u root -p books_db < book/src/main/resources/db/admin_schema.sql
+```
+
+### 2. 启动后端服务
+
+```bash
+cd book
+mvn spring-boot:run
+```
+
+### 3. 启动前端开发服务器
+
+```bash
+cd book-admin
+npm install  # 首次使用需要安装依赖
+npm run dev  # 启动开发服务器
+```
+
+### 4. 登录
+
+- 用户名:`admin`
+- 密码:`admin123`
+
+### 5. 管理书籍
+
+- 查看书籍列表
+- 搜索和筛选书籍
+- 添加新书籍
+- 编辑现有书籍
+- 删除书籍
+- 上架/下架书籍
+- 批量操作
+
+## API接口详细说明
+
+### 管理员登录
+
+**POST** `/api/admin/login`
+
+请求体:
+```json
+{
+  "username": "admin",
+  "password": "admin123"
+}
+```
+
+响应:
+```json
+{
+  "code": 200,
+  "message": "登录成功",
+  "data": {
+    "id": 1,
+    "username": "admin",
+    "nickname": "管理员",
+    "role": "admin",
+    "token": "admin_token_1"
+  }
+}
+```
+
+### 分页查询书籍
+
+**GET** `/api/admin/book/list?page=1&size=10&keyword=&status=1&categoryId=`
+
+参数:
+- `page` - 页码(默认:1)
+- `size` - 每页数量(默认:10)
+- `keyword` - 关键词(可选,搜索书名、作者)
+- `status` - 状态(可选,0-下架,1-上架)
+- `categoryId` - 分类ID(可选)
+
+响应:
+```json
+{
+  "code": 200,
+  "message": "成功",
+  "data": {
+    "list": [...],
+    "total": 100,
+    "page": 1,
+    "size": 10
+  }
+}
+```
+
+### 创建书籍
+
+**POST** `/api/admin/book`
+
+请求头:
+```
+Authorization: admin_token_1
+```
+
+请求体:
+```json
+{
+  "title": "书名",
+  "author": "作者",
+  "cover": "封面URL",
+  "image": "图片URL",
+  "brief": "简介",
+  "desc": "描述",
+  "introduction": "详细介绍",
+  "price": 0.00,
+  "isFree": false,
+  "isVip": false,
+  "categoryId": 1,
+  "status": 1
+}
+```
+
+### 更新书籍
+
+**PUT** `/api/admin/book/{id}`
+
+请求头和请求体同创建书籍。
+
+### 删除书籍
+
+**DELETE** `/api/admin/book/{id}`
+
+请求头:
+```
+Authorization: admin_token_1
+```
+
+### 上架书籍
+
+**PUT** `/api/admin/book/{id}/publish`
+
+请求头:
+```
+Authorization: admin_token_1
+```
+
+### 下架书籍
+
+**PUT** `/api/admin/book/{id}/unpublish`
+
+请求头:
+```
+Authorization: admin_token_1
+```
+
+## 数据库表结构
+
+### users表
+
+- `id` - 用户ID
+- `username` - 用户名
+- `password` - 密码(MD5加密)
+- `role` - 用户角色(admin-管理员,user-普通用户)
+- `status` - 状态(0-禁用,1-启用)
+- 其他字段...
+
+### books表
+
+- `id` - 书籍ID
+- `title` - 书名
+- `author` - 作者
+- `cover` - 封面URL
+- `image` - 图片URL
+- `brief` - 简介
+- `desc` - 描述
+- `introduction` - 详细介绍
+- `price` - 价格
+- `is_free` - 是否免费
+- `is_vip` - 是否VIP
+- `category_id` - 分类ID
+- `status` - 状态(0-下架,1-上架)
+- `view_count` - 浏览次数
+- `like_count` - 点赞数
+- `read_count` - 阅读次数
+- `created_at` - 创建时间
+- `updated_at` - 更新时间
+
+## 注意事项
+
+1. **密码加密**:系统使用MD5加密密码,默认管理员密码 `admin123` 的MD5值为 `0192023a7bbd73250516f069df18b500`
+2. **Token管理**:当前使用的是模拟token(`admin_token_{userId}`),生产环境建议使用JWT Token
+3. **CORS配置**:后端已配置CORS,允许跨域请求
+4. **API地址**:前端API地址配置在 `utils/api.js` 中,默认是 `http://localhost:8081`
+5. **Vue.js版本**:前端使用Vue.js 3,通过CDN引入
+6. **权限验证**:权限拦截器会验证token,如果token无效会返回401错误
+
+## 后续优化建议
+
+1. **JWT Token**:使用JWT Token替代模拟token
+2. **Token过期**:添加token过期时间管理
+3. **操作日志**:记录管理员操作日志
+4. **数据统计**:添加数据统计功能(书籍数量、用户数量等)
+5. **用户管理**:添加用户管理功能
+6. **分类管理**:添加分类管理功能
+7. **图片上传**:添加图片上传功能
+8. **数据导出**:添加数据导出功能(Excel、CSV等)
+9. **权限细化**:细化权限管理(不同管理员有不同的权限)
+10. **操作确认**:添加操作确认对话框,防止误操作
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 279 - 0
book-admin/实现总结.md

@@ -0,0 +1,279 @@
+# 后台管理系统实现总结
+
+## ✅ 已完成功能
+
+### 1. 后端实现
+
+#### 数据库设计
+- ✅ 在 `users` 表中添加 `role` 字段
+- ✅ 创建默认管理员账号(用户名:admin,密码:admin123)
+- ✅ 数据库脚本:`book/src/main/resources/db/admin_schema.sql`
+
+#### 实体类
+- ✅ `User.java` - 添加 `role` 字段
+- ✅ `Book.java` - 书籍实体类(已存在)
+
+#### DTO和VO
+- ✅ `AdminLoginDTO.java` - 管理员登录DTO
+- ✅ `AdminLoginVO.java` - 管理员登录VO
+- ✅ `BookManageDTO.java` - 书籍管理DTO
+- ✅ `AdminBookVO.java` - 后台管理书籍VO
+
+#### 服务层
+- ✅ `AdminService.java` - 管理员登录服务
+- ✅ `AdminBookService.java` - 书籍管理服务
+
+#### 控制器
+- ✅ `AdminController.java` - 管理员登录接口
+- ✅ `AdminBookController.java` - 书籍管理接口(10个接口)
+
+#### 权限控制
+- ✅ `AdminInterceptor.java` - 管理员权限拦截器
+- ✅ `AdminWebConfig.java` - Web配置(注册拦截器)
+
+#### Mapper
+- ✅ `BookMapper.java` - 书籍Mapper接口(已存在,包含所有方法)
+- ✅ `BookMapper.xml` - 书籍Mapper XML(已存在,包含批量删除方法)
+- ✅ `CategoryMapper.java` - 分类Mapper接口(已存在)
+
+### 2. 前端实现
+
+#### 页面
+- ✅ `login.html` - 登录页面
+- ✅ `books.html` - 书籍管理页面
+- ✅ `test-connection.html` - 连接测试页面
+
+#### API接口
+- ✅ `api.js` - API接口文件(统一请求方法、所有API接口)
+
+#### 配置文件
+- ✅ `package.json` - npm配置文件
+- ✅ `.gitignore` - Git忽略文件
+
+#### 文档
+- ✅ `README.md` - 功能说明和API文档
+- ✅ `启动指南.md` - 详细启动步骤
+- ✅ `完整功能说明.md` - 完整功能说明
+- ✅ `快速启动.md` - 快速启动指南
+- ✅ `使用说明.md` - 使用说明
+- ✅ `项目结构说明.md` - 项目结构说明
+
+## 🎯 核心功能
+
+### 1. 管理员登录
+- ✅ 只有管理员(role='admin')可以登录
+- ✅ 普通用户不能登录后台管理系统
+- ✅ Token验证和保存
+
+### 2. 书籍管理
+- ✅ 分页查询书籍(支持搜索、筛选)
+- ✅ 添加书籍
+- ✅ 编辑书籍
+- ✅ 删除书籍(单个/批量)
+- ✅ 上架/下架书籍(单个/批量)
+- ✅ 书籍状态管理
+
+### 3. 权限控制
+- ✅ 接口权限验证(Token验证)
+- ✅ 前端权限控制(登录状态检查)
+- ✅ 权限拦截器(后端)
+
+## 📁 项目结构
+
+```
+book/
+├── src/main/java/com/yu/book/admin/    # 后端代码
+│   ├── controller/                     # 控制器
+│   ├── service/                        # 服务层
+│   ├── dto/                           # DTO
+│   ├── vo/                            # VO
+│   ├── interceptor/                   # 拦截器
+│   └── config/                        # 配置类
+├── src/main/resources/
+│   ├── db/admin_schema.sql            # 数据库脚本
+│   └── mapper/                        # Mapper XML
+└── book-admin/                        # 前端项目
+    ├── src/main/resources/static/     # 静态资源
+    │   ├── pages/                     # 页面
+    │   └── utils/                     # 工具类
+    ├── package.json                   # npm配置
+    └── README.md                      # 说明文档
+```
+
+## 🚀 启动方式
+
+### 1. 启动后端服务
+
+```bash
+cd book
+mvn spring-boot:run
+```
+
+### 2. 启动前端开发服务器
+
+```bash
+cd book-admin
+npm install  # 首次使用
+npm run dev  # 启动开发服务器
+```
+
+### 3. 访问系统
+
+- 登录页面:`http://localhost:8000/pages/login.html`
+- 书籍管理页面:`http://localhost:8000/pages/books.html`
+
+## 🔐 登录信息
+
+- 用户名:`admin`
+- 密码:`admin123`
+
+## 📊 API接口
+
+### 管理员登录
+- `POST /api/admin/login` - 管理员登录
+
+### 书籍管理
+- `GET /api/admin/book/list` - 分页查询书籍
+- `GET /api/admin/book/{id}` - 根据ID查询书籍
+- `POST /api/admin/book` - 创建书籍
+- `PUT /api/admin/book/{id}` - 更新书籍
+- `DELETE /api/admin/book/{id}` - 删除书籍
+- `DELETE /api/admin/book/batch` - 批量删除书籍
+- `PUT /api/admin/book/{id}/publish` - 上架书籍
+- `PUT /api/admin/book/{id}/unpublish` - 下架书籍
+- `PUT /api/admin/book/batch/publish` - 批量上架书籍
+- `PUT /api/admin/book/batch/unpublish` - 批量下架书籍
+
+## 🎨 前端特性
+
+### 1. 用户界面
+- ✅ 现代化的UI设计
+- ✅ 响应式布局
+- ✅ 友好的用户体验
+- ✅ 错误提示和加载状态
+
+### 2. 功能特性
+- ✅ 实时搜索(防抖处理)
+- ✅ 筛选功能(状态、分类)
+- ✅ 分页功能
+- ✅ 批量操作
+- ✅ 数据验证
+- ✅ 状态管理
+
+### 3. 技术栈
+- ✅ Vue.js 3(CDN)
+- ✅ Fetch API
+- ✅ HTML5 + CSS3
+- ✅ ES6模块化
+
+## 🔒 安全特性
+
+### 1. 权限控制
+- ✅ 只有管理员可以登录
+- ✅ Token验证
+- ✅ 接口权限拦截
+
+### 2. 数据验证
+- ✅ 前端表单验证
+- ✅ 后端参数验证
+- ✅ 错误提示信息
+
+## 📝 数据库设计
+
+### users表
+- ✅ 添加 `role` 字段
+- ✅ 默认管理员账号
+- ✅ 角色索引
+
+### books表
+- ✅ 完整的书籍字段
+- ✅ 状态管理(status)
+- ✅ 统计字段(viewCount、likeCount、readCount)
+
+## 🎯 功能实现
+
+### 1. 管理员登录
+- ✅ 用户名和密码验证
+- ✅ 用户角色验证(只有admin可以登录)
+- ✅ 用户状态验证
+- ✅ Token生成和返回
+
+### 2. 书籍管理
+- ✅ 分页查询(支持搜索、筛选)
+- ✅ 添加书籍(参数验证)
+- ✅ 编辑书籍(保留统计信息)
+- ✅ 删除书籍(单个/批量)
+- ✅ 上架/下架(单个/批量)
+- ✅ 状态管理
+
+### 3. 权限控制
+- ✅ 接口权限拦截
+- ✅ Token验证
+- ✅ 前端权限控制
+
+## 📚 文档
+
+### 用户文档
+- ✅ `README.md` - 功能说明和API文档
+- ✅ `启动指南.md` - 详细启动步骤
+- ✅ `使用说明.md` - 使用说明
+- ✅ `快速启动.md` - 快速启动指南
+
+### 技术文档
+- ✅ `完整功能说明.md` - 完整功能说明
+- ✅ `项目结构说明.md` - 项目结构说明
+- ✅ `实现总结.md` - 实现总结(本文档)
+
+## ✅ 测试清单
+
+- [ ] 数据库初始化成功
+- [ ] 管理员账号创建成功
+- [ ] 后端服务启动成功
+- [ ] 前端服务启动成功
+- [ ] 管理员登录成功
+- [ ] 普通用户登录失败(正确)
+- [ ] 书籍列表查询成功
+- [ ] 添加书籍成功
+- [ ] 编辑书籍成功
+- [ ] 删除书籍成功
+- [ ] 上架/下架书籍成功
+- [ ] 批量操作成功
+- [ ] 搜索和筛选功能正常
+- [ ] 分页功能正常
+- [ ] Token验证正常
+- [ ] 权限控制正常
+
+## 🎉 完成状态
+
+所有功能已完成,可以开始使用!
+
+## 📞 后续优化
+
+1. 使用JWT Token进行身份验证
+2. 添加token过期时间管理
+3. 添加操作日志记录
+4. 添加数据统计功能
+5. 添加用户管理功能
+6. 添加分类管理功能
+7. 添加图片上传功能
+8. 添加数据导出功能
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 128 - 0
book-admin/快速修复登录问题.md

@@ -0,0 +1,128 @@
+# 快速修复登录问题
+
+## 🚀 快速修复步骤
+
+### 方法1:执行修复脚本(推荐)
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/fix_admin_user.sql
+```
+
+### 方法2:手动执行SQL
+
+```sql
+-- 1. 连接到MySQL
+mysql -u root -p books_db
+
+-- 2. 删除已存在的admin用户
+DELETE FROM `users` WHERE `username` = 'admin';
+
+-- 3. 创建新的管理员账号
+INSERT INTO `users` (`username`, `nickname`, `password`, `role`, `status`, `created_at`, `updated_at`)
+VALUES ('admin', '管理员', '0192023a7bbd73250516f069df18b500', 'admin', 1, NOW(), NOW());
+
+-- 4. 验证
+SELECT id, username, role, status FROM `users` WHERE `username` = 'admin';
+```
+
+### 方法3:更新现有用户
+
+如果admin用户已存在,但信息不正确:
+
+```sql
+-- 更新admin用户信息
+UPDATE `users` 
+SET 
+    `password` = '0192023a7bbd73250516f069df18b500',
+    `role` = 'admin',
+    `status` = 1,
+    `updated_at` = NOW()
+WHERE `username` = 'admin';
+```
+
+## ✅ 验证修复
+
+执行以下SQL检查管理员账号:
+
+```sql
+SELECT 
+    username,
+    password,
+    role,
+    status,
+    CASE 
+        WHEN password = '0192023a7bbd73250516f069df18b500' AND role = 'admin' AND status = 1 
+        THEN '账号信息正确' 
+        ELSE '账号信息不正确' 
+    END AS status_check
+FROM `users` 
+WHERE `username` = 'admin';
+```
+
+## 🔑 默认管理员账号
+
+- **用户名:** admin
+- **密码:** admin123
+- **密码MD5:** 0192023a7bbd73250516f069df18b500
+- **角色:** admin
+- **状态:** 1
+
+## 🆘 如果仍然无法登录
+
+1. **检查后端服务是否启动**
+   - 访问:`http://localhost:8081/api/admin/login`
+   - 如果无法访问,说明后端服务未启动
+
+2. **检查数据库连接**
+   - 查看 `application.properties` 中的数据库配置
+   - 确保数据库服务已启动
+
+3. **查看后端日志**
+   - 检查后端服务启动日志
+   - 查看是否有错误信息
+
+4. **测试密码加密**
+   - 确保 `PasswordUtil.encrypt("admin123")` 返回 `0192023a7bbd73250516f069df18b500`
+
+## 📝 完整修复SQL脚本
+
+```sql
+-- 完整修复脚本
+USE `books_db`;
+
+-- 1. 添加role字段(如果不存在)
+ALTER TABLE `users` 
+ADD COLUMN IF NOT EXISTS `role` VARCHAR(20) DEFAULT 'user' COMMENT '用户角色:admin-管理员,user-普通用户';
+
+-- 2. 删除已存在的admin用户
+DELETE FROM `users` WHERE `username` = 'admin';
+
+-- 3. 创建新的管理员账号
+INSERT INTO `users` (`username`, `nickname`, `password`, `role`, `status`, `created_at`, `updated_at`)
+VALUES ('admin', '管理员', '0192023a7bbd73250516f069df18b500', 'admin', 1, NOW(), NOW());
+
+-- 4. 验证
+SELECT id, username, nickname, password, role, status 
+FROM `users` 
+WHERE `username` = 'admin';
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 106 - 0
book-admin/快速启动.md

@@ -0,0 +1,106 @@
+# 后台管理系统快速启动
+
+## ⚡ 5分钟快速启动
+
+### 步骤1:初始化数据库(1分钟)
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/admin_schema.sql
+```
+
+### 步骤2:启动后端服务(2分钟)
+
+```bash
+# 在book项目根目录执行
+mvn spring-boot:run
+```
+
+等待看到 `Started BookApplication` 日志。
+
+### 步骤3:启动前端开发服务器(1分钟)
+
+```bash
+# 在book-admin目录执行
+cd book-admin
+npm install  # 首次使用需要安装依赖
+npm run dev  # 启动开发服务器
+```
+
+### 步骤4:登录系统(1分钟)
+
+访问:`http://localhost:8000/pages/login.html`
+
+登录信息:
+- 用户名:`admin`
+- 密码:`admin123`
+
+## ✅ 完成!
+
+登录成功后,您就可以开始管理书籍了。
+
+## 📝 启动命令汇总
+
+### 终端1:启动后端服务
+
+```bash
+cd book
+mvn spring-boot:run
+```
+
+### 终端2:启动前端开发服务器
+
+```bash
+cd book-admin
+npm install  # 首次使用
+npm run dev  # 启动开发服务器
+```
+
+## 🆘 遇到问题?
+
+### 后端服务无法启动
+- 检查端口8081是否被占用
+- 检查数据库配置
+- 检查MySQL服务是否启动
+
+### 无法连接后端服务
+- 检查后端服务是否启动
+- 使用测试页面检查:`http://localhost:8000/pages/test-connection.html`
+
+### 登录失败
+- 检查管理员账号是否创建
+- 检查管理员账号角色是否为 `admin`
+- 检查密码是否正确
+
+### npm install 失败
+- 使用国内镜像:
+  ```bash
+  npm config set registry https://registry.npmmirror.com
+  npm install
+  ```
+
+## 📚 相关文档
+
+- `README.md` - 功能说明和API文档
+- `启动指南.md` - 详细启动步骤
+- `完整功能说明.md` - 完整功能说明
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 203 - 0
book-admin/快速启动指南.md

@@ -0,0 +1,203 @@
+# 后台管理系统快速启动指南
+
+## 🚀 三步启动
+
+### 步骤1:启动后端服务
+
+```bash
+# 在 book 项目根目录执行
+cd book
+mvn spring-boot:run
+```
+
+**等待看到:**
+```
+Started BookApplication
+Tomcat started on port(s): 8081
+```
+
+### 步骤2:启动前端服务
+
+```bash
+# 打开新的终端窗口,进入 book-admin 目录
+cd book-admin
+
+# 首次使用需要安装依赖
+npm install
+
+# 启动前端开发服务器(自动打开浏览器)
+npm run dev
+```
+
+### 步骤3:登录系统
+
+访问:`http://localhost:8000/pages/login.html`
+
+**默认管理员账号:**
+- 用户名:`admin`
+- 密码:`admin123`
+
+## 📋 启动前检查
+
+### 1. 检查数据库
+
+确保MySQL服务已启动,并且数据库 `books_db` 已创建。
+
+如果还没有创建管理员账号,执行:
+
+```bash
+# 在 book 项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/admin_schema.sql
+```
+
+### 2. 检查环境
+
+- ✅ Java 17(已升级)
+- ✅ Maven 3.x
+- ✅ MySQL 5.7+ 或 MySQL 8.0+
+- ✅ Node.js 和 npm
+
+## 🔍 验证服务
+
+### 后端服务验证
+
+访问:`http://localhost:8081/api/admin/login`
+
+如果能看到响应(即使是错误响应),说明后端服务已启动。
+
+### 前端服务验证
+
+访问:`http://localhost:8000/pages/login.html`
+
+如果能看到登录页面,说明前端服务已启动。
+
+## 📝 启动命令汇总
+
+### Windows PowerShell
+
+```powershell
+# 终端1:启动后端服务
+cd book
+mvn spring-boot:run
+
+# 终端2:启动前端服务
+cd book-admin
+npm install  # 首次使用
+npm run dev
+```
+
+### Windows CMD
+
+```cmd
+# 终端1:启动后端服务
+cd book
+mvn spring-boot:run
+
+# 终端2:启动前端服务
+cd book-admin
+npm install  # 首次使用
+npm run dev
+```
+
+### Mac/Linux
+
+```bash
+# 终端1:启动后端服务
+cd book
+mvn spring-boot:run
+
+# 终端2:启动前端服务
+cd book-admin
+npm install  # 首次使用
+npm run dev
+```
+
+## 🆘 常见问题
+
+### 问题1:后端服务无法启动
+
+**错误:** 端口8081被占用
+
+**解决:**
+1. 检查8081端口是否被占用
+2. 或者修改 `application.properties` 中的端口号
+
+### 问题2:前端服务无法启动
+
+**错误:** `npm: command not found`
+
+**解决:**
+1. 检查Node.js是否安装:`node -v`
+2. 检查npm是否安装:`npm -v`
+3. 如果未安装,请先安装Node.js
+
+### 问题3:无法连接到后端服务
+
+**错误:** "Failed to fetch"
+
+**解决:**
+1. 检查后端服务是否启动
+2. 检查后端服务端口是否为8081
+3. 检查前端API地址配置(`utils/api.js`)
+
+### 问题4:登录失败
+
+**错误:** "用户名或密码错误"
+
+**解决:**
+1. 检查管理员账号是否创建
+2. 检查管理员账号角色是否为 `admin`
+3. 检查密码是否正确(默认:admin123)
+
+### 问题5:npm install 失败
+
+**错误:** 网络错误
+
+**解决:**
+```bash
+# 使用国内镜像源
+npm config set registry https://registry.npmmirror.com
+npm install
+```
+
+## ✅ 启动检查清单
+
+- [ ] MySQL服务已启动
+- [ ] 数据库 `books_db` 已创建
+- [ ] 管理员账号已创建(用户名:admin,角色:admin)
+- [ ] 后端服务已启动(端口8081)
+- [ ] 前端依赖已安装(npm install)
+- [ ] 前端服务已启动(端口8000)
+- [ ] 可以访问登录页面
+- [ ] 可以正常登录
+
+## 📚 相关文档
+
+- `README.md` - 完整功能说明
+- `启动指南.md` - 详细启动步骤
+- `使用说明.md` - 使用说明
+
+## 🎯 快速访问
+
+- **登录页面:** `http://localhost:8000/pages/login.html`
+- **书籍管理页面:** `http://localhost:8000/pages/books.html`(需要登录)
+- **后端API:** `http://localhost:8081/api/admin/login`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 233 - 0
book-admin/登录问题排查指南.md

@@ -0,0 +1,233 @@
+# 登录问题排查指南
+
+## 🔴 问题:登录时显示"用户名或密码错误"
+
+## 📋 排查步骤
+
+### 步骤1:检查数据库连接
+
+确保MySQL服务已启动,并且可以连接到数据库。
+
+### 步骤2:检查管理员账号是否存在
+
+#### 方法1:使用SQL查询
+
+```sql
+-- 连接到MySQL
+mysql -u root -p books_db
+
+-- 检查admin用户
+SELECT id, username, nickname, password, role, status 
+FROM users 
+WHERE username = 'admin';
+```
+
+#### 方法2:执行检查脚本
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/check_admin_user.sql
+```
+
+### 步骤3:检查管理员账号信息
+
+管理员账号应该满足以下条件:
+1. ✅ `username` = 'admin'
+2. ✅ `password` = '0192023a7bbd73250516f069df18b500'(admin123的MD5值)
+3. ✅ `role` = 'admin'
+4. ✅ `status` = 1(激活状态)
+
+### 步骤4:修复管理员账号
+
+如果管理员账号不存在或信息不正确,执行修复脚本:
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/fix_admin_user.sql
+```
+
+### 步骤5:手动创建/修复管理员账号
+
+如果脚本执行失败,可以手动执行SQL:
+
+```sql
+-- 1. 确保role字段存在
+ALTER TABLE `users` 
+ADD COLUMN `role` VARCHAR(20) DEFAULT 'user' COMMENT '用户角色:admin-管理员,user-普通用户' AFTER `status`;
+
+-- 2. 删除已存在的admin用户(如果存在)
+DELETE FROM `users` WHERE `username` = 'admin';
+
+-- 3. 创建新的管理员账号
+INSERT INTO `users` (`username`, `nickname`, `password`, `role`, `status`, `created_at`, `updated_at`)
+VALUES ('admin', '管理员', '0192023a7bbd73250516f069df18b500', 'admin', 1, NOW(), NOW());
+
+-- 4. 验证管理员账号
+SELECT id, username, nickname, password, role, status 
+FROM `users` 
+WHERE `username` = 'admin';
+```
+
+## 🔍 常见问题
+
+### 问题1:管理员账号不存在
+
+**现象:** 查询结果为空
+
+**解决:** 执行修复脚本或手动创建管理员账号
+
+### 问题2:密码不正确
+
+**现象:** 密码字段不是 `0192023a7bbd73250516f069df18b500`
+
+**解决:** 
+```sql
+UPDATE `users` 
+SET `password` = '0192023a7bbd73250516f069df18b500'
+WHERE `username` = 'admin';
+```
+
+### 问题3:角色不是admin
+
+**现象:** role字段不是 'admin'
+
+**解决:**
+```sql
+UPDATE `users` 
+SET `role` = 'admin'
+WHERE `username` = 'admin';
+```
+
+### 问题4:用户状态未激活
+
+**现象:** status字段不是 1
+
+**解决:**
+```sql
+UPDATE `users` 
+SET `status` = 1
+WHERE `username` = 'admin';
+```
+
+### 问题5:role字段不存在
+
+**现象:** 执行查询时提示role字段不存在
+
+**解决:**
+```sql
+ALTER TABLE `users` 
+ADD COLUMN `role` VARCHAR(20) DEFAULT 'user' COMMENT '用户角色:admin-管理员,user-普通用户' AFTER `status`;
+```
+
+## ✅ 验证修复
+
+### 1. 检查管理员账号
+
+```sql
+SELECT 
+    id,
+    username,
+    password,
+    CASE 
+        WHEN password = '0192023a7bbd73250516f069df18b500' THEN '密码正确'
+        ELSE '密码不正确'
+    END AS password_status,
+    role,
+    CASE 
+        WHEN role = 'admin' THEN '角色正确'
+        ELSE '角色不正确'
+    END AS role_status,
+    status,
+    CASE 
+        WHEN status = 1 THEN '已激活'
+        ELSE '未激活'
+    END AS status_text
+FROM users
+WHERE username = 'admin';
+```
+
+### 2. 测试登录
+
+1. 确保后端服务已启动
+2. 访问登录页面:`http://localhost:8000/pages/login.html`
+3. 输入用户名:`admin`
+4. 输入密码:`admin123`
+5. 点击登录
+
+## 🆘 如果仍然无法登录
+
+### 1. 检查后端服务日志
+
+查看后端服务启动日志,检查是否有错误信息。
+
+### 2. 检查数据库连接
+
+确保 `application.properties` 中的数据库配置正确:
+
+```properties
+spring.datasource.url=jdbc:mysql://localhost:3306/books_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+spring.datasource.username=root
+spring.datasource.password=root
+```
+
+### 3. 检查PasswordUtil类
+
+确保 `PasswordUtil.verify` 方法正常工作。可以测试:
+
+```java
+String encrypted = PasswordUtil.encrypt("admin123");
+System.out.println(encrypted); // 应该输出:0192023a7bbd73250516f069df18b500
+
+boolean verified = PasswordUtil.verify("admin123", "0192023a7bbd73250516f069df18b500");
+System.out.println(verified); // 应该输出:true
+```
+
+### 4. 检查后端API
+
+使用curl或Postman测试登录接口:
+
+```bash
+curl -X POST http://localhost:8081/api/admin/login \
+  -H "Content-Type: application/json" \
+  -d '{"username":"admin","password":"admin123"}'
+```
+
+## 📝 默认管理员账号信息
+
+- **用户名:** admin
+- **密码:** admin123
+- **密码MD5:** 0192023a7bbd73250516f069df18b500
+- **角色:** admin
+- **状态:** 1(激活)
+
+## 🔧 快速修复命令
+
+```bash
+# 1. 连接到MySQL
+mysql -u root -p books_db
+
+# 2. 执行修复SQL(在MySQL中执行)
+source book/src/main/resources/db/fix_admin_user.sql
+
+# 或者直接执行:
+mysql -u root -p books_db < book/src/main/resources/db/fix_admin_user.sql
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 208 - 0
book-admin/登录问题解决方案.md

@@ -0,0 +1,208 @@
+# 登录问题解决方案
+
+## 🔴 问题:登录时显示"用户名或密码错误"
+
+## 🚀 快速解决方案
+
+### 方法1:执行安全修复脚本(推荐)
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/fix_admin_user_safe.sql
+```
+
+这个脚本会自动检查字段和索引是否存在,不会因为重复执行而报错。
+
+### 方法2:执行普通修复脚本
+
+```bash
+# 在book项目根目录执行
+mysql -u root -p books_db < src/main/resources/db/fix_admin_user.sql
+```
+
+**注意:** 如果role字段已存在,此脚本可能会报错,可以忽略错误继续执行。
+
+### 方法3:手动执行SQL(最安全)
+
+```sql
+-- 1. 连接到MySQL
+mysql -u root -p books_db
+
+-- 2. 检查admin用户是否存在
+SELECT id, username, password, role, status 
+FROM users 
+WHERE username = 'admin';
+
+-- 3. 如果admin用户存在但信息不正确,更新它
+UPDATE `users` 
+SET 
+    `password` = '0192023a7bbd73250516f069df18b500',
+    `role` = 'admin',
+    `status` = 1,
+    `updated_at` = NOW()
+WHERE `username` = 'admin';
+
+-- 4. 如果admin用户不存在,创建它
+INSERT INTO `users` (`username`, `nickname`, `password`, `role`, `status`, `created_at`, `updated_at`)
+SELECT 'admin', '管理员', '0192023a7bbd73250516f069df18b500', 'admin', 1, NOW(), NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `users` WHERE `username` = 'admin');
+
+-- 5. 验证
+SELECT id, username, password, role, status 
+FROM `users` 
+WHERE `username` = 'admin';
+```
+
+## 🔍 问题排查
+
+### 1. 检查管理员账号是否存在
+
+```sql
+SELECT id, username, password, role, status 
+FROM users 
+WHERE username = 'admin';
+```
+
+### 2. 检查账号信息是否正确
+
+管理员账号应该满足:
+- ✅ `username` = 'admin'
+- ✅ `password` = '0192023a7bbd73250516f069df18b500'
+- ✅ `role` = 'admin'
+- ✅ `status` = 1
+
+### 3. 检查role字段是否存在
+
+```sql
+SELECT COLUMN_NAME 
+FROM INFORMATION_SCHEMA.COLUMNS
+WHERE TABLE_SCHEMA = 'books_db' 
+  AND TABLE_NAME = 'users' 
+  AND COLUMN_NAME = 'role';
+```
+
+如果查询结果为空,说明role字段不存在,需要执行:
+
+```sql
+ALTER TABLE `users` 
+ADD COLUMN `role` VARCHAR(20) DEFAULT 'user' COMMENT '用户角色:admin-管理员,user-普通用户' AFTER `status`;
+```
+
+## ✅ 验证修复
+
+执行以下SQL验证管理员账号:
+
+```sql
+SELECT 
+    username,
+    password,
+    role,
+    status,
+    CASE 
+        WHEN password = '0192023a7bbd73250516f069df18b500' AND role = 'admin' AND status = 1 
+        THEN '✅ 账号信息正确' 
+        ELSE '❌ 账号信息不正确' 
+    END AS status_check
+FROM `users` 
+WHERE `username` = 'admin';
+```
+
+## 🔑 默认管理员账号信息
+
+- **用户名:** admin
+- **密码:** admin123
+- **密码MD5:** 0192023a7bbd73250516f069df18b500
+- **角色:** admin
+- **状态:** 1(激活)
+
+## 🆘 其他可能的问题
+
+### 问题1:后端服务未启动
+
+**检查方法:**
+访问 `http://localhost:8081/api/admin/login`
+
+**解决:**
+```bash
+cd book
+mvn spring-boot:run
+```
+
+### 问题2:数据库连接失败
+
+**检查方法:**
+查看后端服务启动日志
+
+**解决:**
+检查 `application.properties` 中的数据库配置:
+```properties
+spring.datasource.url=jdbc:mysql://localhost:3306/books_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+spring.datasource.username=root
+spring.datasource.password=root
+```
+
+### 问题3:密码加密问题
+
+**检查方法:**
+测试 `PasswordUtil.encrypt("admin123")` 是否返回 `0192023a7bbd73250516f069df18b500`
+
+**解决:**
+确保数据库中的密码是MD5加密后的值。
+
+## 📝 完整修复步骤
+
+1. **检查数据库连接**
+   ```bash
+   mysql -u root -p books_db
+   ```
+
+2. **执行修复脚本**
+   ```bash
+   mysql -u root -p books_db < book/src/main/resources/db/fix_admin_user_safe.sql
+   ```
+
+3. **验证管理员账号**
+   ```sql
+   SELECT id, username, role, status FROM users WHERE username = 'admin';
+   ```
+
+4. **重启后端服务**
+   ```bash
+   cd book
+   mvn spring-boot:run
+   ```
+
+5. **测试登录**
+   - 访问:`http://localhost:8000/pages/login.html`
+   - 用户名:admin
+   - 密码:admin123
+
+## 🎯 一键修复(Windows)
+
+双击运行:`book-admin/一键修复登录问题.bat`
+
+## 🎯 一键修复(Mac/Linux)
+
+```bash
+chmod +x book-admin/一键修复登录问题.sh
+./book-admin/一键修复登录问题.sh
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 196 - 0
book-admin/项目结构说明.md

@@ -0,0 +1,196 @@
+# 后台管理系统项目结构说明
+
+## 📁 项目结构
+
+```
+book/
+├── src/main/java/com/yu/book/
+│   ├── admin/                          # 后台管理模块
+│   │   ├── controller/
+│   │   │   ├── AdminController.java    # 管理员登录控制器
+│   │   │   └── AdminBookController.java # 书籍管理控制器
+│   │   ├── service/
+│   │   │   ├── AdminService.java       # 管理员服务
+│   │   │   └── AdminBookService.java   # 书籍管理服务
+│   │   ├── dto/
+│   │   │   ├── AdminLoginDTO.java      # 登录DTO
+│   │   │   └── BookManageDTO.java      # 书籍管理DTO
+│   │   ├── vo/
+│   │   │   ├── AdminLoginVO.java       # 登录VO
+│   │   │   └── AdminBookVO.java        # 书籍管理VO
+│   │   ├── interceptor/
+│   │   │   └── AdminInterceptor.java   # 权限拦截器
+│   │   └── config/
+│   │       └── AdminWebConfig.java     # Web配置
+│   └── ...
+├── src/main/resources/
+│   ├── db/
+│   │   └── admin_schema.sql            # 数据库脚本
+│   └── mapper/
+│       └── BookMapper.xml              # 书籍Mapper XML
+└── book-admin/                         # 前端项目目录
+    ├── src/main/resources/static/      # 静态资源目录
+    │   ├── pages/
+    │   │   ├── login.html              # 登录页面
+    │   │   ├── books.html              # 书籍管理页面
+    │   │   └── test-connection.html    # 连接测试页面
+    │   └── utils/
+    │       └── api.js                  # API接口文件
+    ├── package.json                    # npm配置文件
+    ├── README.md                       # 说明文档
+    ├── 启动指南.md                     # 启动指南
+    ├── 完整功能说明.md                 # 功能说明
+    └── 快速启动.md                     # 快速启动
+```
+
+## 🎯 文件说明
+
+### 后端文件
+
+#### Controller层
+- `AdminController.java` - 管理员登录控制器
+- `AdminBookController.java` - 书籍管理控制器
+
+#### Service层
+- `AdminService.java` - 管理员服务(登录验证)
+- `AdminBookService.java` - 书籍管理服务(增删改查、上架下架)
+
+#### DTO和VO
+- `AdminLoginDTO.java` - 管理员登录DTO
+- `AdminLoginVO.java` - 管理员登录VO
+- `BookManageDTO.java` - 书籍管理DTO
+- `AdminBookVO.java` - 书籍管理VO
+
+#### 权限控制
+- `AdminInterceptor.java` - 管理员权限拦截器
+- `AdminWebConfig.java` - Web配置(注册拦截器)
+
+### 前端文件
+
+#### 页面
+- `login.html` - 登录页面
+- `books.html` - 书籍管理页面
+- `test-connection.html` - 连接测试页面
+
+#### 工具类
+- `api.js` - API接口文件(统一请求方法、所有API接口)
+
+#### 配置文件
+- `package.json` - npm配置文件(启动脚本、依赖配置)
+
+## 🔧 配置文件
+
+### package.json
+
+```json
+{
+  "scripts": {
+    "dev": "http-server src/main/resources/static -p 8000 -o",
+    "start": "http-server src/main/resources/static -p 8000 -o",
+    "serve": "http-server src/main/resources/static -p 8000"
+  }
+}
+```
+
+### api.js
+
+```javascript
+const BASE_URL = 'http://localhost:8081'
+```
+
+## 🚀 启动方式
+
+### 方式1:使用npm(推荐)
+
+```bash
+cd book-admin
+npm install
+npm run dev
+```
+
+### 方式2:使用Python
+
+```bash
+cd book-admin/src/main/resources/static
+python -m http.server 8000
+```
+
+### 方式3:使用Node.js
+
+```bash
+cd book-admin/src/main/resources/static
+npx http-server -p 8000
+```
+
+## 📝 访问地址
+
+- 登录页面:`http://localhost:8000/pages/login.html`
+- 书籍管理页面:`http://localhost:8000/pages/books.html`(需要登录)
+- 连接测试页面:`http://localhost:8000/pages/test-connection.html`
+
+## 🔐 权限控制
+
+### 后端权限控制
+
+- 所有 `/api/admin/**` 接口都需要token
+- 登录接口 `/api/admin/login` 不需要token
+- 权限拦截器验证token,如果无效返回401错误
+
+### 前端权限控制
+
+- 登录页面检查是否已登录,如果已登录跳转到书籍管理页面
+- 书籍管理页面检查token,如果token不存在跳转到登录页面
+
+## 📊 数据库表
+
+### users表
+
+- 添加 `role` 字段(admin-管理员,user-普通用户)
+- 默认管理员账号:admin / admin123
+
+### books表
+
+- 包含书籍的所有字段
+- 支持状态管理(status:0-下架,1-上架)
+
+## 🎨 前端技术
+
+- Vue.js 3(通过CDN引入)
+- Fetch API(用于HTTP请求)
+- HTML5 + CSS3
+- 响应式设计
+
+## 🔄 数据流
+
+1. 用户登录 → 后端验证 → 返回token
+2. 前端保存token → 后续请求携带token
+3. 后端拦截器验证token → 允许或拒绝请求
+4. 前端显示数据 → 用户操作 → 更新数据
+
+## 📌 注意事项
+
+1. **后端服务必须启动**:前端依赖后端服务(端口8081)
+2. **使用本地服务器**:不要直接双击HTML文件
+3. **Token管理**:Token保存在localStorage中
+4. **API地址**:前端API地址配置在 `utils/api.js` 中
+5. **CORS配置**:后端已配置CORS,允许跨域请求
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 103 - 0
pom.xml

@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.yu</groupId>
+    <artifactId>book</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>book</name>
+    <description>book</description>
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <spring-boot.version>2.7.18</spring-boot.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- MyBatis -->
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <!-- MySQL驱动 -->
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <version>8.0.33</version>
+            <scope>runtime</scope>
+        </dependency>
+        <!-- 参数验证已移除,改为手动验证 -->
+        <!-- JSON处理 -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.elasticsearch</groupId>
+            <artifactId>jna</artifactId>
+            <version>5.7.0-1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+    </dependencies>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <source>17</source>
+                    <target>17</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <mainClass>com.yu.book.BookApplication</mainClass>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 15 - 0
src/main/java/com/yu/book/BookApplication.java

@@ -0,0 +1,15 @@
+package com.yu.book;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@MapperScan({"com.yu.book.mapper", "com.yu.book.admin.mapper"})
+public class BookApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(BookApplication.class, args);
+    }
+
+}

+ 45 - 0
src/main/java/com/yu/book/admin/config/AdminWebConfig.java

@@ -0,0 +1,45 @@
+package com.yu.book.admin.config;
+
+import com.yu.book.admin.interceptor.AdminInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 后台管理Web配置
+ * 
+ * Spring Boot 2.0+版本直接实现WebMvcConfigurer接口
+ */
+@Configuration
+public class AdminWebConfig implements WebMvcConfigurer {
+    
+    @Autowired
+    private AdminInterceptor adminInterceptor;
+    
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 拦截所有 /api/admin/** 路径,但排除登录接口
+        registry.addInterceptor(adminInterceptor)
+                .addPathPatterns("/api/admin/**")
+                .excludePathPatterns("/api/admin/login");
+    }
+    
+    /**
+     * 配置CORS跨域
+     */
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/api/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
+                .allowedHeaders("*")
+                .exposedHeaders("*")
+                .allowCredentials(false)
+                .maxAge(3600);
+    }
+}
+
+
+

+ 185 - 0
src/main/java/com/yu/book/admin/controller/AdminAudiobookController.java

@@ -0,0 +1,185 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.dto.AudiobookManageDTO;
+import com.yu.book.admin.service.AdminAudiobookService;
+import com.yu.book.admin.vo.AdminAudiobookVO;
+import com.yu.book.common.PageResult;
+import com.yu.book.common.Result;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 后台管理听书控制器
+ */
+@RestController
+@RequestMapping("/api/admin/audiobook")
+@CrossOrigin(origins = "*")
+public class AdminAudiobookController {
+    
+    private final AdminAudiobookService adminAudiobookService;
+    
+    public AdminAudiobookController(AdminAudiobookService adminAudiobookService) {
+        this.adminAudiobookService = adminAudiobookService;
+    }
+    
+    /**
+     * 分页查询听书
+     */
+    @GetMapping("/list")
+    public Result<PageResult<AdminAudiobookVO>> getAudiobooks(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer categoryId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) Boolean isVip,
+            @RequestParam(required = false) Boolean isFree) {
+        try {
+            PageResult<AdminAudiobookVO> result = adminAudiobookService.getAudiobooks(page, size, keyword, categoryId, status, isVip, isFree);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据ID查询听书
+     */
+    @GetMapping("/{id}")
+    public Result<AdminAudiobookVO> getAudiobookById(@PathVariable Integer id) {
+        try {
+            AdminAudiobookVO audiobook = adminAudiobookService.getAudiobookById(id);
+            return Result.success(audiobook);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建听书
+     */
+    @PostMapping
+    public Result<AdminAudiobookVO> createAudiobook(@RequestBody AudiobookManageDTO dto) {
+        try {
+            AdminAudiobookVO audiobook = adminAudiobookService.createAudiobook(dto);
+            return Result.success("创建成功", audiobook);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新听书
+     */
+    @PutMapping("/{id}")
+    public Result<AdminAudiobookVO> updateAudiobook(@PathVariable Integer id, @RequestBody AudiobookManageDTO dto) {
+        try {
+            AdminAudiobookVO audiobook = adminAudiobookService.updateAudiobook(id, dto);
+            return Result.success("更新成功", audiobook);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除听书
+     */
+    @DeleteMapping("/{id}")
+    public Result<String> deleteAudiobook(@PathVariable Integer id) {
+        try {
+            adminAudiobookService.deleteAudiobook(id);
+            return Result.success("删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量删除听书
+     */
+    @DeleteMapping("/batch")
+    public Result<String> deleteAudiobooks(@RequestBody List<Integer> ids) {
+        try {
+            adminAudiobookService.deleteAudiobooks(ids);
+            return Result.success("批量删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("批量删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 上架听书
+     */
+    @PutMapping("/{id}/publish")
+    public Result<String> publishAudiobook(@PathVariable Integer id) {
+        try {
+            adminAudiobookService.publishAudiobook(id);
+            return Result.success("上架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("上架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 下架听书
+     */
+    @PutMapping("/{id}/unpublish")
+    public Result<String> unpublishAudiobook(@PathVariable Integer id) {
+        try {
+            adminAudiobookService.unpublishAudiobook(id);
+            return Result.success("下架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("下架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量上架听书
+     */
+    @PutMapping("/batch/publish")
+    public Result<String> publishAudiobooks(@RequestBody Map<String, List<Integer>> request) {
+        try {
+            List<Integer> ids = request.get("ids");
+            adminAudiobookService.publishAudiobooks(ids);
+            return Result.success("批量上架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("批量上架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量下架听书
+     */
+    @PutMapping("/batch/unpublish")
+    public Result<String> unpublishAudiobooks(@RequestBody Map<String, List<Integer>> request) {
+        try {
+            List<Integer> ids = request.get("ids");
+            adminAudiobookService.unpublishAudiobooks(ids);
+            return Result.success("批量下架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("批量下架失败: " + e.getMessage());
+        }
+    }
+}
+
+

+ 176 - 0
src/main/java/com/yu/book/admin/controller/AdminBannerController.java

@@ -0,0 +1,176 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.entity.Banner;
+import com.yu.book.admin.entity.BannerGroup;
+import com.yu.book.admin.service.AdminBannerService;
+import com.yu.book.common.Result;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 后台轮播图管理控制器
+ */
+@RestController
+@RequestMapping("/api/admin/banner")
+@CrossOrigin(origins = "*")
+public class AdminBannerController {
+    
+    private final AdminBannerService bannerService;
+    
+    public AdminBannerController(AdminBannerService bannerService) {
+        this.bannerService = bannerService;
+    }
+    
+    // ==================== 分组相关接口 ====================
+    
+    /**
+     * 获取所有分组
+     */
+    @GetMapping("/groups")
+    public Result<List<BannerGroup>> listGroups() {
+        try {
+            List<BannerGroup> groups = bannerService.listGroups();
+            return Result.success(groups);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建分组
+     */
+    @PostMapping("/groups")
+    public Result<BannerGroup> createGroup(@RequestBody BannerGroup group) {
+        try {
+            // 参数验证
+            if (group.getName() == null || group.getName().trim().isEmpty()) {
+                return Result.error("分组名称不能为空");
+            }
+            if (group.getCode() == null || group.getCode().trim().isEmpty()) {
+                return Result.error("分组编码不能为空");
+            }
+            BannerGroup created = bannerService.createGroup(group);
+            return Result.success("创建成功", created);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新分组
+     */
+    @PutMapping("/groups/{id}")
+    public Result<BannerGroup> updateGroup(@PathVariable Integer id, @RequestBody BannerGroup group) {
+        try {
+            group.setId(id);
+            BannerGroup updated = bannerService.updateGroup(group);
+            return Result.success("更新成功", updated);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除分组
+     */
+    @DeleteMapping("/groups/{id}")
+    public Result<String> deleteGroup(@PathVariable Integer id) {
+        try {
+            bannerService.deleteGroup(id);
+            return Result.success("删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    // ==================== 轮播图相关接口 ====================
+    
+    /**
+     * 获取轮播图列表
+     */
+    @GetMapping("/list")
+    public Result<List<Banner>> listBanners(@RequestParam Integer groupId,
+                                            @RequestParam(required = false) Integer status) {
+        try {
+            if (groupId == null) {
+                return Result.error("分组ID不能为空");
+            }
+            List<Banner> banners = bannerService.listBanners(groupId, status);
+            return Result.success(banners);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建轮播图
+     */
+    @PostMapping
+    public Result<Banner> createBanner(@RequestBody Banner banner) {
+        try {
+            // 参数验证
+            if (banner.getGroupId() == null) {
+                return Result.error("分组ID不能为空");
+            }
+            if (banner.getImage() == null || banner.getImage().trim().isEmpty()) {
+                return Result.error("图片URL不能为空");
+            }
+            Banner created = bannerService.createBanner(banner);
+            return Result.success("创建成功", created);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新轮播图
+     */
+    @PutMapping("/{id}")
+    public Result<Banner> updateBanner(@PathVariable Integer id, @RequestBody Banner banner) {
+        try {
+            banner.setId(id);
+            Banner updated = bannerService.updateBanner(banner);
+            return Result.success("更新成功", updated);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除轮播图
+     */
+    @DeleteMapping("/{id}")
+    public Result<String> deleteBanner(@PathVariable Integer id) {
+        try {
+            bannerService.deleteBanner(id);
+            return Result.success("删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("删除失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 204 - 0
src/main/java/com/yu/book/admin/controller/AdminBookController.java

@@ -0,0 +1,204 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.dto.BookManageDTO;
+import com.yu.book.admin.service.AdminBookService;
+import com.yu.book.admin.vo.AdminBookVO;
+import com.yu.book.common.PageResult;
+import com.yu.book.common.Result;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 后台管理书籍控制器
+ */
+@RestController
+@RequestMapping("/api/admin/book")
+@CrossOrigin(origins = "*")
+public class AdminBookController {
+    
+    private final AdminBookService adminBookService;
+    
+    public AdminBookController(AdminBookService adminBookService) {
+        this.adminBookService = adminBookService;
+    }
+    
+    /**
+     * 分页查询书籍(后台管理)
+     */
+    @GetMapping("/list")
+    public Result<PageResult<AdminBookVO>> getBooks(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer categoryId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) Boolean isVip) {
+        try {
+            PageResult<AdminBookVO> result = adminBookService.getBooks(page, size, keyword, categoryId, status, isVip);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据ID查询书籍
+     */
+    @GetMapping("/{id}")
+    public Result<AdminBookVO> getBookById(@PathVariable Integer id) {
+        try {
+            AdminBookVO book = adminBookService.getBookById(id);
+            return Result.success(book);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建书籍
+     */
+    @PostMapping
+    public Result<AdminBookVO> createBook(@RequestBody BookManageDTO bookDTO) {
+        try {
+            AdminBookVO book = adminBookService.createBook(bookDTO);
+            return Result.success("创建成功", book);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新书籍
+     */
+    @PutMapping("/{id}")
+    public Result<AdminBookVO> updateBook(@PathVariable Integer id, @RequestBody BookManageDTO bookDTO) {
+        try {
+            AdminBookVO book = adminBookService.updateBook(id, bookDTO);
+            return Result.success("更新成功", book);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除书籍
+     */
+    @DeleteMapping("/{id}")
+    public Result<String> deleteBook(@PathVariable Integer id) {
+        try {
+            adminBookService.deleteBook(id);
+            return Result.success("删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量删除书籍
+     */
+    @DeleteMapping("/batch")
+    public Result<String> deleteBooks(@RequestBody List<Integer> ids) {
+        try {
+            adminBookService.deleteBooks(ids);
+            return Result.success("批量删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("批量删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 上架书籍
+     */
+    @PutMapping("/{id}/publish")
+    public Result<String> publishBook(@PathVariable Integer id) {
+        try {
+            adminBookService.publishBook(id);
+            return Result.success("上架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("上架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 下架书籍
+     */
+    @PutMapping("/{id}/unpublish")
+    public Result<String> unpublishBook(@PathVariable Integer id) {
+        try {
+            adminBookService.unpublishBook(id);
+            return Result.success("下架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("下架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量上架书籍
+     */
+    @PutMapping("/batch/publish")
+    public Result<String> publishBooks(@RequestBody Map<String, List<Integer>> request) {
+        try {
+            List<Integer> ids = request.get("ids");
+            adminBookService.publishBooks(ids);
+            return Result.success("批量上架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("批量上架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量下架书籍
+     */
+    @PutMapping("/batch/unpublish")
+    public Result<String> unpublishBooks(@RequestBody Map<String, List<Integer>> request) {
+        try {
+            List<Integer> ids = request.get("ids");
+            adminBookService.unpublishBooks(ids);
+            return Result.success("批量下架成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("批量下架失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 194 - 0
src/main/java/com/yu/book/admin/controller/AdminChapterController.java

@@ -0,0 +1,194 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.service.AdminChapterService;
+import com.yu.book.common.Result;
+import com.yu.book.domain.BookChapter;
+import com.yu.book.dto.ChapterDTO;
+import com.yu.book.vo.ChapterVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 后台管理章节控制器
+ */
+@RestController
+@RequestMapping("/api/admin/chapter")
+@CrossOrigin(origins = "*")
+public class AdminChapterController {
+    
+    private final AdminChapterService adminChapterService;
+    
+    public AdminChapterController(AdminChapterService adminChapterService) {
+        this.adminChapterService = adminChapterService;
+    }
+    
+    /**
+     * 根据书籍ID获取章节列表
+     */
+    @GetMapping("/list")
+    public Result<List<ChapterVO>> getChaptersByBookId(@RequestParam Integer bookId) {
+        try {
+            List<ChapterVO> chapters = adminChapterService.getChaptersByBookId(bookId);
+            return Result.success(chapters);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据章节ID获取章节详情(包含内容)
+     */
+    @GetMapping("/{id}")
+    public Result<BookChapter> getChapterById(@PathVariable Integer id) {
+        try {
+            BookChapter chapter = adminChapterService.getChapterById(id);
+            if (chapter == null) {
+                return Result.error("章节不存在");
+            }
+            return Result.success(chapter);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建章节
+     */
+    @PostMapping("/create")
+    public Result<BookChapter> createChapter(@RequestBody Map<String, Object> params) {
+        try {
+            ChapterDTO dto = new ChapterDTO();
+            dto.setBookId(getIntegerFromMap(params, "bookId"));
+            dto.setChapterNumber(getIntegerFromMap(params, "chapterNumber"));
+            dto.setTitle((String) params.get("title"));
+            dto.setContent((String) params.get("content"));
+            dto.setIsFree(getBooleanFromMap(params, "isFree"));
+            dto.setIsVip(getBooleanFromMap(params, "isVip"));
+            dto.setStatus(getIntegerFromMap(params, "status"));
+            
+            BookChapter chapter = adminChapterService.createChapter(dto);
+            return Result.success(chapter);
+        } catch (Exception e) {
+            return Result.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新章节
+     */
+    @PutMapping("/update")
+    public Result<BookChapter> updateChapter(@RequestBody Map<String, Object> params) {
+        try {
+            ChapterDTO dto = new ChapterDTO();
+            dto.setId(getIntegerFromMap(params, "id"));
+            dto.setBookId(getIntegerFromMap(params, "bookId"));
+            dto.setChapterNumber(getIntegerFromMap(params, "chapterNumber"));
+            dto.setTitle((String) params.get("title"));
+            dto.setContent((String) params.get("content"));
+            dto.setIsFree(getBooleanFromMap(params, "isFree"));
+            dto.setIsVip(getBooleanFromMap(params, "isVip"));
+            dto.setStatus(getIntegerFromMap(params, "status"));
+            
+            BookChapter chapter = adminChapterService.updateChapter(dto);
+            return Result.success(chapter);
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除章节
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> deleteChapter(@PathVariable Integer id) {
+        try {
+            adminChapterService.deleteChapter(id);
+            return Result.success(null);
+        } catch (Exception e) {
+            return Result.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量删除章节
+     */
+    @PostMapping("/batch-delete")
+    public Result<Void> deleteChapters(@RequestBody Map<String, Object> params) {
+        try {
+            @SuppressWarnings("unchecked")
+            List<Integer> chapterIds = (List<Integer>) params.get("chapterIds");
+            if (chapterIds == null || chapterIds.isEmpty()) {
+                return Result.error("请选择要删除的章节");
+            }
+            adminChapterService.deleteChapters(chapterIds);
+            return Result.success(null);
+        } catch (Exception e) {
+            return Result.error("批量删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从Map中安全获取Integer值
+     */
+    private Integer getIntegerFromMap(Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Integer) {
+            return (Integer) value;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        if (value instanceof String) {
+            try {
+                return Integer.parseInt((String) value);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * 从Map中安全获取Boolean值
+     */
+    private Boolean getBooleanFromMap(Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue() != 0;
+        }
+        if (value instanceof String) {
+            return "true".equalsIgnoreCase((String) value) || "1".equals(value);
+        }
+        return null;
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 68 - 0
src/main/java/com/yu/book/admin/controller/AdminController.java

@@ -0,0 +1,68 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.dto.AdminLoginDTO;
+import com.yu.book.admin.service.AdminService;
+import com.yu.book.admin.vo.AdminLoginVO;
+import com.yu.book.common.Result;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 管理员控制器
+ */
+@RestController
+@RequestMapping("/api/admin")
+@CrossOrigin(origins = "*")
+public class AdminController {
+    
+    private final AdminService adminService;
+    
+    public AdminController(AdminService adminService) {
+        this.adminService = adminService;
+    }
+    
+    /**
+     * 管理员登录
+     * 只有role为'admin'的用户可以登录
+     */
+    @PostMapping("/login")
+    public Result<AdminLoginVO> login(@RequestBody AdminLoginDTO loginDTO) {
+        try {
+            // 参数验证
+            if (loginDTO == null || loginDTO.getUsername() == null || loginDTO.getUsername().trim().isEmpty()) {
+                return Result.error("用户名不能为空");
+            }
+            if (loginDTO.getPassword() == null || loginDTO.getPassword().trim().isEmpty()) {
+                return Result.error("密码不能为空");
+            }
+            
+            AdminLoginVO loginVO = adminService.login(loginDTO);
+            return Result.success("登录成功", loginVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("登录失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 148 - 0
src/main/java/com/yu/book/admin/controller/AdminRankingController.java

@@ -0,0 +1,148 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.dto.AddBookToRankingDTO;
+import com.yu.book.admin.dto.RankingSortDTO;
+import com.yu.book.admin.entity.RankingGroup;
+import com.yu.book.admin.entity.RankingItem;
+import com.yu.book.admin.service.AdminRankingService;
+import com.yu.book.admin.vo.RankingItemVO;
+import com.yu.book.common.Result;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/ranking")
+@CrossOrigin(origins = "*")
+public class AdminRankingController {
+    private final AdminRankingService rankingService;
+
+    public AdminRankingController(AdminRankingService rankingService) {
+        this.rankingService = rankingService;
+    }
+
+    // groups
+    @GetMapping("/groups")
+    public Result<List<RankingGroup>> listGroups() {
+        return Result.success(rankingService.listGroups());
+    }
+
+    @PostMapping("/groups")
+    public Result<RankingGroup> createGroup(@RequestBody RankingGroup group) {
+        return Result.success("创建成功", rankingService.createGroup(group));
+    }
+
+    @PutMapping("/groups/{id}")
+    public Result<RankingGroup> updateGroup(@PathVariable Integer id, @RequestBody RankingGroup group) {
+        group.setId(id);
+        return Result.success("更新成功", rankingService.updateGroup(group));
+    }
+
+    @DeleteMapping("/groups/{id}")
+    public Result<String> deleteGroup(@PathVariable Integer id) {
+        rankingService.deleteGroup(id);
+        return Result.success("删除成功");
+    }
+
+    // items
+    @GetMapping("/items")
+    public Result<List<RankingItem>> listItems(@RequestParam Integer groupId,
+                                               @RequestParam(required = false) Integer status) {
+        return Result.success(rankingService.listItems(groupId, status));
+    }
+
+    @PostMapping("/items")
+    public Result<RankingItem> createItem(@RequestBody RankingItem item) {
+        return Result.success("创建成功", rankingService.createItem(item));
+    }
+
+    @PutMapping("/items/{id}")
+    public Result<RankingItem> updateItem(@PathVariable Integer id, @RequestBody RankingItem item) {
+        item.setId(id);
+        return Result.success("更新成功", rankingService.updateItem(item));
+    }
+
+    @DeleteMapping("/items/{id}")
+    public Result<String> deleteItem(@PathVariable Integer id) {
+        rankingService.deleteItem(id);
+        return Result.success("删除成功");
+    }
+
+    // ========== 扩展接口:排序管理 ==========
+
+    /**
+     * 查询排行榜项(包含书籍信息,支持按分类筛选)
+     */
+    @GetMapping("/items/with-books")
+    public Result<List<RankingItemVO>> listItemsWithBooks(
+            @RequestParam Integer groupId,
+            @RequestParam(required = false) Integer categoryId,
+            @RequestParam(required = false) Integer status) {
+        try {
+            List<RankingItemVO> items = rankingService.listItemsWithBooks(groupId, categoryId, status);
+            return Result.success(items);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 批量更新排序
+     */
+    @PostMapping("/sort")
+    public Result<String> batchUpdateSort(@RequestBody RankingSortDTO sortDTO) {
+        try {
+            rankingService.batchUpdateSort(sortDTO);
+            return Result.success("排序更新成功");
+        } catch (Exception e) {
+            return Result.error("排序更新失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 添加书籍到排行榜
+     */
+    @PostMapping("/items/add")
+    public Result<RankingItem> addBookToRanking(@RequestBody AddBookToRankingDTO dto) {
+        try {
+            // 参数验证
+            if (dto == null || dto.getGroupId() == null) {
+                return Result.error("排行榜组ID不能为空");
+            }
+            if (dto.getBookId() == null) {
+                return Result.error("书籍ID不能为空");
+            }
+            
+            RankingItem item = rankingService.addBookToRanking(
+                dto.getGroupId(), 
+                dto.getBookId(), 
+                dto.getCategoryId()
+            );
+            return Result.success("添加成功", item);
+        } catch (Exception e) {
+            return Result.error("添加失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 从排行榜移除书籍
+     */
+    @DeleteMapping("/items/{id}/remove")
+    public Result<String> removeBookFromRanking(@PathVariable Integer id) {
+        try {
+            rankingService.removeBookFromRanking(id);
+            return Result.success("移除成功");
+        } catch (Exception e) {
+            return Result.error("移除失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+

+ 217 - 0
src/main/java/com/yu/book/admin/controller/AdminUserController.java

@@ -0,0 +1,217 @@
+package com.yu.book.admin.controller;
+
+import com.yu.book.admin.dto.AdminUserDTO;
+import com.yu.book.admin.dto.ResetPasswordDTO;
+import com.yu.book.admin.service.AdminUserService;
+import com.yu.book.admin.vo.AdminUserVO;
+import com.yu.book.common.PageResult;
+import com.yu.book.common.Result;
+import com.yu.book.domain.User;
+import com.yu.book.mapper.UserMapper;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 后台管理用户控制器
+ */
+@RestController
+@RequestMapping("/api/admin/user")
+@CrossOrigin(origins = "*")
+public class AdminUserController {
+    
+    private final AdminUserService adminUserService;
+    private final UserMapper userMapper;
+    
+    public AdminUserController(AdminUserService adminUserService, UserMapper userMapper) {
+        this.adminUserService = adminUserService;
+        this.userMapper = userMapper;
+    }
+    
+    /**
+     * 分页查询用户(后台管理)
+     */
+    @GetMapping("/list")
+    public Result<PageResult<AdminUserVO>> getUsers(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) Boolean isVip,
+            @RequestParam(required = false) String role) {
+        try {
+            PageResult<AdminUserVO> result = adminUserService.getUsers(page, size, keyword, status, isVip, role);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据ID查询用户
+     */
+    @GetMapping("/{id}")
+    public Result<AdminUserVO> getUserById(@PathVariable Integer id) {
+        try {
+            AdminUserVO user = adminUserService.getUserById(id);
+            return Result.success(user);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新用户信息
+     */
+    @PutMapping("/{id}")
+    public Result<AdminUserVO> updateUser(@PathVariable Integer id, 
+                                          @RequestBody AdminUserDTO userDTO,
+                                          HttpServletRequest request) {
+        try {
+            // 获取当前管理员信息
+            AdminInfo adminInfo = getCurrentAdmin(request);
+            if (adminInfo == null) {
+                return Result.error("未授权,请先登录");
+            }
+            
+            AdminUserVO user = adminUserService.updateUser(id, userDTO, adminInfo.getId(), adminInfo.getUsername(), request);
+            return Result.success("更新成功", user);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 禁用用户
+     */
+    @PutMapping("/{id}/disable")
+    public Result<String> disableUser(@PathVariable Integer id, HttpServletRequest request) {
+        try {
+            // 获取当前管理员信息
+            AdminInfo adminInfo = getCurrentAdmin(request);
+            if (adminInfo == null) {
+                return Result.error("未授权,请先登录");
+            }
+            
+            adminUserService.disableUser(id, adminInfo.getId(), adminInfo.getUsername(), request);
+            return Result.success("禁用成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("禁用失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 启用用户
+     */
+    @PutMapping("/{id}/enable")
+    public Result<String> enableUser(@PathVariable Integer id, HttpServletRequest request) {
+        try {
+            // 获取当前管理员信息
+            AdminInfo adminInfo = getCurrentAdmin(request);
+            if (adminInfo == null) {
+                return Result.error("未授权,请先登录");
+            }
+            
+            adminUserService.enableUser(id, adminInfo.getId(), adminInfo.getUsername(), request);
+            return Result.success("启用成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("启用失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 重置用户密码
+     */
+    @PutMapping("/{id}/reset-password")
+    public Result<String> resetPassword(@PathVariable Integer id, 
+                                        @RequestBody ResetPasswordDTO dto,
+                                        HttpServletRequest request) {
+        try {
+            // 获取当前管理员信息
+            AdminInfo adminInfo = getCurrentAdmin(request);
+            if (adminInfo == null) {
+                return Result.error("未授权,请先登录");
+            }
+            
+            if (dto == null || dto.getPassword() == null || dto.getPassword().trim().isEmpty()) {
+                return Result.error("新密码不能为空");
+            }
+            
+            adminUserService.resetPassword(id, dto.getPassword(), adminInfo.getId(), adminInfo.getUsername(), request);
+            return Result.success("密码重置成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("密码重置失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从请求中获取当前管理员信息
+     */
+    private AdminInfo getCurrentAdmin(HttpServletRequest request) {
+        // 从请求头中获取token
+        String token = request.getHeader("Authorization");
+        if (token == null || token.isEmpty()) {
+            // 尝试从参数中获取
+            token = request.getParameter("token");
+        }
+        
+        if (token == null || !token.startsWith("admin_token_")) {
+            return null;
+        }
+        
+        // 从token中解析管理员ID
+        try {
+            String adminIdStr = token.substring("admin_token_".length());
+            Integer adminId = Integer.parseInt(adminIdStr);
+            
+            // 查询管理员信息
+            User admin = userMapper.selectById(adminId);
+            if (admin == null || !"admin".equals(admin.getRole())) {
+                return null;
+            }
+            
+            AdminInfo adminInfo = new AdminInfo();
+            adminInfo.setId(admin.getId());
+            adminInfo.setUsername(admin.getUsername());
+            return adminInfo;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    
+    /**
+     * 管理员信息内部类
+     */
+    private static class AdminInfo {
+        private Integer id;
+        private String username;
+        
+        public Integer getId() {
+            return id;
+        }
+        
+        public void setId(Integer id) {
+            this.id = id;
+        }
+        
+        public String getUsername() {
+            return username;
+        }
+        
+        public void setUsername(String username) {
+            this.username = username;
+        }
+    }
+    
+}
+

+ 32 - 0
src/main/java/com/yu/book/admin/dto/AddBookToRankingDTO.java

@@ -0,0 +1,32 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+
+/**
+ * 添加书籍到排行榜DTO
+ */
+@Data
+public class AddBookToRankingDTO {
+    /**
+     * 排行榜组ID
+     */
+    private Integer groupId;
+    
+    /**
+     * 书籍ID
+     */
+    private Integer bookId;
+    
+    /**
+     * 分类ID(可选)
+     */
+    private Integer categoryId;
+}
+
+
+
+
+
+
+
+

+ 42 - 0
src/main/java/com/yu/book/admin/dto/AdminLoginDTO.java

@@ -0,0 +1,42 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+
+/**
+ * 管理员登录DTO
+ */
+@Data
+public class AdminLoginDTO {
+    
+    /**
+     * 用户名
+     */
+    private String username;
+    
+    /**
+     * 密码
+     */
+    private String password;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 87 - 0
src/main/java/com/yu/book/admin/dto/AdminUserDTO.java

@@ -0,0 +1,87 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 用户管理DTO
+ */
+@Data
+public class AdminUserDTO {
+    
+    /**
+     * 用户ID(更新时使用)
+     */
+    private Integer id;
+    
+    /**
+     * 用户名
+     */
+    private String username;
+    
+    /**
+     * 昵称
+     */
+    private String nickname;
+    
+    /**
+     * 头像URL
+     */
+    private String avatar;
+    
+    /**
+     * 性别(男/女/保密)
+     */
+    private String gender;
+    
+    /**
+     * 生日(yyyy-MM-dd)
+     */
+    private String birthday;
+    
+    /**
+     * 个人简介
+     */
+    private String bio;
+    
+    /**
+     * 手机号
+     */
+    private String phone;
+    
+    /**
+     * 邮箱
+     */
+    private String email;
+    
+    /**
+     * 密码(重置密码时使用)
+     */
+    private String password;
+    
+    /**
+     * 是否VIP:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * VIP过期时间
+     */
+    private Date vipExpireTime;
+    
+    /**
+     * 状态:0-禁用,1-启用
+     */
+    private Integer status;
+    
+    /**
+     * 用户角色:admin-管理员,user-普通用户
+     */
+    private String role;
+}
+
+
+
+
+
+

+ 98 - 0
src/main/java/com/yu/book/admin/dto/AudiobookManageDTO.java

@@ -0,0 +1,98 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+
+/**
+ * 后台管理听书DTO
+ */
+@Data
+public class AudiobookManageDTO {
+    
+    /**
+     * 书名
+     */
+    private String title;
+    
+    /**
+     * 作者
+     */
+    private String author;
+    
+    /**
+     * 主播
+     */
+    private String narrator;
+    
+    /**
+     * 封面
+     */
+    private String cover;
+    
+    /**
+     * 图片(兼容字段)
+     */
+    private String image;
+    
+    /**
+     * 简介
+     */
+    private String brief;
+    
+    /**
+     * 描述
+     */
+    private String desc;
+    
+    /**
+     * 详细介绍
+     */
+    private String introduction;
+    
+    /**
+     * 分类ID
+     */
+    private Integer categoryId;
+    
+    /**
+     * 状态:0-下架,1-上架
+     */
+    private Integer status;
+    
+    /**
+     * 是否免费
+     */
+    private Boolean isFree;
+    
+    /**
+     * 是否VIP专享
+     */
+    private Boolean isVip;
+    
+    /**
+     * 章节数量
+     */
+    private Integer chapterCount;
+    
+    /**
+     * 总时长(秒)
+     */
+    private Integer totalDuration;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 99 - 0
src/main/java/com/yu/book/admin/dto/BookManageDTO.java

@@ -0,0 +1,99 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 书籍管理DTO
+ */
+@Data
+public class BookManageDTO {
+    
+    /**
+     * 书籍ID(更新时使用)
+     */
+    private Integer id;
+    
+    /**
+     * 书名
+     */
+    private String title;
+    
+    /**
+     * 作者
+     */
+    private String author;
+    
+    /**
+     * 封面图片URL
+     */
+    private String cover;
+    
+    /**
+     * 图片URL(兼容字段)
+     */
+    private String image;
+    
+    /**
+     * 简介(简短)
+     */
+    private String brief;
+    
+    /**
+     * 描述
+     */
+    private String desc;
+    
+    /**
+     * 详细介绍
+     */
+    private String introduction;
+    
+    /**
+     * 价格
+     */
+    private BigDecimal price;
+    
+    /**
+     * 是否免费:0-否,1-是
+     */
+    private Boolean isFree;
+    
+    /**
+     * 是否VIP专享:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * 分类ID
+     */
+    private Integer categoryId;
+    
+    /**
+     * 状态:0-下架,1-上架
+     */
+    private Integer status;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 53 - 0
src/main/java/com/yu/book/admin/dto/RankingSortDTO.java

@@ -0,0 +1,53 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+import java.util.List;
+
+/**
+ * 排行榜排序DTO
+ * 用于批量更新排行榜书籍排序
+ */
+@Data
+public class RankingSortDTO {
+    /**
+     * 排行榜组ID
+     */
+    private Integer groupId;
+    
+    /**
+     * 分类ID(可选,NULL表示全部分类)
+     */
+    private Integer categoryId;
+    
+    /**
+     * 排序项列表
+     */
+    private List<SortItem> items;
+    
+    @Data
+    public static class SortItem {
+        /**
+         * 排行榜项ID
+         */
+        private Integer id;
+        
+        /**
+         * 书籍ID
+         */
+        private Integer bookId;
+        
+        /**
+         * 排序值(数字越小越靠前)
+         */
+        private Integer sort;
+    }
+}
+
+
+
+
+
+
+
+
+

+ 21 - 0
src/main/java/com/yu/book/admin/dto/ResetPasswordDTO.java

@@ -0,0 +1,21 @@
+package com.yu.book.admin.dto;
+
+import lombok.Data;
+
+/**
+ * 重置密码DTO
+ */
+@Data
+public class ResetPasswordDTO {
+    
+    /**
+     * 新密码
+     */
+    private String password;
+}
+
+
+
+
+
+

+ 67 - 0
src/main/java/com/yu/book/admin/entity/AdminOperationLog.java

@@ -0,0 +1,67 @@
+package com.yu.book.admin.entity;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 管理员操作日志实体类
+ */
+@Data
+public class AdminOperationLog {
+    
+    /**
+     * 日志ID
+     */
+    private Integer id;
+    
+    /**
+     * 管理员ID
+     */
+    private Integer adminId;
+    
+    /**
+     * 管理员用户名
+     */
+    private String adminUsername;
+    
+    /**
+     * 操作类型:disable_user-禁用用户, enable_user-启用用户, update_user-修改用户信息, reset_password-重置密码等
+     */
+    private String operationType;
+    
+    /**
+     * 目标类型:user-用户, book-书籍等
+     */
+    private String targetType;
+    
+    /**
+     * 目标ID(如用户ID、书籍ID等)
+     */
+    private Integer targetId;
+    
+    /**
+     * 目标名称(如用户名、书籍名等)
+     */
+    private String targetName;
+    
+    /**
+     * 操作描述
+     */
+    private String description;
+    
+    /**
+     * 操作IP地址
+     */
+    private String ipAddress;
+    
+    /**
+     * 操作时间
+     */
+    private Date createdAt;
+}
+
+
+
+
+
+

+ 77 - 0
src/main/java/com/yu/book/admin/entity/Banner.java

@@ -0,0 +1,77 @@
+package com.yu.book.admin.entity;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 轮播图实体类
+ */
+@Data
+public class Banner {
+    /**
+     * 主键ID
+     */
+    private Integer id;
+    
+    /**
+     * 分组ID
+     */
+    private Integer groupId;
+    
+    /**
+     * 标题
+     */
+    private String title;
+    
+    /**
+     * 图片URL
+     */
+    private String image;
+    
+    /**
+     * 外链URL
+     */
+    private String link;
+    
+    /**
+     * 跳转类型:book-书籍,audiobook-有声书,url-外链
+     */
+    private String targetType;
+    
+    /**
+     * 目标ID(书籍ID或有声书ID)
+     */
+    private Integer targetId;
+    
+    /**
+     * 排序值,数字越小越靠前
+     */
+    private Integer sort;
+    
+    /**
+     * 状态:1-启用,0-停用
+     */
+    private Integer status;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 57 - 0
src/main/java/com/yu/book/admin/entity/BannerGroup.java

@@ -0,0 +1,57 @@
+package com.yu.book.admin.entity;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 轮播图分组实体类
+ */
+@Data
+public class BannerGroup {
+    /**
+     * 主键ID
+     */
+    private Integer id;
+    
+    /**
+     * 分组名称
+     */
+    private String name;
+    
+    /**
+     * 分组编码(唯一标识)
+     */
+    private String code;
+    
+    /**
+     * 状态:1-启用,0-停用
+     */
+    private Integer status;
+    
+    /**
+     * 排序值,数字越小越靠前
+     */
+    private Integer sort;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 33 - 0
src/main/java/com/yu/book/admin/entity/RankingGroup.java

@@ -0,0 +1,33 @@
+package com.yu.book.admin.entity;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class RankingGroup {
+    private Integer id;
+    private String name;
+    private String code;
+    private String description;
+    private Integer sort;
+    private Integer status;
+    private Date createdAt;
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 26 - 0
src/main/java/com/yu/book/admin/entity/RankingItem.java

@@ -0,0 +1,26 @@
+package com.yu.book.admin.entity;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class RankingItem {
+    private Integer id;
+    private Integer groupId;
+    private Integer categoryId; // 分类ID,NULL表示不按分类
+    private Integer bookId;
+    private Integer weight;
+    private Integer sort;
+    private Integer status;
+    private Date createdAt;
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+

+ 76 - 0
src/main/java/com/yu/book/admin/interceptor/AdminInterceptor.java

@@ -0,0 +1,76 @@
+package com.yu.book.admin.interceptor;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 管理员权限拦截器
+ * 注意:这是一个简单的实现,实际项目中应该使用JWT Token进行身份验证
+ */
+@Component
+public class AdminInterceptor implements HandlerInterceptor {
+    
+    /**
+     * 在处理器执行之前调用
+     * 返回true表示继续执行,返回false表示中断执行
+     */
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String requestURI = request.getRequestURI();
+        String method = request.getMethod();
+        
+        // 设置CORS头(所有请求都设置,确保跨域请求正常)
+        String origin = request.getHeader("Origin");
+        if (origin != null) {
+            response.setHeader("Access-Control-Allow-Origin", origin);
+        } else {
+            response.setHeader("Access-Control-Allow-Origin", "*");
+        }
+        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH");
+        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
+        response.setHeader("Access-Control-Allow-Credentials", "false");
+        response.setHeader("Access-Control-Max-Age", "3600");
+        
+        // OPTIONS预检请求直接返回
+        if ("OPTIONS".equals(method)) {
+            response.setStatus(HttpServletResponse.SC_OK);
+            return false;
+        }
+        
+        // 登录接口不需要拦截
+        if (requestURI != null && requestURI.contains("/api/admin/login")) {
+            return true;
+        }
+        
+        // 从请求头中获取token
+        String token = request.getHeader("Authorization");
+        if (token == null || token.isEmpty()) {
+            // 尝试从参数中获取
+            token = request.getParameter("token");
+        }
+        
+        // 简单的token验证(实际应该使用JWT验证)
+        // 这里只是示例,实际项目中应该解析JWT token并验证用户角色
+        if (token != null && token.startsWith("admin_token_")) {
+            // token格式正确,允许通过
+            // 实际项目中应该:
+            // 1. 解析JWT token
+            // 2. 验证token是否过期
+            // 3. 验证用户角色是否为admin
+            // 4. 将用户信息存储到request attribute中
+            return true;
+        }
+        
+        // token无效,返回401未授权
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write("{\"code\":401,\"message\":\"未授权,请先登录\"}");
+        return false;
+    }
+}
+
+
+

+ 23 - 0
src/main/java/com/yu/book/admin/mapper/AdminOperationLogMapper.java

@@ -0,0 +1,23 @@
+package com.yu.book.admin.mapper;
+
+import com.yu.book.admin.entity.AdminOperationLog;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 管理员操作日志Mapper接口
+ */
+@Mapper
+public interface AdminOperationLogMapper {
+    
+    /**
+     * 插入操作日志
+     */
+    int insert(AdminOperationLog log);
+}
+
+
+
+
+
+

+ 86 - 0
src/main/java/com/yu/book/admin/mapper/BannerMapper.java

@@ -0,0 +1,86 @@
+package com.yu.book.admin.mapper;
+
+import com.yu.book.admin.entity.Banner;
+import com.yu.book.admin.entity.BannerGroup;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 轮播图Mapper接口
+ */
+@Mapper
+public interface BannerMapper {
+    
+    // ==================== 分组相关 ====================
+    
+    /**
+     * 插入分组
+     */
+    int insertGroup(BannerGroup group);
+    
+    /**
+     * 更新分组
+     */
+    int updateGroup(BannerGroup group);
+    
+    /**
+     * 删除分组
+     */
+    int deleteGroup(@Param("id") Integer id);
+    
+    /**
+     * 查询所有分组
+     */
+    List<BannerGroup> selectGroups();
+    
+    /**
+     * 根据编码查询分组
+     */
+    BannerGroup selectGroupByCode(@Param("code") String code);
+    
+    /**
+     * 根据ID查询分组
+     */
+    BannerGroup selectGroupById(@Param("id") Integer id);
+    
+    // ==================== 轮播图相关 ====================
+    
+    /**
+     * 插入轮播图
+     */
+    int insertBanner(Banner banner);
+    
+    /**
+     * 更新轮播图
+     */
+    int updateBanner(Banner banner);
+    
+    /**
+     * 删除轮播图
+     */
+    int deleteBanner(@Param("id") Integer id);
+    
+    /**
+     * 根据分组ID和状态查询轮播图列表
+     */
+    List<Banner> selectByGroup(@Param("groupId") Integer groupId, @Param("status") Integer status);
+    
+    /**
+     * 根据ID查询轮播图
+     */
+    Banner selectBannerById(@Param("id") Integer id);
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 46 - 0
src/main/java/com/yu/book/admin/mapper/RankingMapper.java

@@ -0,0 +1,46 @@
+package com.yu.book.admin.mapper;
+
+import com.yu.book.admin.entity.RankingGroup;
+import com.yu.book.admin.entity.RankingItem;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface RankingMapper {
+    // groups
+    int insertGroup(RankingGroup group);
+    int updateGroup(RankingGroup group);
+    int deleteGroup(@Param("id") Integer id);
+    List<RankingGroup> selectGroups();
+    RankingGroup selectGroupByCode(@Param("code") String code);
+
+    // items
+    int insertItem(RankingItem item);
+    int updateItem(RankingItem item);
+    int deleteItem(@Param("id") Integer id);
+    List<RankingItem> selectItems(@Param("groupId") Integer groupId, @Param("status") Integer status);
+    
+    // 扩展方法:按分类查询排行榜项
+    List<RankingItem> selectItemsByCategory(@Param("groupId") Integer groupId, 
+                                            @Param("categoryId") Integer categoryId, 
+                                            @Param("status") Integer status);
+    
+    // 批量更新排序
+    int batchUpdateSort(@Param("items") List<RankingItem> items);
+    
+    // 根据groupId和bookId查询
+    RankingItem selectItemByGroupAndBook(@Param("groupId") Integer groupId, 
+                                         @Param("bookId") Integer bookId,
+                                         @Param("categoryId") Integer categoryId);
+}
+
+
+
+
+
+
+
+
+

+ 235 - 0
src/main/java/com/yu/book/admin/service/AdminAudiobookService.java

@@ -0,0 +1,235 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.admin.dto.AudiobookManageDTO;
+import com.yu.book.admin.vo.AdminAudiobookVO;
+import com.yu.book.common.PageResult;
+import com.yu.book.domain.Audiobook;
+import com.yu.book.domain.Category;
+import com.yu.book.mapper.AudiobookMapper;
+import com.yu.book.mapper.CategoryMapper;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 后台管理听书服务类
+ */
+@Service
+public class AdminAudiobookService {
+    
+    private final AudiobookMapper audiobookMapper;
+    private final CategoryMapper categoryMapper;
+    
+    public AdminAudiobookService(AudiobookMapper audiobookMapper,
+                                CategoryMapper categoryMapper) {
+        this.audiobookMapper = audiobookMapper;
+        this.categoryMapper = categoryMapper;
+    }
+    
+    /**
+     * 分页查询听书
+     */
+    public PageResult<AdminAudiobookVO> getAudiobooks(Integer page, Integer size, String keyword,
+                                                     Integer categoryId, Integer status, Boolean isVip, Boolean isFree) {
+        Integer queryStatus = status == null ? -1 : status;
+        Integer offset = (page - 1) * size;
+        List<Audiobook> list = audiobookMapper.selectPage(keyword, categoryId, queryStatus, isVip, isFree, offset, size);
+        Long total = audiobookMapper.count(keyword, categoryId, queryStatus, isVip, isFree);
+        
+        List<AdminAudiobookVO> voList = new ArrayList<>();
+        for (Audiobook audiobook : list) {
+            AdminAudiobookVO vo = convertToVO(audiobook);
+            if (audiobook.getCategoryId() != null) {
+                Category category = categoryMapper.selectById(audiobook.getCategoryId());
+                if (category != null) {
+                    vo.setCategoryName(category.getName());
+                }
+            }
+            voList.add(vo);
+        }
+        
+        return new PageResult<>(voList, total, page, size);
+    }
+    
+    /**
+     * 根据ID查询听书
+     */
+    public AdminAudiobookVO getAudiobookById(Integer id) {
+        Audiobook audiobook = audiobookMapper.selectById(id);
+        if (audiobook == null) {
+            throw new RuntimeException("听书不存在");
+        }
+        AdminAudiobookVO vo = convertToVO(audiobook);
+        if (audiobook.getCategoryId() != null) {
+            Category category = categoryMapper.selectById(audiobook.getCategoryId());
+            if (category != null) {
+                vo.setCategoryName(category.getName());
+            }
+        }
+        return vo;
+    }
+    
+    /**
+     * 创建听书
+     */
+    @Transactional
+    public AdminAudiobookVO createAudiobook(AudiobookManageDTO dto) {
+        if (dto.getTitle() == null || dto.getTitle().trim().isEmpty()) {
+            throw new RuntimeException("书名不能为空");
+        }
+        if (dto.getStatus() == null) {
+            throw new RuntimeException("状态不能为空");
+        }
+        
+        Audiobook audiobook = convertToEntity(dto);
+        audiobook.setViewCount(0);
+        audiobook.setLikeCount(0);
+        audiobook.setPlayCount(0);
+        if (audiobook.getChapterCount() == null) {
+            audiobook.setChapterCount(0);
+        }
+        if (audiobook.getTotalDuration() == null) {
+            audiobook.setTotalDuration(0);
+        }
+        
+        audiobookMapper.insert(audiobook);
+        return getAudiobookById(audiobook.getId());
+    }
+    
+    /**
+     * 更新听书
+     */
+    @Transactional
+    public AdminAudiobookVO updateAudiobook(Integer id, AudiobookManageDTO dto) {
+        Audiobook existing = audiobookMapper.selectById(id);
+        if (existing == null) {
+            throw new RuntimeException("听书不存在");
+        }
+        if (dto.getTitle() != null && dto.getTitle().trim().isEmpty()) {
+            throw new RuntimeException("书名不能为空");
+        }
+        
+        Audiobook audiobook = convertToEntity(dto);
+        audiobook.setId(id);
+        audiobook.setViewCount(existing.getViewCount());
+        audiobook.setLikeCount(existing.getLikeCount());
+        audiobook.setPlayCount(existing.getPlayCount());
+        if (audiobook.getChapterCount() == null) {
+            audiobook.setChapterCount(existing.getChapterCount());
+        }
+        if (audiobook.getTotalDuration() == null) {
+            audiobook.setTotalDuration(existing.getTotalDuration());
+        }
+        
+        audiobookMapper.update(audiobook);
+        return getAudiobookById(id);
+    }
+    
+    /**
+     * 删除听书
+     */
+    @Transactional
+    public void deleteAudiobook(Integer id) {
+        Audiobook audiobook = audiobookMapper.selectById(id);
+        if (audiobook == null) {
+            throw new RuntimeException("听书不存在");
+        }
+        audiobookMapper.deleteById(id);
+    }
+    
+    /**
+     * 批量删除听书
+     */
+    @Transactional
+    public void deleteAudiobooks(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new RuntimeException("听书ID列表不能为空");
+        }
+        audiobookMapper.deleteByIds(ids);
+    }
+    
+    /**
+     * 上架听书
+     */
+    @Transactional
+    public void publishAudiobook(Integer id) {
+        Audiobook audiobook = audiobookMapper.selectById(id);
+        if (audiobook == null) {
+            throw new RuntimeException("听书不存在");
+        }
+        audiobookMapper.updateStatus(id, 1);
+    }
+    
+    /**
+     * 下架听书
+     */
+    @Transactional
+    public void unpublishAudiobook(Integer id) {
+        Audiobook audiobook = audiobookMapper.selectById(id);
+        if (audiobook == null) {
+            throw new RuntimeException("听书不存在");
+        }
+        audiobookMapper.updateStatus(id, 0);
+    }
+    
+    /**
+     * 批量上架
+     */
+    @Transactional
+    public void publishAudiobooks(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new RuntimeException("听书ID列表不能为空");
+        }
+        audiobookMapper.updateStatusBatch(ids, 1);
+    }
+    
+    /**
+     * 批量下架
+     */
+    @Transactional
+    public void unpublishAudiobooks(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new RuntimeException("听书ID列表不能为空");
+        }
+        audiobookMapper.updateStatusBatch(ids, 0);
+    }
+    
+    /**
+     * DTO转实体
+     */
+    private Audiobook convertToEntity(AudiobookManageDTO dto) {
+        Audiobook audiobook = new Audiobook();
+        BeanUtils.copyProperties(dto, audiobook);
+        return audiobook;
+    }
+    
+    /**
+     * 实体转VO
+     */
+    private AdminAudiobookVO convertToVO(Audiobook audiobook) {
+        AdminAudiobookVO vo = new AdminAudiobookVO();
+        BeanUtils.copyProperties(audiobook, vo);
+        if (audiobook.getTotalDuration() != null) {
+            vo.setTotalDurationText(formatDuration(audiobook.getTotalDuration()));
+        }
+        return vo;
+    }
+    
+    /**
+     * 格式化时长
+     */
+    private String formatDuration(Integer seconds) {
+        if (seconds == null || seconds <= 0) {
+            return "00:00:00";
+        }
+        int hours = seconds / 3600;
+        int minutes = (seconds % 3600) / 60;
+        int secs = seconds % 60;
+        return String.format("%02d:%02d:%02d", hours, minutes, secs);
+    }
+}
+
+

+ 158 - 0
src/main/java/com/yu/book/admin/service/AdminBannerService.java

@@ -0,0 +1,158 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.admin.entity.Banner;
+import com.yu.book.admin.entity.BannerGroup;
+import com.yu.book.admin.mapper.BannerMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 后台轮播图管理服务
+ */
+@Service
+public class AdminBannerService {
+    
+    private final BannerMapper bannerMapper;
+    
+    public AdminBannerService(BannerMapper bannerMapper) {
+        this.bannerMapper = bannerMapper;
+    }
+    
+    // ==================== 分组相关方法 ====================
+    
+    /**
+     * 查询所有分组
+     */
+    public List<BannerGroup> listGroups() {
+        return bannerMapper.selectGroups();
+    }
+    
+    /**
+     * 创建分组
+     */
+    @Transactional
+    public BannerGroup createGroup(BannerGroup group) {
+        // 设置默认值
+        if (group.getStatus() == null) {
+            group.setStatus(1);
+        }
+        if (group.getSort() == null) {
+            group.setSort(0);
+        }
+        // 检查编码是否已存在
+        BannerGroup existing = bannerMapper.selectGroupByCode(group.getCode());
+        if (existing != null) {
+            throw new RuntimeException("分组编码已存在: " + group.getCode());
+        }
+        bannerMapper.insertGroup(group);
+        return group;
+    }
+    
+    /**
+     * 更新分组
+     */
+    @Transactional
+    public BannerGroup updateGroup(BannerGroup group) {
+        BannerGroup existing = bannerMapper.selectGroupById(group.getId());
+        if (existing == null) {
+            throw new RuntimeException("分组不存在: " + group.getId());
+        }
+        // 如果修改了编码,检查新编码是否已存在
+        if (group.getCode() != null && !group.getCode().equals(existing.getCode())) {
+            BannerGroup codeExists = bannerMapper.selectGroupByCode(group.getCode());
+            if (codeExists != null && !codeExists.getId().equals(group.getId())) {
+                throw new RuntimeException("分组编码已存在: " + group.getCode());
+            }
+        }
+        bannerMapper.updateGroup(group);
+        return bannerMapper.selectGroupById(group.getId());
+    }
+    
+    /**
+     * 删除分组
+     */
+    @Transactional
+    public void deleteGroup(Integer id) {
+        BannerGroup group = bannerMapper.selectGroupById(id);
+        if (group == null) {
+            throw new RuntimeException("分组不存在: " + id);
+        }
+        bannerMapper.deleteGroup(id);
+    }
+    
+    // ==================== 轮播图相关方法 ====================
+    
+    /**
+     * 查询轮播图列表
+     */
+    public List<Banner> listBanners(Integer groupId, Integer status) {
+        return bannerMapper.selectByGroup(groupId, status);
+    }
+    
+    /**
+     * 创建轮播图
+     */
+    @Transactional
+    public Banner createBanner(Banner banner) {
+        // 设置默认值
+        if (banner.getStatus() == null) {
+            banner.setStatus(1);
+        }
+        if (banner.getSort() == null) {
+            banner.setSort(0);
+        }
+        // 验证分组是否存在
+        BannerGroup group = bannerMapper.selectGroupById(banner.getGroupId());
+        if (group == null) {
+            throw new RuntimeException("分组不存在: " + banner.getGroupId());
+        }
+        bannerMapper.insertBanner(banner);
+        return banner;
+    }
+    
+    /**
+     * 更新轮播图
+     */
+    @Transactional
+    public Banner updateBanner(Banner banner) {
+        Banner existing = bannerMapper.selectBannerById(banner.getId());
+        if (existing == null) {
+            throw new RuntimeException("轮播图不存在: " + banner.getId());
+        }
+        // 如果修改了分组,验证新分组是否存在
+        if (banner.getGroupId() != null && !banner.getGroupId().equals(existing.getGroupId())) {
+            BannerGroup group = bannerMapper.selectGroupById(banner.getGroupId());
+            if (group == null) {
+                throw new RuntimeException("分组不存在: " + banner.getGroupId());
+            }
+        }
+        bannerMapper.updateBanner(banner);
+        return bannerMapper.selectBannerById(banner.getId());
+    }
+    
+    /**
+     * 删除轮播图
+     */
+    @Transactional
+    public void deleteBanner(Integer id) {
+        Banner banner = bannerMapper.selectBannerById(id);
+        if (banner == null) {
+            throw new RuntimeException("轮播图不存在: " + id);
+        }
+        bannerMapper.deleteBanner(id);
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 243 - 0
src/main/java/com/yu/book/admin/service/AdminBookService.java

@@ -0,0 +1,243 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.admin.dto.BookManageDTO;
+import com.yu.book.admin.vo.AdminBookVO;
+import com.yu.book.domain.Book;
+import com.yu.book.domain.Category;
+import com.yu.book.mapper.BookMapper;
+import com.yu.book.mapper.CategoryMapper;
+import com.yu.book.common.PageResult;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 后台管理书籍服务类
+ */
+@Service
+public class AdminBookService {
+    
+    private final BookMapper bookMapper;
+    private final CategoryMapper categoryMapper;
+    
+    public AdminBookService(BookMapper bookMapper, CategoryMapper categoryMapper) {
+        this.bookMapper = bookMapper;
+        this.categoryMapper = categoryMapper;
+    }
+    
+    /**
+     * 分页查询书籍(后台管理)
+     */
+    public PageResult<AdminBookVO> getBooks(Integer page, Integer size, String keyword, 
+                                           Integer categoryId, Integer status, Boolean isVip) {
+        // 计算偏移量
+        Integer offset = (page - 1) * size;
+        
+        // 查询数据(后台管理可以查看所有状态的书籍)
+        List<Book> books = bookMapper.selectPage(keyword, categoryId, status, isVip, offset, size);
+        
+        // 查询总数
+        Long total = bookMapper.count(keyword, categoryId, status, isVip);
+        
+        // 转换为VO
+        List<AdminBookVO> voList = new ArrayList<>();
+        for (Book book : books) {
+            AdminBookVO vo = convertToVO(book);
+            // 查询分类名称
+            if (book.getCategoryId() != null) {
+                Category category = categoryMapper.selectById(book.getCategoryId());
+                if (category != null) {
+                    vo.setCategoryName(category.getName());
+                }
+            }
+            voList.add(vo);
+        }
+        
+        return new PageResult<>(voList, total, page, size);
+    }
+    
+    /**
+     * 根据ID查询书籍(后台管理)
+     */
+    public AdminBookVO getBookById(Integer id) {
+        Book book = bookMapper.selectById(id);
+        if (book == null) {
+            throw new RuntimeException("书籍不存在");
+        }
+        AdminBookVO vo = convertToVO(book);
+        // 查询分类名称
+        if (book.getCategoryId() != null) {
+            Category category = categoryMapper.selectById(book.getCategoryId());
+            if (category != null) {
+                vo.setCategoryName(category.getName());
+            }
+        }
+        return vo;
+    }
+    
+    /**
+     * 创建书籍
+     */
+    @Transactional
+    public AdminBookVO createBook(BookManageDTO bookDTO) {
+        // 参数验证
+        if (bookDTO.getTitle() == null || bookDTO.getTitle().trim().isEmpty()) {
+            throw new RuntimeException("书名不能为空");
+        }
+        if (bookDTO.getStatus() == null) {
+            throw new RuntimeException("状态不能为空");
+        }
+        
+        Book book = convertToEntity(bookDTO);
+        book.setViewCount(0);
+        book.setLikeCount(0);
+        book.setReadCount(0);
+        
+        bookMapper.insert(book);
+        return getBookById(book.getId());
+    }
+    
+    /**
+     * 更新书籍
+     */
+    @Transactional
+    public AdminBookVO updateBook(Integer id, BookManageDTO bookDTO) {
+        Book existingBook = bookMapper.selectById(id);
+        if (existingBook == null) {
+            throw new RuntimeException("书籍不存在");
+        }
+        
+        // 参数验证
+        if (bookDTO.getTitle() != null && bookDTO.getTitle().trim().isEmpty()) {
+            throw new RuntimeException("书名不能为空");
+        }
+        
+        Book book = convertToEntity(bookDTO);
+        book.setId(id);
+        // 保留原有的统计信息
+        book.setViewCount(existingBook.getViewCount());
+        book.setLikeCount(existingBook.getLikeCount());
+        book.setReadCount(existingBook.getReadCount());
+        
+        bookMapper.update(book);
+        return getBookById(id);
+    }
+    
+    /**
+     * 删除书籍
+     */
+    @Transactional
+    public void deleteBook(Integer id) {
+        Book book = bookMapper.selectById(id);
+        if (book == null) {
+            throw new RuntimeException("书籍不存在");
+        }
+        bookMapper.deleteById(id);
+    }
+    
+    /**
+     * 批量删除书籍
+     */
+    @Transactional
+    public void deleteBooks(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new RuntimeException("书籍ID列表不能为空");
+        }
+        bookMapper.deleteByIds(ids);
+    }
+    
+    /**
+     * 上架书籍
+     */
+    @Transactional
+    public void publishBook(Integer id) {
+        Book book = bookMapper.selectById(id);
+        if (book == null) {
+            throw new RuntimeException("书籍不存在");
+        }
+        book.setStatus(1); // 上架
+        bookMapper.update(book);
+    }
+    
+    /**
+     * 下架书籍
+     */
+    @Transactional
+    public void unpublishBook(Integer id) {
+        Book book = bookMapper.selectById(id);
+        if (book == null) {
+            throw new RuntimeException("书籍不存在");
+        }
+        book.setStatus(0); // 下架
+        bookMapper.update(book);
+    }
+    
+    /**
+     * 批量上架书籍
+     */
+    @Transactional
+    public void publishBooks(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new RuntimeException("书籍ID列表不能为空");
+        }
+        for (Integer id : ids) {
+            publishBook(id);
+        }
+    }
+    
+    /**
+     * 批量下架书籍
+     */
+    @Transactional
+    public void unpublishBooks(List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            throw new RuntimeException("书籍ID列表不能为空");
+        }
+        for (Integer id : ids) {
+            unpublishBook(id);
+        }
+    }
+    
+    /**
+     * DTO转实体
+     */
+    private Book convertToEntity(BookManageDTO dto) {
+        Book book = new Book();
+        BeanUtils.copyProperties(dto, book);
+        return book;
+    }
+    
+    /**
+     * 实体转VO
+     */
+    private AdminBookVO convertToVO(Book book) {
+        AdminBookVO vo = new AdminBookVO();
+        BeanUtils.copyProperties(book, vo);
+        return vo;
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 149 - 0
src/main/java/com/yu/book/admin/service/AdminChapterService.java

@@ -0,0 +1,149 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.domain.BookChapter;
+import com.yu.book.dto.ChapterDTO;
+import com.yu.book.mapper.BookChapterMapper;
+import com.yu.book.vo.ChapterVO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 后台管理章节服务类
+ */
+@Service
+public class AdminChapterService {
+    
+    private final BookChapterMapper chapterMapper;
+    
+    public AdminChapterService(BookChapterMapper chapterMapper) {
+        this.chapterMapper = chapterMapper;
+    }
+    
+    /**
+     * 根据书籍ID获取章节列表
+     */
+    public List<ChapterVO> getChaptersByBookId(Integer bookId) {
+        List<BookChapter> chapters = chapterMapper.selectChaptersByBookId(bookId);
+        List<ChapterVO> voList = new ArrayList<>();
+        for (BookChapter chapter : chapters) {
+            ChapterVO vo = new ChapterVO();
+            BeanUtils.copyProperties(chapter, vo);
+            voList.add(vo);
+        }
+        return voList;
+    }
+    
+    /**
+     * 根据章节ID获取章节详情(包含内容)
+     */
+    public BookChapter getChapterById(Integer chapterId) {
+        return chapterMapper.selectChapterById(chapterId);
+    }
+    
+    /**
+     * 创建章节
+     */
+    @Transactional
+    public BookChapter createChapter(ChapterDTO dto) {
+        BookChapter chapter = new BookChapter();
+        BeanUtils.copyProperties(dto, chapter);
+        
+        // 计算字数
+        if (chapter.getContent() != null) {
+            chapter.setWordCount(chapter.getContent().length());
+        } else {
+            chapter.setWordCount(0);
+        }
+        
+        // 设置默认值
+        if (chapter.getIsFree() == null) {
+            chapter.setIsFree(true);
+        }
+        if (chapter.getIsVip() == null) {
+            chapter.setIsVip(false);
+        }
+        if (chapter.getStatus() == null) {
+            chapter.setStatus(1);
+        }
+        
+        chapterMapper.insertChapter(chapter);
+        return chapter;
+    }
+    
+    /**
+     * 更新章节
+     */
+    @Transactional
+    public BookChapter updateChapter(ChapterDTO dto) {
+        BookChapter chapter = chapterMapper.selectChapterById(dto.getId());
+        if (chapter == null) {
+            throw new RuntimeException("章节不存在");
+        }
+        
+        // 更新字段
+        if (dto.getChapterNumber() != null) {
+            chapter.setChapterNumber(dto.getChapterNumber());
+        }
+        if (dto.getTitle() != null) {
+            chapter.setTitle(dto.getTitle());
+        }
+        if (dto.getContent() != null) {
+            chapter.setContent(dto.getContent());
+            // 重新计算字数
+            chapter.setWordCount(dto.getContent().length());
+        }
+        if (dto.getIsFree() != null) {
+            chapter.setIsFree(dto.getIsFree());
+        }
+        if (dto.getIsVip() != null) {
+            chapter.setIsVip(dto.getIsVip());
+        }
+        if (dto.getStatus() != null) {
+            chapter.setStatus(dto.getStatus());
+        }
+        
+        chapterMapper.updateChapter(chapter);
+        return chapter;
+    }
+    
+    /**
+     * 删除章节
+     */
+    @Transactional
+    public void deleteChapter(Integer chapterId) {
+        chapterMapper.deleteChapter(chapterId);
+    }
+    
+    /**
+     * 批量删除章节
+     */
+    @Transactional
+    public void deleteChapters(List<Integer> chapterIds) {
+        for (Integer chapterId : chapterIds) {
+            chapterMapper.deleteChapter(chapterId);
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 215 - 0
src/main/java/com/yu/book/admin/service/AdminRankingService.java

@@ -0,0 +1,215 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.admin.dto.RankingSortDTO;
+import com.yu.book.admin.entity.RankingGroup;
+import com.yu.book.admin.entity.RankingItem;
+import com.yu.book.admin.mapper.RankingMapper;
+import com.yu.book.admin.vo.RankingItemVO;
+import com.yu.book.domain.Book;
+import com.yu.book.domain.Category;
+import com.yu.book.mapper.BookMapper;
+import com.yu.book.mapper.CategoryMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class AdminRankingService {
+    private final RankingMapper rankingMapper;
+    private final BookMapper bookMapper;
+    private final CategoryMapper categoryMapper;
+
+    public AdminRankingService(RankingMapper rankingMapper, BookMapper bookMapper, CategoryMapper categoryMapper) {
+        this.rankingMapper = rankingMapper;
+        this.bookMapper = bookMapper;
+        this.categoryMapper = categoryMapper;
+    }
+
+    // groups
+    public List<RankingGroup> listGroups() {
+        return rankingMapper.selectGroups();
+    }
+
+    public RankingGroup createGroup(RankingGroup group) {
+        if (group.getStatus() == null) group.setStatus(1);
+        if (group.getSort() == null) group.setSort(0);
+        rankingMapper.insertGroup(group);
+        return group;
+    }
+
+    public RankingGroup updateGroup(RankingGroup group) {
+        rankingMapper.updateGroup(group);
+        return group;
+    }
+
+    public void deleteGroup(Integer id) {
+        rankingMapper.deleteGroup(id);
+    }
+
+    // items
+    public List<RankingItem> listItems(Integer groupId, Integer status) {
+        return rankingMapper.selectItems(groupId, status);
+    }
+
+    public RankingItem createItem(RankingItem item) {
+        if (item.getStatus() == null) item.setStatus(1);
+        if (item.getSort() == null) item.setSort(0);
+        rankingMapper.insertItem(item);
+        return item;
+    }
+
+    public RankingItem updateItem(RankingItem item) {
+        rankingMapper.updateItem(item);
+        return item;
+    }
+
+    public void deleteItem(Integer id) {
+        rankingMapper.deleteItem(id);
+    }
+
+    // ========== 扩展方法:排序管理 ==========
+
+    /**
+     * 按分类查询排行榜项(包含书籍信息)
+     */
+    public List<RankingItemVO> listItemsWithBooks(Integer groupId, Integer categoryId, Integer status) {
+        List<RankingItem> items;
+        if (categoryId != null) {
+            items = rankingMapper.selectItemsByCategory(groupId, categoryId, status);
+        } else {
+            items = rankingMapper.selectItems(groupId, status);
+        }
+
+        List<RankingItemVO> voList = new ArrayList<>();
+        String groupName = null;
+        if (groupId != null) {
+            List<RankingGroup> groups = rankingMapper.selectGroups();
+            RankingGroup group = groups.stream().filter(g -> g.getId().equals(groupId)).findFirst().orElse(null);
+            if (group != null) {
+                groupName = group.getName();
+            }
+        }
+
+        for (RankingItem item : items) {
+            RankingItemVO vo = convertToVO(item);
+            vo.setGroupName(groupName);
+
+            // 查询书籍信息
+            if (item.getBookId() != null) {
+                Book book = bookMapper.selectById(item.getBookId());
+                if (book != null) {
+                    vo.setBookTitle(book.getTitle());
+                    vo.setBookAuthor(book.getAuthor());
+                    vo.setBookCover(book.getCover() != null ? book.getCover() : book.getImage());
+                }
+            }
+
+            // 查询分类信息
+            if (item.getCategoryId() != null) {
+                Category category = categoryMapper.selectById(item.getCategoryId());
+                if (category != null) {
+                    vo.setCategoryName(category.getName());
+                }
+            }
+
+            voList.add(vo);
+        }
+
+        return voList;
+    }
+
+    /**
+     * 批量更新排序
+     */
+    @Transactional
+    public void batchUpdateSort(RankingSortDTO sortDTO) {
+        if (sortDTO == null || sortDTO.getItems() == null || sortDTO.getItems().isEmpty()) {
+            throw new RuntimeException("排序数据不能为空");
+        }
+
+        List<RankingItem> items = new ArrayList<>();
+        for (RankingSortDTO.SortItem sortItem : sortDTO.getItems()) {
+            RankingItem item = new RankingItem();
+            item.setId(sortItem.getId());
+            item.setSort(sortItem.getSort());
+            items.add(item);
+        }
+
+        rankingMapper.batchUpdateSort(items);
+    }
+
+    /**
+     * 添加书籍到排行榜
+     */
+    @Transactional
+    public RankingItem addBookToRanking(Integer groupId, Integer bookId, Integer categoryId) {
+        // 检查书籍是否存在
+        Book book = bookMapper.selectById(bookId);
+        if (book == null) {
+            throw new RuntimeException("书籍不存在");
+        }
+
+        // 检查是否已存在
+        RankingItem existing = rankingMapper.selectItemByGroupAndBook(groupId, bookId, categoryId);
+        if (existing != null) {
+            throw new RuntimeException("该书籍已在此排行榜中");
+        }
+
+        // 获取当前最大排序值
+        List<RankingItem> items;
+        if (categoryId != null) {
+            items = rankingMapper.selectItemsByCategory(groupId, categoryId, 1);
+        } else {
+            items = rankingMapper.selectItems(groupId, 1);
+        }
+        int maxSort = items.stream().mapToInt(item -> item.getSort() != null ? item.getSort() : 0).max().orElse(0);
+
+        // 创建新项
+        RankingItem item = new RankingItem();
+        item.setGroupId(groupId);
+        item.setCategoryId(categoryId);
+        item.setBookId(bookId);
+        item.setSort(maxSort + 1);
+        item.setStatus(1);
+        item.setWeight(0);
+
+        rankingMapper.insertItem(item);
+        return item;
+    }
+
+    /**
+     * 从排行榜移除书籍
+     */
+    @Transactional
+    public void removeBookFromRanking(Integer itemId) {
+        rankingMapper.deleteItem(itemId);
+    }
+
+    /**
+     * 转换为VO
+     */
+    private RankingItemVO convertToVO(RankingItem item) {
+        RankingItemVO vo = new RankingItemVO();
+        vo.setId(item.getId());
+        vo.setGroupId(item.getGroupId());
+        vo.setCategoryId(item.getCategoryId());
+        vo.setBookId(item.getBookId());
+        vo.setWeight(item.getWeight());
+        vo.setSort(item.getSort());
+        vo.setStatus(item.getStatus());
+        vo.setCreatedAt(item.getCreatedAt());
+        vo.setUpdatedAt(item.getUpdatedAt());
+        return vo;
+    }
+}
+
+
+
+
+
+
+
+
+

+ 80 - 0
src/main/java/com/yu/book/admin/service/AdminService.java

@@ -0,0 +1,80 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.admin.dto.AdminLoginDTO;
+import com.yu.book.admin.vo.AdminLoginVO;
+import com.yu.book.domain.User;
+import com.yu.book.mapper.UserMapper;
+import com.yu.book.util.PasswordUtil;
+import org.springframework.stereotype.Service;
+
+/**
+ * 管理员服务类
+ */
+@Service
+public class AdminService {
+    
+    private final UserMapper userMapper;
+    
+    public AdminService(UserMapper userMapper) {
+        this.userMapper = userMapper;
+    }
+    
+    /**
+     * 管理员登录
+     * 只有role为'admin'的用户可以登录
+     */
+    public AdminLoginVO login(AdminLoginDTO loginDTO) {
+        // 根据用户名查询用户
+        User user = userMapper.selectByUsername(loginDTO.getUsername());
+        if (user == null) {
+            throw new RuntimeException("用户名或密码错误");
+        }
+        
+        // 验证密码
+        if (!PasswordUtil.verify(loginDTO.getPassword(), user.getPassword())) {
+            throw new RuntimeException("用户名或密码错误");
+        }
+        
+        // 检查用户角色,只有管理员可以登录
+        if (user.getRole() == null || !"admin".equals(user.getRole())) {
+            throw new RuntimeException("您不是管理员,无权登录后台管理系统");
+        }
+        
+        // 检查用户状态
+        if (user.getStatus() == null || user.getStatus() == 0) {
+            throw new RuntimeException("用户已被禁用");
+        }
+        
+        // 转换为VO
+        AdminLoginVO loginVO = new AdminLoginVO();
+        loginVO.setId(user.getId());
+        loginVO.setUsername(user.getUsername());
+        loginVO.setNickname(user.getNickname());
+        loginVO.setRole(user.getRole());
+        loginVO.setToken("admin_token_" + user.getId()); // 模拟token,实际应使用JWT
+        
+        return loginVO;
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 257 - 0
src/main/java/com/yu/book/admin/service/AdminUserService.java

@@ -0,0 +1,257 @@
+package com.yu.book.admin.service;
+
+import com.yu.book.admin.dto.AdminUserDTO;
+import com.yu.book.admin.entity.AdminOperationLog;
+import com.yu.book.admin.mapper.AdminOperationLogMapper;
+import com.yu.book.admin.vo.AdminUserVO;
+import com.yu.book.common.PageResult;
+import com.yu.book.domain.User;
+import com.yu.book.mapper.UserMapper;
+import com.yu.book.util.PasswordUtil;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 后台管理用户服务类
+ */
+@Service
+public class AdminUserService {
+    
+    private final UserMapper userMapper;
+    private final AdminOperationLogMapper operationLogMapper;
+    
+    public AdminUserService(UserMapper userMapper, AdminOperationLogMapper operationLogMapper) {
+        this.userMapper = userMapper;
+        this.operationLogMapper = operationLogMapper;
+    }
+    
+    /**
+     * 分页查询用户(后台管理)
+     */
+    public PageResult<AdminUserVO> getUsers(Integer page, Integer size, String keyword, 
+                                          Integer status, Boolean isVip, String role) {
+        // 计算偏移量
+        Integer offset = (page - 1) * size;
+        
+        // 查询数据
+        List<User> users = userMapper.selectPage(keyword, status, isVip, role, offset, size);
+        
+        // 查询总数
+        Long total = userMapper.count(keyword, status, isVip, role);
+        
+        // 转换为VO
+        List<AdminUserVO> voList = new ArrayList<>();
+        for (User user : users) {
+            AdminUserVO vo = convertToVO(user);
+            voList.add(vo);
+        }
+        
+        return new PageResult<>(voList, total, page, size);
+    }
+    
+    /**
+     * 根据ID查询用户(后台管理)
+     */
+    public AdminUserVO getUserById(Integer id) {
+        User user = userMapper.selectById(id);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        return convertToVO(user);
+    }
+    
+    /**
+     * 更新用户信息
+     */
+    @Transactional
+    public AdminUserVO updateUser(Integer id, AdminUserDTO userDTO, Integer adminId, String adminUsername, HttpServletRequest request) {
+        User existingUser = userMapper.selectById(id);
+        if (existingUser == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        
+        // 更新用户信息
+        User user = convertToEntity(userDTO);
+        user.setId(id);
+        
+        // 如果提供了密码,则加密后更新
+        if (userDTO.getPassword() != null && !userDTO.getPassword().trim().isEmpty()) {
+            user.setPassword(PasswordUtil.encrypt(userDTO.getPassword()));
+        } else {
+            // 不更新密码,保持原密码
+            user.setPassword(null);
+        }
+        
+        // 验证唯一性(用户名、手机号、邮箱)
+        if (userDTO.getUsername() != null && !userDTO.getUsername().trim().isEmpty()) {
+            String username = userDTO.getUsername().trim();
+            if (!username.equals(existingUser.getUsername())) {
+                User usernameUser = userMapper.selectByUsername(username);
+                if (usernameUser != null) {
+                    throw new RuntimeException("用户名已被使用");
+                }
+            }
+        }
+        
+        if (userDTO.getPhone() != null && !userDTO.getPhone().trim().isEmpty()) {
+            String phone = userDTO.getPhone().trim();
+            if (!phone.equals(existingUser.getPhone())) {
+                User phoneUser = userMapper.selectByPhone(phone);
+                if (phoneUser != null) {
+                    throw new RuntimeException("手机号已被使用");
+                }
+            }
+        }
+        
+        if (userDTO.getEmail() != null && !userDTO.getEmail().trim().isEmpty()) {
+            String email = userDTO.getEmail().trim();
+            if (!email.equals(existingUser.getEmail())) {
+                User emailUser = userMapper.selectByEmail(email);
+                if (emailUser != null) {
+                    throw new RuntimeException("邮箱已被使用");
+                }
+            }
+        }
+        
+        userMapper.update(user);
+        
+        // 记录操作日志
+        recordOperationLog(adminId, adminUsername, "update_user", "user", id, 
+                existingUser.getUsername(), "修改用户信息", getClientIpAddress(request));
+        
+        return getUserById(id);
+    }
+    
+    /**
+     * 禁用用户
+     */
+    @Transactional
+    public void disableUser(Integer id, Integer adminId, String adminUsername, HttpServletRequest request) {
+        User user = userMapper.selectById(id);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        if (user.getStatus() == 0) {
+            throw new RuntimeException("用户已被禁用");
+        }
+        
+        user.setStatus(0);
+        userMapper.update(user);
+        
+        // 记录操作日志
+        recordOperationLog(adminId, adminUsername, "disable_user", "user", id, 
+                user.getUsername(), "禁用用户账号", getClientIpAddress(request));
+    }
+    
+    /**
+     * 启用用户
+     */
+    @Transactional
+    public void enableUser(Integer id, Integer adminId, String adminUsername, HttpServletRequest request) {
+        User user = userMapper.selectById(id);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        if (user.getStatus() == 1) {
+            throw new RuntimeException("用户已启用");
+        }
+        
+        user.setStatus(1);
+        userMapper.update(user);
+        
+        // 记录操作日志
+        recordOperationLog(adminId, adminUsername, "enable_user", "user", id, 
+                user.getUsername(), "启用用户账号", getClientIpAddress(request));
+    }
+    
+    /**
+     * 重置用户密码
+     */
+    @Transactional
+    public void resetPassword(Integer id, String newPassword, Integer adminId, String adminUsername, HttpServletRequest request) {
+        User user = userMapper.selectById(id);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        if (newPassword == null || newPassword.trim().isEmpty()) {
+            throw new RuntimeException("新密码不能为空");
+        }
+        if (newPassword.length() < 6 || newPassword.length() > 20) {
+            throw new RuntimeException("密码长度必须在6-20位之间");
+        }
+        
+        user.setPassword(PasswordUtil.encrypt(newPassword));
+        userMapper.update(user);
+        
+        // 记录操作日志
+        recordOperationLog(adminId, adminUsername, "reset_password", "user", id, 
+                user.getUsername(), "重置用户密码", getClientIpAddress(request));
+    }
+    
+    /**
+     * 记录操作日志
+     */
+    private void recordOperationLog(Integer adminId, String adminUsername, String operationType, 
+                                   String targetType, Integer targetId, String targetName, 
+                                   String description, String ipAddress) {
+        AdminOperationLog log = new AdminOperationLog();
+        log.setAdminId(adminId);
+        log.setAdminUsername(adminUsername);
+        log.setOperationType(operationType);
+        log.setTargetType(targetType);
+        log.setTargetId(targetId);
+        log.setTargetName(targetName);
+        log.setDescription(description);
+        log.setIpAddress(ipAddress);
+        operationLogMapper.insert(log);
+    }
+    
+    /**
+     * 获取客户端IP地址
+     */
+    private String getClientIpAddress(HttpServletRequest request) {
+        if (request == null) {
+            return null;
+        }
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        return ip;
+    }
+    
+    /**
+     * DTO转实体
+     */
+    private User convertToEntity(AdminUserDTO dto) {
+        User user = new User();
+        BeanUtils.copyProperties(dto, user);
+        return user;
+    }
+    
+    /**
+     * 实体转VO
+     */
+    private AdminUserVO convertToVO(User user) {
+        AdminUserVO vo = new AdminUserVO();
+        BeanUtils.copyProperties(user, vo);
+        return vo;
+    }
+}
+
+
+
+
+
+

+ 239 - 0
src/main/java/com/yu/book/admin/util/AdminAccountUtil.java

@@ -0,0 +1,239 @@
+package com.yu.book.admin.util;
+
+import com.yu.book.domain.User;
+import com.yu.book.mapper.UserMapper;
+import com.yu.book.util.PasswordUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 管理员账号工具类
+ * 用于创建和管理管理员账号
+ * 
+ * 使用示例:
+ * 1. 在测试类中注入并使用:
+ *    @Autowired
+ *    private AdminAccountUtil adminAccountUtil;
+ *    adminAccountUtil.createDefaultAdmin();
+ * 
+ * 2. 在Controller或Service中使用:
+ *    @Autowired
+ *    private AdminAccountUtil adminAccountUtil;
+ *    String result = adminAccountUtil.createAdmin("admin", "admin123", "管理员");
+ */
+@Component
+public class AdminAccountUtil {
+    
+    private final UserMapper userMapper;
+    
+    @Autowired
+    public AdminAccountUtil(UserMapper userMapper) {
+        this.userMapper = userMapper;
+    }
+    
+    /**
+     * 创建管理员账号
+     * 
+     * @param username 用户名(必填)
+     * @param password 密码(明文,必填)
+     * @param nickname 昵称(可选,默认为"管理员")
+     * @return 创建结果信息
+     * @throws RuntimeException 如果参数无效或创建失败
+     */
+    public String createAdmin(String username, String password, String nickname) {
+        // 参数验证
+        if (username == null || username.trim().isEmpty()) {
+            throw new RuntimeException("用户名不能为空");
+        }
+        if (password == null || password.trim().isEmpty()) {
+            throw new RuntimeException("密码不能为空");
+        }
+        if (password.length() < 6) {
+            throw new RuntimeException("密码长度不能少于6位");
+        }
+        
+        username = username.trim();
+        password = password.trim();
+        
+        // 检查用户名是否已存在
+        User existingUser = userMapper.selectByUsername(username);
+        if (existingUser != null) {
+            // 如果用户已存在,更新为管理员
+            existingUser.setRole("admin");
+            existingUser.setPassword(PasswordUtil.encrypt(password)); // 更新密码
+            existingUser.setStatus(1); // 确保状态为激活
+            userMapper.update(existingUser);
+            return String.format("用户 '%s' 已存在,已更新为管理员账号", username);
+        }
+        
+        // 创建新管理员账号
+        User admin = new User();
+        admin.setUsername(username);
+        admin.setPassword(PasswordUtil.encrypt(password)); // 密码加密
+        admin.setNickname(nickname != null && !nickname.trim().isEmpty() 
+            ? nickname.trim() : "管理员");
+        admin.setRole("admin"); // 设置为管理员角色
+        admin.setStatus(1); // 状态:1-激活,0-禁用
+        admin.setIsVip(false); // 默认不是VIP
+        
+        int result = userMapper.insert(admin);
+        if (result > 0) {
+            return String.format("管理员账号 '%s' 创建成功", username);
+        } else {
+            throw new RuntimeException("创建管理员账号失败");
+        }
+    }
+    
+    /**
+     * 创建默认管理员账号
+     * 用户名:admin
+     * 密码:admin123
+     * 昵称:管理员
+     * 
+     * @return 创建结果信息
+     */
+    public String createDefaultAdmin() {
+        return createAdmin("admin", "admin123", "管理员");
+    }
+    
+    /**
+     * 更新管理员密码
+     * 
+     * @param username 用户名
+     * @param newPassword 新密码(明文)
+     * @return 更新结果信息
+     * @throws RuntimeException 如果用户不存在或参数无效
+     */
+    public String updateAdminPassword(String username, String newPassword) {
+        if (username == null || username.trim().isEmpty()) {
+            throw new RuntimeException("用户名不能为空");
+        }
+        if (newPassword == null || newPassword.trim().isEmpty()) {
+            throw new RuntimeException("密码不能为空");
+        }
+        if (newPassword.length() < 6) {
+            throw new RuntimeException("密码长度不能少于6位");
+        }
+        
+        User user = userMapper.selectByUsername(username.trim());
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        
+        if (!"admin".equals(user.getRole())) {
+            throw new RuntimeException("该用户不是管理员");
+        }
+        
+        user.setPassword(PasswordUtil.encrypt(newPassword.trim()));
+        int result = userMapper.update(user);
+        
+        if (result > 0) {
+            return String.format("管理员 '%s' 的密码已更新", username);
+        } else {
+            throw new RuntimeException("更新密码失败");
+        }
+    }
+    
+    /**
+     * 将普通用户升级为管理员
+     * 
+     * @param username 用户名
+     * @return 升级结果信息
+     * @throws RuntimeException 如果用户不存在
+     */
+    public String upgradeToAdmin(String username) {
+        if (username == null || username.trim().isEmpty()) {
+            throw new RuntimeException("用户名不能为空");
+        }
+        
+        User user = userMapper.selectByUsername(username.trim());
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        
+        if ("admin".equals(user.getRole())) {
+            return String.format("用户 '%s' 已经是管理员", username);
+        }
+        
+        user.setRole("admin");
+        user.setStatus(1); // 确保状态为激活
+        int result = userMapper.update(user);
+        
+        if (result > 0) {
+            return String.format("用户 '%s' 已升级为管理员", username);
+        } else {
+            throw new RuntimeException("升级失败");
+        }
+    }
+    
+    /**
+     * 禁用管理员账号
+     * 
+     * @param username 用户名
+     * @return 操作结果信息
+     * @throws RuntimeException 如果用户不存在
+     */
+    public String disableAdmin(String username) {
+        if (username == null || username.trim().isEmpty()) {
+            throw new RuntimeException("用户名不能为空");
+        }
+        
+        User user = userMapper.selectByUsername(username.trim());
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        
+        if (!"admin".equals(user.getRole())) {
+            throw new RuntimeException("该用户不是管理员");
+        }
+        
+        user.setStatus(0); // 禁用
+        int result = userMapper.update(user);
+        
+        if (result > 0) {
+            return String.format("管理员 '%s' 已被禁用", username);
+        } else {
+            throw new RuntimeException("禁用失败");
+        }
+    }
+    
+    /**
+     * 启用管理员账号
+     * 
+     * @param username 用户名
+     * @return 操作结果信息
+     * @throws RuntimeException 如果用户不存在
+     */
+    public String enableAdmin(String username) {
+        if (username == null || username.trim().isEmpty()) {
+            throw new RuntimeException("用户名不能为空");
+        }
+        
+        User user = userMapper.selectByUsername(username.trim());
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        
+        if (!"admin".equals(user.getRole())) {
+            throw new RuntimeException("该用户不是管理员");
+        }
+        
+        user.setStatus(1); // 启用
+        int result = userMapper.update(user);
+        
+        if (result > 0) {
+            return String.format("管理员 '%s' 已启用", username);
+        } else {
+            throw new RuntimeException("启用失败");
+        }
+    }
+}
+
+
+
+
+
+
+
+
+

+ 74 - 0
src/main/java/com/yu/book/admin/vo/AdminAudiobookVO.java

@@ -0,0 +1,74 @@
+package com.yu.book.admin.vo;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 后台管理听书VO
+ */
+@Data
+public class AdminAudiobookVO {
+    
+    private Integer id;
+    
+    private String title;
+    
+    private String author;
+    
+    private String narrator;
+    
+    private String cover;
+    
+    private String image;
+    
+    private String brief;
+    
+    private String desc;
+    
+    private String introduction;
+    
+    private Integer categoryId;
+    
+    private String categoryName;
+    
+    private Integer status;
+    
+    private Boolean isFree;
+    
+    private Boolean isVip;
+    
+    private Integer chapterCount;
+    
+    private Integer totalDuration;
+    
+    private String totalDurationText;
+    
+    private Integer viewCount;
+    
+    private Integer likeCount;
+    
+    private Integer playCount;
+    
+    private Date createdAt;
+    
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 130 - 0
src/main/java/com/yu/book/admin/vo/AdminBookVO.java

@@ -0,0 +1,130 @@
+package com.yu.book.admin.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 后台管理书籍VO
+ */
+@Data
+public class AdminBookVO {
+    
+    /**
+     * 书籍ID
+     */
+    private Integer id;
+    
+    /**
+     * 书名
+     */
+    private String title;
+    
+    /**
+     * 作者
+     */
+    private String author;
+    
+    /**
+     * 封面图片URL
+     */
+    private String cover;
+    
+    /**
+     * 图片URL(兼容字段)
+     */
+    private String image;
+    
+    /**
+     * 简介(简短)
+     */
+    private String brief;
+    
+    /**
+     * 描述
+     */
+    private String desc;
+    
+    /**
+     * 详细介绍
+     */
+    private String introduction;
+    
+    /**
+     * 价格
+     */
+    private BigDecimal price;
+    
+    /**
+     * 是否免费:0-否,1-是
+     */
+    private Boolean isFree;
+    
+    /**
+     * 是否VIP专享:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * 分类ID
+     */
+    private Integer categoryId;
+    
+    /**
+     * 分类名称
+     */
+    private String categoryName;
+    
+    /**
+     * 状态:0-下架,1-上架
+     */
+    private Integer status;
+    
+    /**
+     * 浏览次数
+     */
+    private Integer viewCount;
+    
+    /**
+     * 点赞数
+     */
+    private Integer likeCount;
+    
+    /**
+     * 阅读次数
+     */
+    private Integer readCount;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 57 - 0
src/main/java/com/yu/book/admin/vo/AdminLoginVO.java

@@ -0,0 +1,57 @@
+package com.yu.book.admin.vo;
+
+import lombok.Data;
+
+/**
+ * 管理员登录VO
+ */
+@Data
+public class AdminLoginVO {
+    
+    /**
+     * 管理员ID
+     */
+    private Integer id;
+    
+    /**
+     * 用户名
+     */
+    private String username;
+    
+    /**
+     * 昵称
+     */
+    private String nickname;
+    
+    /**
+     * 角色
+     */
+    private String role;
+    
+    /**
+     * Token(后续可扩展JWT)
+     */
+    private String token;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 92 - 0
src/main/java/com/yu/book/admin/vo/AdminUserVO.java

@@ -0,0 +1,92 @@
+package com.yu.book.admin.vo;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 后台管理用户VO
+ */
+@Data
+public class AdminUserVO {
+    
+    /**
+     * 用户ID
+     */
+    private Integer id;
+    
+    /**
+     * 用户名
+     */
+    private String username;
+    
+    /**
+     * 昵称
+     */
+    private String nickname;
+    
+    /**
+     * 头像URL
+     */
+    private String avatar;
+    
+    /**
+     * 性别(男/女/保密)
+     */
+    private String gender;
+    
+    /**
+     * 生日(yyyy-MM-dd)
+     */
+    private String birthday;
+    
+    /**
+     * 个人简介
+     */
+    private String bio;
+    
+    /**
+     * 手机号
+     */
+    private String phone;
+    
+    /**
+     * 邮箱
+     */
+    private String email;
+    
+    /**
+     * 是否VIP:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * VIP过期时间
+     */
+    private Date vipExpireTime;
+    
+    /**
+     * 状态:0-禁用,1-启用
+     */
+    private Integer status;
+    
+    /**
+     * 用户角色:admin-管理员,user-普通用户
+     */
+    private String role;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+

+ 90 - 0
src/main/java/com/yu/book/admin/vo/RankingItemVO.java

@@ -0,0 +1,90 @@
+package com.yu.book.admin.vo;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 排行榜项VO
+ * 包含书籍详细信息
+ */
+@Data
+public class RankingItemVO {
+    /**
+     * 排行榜项ID
+     */
+    private Integer id;
+    
+    /**
+     * 排行榜组ID
+     */
+    private Integer groupId;
+    
+    /**
+     * 排行榜组名称
+     */
+    private String groupName;
+    
+    /**
+     * 分类ID
+     */
+    private Integer categoryId;
+    
+    /**
+     * 分类名称
+     */
+    private String categoryName;
+    
+    /**
+     * 书籍ID
+     */
+    private Integer bookId;
+    
+    /**
+     * 书籍标题
+     */
+    private String bookTitle;
+    
+    /**
+     * 书籍作者
+     */
+    private String bookAuthor;
+    
+    /**
+     * 书籍封面
+     */
+    private String bookCover;
+    
+    /**
+     * 权重值
+     */
+    private Integer weight;
+    
+    /**
+     * 排序值
+     */
+    private Integer sort;
+    
+    /**
+     * 状态
+     */
+    private Integer status;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+

+ 75 - 0
src/main/java/com/yu/book/common/PageResult.java

@@ -0,0 +1,75 @@
+package com.yu.book.common;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 分页结果
+ */
+@Data
+public class PageResult<T> {
+    
+    /**
+     * 数据列表
+     */
+    private List<T> list;
+    
+    /**
+     * 总记录数
+     */
+    private Long total;
+    
+    /**
+     * 当前页码
+     */
+    private Integer page;
+    
+    /**
+     * 每页数量
+     */
+    private Integer size;
+    
+    /**
+     * 总页数
+     */
+    private Integer totalPages;
+    
+    public PageResult(List<T> list, Long total, Integer page, Integer size) {
+        this.list = list;
+        this.total = total;
+        this.page = page;
+        this.size = size;
+        this.totalPages = (int) Math.ceil((double) total / size);
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 107 - 0
src/main/java/com/yu/book/common/Result.java

@@ -0,0 +1,107 @@
+package com.yu.book.common;
+
+import lombok.Data;
+
+/**
+ * 统一响应结果
+ */
+@Data
+public class Result<T> {
+    
+    /**
+     * 状态码
+     */
+    private Integer code;
+    
+    /**
+     * 消息
+     */
+    private String message;
+    
+    /**
+     * 数据
+     */
+    private T data;
+    
+    /**
+     * 成功响应
+     */
+    public static <T> Result<T> success() {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("操作成功");
+        return result;
+    }
+    
+    /**
+     * 成功响应(带数据)
+     */
+    public static <T> Result<T> success(T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage("操作成功");
+        result.setData(data);
+        return result;
+    }
+    
+    /**
+     * 成功响应(自定义消息)
+     */
+    public static <T> Result<T> success(String message, T data) {
+        Result<T> result = new Result<>();
+        result.setCode(200);
+        result.setMessage(message);
+        result.setData(data);
+        return result;
+    }
+    
+    /**
+     * 失败响应
+     */
+    public static <T> Result<T> error(String message) {
+        Result<T> result = new Result<>();
+        result.setCode(500);
+        result.setMessage(message);
+        return result;
+    }
+    
+    /**
+     * 失败响应(自定义状态码)
+     */
+    public static <T> Result<T> error(Integer code, String message) {
+        Result<T> result = new Result<>();
+        result.setCode(code);
+        result.setMessage(message);
+        return result;
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 50 - 0
src/main/java/com/yu/book/config/ValidationConfig.java

@@ -0,0 +1,50 @@
+package com.yu.book.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
+
+/**
+ * 验证配置类
+ */
+@Configuration
+public class ValidationConfig {
+    
+    /**
+     * 方法级别验证支持
+     */
+    @Bean
+    public MethodValidationPostProcessor methodValidationPostProcessor() {
+        return new MethodValidationPostProcessor();
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 169 - 0
src/main/java/com/yu/book/controller/AudiobookBookshelfController.java

@@ -0,0 +1,169 @@
+package com.yu.book.controller;
+
+import com.yu.book.dto.AudiobookBookshelfDTO;
+import com.yu.book.service.AudiobookBookshelfService;
+import com.yu.book.common.Result;
+import com.yu.book.vo.AudiobookBookshelfVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 用户听书书架控制器
+ */
+@RestController
+@RequestMapping("/api/audiobook-bookshelf")
+@CrossOrigin(origins = "*")
+public class AudiobookBookshelfController {
+    
+    private final AudiobookBookshelfService audiobookBookshelfService;
+    
+    public AudiobookBookshelfController(AudiobookBookshelfService audiobookBookshelfService) {
+        this.audiobookBookshelfService = audiobookBookshelfService;
+    }
+    
+    /**
+     * 添加听书到书架
+     */
+    @PostMapping("/add")
+    public Result<AudiobookBookshelfVO> addToBookshelf(@RequestBody AudiobookBookshelfDTO dto) {
+        try {
+            // 手动验证
+            if (dto == null) {
+                return Result.error("参数不能为空");
+            }
+            if (dto.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (dto.getAudiobookId() == null) {
+                return Result.error("听书ID不能为空");
+            }
+            
+            AudiobookBookshelfVO vo = audiobookBookshelfService.addToBookshelf(
+                dto.getUserId(), 
+                dto.getAudiobookId()
+            );
+            return Result.success("加入书架成功", vo);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("加入书架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从书架移除听书
+     */
+    @DeleteMapping("/remove")
+    public Result<String> removeFromBookshelf(@RequestBody AudiobookBookshelfDTO dto) {
+        try {
+            if (dto == null) {
+                return Result.error("参数不能为空");
+            }
+            if (dto.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (dto.getAudiobookId() == null) {
+                return Result.error("听书ID不能为空");
+            }
+            
+            audiobookBookshelfService.removeFromBookshelf(dto.getUserId(), dto.getAudiobookId());
+            return Result.success("移除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("移除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户听书书架列表
+     */
+    @GetMapping("/list")
+    public Result<List<AudiobookBookshelfVO>> getBookshelfList(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<AudiobookBookshelfVO> list = audiobookBookshelfService.getBookshelfList(userId);
+            return Result.success(list);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 检查听书是否在书架中
+     */
+    @GetMapping("/check")
+    public Result<Boolean> checkInBookshelf(@RequestParam Integer userId, @RequestParam Integer audiobookId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (audiobookId == null) {
+                return Result.error("听书ID不能为空");
+            }
+            
+            boolean isInBookshelf = audiobookBookshelfService.isInBookshelf(userId, audiobookId);
+            return Result.success(isInBookshelf);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新听书进度
+     */
+    @PutMapping("/progress")
+    public Result<AudiobookBookshelfVO> updateListeningProgress(@RequestBody AudiobookBookshelfDTO dto) {
+        try {
+            if (dto == null) {
+                return Result.error("参数不能为空");
+            }
+            if (dto.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (dto.getAudiobookId() == null) {
+                return Result.error("听书ID不能为空");
+            }
+            
+            AudiobookBookshelfVO vo = audiobookBookshelfService.updateListeningProgress(
+                dto.getUserId(),
+                dto.getAudiobookId(),
+                dto.getLastListenedChapterId(),
+                dto.getLastListenedPosition()
+            );
+            return Result.success("更新成功", vo);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 228 - 0
src/main/java/com/yu/book/controller/AudiobookController.java

@@ -0,0 +1,228 @@
+package com.yu.book.controller;
+
+import com.yu.book.service.AudiobookService;
+import com.yu.book.common.Result;
+import com.yu.book.common.PageResult;
+import com.yu.book.vo.AudiobookDetailVO;
+import com.yu.book.vo.AudiobookVO;
+import com.yu.book.vo.ChapterVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 听书控制器
+ */
+@RestController
+@RequestMapping("/api/audiobook")
+@CrossOrigin(origins = "*")
+public class AudiobookController {
+    
+    private final AudiobookService audiobookService;
+    
+    public AudiobookController(AudiobookService audiobookService) {
+        this.audiobookService = audiobookService;
+    }
+    
+    /**
+     * 听书分页检索(支持关键词/分类/状态/VIP/免费)
+     */
+    @GetMapping("/list")
+    public Result<PageResult<AudiobookVO>> getAudiobookList(@RequestParam(defaultValue = "1") Integer page,
+                                                            @RequestParam(defaultValue = "10") Integer size,
+                                                            @RequestParam(required = false) String keyword,
+                                                            @RequestParam(required = false) Integer categoryId,
+                                                            @RequestParam(required = false) Integer status,
+                                                            @RequestParam(required = false) Boolean isVip,
+                                                            @RequestParam(required = false) Boolean isFree) {
+        try {
+            PageResult<AudiobookVO> result = audiobookService.getAudiobooks(page, size, keyword, categoryId, status, isVip, isFree);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取最近上新
+     */
+    @GetMapping("/recent")
+    public Result<List<AudiobookVO>> getRecentAudiobooks(@RequestParam(defaultValue = "8") Integer limit) {
+        try {
+            List<AudiobookVO> audiobooks = audiobookService.getRecentAudiobooks(limit);
+            return Result.success(audiobooks);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取热门听书
+     */
+    @GetMapping("/popular")
+    public Result<List<AudiobookVO>> getPopularAudiobooks(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<AudiobookVO> audiobooks = audiobookService.getPopularAudiobooks(limit);
+            return Result.success(audiobooks);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取推荐听书
+     */
+    @GetMapping("/recommend")
+    public Result<List<AudiobookVO>> getRecommendAudiobooks(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<AudiobookVO> audiobooks = audiobookService.getRecommendAudiobooks(limit);
+            return Result.success(audiobooks);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取畅听榜
+     */
+    @GetMapping("/ranking")
+    public Result<List<AudiobookVO>> getRankingAudiobooks(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<AudiobookVO> audiobooks = audiobookService.getRankingAudiobooks(limit);
+            return Result.success(audiobooks);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取听书详情(包含章节列表)
+     */
+    @GetMapping("/{id}")
+    public Result<AudiobookDetailVO> getAudiobookDetail(@PathVariable Integer id,
+                                                        @RequestParam(required = false) Integer userId) {
+        try {
+            AudiobookDetailVO detail = audiobookService.getAudiobookDetail(id, userId);
+            return Result.success(detail);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取章节详情
+     */
+    @GetMapping("/chapter/{chapterId}")
+    public Result<ChapterVO> getChapterDetail(@PathVariable Integer chapterId, @RequestParam(required = false) Integer userId) {
+        try {
+            ChapterVO chapter = audiobookService.getChapterDetail(chapterId, userId);
+            return Result.success(chapter);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 记录听书历史
+     */
+    @PostMapping("/history")
+    public Result<String> recordListeningHistory(@RequestBody java.util.Map<String, Object> params) {
+        try {
+            Integer userId = getIntegerFromMap(params, "userId");
+            Integer audiobookId = getIntegerFromMap(params, "audiobookId");
+            Integer chapterId = getIntegerFromMap(params, "chapterId");
+            if (userId == null || audiobookId == null || chapterId == null) {
+                return Result.error("参数错误");
+            }
+            audiobookService.recordListeningHistory(userId, audiobookId, chapterId);
+            return Result.success("记录成功");
+        } catch (Exception e) {
+            return Result.error("记录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 保存听书进度
+     */
+    @PostMapping("/progress")
+    public Result<String> saveListeningProgress(@RequestBody java.util.Map<String, Object> params) {
+        try {
+            // 处理可能为Number类型的参数
+            Integer userId = getIntegerFromMap(params, "userId");
+            Integer audiobookId = getIntegerFromMap(params, "audiobookId");
+            Integer chapterId = getIntegerFromMap(params, "chapterId");
+            Integer currentPosition = getIntegerFromMap(params, "currentPosition");
+            Integer duration = getIntegerFromMap(params, "duration");
+            
+            if (userId == null || audiobookId == null || chapterId == null || currentPosition == null || duration == null) {
+                return Result.error("参数错误");
+            }
+            audiobookService.saveListeningProgress(userId, audiobookId, chapterId, currentPosition, duration);
+            return Result.success("保存成功");
+        } catch (Exception e) {
+            return Result.error("保存失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从Map中获取Integer值(支持Number类型转换)
+     */
+    private Integer getIntegerFromMap(java.util.Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Integer) {
+            return (Integer) value;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        if (value instanceof String) {
+            try {
+                return Integer.parseInt((String) value);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * 获取听书进度
+     */
+    @GetMapping("/progress")
+    public Result<com.yu.book.domain.UserListeningProgress> getListeningProgress(@RequestParam Integer userId,
+                                                                                 @RequestParam Integer chapterId) {
+        try {
+            com.yu.book.domain.UserListeningProgress progress = 
+                audiobookService.getListeningProgress(userId, chapterId);
+            return Result.success(progress);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 播放章节(增加播放次数)
+     */
+    @PostMapping("/play")
+    public Result<String> playChapter(@RequestBody java.util.Map<String, Object> params) {
+        try {
+            Integer audiobookId = getIntegerFromMap(params, "audiobookId");
+            Integer chapterId = getIntegerFromMap(params, "chapterId");
+            if (audiobookId == null || chapterId == null) {
+                return Result.error("参数错误");
+            }
+            audiobookService.playChapter(audiobookId, chapterId);
+            return Result.success("播放成功");
+        } catch (Exception e) {
+            return Result.error("播放失败: " + e.getMessage());
+        }
+    }
+}
+

+ 51 - 0
src/main/java/com/yu/book/controller/BannerController.java

@@ -0,0 +1,51 @@
+package com.yu.book.controller;
+
+import com.yu.book.admin.entity.Banner;
+import com.yu.book.common.Result;
+import com.yu.book.service.BannerQueryService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 前端轮播图查询控制器
+ */
+@RestController
+@RequestMapping("/api/banner")
+@CrossOrigin(origins = "*")
+public class BannerController {
+    
+    private final BannerQueryService bannerQueryService;
+    
+    public BannerController(BannerQueryService bannerQueryService) {
+        this.bannerQueryService = bannerQueryService;
+    }
+    
+    /**
+     * 根据分组编码查询轮播图列表
+     */
+    @GetMapping("/list")
+    public Result<List<Banner>> getBanners(@RequestParam String code) {
+        try {
+            if (code == null || code.trim().isEmpty()) {
+                return Result.error("分组编码不能为空");
+            }
+            List<Banner> banners = bannerQueryService.getBannersByCode(code);
+            return Result.success(banners);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 73 - 0
src/main/java/com/yu/book/controller/BookChapterController.java

@@ -0,0 +1,73 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.service.BookChapterService;
+import com.yu.book.vo.ChapterDetailVO;
+import com.yu.book.vo.ChapterVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 书籍章节控制器
+ */
+@RestController
+@RequestMapping("/api/chapter")
+@CrossOrigin(origins = "*")
+public class BookChapterController {
+    
+    private final BookChapterService chapterService;
+    
+    public BookChapterController(BookChapterService chapterService) {
+        this.chapterService = chapterService;
+    }
+    
+    /**
+     * 根据书籍ID获取章节列表
+     */
+    @GetMapping("/list")
+    public Result<List<ChapterVO>> getChaptersByBookId(@RequestParam Integer bookId) {
+        try {
+            List<ChapterVO> chapters = chapterService.getChaptersByBookId(bookId);
+            return Result.success(chapters);
+        } catch (Exception e) {
+            return Result.error("获取章节列表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据章节ID获取章节详情(包含内容)
+     */
+    @GetMapping("/{id}")
+    public Result<ChapterDetailVO> getChapterDetail(@PathVariable Integer id) {
+        try {
+            ChapterDetailVO chapter = chapterService.getChapterDetail(id);
+            if (chapter == null) {
+                return Result.error("章节不存在");
+            }
+            return Result.success(chapter);
+        } catch (Exception e) {
+            return Result.error("获取章节详情失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 297 - 0
src/main/java/com/yu/book/controller/BookController.java

@@ -0,0 +1,297 @@
+package com.yu.book.controller;
+
+import com.yu.book.dto.BookDTO;
+import com.yu.book.service.BookService;
+import com.yu.book.common.Result;
+import com.yu.book.common.PageResult;
+import com.yu.book.vo.BookVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 书籍控制器
+ */
+@RestController
+@RequestMapping("/api/book")
+@CrossOrigin(origins = "*")
+public class BookController {
+    
+    private final BookService bookService;
+    
+    public BookController(BookService bookService) {
+        this.bookService = bookService;
+    }
+    
+    /**
+     * 分页查询书籍
+     */
+    @GetMapping("/list")
+    public Result<PageResult<BookVO>> getBooks(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer categoryId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) Boolean isVip) {
+        try {
+            PageResult<BookVO> result = bookService.getBooks(page, size, keyword, categoryId, status, isVip);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 搜索书籍(模糊查询)
+     * 支持按书名、作者、简介、描述进行搜索
+     */
+    @GetMapping("/search")
+    public Result<PageResult<BookVO>> searchBooks(
+            @RequestParam String keyword,
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "20") Integer size) {
+        try {
+            if (keyword == null || keyword.trim().isEmpty()) {
+                return Result.error("搜索关键词不能为空");
+            }
+            // 只搜索上架的书籍
+            PageResult<BookVO> result = bookService.getBooks(page, size, keyword.trim(), null, 1, null);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("搜索失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取热门搜索关键词
+     */
+    @GetMapping("/popular-keywords")
+    public Result<List<String>> getPopularKeywords(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<String> keywords = bookService.getPopularKeywords(limit);
+            return Result.success(keywords);
+        } catch (Exception e) {
+            return Result.error("获取热门搜索失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取精品书单
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/featured-list")
+    public Result<List<BookVO>> getFeaturedList(@RequestParam(defaultValue = "4") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getFeaturedBooks(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取更多推荐书籍
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/more-recommend")
+    public Result<List<BookVO>> getMoreRecommend(@RequestParam(defaultValue = "6") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getMoreRecommend(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取今日推荐
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/today-recommend")
+    public Result<List<BookVO>> getTodayRecommend(@RequestParam(defaultValue = "8") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getTodayRecommend(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取畅销书籍
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/bestsellers")
+    public Result<List<BookVO>> getBestsellers(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getBestsellers(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取全部书籍(分页)
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/all")
+    public Result<PageResult<BookVO>> getAllBooks(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size) {
+        try {
+            PageResult<BookVO> result = bookService.getAllBooks(page, size);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取排行榜书籍
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/ranking")
+    public Result<List<BookVO>> getRankingBooks(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getRankingBooks(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取热门书籍
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/popular")
+    public Result<List<BookVO>> getPopularBooks(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getPopularBooks(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取新书榜
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/new")
+    public Result<List<BookVO>> getNewBooks(@RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<BookVO> books = bookService.getNewBooks(limit);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取VIP书籍(分页)
+     * 注意:必须放在 /{id} 路由之前,避免路径冲突
+     */
+    @GetMapping("/vip")
+    public Result<PageResult<BookVO>> getVipBooks(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size) {
+        try {
+            PageResult<BookVO> result = bookService.getVipBooks(page, size);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据ID查询书籍详情
+     * 注意:必须放在所有具体路径路由之后,避免路径冲突
+     */
+    @GetMapping("/{id}")
+    public Result<BookVO> getBookById(@PathVariable Integer id, @RequestParam(required = false) Integer userId) {
+        try {
+            BookVO book = bookService.getBookById(id, userId);
+            return Result.success(book);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 创建书籍
+     */
+    @PostMapping
+    public Result<BookVO> createBook(@RequestBody BookDTO bookDTO) {
+        try {
+            // 手动验证
+            if (bookDTO == null || bookDTO.getTitle() == null || bookDTO.getTitle().trim().isEmpty()) {
+                return Result.error("书名不能为空");
+            }
+            if (bookDTO.getStatus() == null) {
+                return Result.error("状态不能为空");
+            }
+            
+            BookVO book = bookService.createBook(bookDTO);
+            return Result.success("创建成功", book);
+        } catch (Exception e) {
+            return Result.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新书籍
+     */
+    @PutMapping("/{id}")
+    public Result<BookVO> updateBook(@PathVariable Integer id, @RequestBody BookDTO bookDTO) {
+        try {
+            // 手动验证
+            if (bookDTO == null || bookDTO.getTitle() == null || bookDTO.getTitle().trim().isEmpty()) {
+                return Result.error("书名不能为空");
+            }
+            if (bookDTO.getStatus() == null) {
+                return Result.error("状态不能为空");
+            }
+            
+            BookVO book = bookService.updateBook(id, bookDTO);
+            return Result.success("更新成功", book);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除书籍
+     */
+    @DeleteMapping("/{id}")
+    public Result<String> deleteBook(@PathVariable Integer id) {
+        try {
+            bookService.deleteBook(id);
+            return Result.success("删除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量删除书籍
+     */
+    @DeleteMapping("/batch")
+    public Result<String> deleteBooks(@RequestBody List<Integer> ids) {
+        try {
+            bookService.deleteBooks(ids);
+            return Result.success("批量删除成功");
+        } catch (Exception e) {
+            return Result.error("批量删除失败: " + e.getMessage());
+        }
+    }
+}
+

+ 148 - 0
src/main/java/com/yu/book/controller/BookshelfController.java

@@ -0,0 +1,148 @@
+package com.yu.book.controller;
+
+import com.yu.book.dto.BookshelfDTO;
+import com.yu.book.service.BookshelfService;
+import com.yu.book.common.Result;
+import com.yu.book.vo.BookshelfVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 用户书架控制器
+ */
+@RestController
+@RequestMapping("/api/bookshelf")
+@CrossOrigin(origins = "*")
+public class BookshelfController {
+    
+    private final BookshelfService bookshelfService;
+    
+    public BookshelfController(BookshelfService bookshelfService) {
+        this.bookshelfService = bookshelfService;
+    }
+    
+    /**
+     * 添加书籍到书架
+     */
+    @PostMapping("/add")
+    public Result<BookshelfVO> addToBookshelf(@RequestBody BookshelfDTO bookshelfDTO) {
+        try {
+            // 手动验证
+            if (bookshelfDTO == null) {
+                return Result.error("参数不能为空");
+            }
+            if (bookshelfDTO.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (bookshelfDTO.getBookId() == null) {
+                return Result.error("书籍ID不能为空");
+            }
+            
+            BookshelfVO bookshelfVO = bookshelfService.addToBookshelf(
+                bookshelfDTO.getUserId(), 
+                bookshelfDTO.getBookId()
+            );
+            return Result.success("加入书架成功", bookshelfVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("加入书架失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 从书架移除书籍
+     */
+    @DeleteMapping("/remove")
+    public Result<String> removeFromBookshelf(@RequestBody BookshelfDTO bookshelfDTO) {
+        try {
+            if (bookshelfDTO == null) {
+                return Result.error("参数不能为空");
+            }
+            if (bookshelfDTO.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (bookshelfDTO.getBookId() == null) {
+                return Result.error("书籍ID不能为空");
+            }
+            
+            bookshelfService.removeFromBookshelf(bookshelfDTO.getUserId(), bookshelfDTO.getBookId());
+            return Result.success("移除成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("移除失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户书架列表
+     */
+    @GetMapping("/list")
+    public Result<List<BookshelfVO>> getBookshelfList(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<BookshelfVO> list = bookshelfService.getBookshelfList(userId);
+            return Result.success(list);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 检查书籍是否在书架中
+     */
+    @GetMapping("/check")
+    public Result<Boolean> checkInBookshelf(@RequestParam Integer userId, @RequestParam Integer bookId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (bookId == null) {
+                return Result.error("书籍ID不能为空");
+            }
+            
+            boolean isInBookshelf = bookshelfService.isInBookshelf(userId, bookId);
+            return Result.success(isInBookshelf);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新阅读进度
+     */
+    @PutMapping("/progress")
+    public Result<BookshelfVO> updateReadProgress(@RequestBody BookshelfDTO bookshelfDTO) {
+        try {
+            if (bookshelfDTO == null) {
+                return Result.error("参数不能为空");
+            }
+            if (bookshelfDTO.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (bookshelfDTO.getBookId() == null) {
+                return Result.error("书籍ID不能为空");
+            }
+            if (bookshelfDTO.getReadProgress() == null) {
+                return Result.error("阅读进度不能为空");
+            }
+            
+            BookshelfVO bookshelfVO = bookshelfService.updateReadProgress(
+                bookshelfDTO.getUserId(),
+                bookshelfDTO.getBookId(),
+                bookshelfDTO.getReadProgress()
+            );
+            return Result.success("更新成功", bookshelfVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("更新失败: " + e.getMessage());
+        }
+    }
+}
+

+ 76 - 0
src/main/java/com/yu/book/controller/BrowsingHistoryController.java

@@ -0,0 +1,76 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.dto.RecordBrowsingHistoryDTO;
+import com.yu.book.service.BrowsingHistoryService;
+import com.yu.book.vo.BrowsingHistoryVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 浏览记录控制器
+ */
+@RestController
+@RequestMapping("/api/browsing-history")
+@CrossOrigin(origins = "*")
+public class BrowsingHistoryController {
+    
+    private final BrowsingHistoryService browsingHistoryService;
+    
+    public BrowsingHistoryController(BrowsingHistoryService browsingHistoryService) {
+        this.browsingHistoryService = browsingHistoryService;
+    }
+    
+    /**
+     * 记录浏览历史
+     */
+    @PostMapping("/record")
+    public Result<String> recordBrowsingHistory(@RequestBody RecordBrowsingHistoryDTO dto) {
+        try {
+            if (dto == null || dto.getUserId() == null || dto.getBookId() == null) {
+                return Result.error("用户ID和书籍ID不能为空");
+            }
+            
+            browsingHistoryService.recordBrowsingHistory(dto.getUserId(), dto.getBookId());
+            return Result.success("记录成功");
+        } catch (Exception e) {
+            return Result.error("记录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户的浏览记录列表
+     */
+    @GetMapping("/list")
+    public Result<List<BrowsingHistoryVO>> getBrowsingHistory(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<BrowsingHistoryVO> list = browsingHistoryService.getBrowsingHistory(userId);
+            return Result.success(list);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 清空用户的浏览记录
+     */
+    @DeleteMapping("/clear")
+    public Result<String> clearBrowsingHistory(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            browsingHistoryService.clearBrowsingHistory(userId);
+            return Result.success("清空成功");
+        } catch (Exception e) {
+            return Result.error("清空失败: " + e.getMessage());
+        }
+    }
+}
+

+ 73 - 0
src/main/java/com/yu/book/controller/CategoryController.java

@@ -0,0 +1,73 @@
+package com.yu.book.controller;
+
+import com.yu.book.service.CategoryService;
+import com.yu.book.common.Result;
+import com.yu.book.vo.CategoryVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 分类控制器
+ */
+@RestController
+@RequestMapping("/api/category")
+@CrossOrigin(origins = "*")
+public class CategoryController {
+    
+    private final CategoryService categoryService;
+    
+    public CategoryController(CategoryService categoryService) {
+        this.categoryService = categoryService;
+    }
+    
+    /**
+     * 获取所有启用的分类列表(包含书籍数量)
+     */
+    @GetMapping("/list")
+    public Result<List<CategoryVO>> getAllCategories() {
+        try {
+            List<CategoryVO> categories = categoryService.getAllCategories();
+            return Result.success(categories);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据ID获取分类详情
+     */
+    @GetMapping("/{id}")
+    public Result<CategoryVO> getCategoryById(@PathVariable Integer id) {
+        try {
+            CategoryVO category = categoryService.getCategoryById(id);
+            return Result.success(category);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 83 - 0
src/main/java/com/yu/book/controller/CommentController.java

@@ -0,0 +1,83 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.domain.BookComment;
+import com.yu.book.dto.AddCommentDTO;
+import com.yu.book.dto.ToggleCommentLikeDTO;
+import com.yu.book.service.CommentService;
+import com.yu.book.vo.CommentVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 评论控制器
+ */
+@RestController
+@RequestMapping("/api/comment")
+@CrossOrigin(origins = "*")
+public class CommentController {
+    
+    private final CommentService commentService;
+    
+    public CommentController(CommentService commentService) {
+        this.commentService = commentService;
+    }
+    
+    /**
+     * 发表评论
+     */
+    @PostMapping("/add")
+    public Result<BookComment> addComment(@RequestBody AddCommentDTO dto) {
+        try {
+            if (dto == null || dto.getUserId() == null || dto.getBookId() == null) {
+                return Result.error("用户ID和书籍ID不能为空");
+            }
+            if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
+                return Result.error("评论内容不能为空");
+            }
+            
+            BookComment comment = commentService.addComment(dto.getUserId(), dto.getBookId(), dto.getContent());
+            return Result.success("发表成功", comment);
+        } catch (Exception e) {
+            return Result.error("发表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取书籍的评论列表
+     */
+    @GetMapping("/list")
+    public Result<List<CommentVO>> getComments(
+            @RequestParam Integer bookId,
+            @RequestParam(required = false) Integer userId) {
+        try {
+            if (bookId == null) {
+                return Result.error("书籍ID不能为空");
+            }
+            
+            List<CommentVO> comments = commentService.getCommentsByBookId(bookId, userId);
+            return Result.success(comments);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 点赞/取消点赞评论
+     */
+    @PostMapping("/like")
+    public Result<Boolean> toggleLike(@RequestBody ToggleCommentLikeDTO dto) {
+        try {
+            if (dto == null || dto.getUserId() == null || dto.getCommentId() == null) {
+                return Result.error("用户ID和评论ID不能为空");
+            }
+            
+            boolean isLiked = commentService.toggleLike(dto.getUserId(), dto.getCommentId());
+            return Result.success(isLiked ? "点赞成功" : "取消点赞成功", isLiked);
+        } catch (Exception e) {
+            return Result.error("操作失败: " + e.getMessage());
+        }
+    }
+}
+

+ 102 - 0
src/main/java/com/yu/book/controller/FeedbackController.java

@@ -0,0 +1,102 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.domain.Feedback;
+import com.yu.book.dto.FeedbackDTO;
+import com.yu.book.service.FeedbackService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 意见反馈控制器
+ */
+@RestController
+@RequestMapping("/api/feedback")
+@CrossOrigin(origins = "*")
+public class FeedbackController {
+    
+    private final FeedbackService feedbackService;
+    
+    public FeedbackController(FeedbackService feedbackService) {
+        this.feedbackService = feedbackService;
+    }
+    
+    /**
+     * 提交反馈
+     */
+    @PostMapping("/submit")
+    public Result<Feedback> submitFeedback(@RequestBody FeedbackDTO feedbackDTO) {
+        try {
+            // 参数验证
+            if (feedbackDTO == null) {
+                return Result.error("反馈数据不能为空");
+            }
+            if (feedbackDTO.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (feedbackDTO.getDescription() == null || feedbackDTO.getDescription().trim().isEmpty()) {
+                return Result.error("详细描述不能为空");
+            }
+            
+            Feedback feedback = feedbackService.submitFeedback(feedbackDTO);
+            return Result.success("提交成功", feedback);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("提交失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 查询用户的反馈列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> getUserFeedbacks(
+            @RequestParam Integer userId,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer size) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<Feedback> feedbacks = feedbackService.getUserFeedbacks(userId, page, size);
+            Long total = feedbackService.countUserFeedbacks(userId);
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("list", feedbacks);
+            result.put("total", total);
+            result.put("page", page);
+            result.put("size", size);
+            
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 查询反馈详情
+     */
+    @GetMapping("/{id}")
+    public Result<Feedback> getFeedbackById(@PathVariable Integer id) {
+        try {
+            Feedback feedback = feedbackService.getFeedbackById(id);
+            return Result.success(feedback);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+

+ 174 - 0
src/main/java/com/yu/book/controller/MessageController.java

@@ -0,0 +1,174 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.domain.Message;
+import com.yu.book.domain.User;
+import com.yu.book.mapper.UserMapper;
+import com.yu.book.service.MessageService;
+import org.springframework.web.bind.annotation.*;
+
+import java.text.SimpleDateFormat;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 消息控制器
+ */
+@RestController
+@RequestMapping("/api/message")
+@CrossOrigin(origins = "*")
+public class MessageController {
+    
+    private final MessageService messageService;
+    private final UserMapper userMapper;
+    
+    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm");
+    
+    public MessageController(MessageService messageService, UserMapper userMapper) {
+        this.messageService = messageService;
+        this.userMapper = userMapper;
+    }
+    
+    /**
+     * 获取用户的消息列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> getMessages(
+            @RequestParam Integer userId,
+            @RequestParam(required = false) Integer isRead,
+            @RequestParam(required = false) String type,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "20") Integer size) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<Message> messages = messageService.getUserMessages(userId, isRead, type, page, size);
+            Long total = messageService.countUserMessages(userId, isRead, type);
+            
+            // 转换为前端需要的格式
+            List<Map<String, Object>> messageList = messages.stream().map(msg -> {
+                Map<String, Object> item = new HashMap<>();
+                
+                // 查询发送者信息
+                User fromUser = userMapper.selectById(msg.getFromUserId());
+                if (fromUser != null) {
+                    item.put("sender", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
+                    item.put("avatar", fromUser.getAvatar() != null ? fromUser.getAvatar() : "https://picsum.photos/seed/default/200/200");
+                } else {
+                    item.put("sender", "未知用户");
+                    item.put("avatar", "https://picsum.photos/seed/default/200/200");
+                }
+                
+                // 格式化时间
+                if (msg.getCreatedAt() != null) {
+                    item.put("time", DATE_FORMAT.format(msg.getCreatedAt()));
+                } else {
+                    item.put("time", "");
+                }
+                
+                // 设置操作图标和文本
+                String actionIcon = "💬";
+                String actionText = "";
+                if ("like".equals(msg.getType())) {
+                    actionIcon = "👍";
+                    actionText = "赞了你的评论";
+                } else if ("comment".equals(msg.getType())) {
+                    actionIcon = "💬";
+                    actionText = "评论了你的书籍";
+                } else if ("reply".equals(msg.getType())) {
+                    actionIcon = "💬";
+                    actionText = "回复了你的评论";
+                }
+                
+                item.put("actionIcon", actionIcon);
+                item.put("actionText", actionText);
+                item.put("comment", msg.getContent());
+                item.put("id", msg.getId());
+                item.put("type", msg.getType());
+                item.put("relatedId", msg.getRelatedId());
+                item.put("relatedType", msg.getRelatedType());
+                item.put("isRead", msg.getIsRead());
+                
+                return item;
+            }).collect(Collectors.toList());
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("list", messageList);
+            result.put("total", total);
+            result.put("page", page);
+            result.put("size", size);
+            
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取未读消息数量
+     */
+    @GetMapping("/unread-count")
+    public Result<Long> getUnreadCount(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            Long count = messageService.getUnreadCount(userId);
+            return Result.success(count);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 标记消息为已读
+     */
+    @PutMapping("/read/{id}")
+    public Result<String> markAsRead(@PathVariable Integer id) {
+        try {
+            if (id == null) {
+                return Result.error("消息ID不能为空");
+            }
+            
+            messageService.markAsRead(id);
+            return Result.success("标记成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("标记失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量标记所有消息为已读
+     */
+    @PutMapping("/read-all")
+    public Result<String> markAllAsRead(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            messageService.markAllAsRead(userId);
+            return Result.success("全部标记成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("标记失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+

+ 59 - 0
src/main/java/com/yu/book/controller/RankingController.java

@@ -0,0 +1,59 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.service.RankingQueryService;
+import com.yu.book.vo.BookVO;
+import com.yu.book.vo.RankingGroupVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/ranking")
+@CrossOrigin(origins = "*")
+public class RankingController {
+    private final RankingQueryService rankingQueryService;
+
+    public RankingController(RankingQueryService rankingQueryService) {
+        this.rankingQueryService = rankingQueryService;
+    }
+
+    /**
+     * 获取所有启用的排行榜类型
+     */
+    @GetMapping("/groups")
+    public Result<List<RankingGroupVO>> getAllRankingGroups() {
+        try {
+            List<RankingGroupVO> groups = rankingQueryService.getAllActiveRankingGroups();
+            return Result.success(groups);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 自定义榜单:按分组code返回书籍列表(按分组内排序)
+     * 支持分类筛选
+     */
+    @GetMapping("/list")
+    public Result<List<BookVO>> getRankingList(
+            @RequestParam String code,
+            @RequestParam(required = false) Integer categoryId) {
+        try {
+            List<BookVO> books = rankingQueryService.getRankingBooksByCode(code, categoryId);
+            return Result.success(books);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+

+ 78 - 0
src/main/java/com/yu/book/controller/SearchHistoryController.java

@@ -0,0 +1,78 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.dto.RecordSearchHistoryDTO;
+import com.yu.book.service.SearchHistoryService;
+import com.yu.book.vo.SearchHistoryVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 搜索历史控制器
+ */
+@RestController
+@RequestMapping("/api/search-history")
+@CrossOrigin(origins = "*")
+public class SearchHistoryController {
+    
+    private final SearchHistoryService searchHistoryService;
+    
+    public SearchHistoryController(SearchHistoryService searchHistoryService) {
+        this.searchHistoryService = searchHistoryService;
+    }
+    
+    /**
+     * 记录搜索历史
+     */
+    @PostMapping("/record")
+    public Result<String> recordSearchHistory(@RequestBody RecordSearchHistoryDTO dto) {
+        try {
+            if (dto == null || dto.getUserId() == null || dto.getKeyword() == null || dto.getKeyword().trim().isEmpty()) {
+                return Result.error("用户ID和搜索关键词不能为空");
+            }
+            
+            searchHistoryService.recordSearchHistory(dto.getUserId(), dto.getKeyword());
+            return Result.success("记录成功");
+        } catch (Exception e) {
+            return Result.error("记录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户的搜索历史列表
+     */
+    @GetMapping("/list")
+    public Result<List<SearchHistoryVO>> getSearchHistory(
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<SearchHistoryVO> list = searchHistoryService.getSearchHistory(userId, limit);
+            return Result.success(list);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 清空用户的搜索历史
+     */
+    @DeleteMapping("/clear")
+    public Result<String> clearSearchHistory(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            searchHistoryService.clearSearchHistory(userId);
+            return Result.success("清空成功");
+        } catch (Exception e) {
+            return Result.error("清空失败: " + e.getMessage());
+        }
+    }
+}
+

+ 123 - 0
src/main/java/com/yu/book/controller/UserController.java

@@ -0,0 +1,123 @@
+package com.yu.book.controller;
+
+import com.yu.book.dto.LoginDTO;
+import com.yu.book.dto.RegisterDTO;
+import com.yu.book.dto.UpdateProfileDTO;
+import com.yu.book.service.UserService;
+import com.yu.book.common.Result;
+import com.yu.book.vo.LoginVO;
+import com.yu.book.vo.UserVO;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 用户控制器
+ */
+@RestController
+@RequestMapping("/api/user")
+@CrossOrigin(origins = "*")
+public class UserController {
+    
+    private final UserService userService;
+    
+    public UserController(UserService userService) {
+        this.userService = userService;
+    }
+    
+    /**
+     * 用户登录
+     */
+    @PostMapping("/login")
+    public Result<LoginVO> login(@RequestBody LoginDTO loginDTO) {
+        try {
+            // 手动验证
+            if (loginDTO == null || loginDTO.getUsername() == null || loginDTO.getUsername().trim().isEmpty()) {
+                return Result.error("用户名不能为空");
+            }
+            if (loginDTO.getPassword() == null || loginDTO.getPassword().trim().isEmpty()) {
+                return Result.error("密码不能为空");
+            }
+            
+            LoginVO loginVO = userService.login(loginDTO);
+            return Result.success("登录成功", loginVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("登录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 用户注册
+     */
+    @PostMapping("/register")
+    public Result<UserVO> register(@RequestBody RegisterDTO registerDTO) {
+        try {
+            // 手动验证
+            if (registerDTO == null || registerDTO.getUsername() == null || registerDTO.getUsername().trim().isEmpty()) {
+                return Result.error("用户名不能为空");
+            }
+            if (registerDTO.getPassword() == null || registerDTO.getPassword().trim().isEmpty()) {
+                return Result.error("密码不能为空");
+            }
+            if (registerDTO.getPassword().length() < 6 || registerDTO.getPassword().length() > 20) {
+                return Result.error("密码长度必须在6-20位之间");
+            }
+            
+            UserVO userVO = userService.register(registerDTO);
+            return Result.success("注册成功", userVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("注册失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据ID查询用户信息
+     */
+    @GetMapping("/{id}")
+    public Result<UserVO> getUserById(@PathVariable Integer id) {
+        try {
+            UserVO userVO = userService.getUserById(id);
+            return Result.success(userVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取个人资料(基于userId参数;后续可从token解析)
+     */
+    @GetMapping("/profile")
+    public Result<UserVO> getProfile(@RequestParam Integer userId) {
+        try {
+            UserVO userVO = userService.getProfile(userId);
+            return Result.success(userVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新个人资料
+     */
+    @PutMapping("/profile")
+    public Result<UserVO> updateProfile(@RequestBody UpdateProfileDTO dto) {
+        try {
+            if (dto == null || dto.getUserId() == null) {
+                return Result.error("参数错误");
+            }
+            UserVO userVO = userService.updateProfile(dto);
+            return Result.success("保存成功", userVO);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("保存失败: " + e.getMessage());
+        }
+    }
+}
+

+ 195 - 0
src/main/java/com/yu/book/controller/VipController.java

@@ -0,0 +1,195 @@
+package com.yu.book.controller;
+
+import com.yu.book.common.Result;
+import com.yu.book.domain.VipRecord;
+import com.yu.book.dto.PurchaseVipDTO;
+import com.yu.book.service.VipService;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * VIP控制器
+ */
+@RestController
+@RequestMapping("/api/vip")
+@CrossOrigin(origins = "*")
+public class VipController {
+    
+    private final VipService vipService;
+    
+    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+    
+    public VipController(VipService vipService) {
+        this.vipService = vipService;
+    }
+    
+    /**
+     * 获取VIP套餐列表
+     */
+    @GetMapping("/plans")
+    public Result<List<Map<String, Object>>> getVipPlans() {
+        try {
+            List<Map<String, Object>> plans = List.of(
+                Map.of(
+                    "type", "month",
+                    "name", "月卡VIP",
+                    "price", new BigDecimal("30.00"),
+                    "duration", 30,
+                    "desc", "适合短期阅读",
+                    "benefits", List.of("无限阅读", "免费听书", "专属客服")
+                ),
+                Map.of(
+                    "type", "quarter",
+                    "name", "季卡VIP",
+                    "price", new BigDecimal("80.00"),
+                    "duration", 90,
+                    "desc", "超值优惠",
+                    "benefits", List.of("无限阅读", "免费听书", "专属客服", "优先更新")
+                ),
+                Map.of(
+                    "type", "year",
+                    "name", "年卡VIP",
+                    "price", new BigDecimal("288.00"),
+                    "duration", 365,
+                    "desc", "最超值选择",
+                    "benefits", List.of("无限阅读", "免费听书", "专属客服", "优先更新", "专属活动")
+                )
+            );
+            return Result.success(plans);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 购买VIP
+     */
+    @PostMapping("/purchase")
+    public Result<Map<String, Object>> purchaseVip(@RequestBody PurchaseVipDTO dto) {
+        try {
+            // 参数验证
+            if (dto == null || dto.getUserId() == null) {
+                return Result.error("用户ID不能为空");
+            }
+            if (dto.getVipType() == null || dto.getVipType().trim().isEmpty()) {
+                return Result.error("VIP类型不能为空");
+            }
+            if (dto.getPrice() == null) {
+                return Result.error("价格不能为空");
+            }
+            
+            // 购买VIP
+            VipRecord record = vipService.purchaseVip(dto);
+            
+            // 转换为前端需要的格式
+            Map<String, Object> result = new HashMap<>();
+            result.put("id", record.getId());
+            result.put("type", record.getVipName());
+            result.put("price", record.getPrice());
+            result.put("activateTime", DATE_FORMAT.format(record.getStartTime()));
+            result.put("expireTime", DATE_FORMAT.format(record.getExpireTime()));
+            result.put("orderNo", record.getOrderNo());
+            result.put("paymentStatus", record.getPaymentStatus());
+            
+            return Result.success("购买成功", result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("购买失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 确认支付(支付回调)
+     */
+    @PostMapping("/confirm-payment")
+    public Result<String> confirmPayment(@RequestParam Integer recordId, @RequestParam(required = false) String transactionId) {
+        try {
+            if (recordId == null) {
+                return Result.error("记录ID不能为空");
+            }
+            
+            vipService.confirmPayment(recordId, transactionId);
+            return Result.success("支付确认成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("支付确认失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 检查用户VIP状态
+     */
+    @GetMapping("/check")
+    public Result<Map<String, Object>> checkVipStatus(@RequestParam Integer userId) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            boolean isVip = vipService.checkVipStatus(userId);
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("isVip", isVip);
+            
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户的VIP充值记录列表
+     */
+    @GetMapping("/records")
+    public Result<Map<String, Object>> getVipRecords(
+            @RequestParam Integer userId,
+            @RequestParam(required = false) Integer paymentStatus,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer size) {
+        try {
+            if (userId == null) {
+                return Result.error("用户ID不能为空");
+            }
+            
+            List<VipRecord> records = vipService.getUserVipRecords(userId, paymentStatus, page, size);
+            Long total = vipService.countUserVipRecords(userId, paymentStatus);
+            
+            // 转换为前端需要的格式
+            List<Map<String, Object>> recordList = records.stream().map(record -> {
+                Map<String, Object> item = new HashMap<>();
+                item.put("id", record.getId());
+                item.put("type", record.getVipName());
+                item.put("price", record.getPrice());
+                item.put("activateTime", DATE_FORMAT.format(record.getStartTime()));
+                item.put("expireTime", DATE_FORMAT.format(record.getExpireTime()));
+                item.put("paymentStatus", record.getPaymentStatus());
+                item.put("orderNo", record.getOrderNo());
+                return item;
+            }).toList();
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("list", recordList);
+            result.put("total", total);
+            result.put("page", page);
+            result.put("size", size);
+            
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            return Result.error("查询失败: " + e.getMessage());
+        }
+    }
+}
+
+
+
+
+

+ 67 - 0
src/main/java/com/yu/book/demos/web/BasicController.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yu.book.demos.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+/**
+ * @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
+ */
+@Controller
+public class BasicController {
+
+    // http://127.0.0.1:8080/hello?name=lisi
+    @RequestMapping("/hello")
+    @ResponseBody
+    public String hello(@RequestParam(name = "name", defaultValue = "unknown user") String name) {
+        return "Hello " + name;
+    }
+
+    // http://127.0.0.1:8080/user
+    @RequestMapping("/user")
+    @ResponseBody
+    public User user() {
+        User user = new User();
+        user.setName("theonefx");
+        user.setAge(666);
+        return user;
+    }
+
+    // http://127.0.0.1:8080/save_user?name=newName&age=11
+    @RequestMapping("/save_user")
+    @ResponseBody
+    public String saveUser(User u) {
+        return "user will save: name=" + u.getName() + ", age=" + u.getAge();
+    }
+
+    // http://127.0.0.1:8080/html
+    @RequestMapping("/html")
+    public String html() {
+        return "index.html";
+    }
+
+    @ModelAttribute
+    public void parseUser(@RequestParam(name = "name", defaultValue = "unknown user") String name
+            , @RequestParam(name = "age", defaultValue = "12") Integer age, User user) {
+        user.setName("zhangsan");
+        user.setAge(18);
+    }
+}

+ 44 - 0
src/main/java/com/yu/book/demos/web/PathVariableController.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yu.book.demos.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+/**
+ * @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
+ */
+@Controller
+public class PathVariableController {
+
+    // http://127.0.0.1:8080/user/123/roles/222
+    @RequestMapping(value = "/user/{userId}/roles/{roleId}", method = RequestMethod.GET)
+    @ResponseBody
+    public String getLogin(@PathVariable("userId") String userId, @PathVariable("roleId") String roleId) {
+        return "User Id : " + userId + " Role Id : " + roleId;
+    }
+
+    // http://127.0.0.1:8080/javabeat/somewords
+    @RequestMapping(value = "/javabeat/{regexp1:[a-z-]+}", method = RequestMethod.GET)
+    @ResponseBody
+    public String getRegExp(@PathVariable("regexp1") String regexp1) {
+        return "URI Part : " + regexp1;
+    }
+}

+ 43 - 0
src/main/java/com/yu/book/demos/web/User.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yu.book.demos.web;
+
+/**
+ * @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
+ */
+public class User {
+
+    private String name;
+
+    private Integer age;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+}

+ 136 - 0
src/main/java/com/yu/book/domain/Audiobook.java

@@ -0,0 +1,136 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 听书专辑实体类
+ */
+@Data
+public class Audiobook {
+    
+    /**
+     * 听书ID
+     */
+    private Integer id;
+    
+    /**
+     * 书名
+     */
+    private String title;
+    
+    /**
+     * 作者
+     */
+    private String author;
+    
+    /**
+     * 主播
+     */
+    private String narrator;
+    
+    /**
+     * 封面图片URL
+     */
+    private String cover;
+    
+    /**
+     * 图片URL(兼容字段)
+     */
+    private String image;
+    
+    /**
+     * 简介(简短)
+     */
+    private String brief;
+    
+    /**
+     * 描述
+     */
+    private String desc;
+    
+    /**
+     * 详细介绍
+     */
+    private String introduction;
+    
+    /**
+     * 分类ID
+     */
+    private Integer categoryId;
+    
+    /**
+     * 状态:0-下架,1-上架
+     */
+    private Integer status;
+    
+    /**
+     * 是否免费:0-否,1-是
+     */
+    private Boolean isFree;
+    
+    /**
+     * 是否VIP专享:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * 章节数
+     */
+    private Integer chapterCount;
+    
+    /**
+     * 总时长(秒)
+     */
+    private Integer totalDuration;
+    
+    /**
+     * 浏览次数
+     */
+    private Integer viewCount;
+    
+    /**
+     * 点赞数
+     */
+    private Integer likeCount;
+    
+    /**
+     * 播放次数
+     */
+    private Integer playCount;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 91 - 0
src/main/java/com/yu/book/domain/AudiobookChapter.java

@@ -0,0 +1,91 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 听书章节实体类
+ */
+@Data
+public class AudiobookChapter {
+    
+    /**
+     * 章节ID
+     */
+    private Integer id;
+    
+    /**
+     * 听书ID
+     */
+    private Integer audiobookId;
+    
+    /**
+     * 章节序号
+     */
+    private Integer chapterNumber;
+    
+    /**
+     * 章节标题
+     */
+    private String title;
+    
+    /**
+     * 音频文件URL
+     */
+    private String audioUrl;
+    
+    /**
+     * 时长(秒)
+     */
+    private Integer duration;
+    
+    /**
+     * 文件大小(字节)
+     */
+    private Long fileSize;
+    
+    /**
+     * 是否免费:0-否,1-是
+     */
+    private Boolean isFree;
+    
+    /**
+     * 播放次数
+     */
+    private Integer playCount;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 104 - 0
src/main/java/com/yu/book/domain/Book.java

@@ -0,0 +1,104 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 书籍实体类
+ */
+@Data
+public class Book {
+    
+    /**
+     * 书籍ID
+     */
+    private Integer id;
+    
+    /**
+     * 书名
+     */
+    private String title;
+    
+    /**
+     * 作者
+     */
+    private String author;
+    
+    /**
+     * 封面图片URL
+     */
+    private String cover;
+    
+    /**
+     * 图片URL(兼容字段)
+     */
+    private String image;
+    
+    /**
+     * 简介(简短)
+     */
+    private String brief;
+    
+    /**
+     * 描述
+     */
+    private String desc;
+    
+    /**
+     * 详细介绍
+     */
+    private String introduction;
+    
+    /**
+     * 价格
+     */
+    private BigDecimal price;
+    
+    /**
+     * 是否免费:0-否,1-是
+     */
+    private Boolean isFree;
+    
+    /**
+     * 是否VIP专享:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * 分类ID
+     */
+    private Integer categoryId;
+    
+    /**
+     * 状态:0-下架,1-上架
+     */
+    private Integer status;
+    
+    /**
+     * 浏览次数
+     */
+    private Integer viewCount;
+    
+    /**
+     * 点赞数
+     */
+    private Integer likeCount;
+    
+    /**
+     * 阅读次数
+     */
+    private Integer readCount;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+

+ 86 - 0
src/main/java/com/yu/book/domain/BookChapter.java

@@ -0,0 +1,86 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 书籍章节实体类
+ */
+@Data
+public class BookChapter {
+    
+    /**
+     * 章节ID
+     */
+    private Integer id;
+    
+    /**
+     * 书籍ID
+     */
+    private Integer bookId;
+    
+    /**
+     * 章节序号
+     */
+    private Integer chapterNumber;
+    
+    /**
+     * 章节标题
+     */
+    private String title;
+    
+    /**
+     * 章节内容
+     */
+    private String content;
+    
+    /**
+     * 字数
+     */
+    private Integer wordCount;
+    
+    /**
+     * 是否免费:0-否,1-是
+     */
+    private Boolean isFree;
+    
+    /**
+     * 是否VIP专享:0-否,1-是
+     */
+    private Boolean isVip;
+    
+    /**
+     * 状态:0-禁用,1-启用
+     */
+    private Integer status;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 60 - 0
src/main/java/com/yu/book/domain/BookComment.java

@@ -0,0 +1,60 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 书籍评论实体类
+ */
+@Data
+public class BookComment {
+    
+    /**
+     * 评论ID
+     */
+    private Integer id;
+    
+    /**
+     * 用户ID
+     */
+    private Integer userId;
+    
+    /**
+     * 书籍ID
+     */
+    private Integer bookId;
+    
+    /**
+     * 评论内容
+     */
+    private String content;
+    
+    /**
+     * 点赞数
+     */
+    private Integer likeCount;
+    
+    /**
+     * 状态:0-删除,1-正常
+     */
+    private Integer status;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+

+ 50 - 0
src/main/java/com/yu/book/domain/BrowsingHistory.java

@@ -0,0 +1,50 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户浏览记录实体类
+ */
+@Data
+public class BrowsingHistory {
+    
+    /**
+     * 浏览记录ID
+     */
+    private Integer id;
+    
+    /**
+     * 用户ID
+     */
+    private Integer userId;
+    
+    /**
+     * 书籍ID
+     */
+    private Integer bookId;
+    
+    /**
+     * 浏览时间
+     */
+    private Date browsedAt;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+
+
+
+
+
+
+
+

+ 53 - 0
src/main/java/com/yu/book/domain/Category.java

@@ -0,0 +1,53 @@
+package com.yu.book.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 分类实体类
+ */
+@Data
+public class Category {
+    
+    /**
+     * 分类ID
+     */
+    private Integer id;
+    
+    /**
+     * 分类名称
+     */
+    private String name;
+    
+    /**
+     * 图标(emoji或图标代码)
+     */
+    private String icon;
+    
+    /**
+     * 颜色代码
+     */
+    private String color;
+    
+    /**
+     * 排序顺序
+     */
+    private Integer sortOrder;
+    
+    /**
+     * 状态:0-禁用,1-启用
+     */
+    private Integer status;
+    
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+}
+

Some files were not shown because too many files changed in this diff