基于Spring Boot 3.5.4 + PostgreSQL + Redis + JDK21的内容管理系统服务器
-
定时爬取数据
- 支持从MacCMS接口(https://json02.heimuer.xyz/api.php/provide/vod/?ac=detail)爬取视频数据
- 支持全量爬取和增量爬取两种模式
- 自动去重和数据更新
- 可配置的爬取频率和重试机制
-
数据查询服务
- 提供丰富的REST API接口
- 支持分页查询、条件筛选、关键词搜索
- Redis缓存优化查询性能
- 支持按类型、演员、导演、年份、地区等多维度查询
- 高性能: 使用Redis缓存提升查询速度
- 高可靠: 分布式锁防止重复爬取,异常重试机制
- 易扩展: 模块化设计,便于功能扩展
- 易监控: 完整的日志记录和健康检查
- 框架: Spring Boot 3.5.4
- 数据库: PostgreSQL
- 缓存: Redis
- JDK版本: 21
- 构建工具: Maven
- HTTP客户端: WebFlux WebClient
src/main/java/com/hankshen/cms/server/
├── CmsServerApplication.java # 主启动类
├── config/
│ └── AppConfig.java # 应用配置
├── controller/
│ ├── VideoController.java # 视频查询API
│ └── CrawlerController.java # 爬虫管理API
├── dto/
│ └── MacCmsResponse.java # MacCMS API响应DTO
├── entity/
│ └── VideoEntity.java # 视频实体类
├── exception/
│ └── GlobalExceptionHandler.java # 全局异常处理
├── repository/
│ └── VideoRepository.java # 数据访问层
├── scheduler/
│ └── CrawlerScheduler.java # 定时任务调度
└── service/
├── CrawlerService.java # 爬虫服务
└── VideoService.java # 视频查询服务
- JDK 21+
- PostgreSQL 12+
- Redis 6+
- Maven 3.8+
# macOS
brew install postgresql
brew services start postgresql
# 创建数据库
psql postgres
CREATE DATABASE cms_db;
CREATE USER postgres WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE cms_db TO postgres;# macOS
brew install redis
brew services start redis编辑 src/main/resources/application.yml 文件,修改数据库和Redis连接配置:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/cms_db
username: postgres
password: your_password
data:
redis:
host: localhost
port: 6379
password: your_redis_password # 如果有密码# 编译项目
mvn clean compile
# 运行应用
mvn spring-boot:run应用启动后,访问 http://localhost:8080/api
所有API接口都使用统一的响应格式:
{
"success": true,
"message": "操作成功",
"data": {}
}success: 布尔值,表示请求是否成功message: 字符串,响应消息data: 对象,具体的响应数据
接口说明: 分页获取所有视频列表,支持按更新时间倒序排列
请求方式: GET
请求URL: /api/videos
请求参数:
page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20,最大100
请求示例:
GET /api/videos?page=0&size=20返回示例:
{
"success": true,
"message": "查询成功",
"data": {
"content": [
{
"id": 1,
"videoId": 12345,
"typeId": 1,
"typeName": "电影",
"name": "复仇者联盟",
"subName": "Avengers",
"nameEn": "The Avengers",
"state": 1,
"pic": "https://example.com/poster.jpg",
"lang": "国语",
"area": "美国",
"year": "2012",
"note": "HD高清",
"actor": "小罗伯特·唐尼,克里斯·埃文斯",
"director": "乔斯·韦登",
"des": "超级英雄集结拯救世界的故事",
"tag": "动作,科幻,冒险",
"className": "动作片",
"vodPlayUrl": "1$https://m3u8.hmrvideo.com/play/55f6b3a441a04f70a13d071ab0e2f17f.m3u8#2$https://m3u8.hmrvideo.com/play/2707ed8fb13f410187ce2a01fc2fe4f0.m3u8",
"playUrls": [
{
"episode": "1",
"url": "https://m3u8.hmrvideo.com/play/55f6b3a441a04f70a13d071ab0e2f17f.m3u8"
},
{
"episode": "2",
"url": "https://m3u8.hmrvideo.com/play/2707ed8fb13f410187ce2a01fc2fe4f0.m3u8"
}
],
"vodDownUrl": "https://example.com/download.mp4",
"vodPlayFrom": "优酷",
"vodServer": "server1",
"vodPlayNote": "高清播放",
"duration": "143分钟",
"updateTime": 1640995200,
"addTime": 1640995200,
"createdAt": "2024-01-01T10:00:00",
"updatedAt": "2024-01-01T10:00:00",
"deleted": false
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"sort": {
"sorted": true,
"empty": false
}
},
"totalElements": 1000,
"totalPages": 50,
"last": false,
"first": true,
"numberOfElements": 20,
"size": 20,
"number": 0,
"sort": {
"sorted": true,
"empty": false
},
"empty": false
}
}接口说明: 根据视频ID获取单个视频的详细信息
请求方式: GET
请求URL: /api/videos/{videoId}
路径参数:
videoId: 视频ID(必填)
请求示例:
GET /api/videos/12345返回示例:
{
"success": true,
"message": "查询成功",
"data": {
"id": 1,
"videoId": 12345,
"typeId": 1,
"typeName": "电影",
"name": "复仇者联盟",
"subName": "Avengers",
"nameEn": "The Avengers",
"state": 1,
"pic": "https://example.com/poster.jpg",
"lang": "国语",
"area": "美国",
"year": "2012",
"note": "HD高清",
"actor": "小罗伯特·唐尼,克里斯·埃文斯",
"director": "乔斯·韦登",
"des": "超级英雄集结拯救世界的故事",
"tag": "动作,科幻,冒险",
"className": "动作片",
"vodPlayUrl": "1$https://m3u8.hmrvideo.com/play/55f6b3a441a04f70a13d071ab0e2f17f.m3u8#2$https://m3u8.hmrvideo.com/play/2707ed8fb13f410187ce2a01fc2fe4f0.m3u8",
"playUrls": [
{
"episode": "1",
"url": "https://m3u8.hmrvideo.com/play/55f6b3a441a04f70a13d071ab0e2f17f.m3u8"
},
{
"episode": "2",
"url": "https://m3u8.hmrvideo.com/play/2707ed8fb13f410187ce2a01fc2fe4f0.m3u8"
}
],
"vodDownUrl": "https://example.com/download.mp4",
"vodPlayFrom": "优酷",
"vodServer": "server1",
"vodPlayNote": "高清播放",
"duration": "143分钟",
"updateTime": 1640995200,
"addTime": 1640995200,
"createdAt": "2024-01-01T10:00:00",
"updatedAt": "2024-01-01T10:00:00",
"deleted": false
}
}接口说明: 根据视频类型ID分页查询视频列表
请求方式: GET
请求URL: /api/videos/type/{typeId}
路径参数:
typeId: 类型ID(必填)
请求参数:
page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/type/1?page=0&size=20返回示例: 同分页查询格式
接口说明: 支持按名称、演员、导演、标签等多字段进行关键词搜索
请求方式: GET
请求URL: /api/videos/search
请求参数:
keyword: 搜索关键词(必填)page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/search?keyword=复仇者&page=0&size=20返回示例: 同分页查询格式
接口说明: 根据视频名称进行模糊搜索
请求方式: GET
请求URL: /api/videos/search/name
请求参数:
name: 视频名称(必填)page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/search/name?name=复仇者联盟&page=0&size=20返回示例: 同分页查询格式
接口说明: 根据演员名称进行模糊搜索
请求方式: GET
请求URL: /api/videos/search/actor
请求参数:
actor: 演员名称(必填)page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/search/actor?actor=小罗伯特·唐尼&page=0&size=20返回示例: 同分页查询格式
接口说明: 根据导演名称进行模糊搜索
请求方式: GET
请求URL: /api/videos/search/director
请求参数:
director: 导演名称(必填)page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/search/director?director=乔斯·韦登&page=0&size=20返回示例: 同分页查询格式
接口说明: 根据年份查询视频列表
请求方式: GET
请求URL: /api/videos/year/{year}
路径参数:
year: 年份(必填)
请求参数:
page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/year/2012?page=0&size=20返回示例: 同分页查询格式
接口说明: 根据地区查询视频列表
请求方式: GET
请求URL: /api/videos/area/{area}
路径参数:
area: 地区名称(必填)
请求参数:
page(可选): 页码,从0开始,默认为0size(可选): 每页大小,默认为20
请求示例:
GET /api/videos/area/美国?page=0&size=20返回示例: 同分页查询格式
接口说明: 获取最近更新的视频列表,按更新时间倒序排列
请求方式: GET
请求URL: /api/videos/recent
请求参数:
limit(可选): 数量限制,默认为10,最大50
请求示例:
GET /api/videos/recent?limit=10返回示例:
{
"success": true,
"message": "查询成功",
"data": [
{
"id": 1,
"videoId": 12345,
"typeId": 1,
"typeName": "电影",
"name": "复仇者联盟",
"subName": "Avengers",
"nameEn": "The Avengers",
"state": 1,
"pic": "https://example.com/poster.jpg",
"lang": "国语",
"area": "美国",
"year": "2012",
"note": "HD高清",
"actor": "小罗伯特·唐尼,克里斯·埃文斯",
"director": "乔斯·韦登",
"des": "超级英雄集结拯救世界的故事",
"tag": "动作,科幻,冒险",
"className": "动作片",
"vodPlayUrl": "1$https://m3u8.hmrvideo.com/play/55f6b3a441a04f70a13d071ab0e2f17f.m3u8#2$https://m3u8.hmrvideo.com/play/2707ed8fb13f410187ce2a01fc2fe4f0.m3u8",
"playUrls": [
{
"episode": "1",
"url": "https://m3u8.hmrvideo.com/play/55f6b3a441a04f70a13d071ab0e2f17f.m3u8"
},
{
"episode": "2",
"url": "https://m3u8.hmrvideo.com/play/2707ed8fb13f410187ce2a01fc2fe4f0.m3u8"
}
],
"vodDownUrl": "https://example.com/download.mp4",
"vodPlayFrom": "优酷",
"vodServer": "server1",
"vodPlayNote": "高清播放",
"duration": "143分钟",
"updateTime": 1640995200,
"addTime": 1640995200,
"createdAt": "2024-01-01T10:00:00",
"updatedAt": "2024-01-01T10:00:00",
"deleted": false
}
]
}接口说明: 获取系统中视频的统计信息
请求方式: GET
请求URL: /api/videos/statistics
请求示例:
GET /api/videos/statistics返回示例:
{
"success": true,
"message": "查询成功",
"data": {
"totalVideos": 10000,
"todayAdded": 50,
"weekAdded": 300,
"monthAdded": 1200
}
}接口说明: 获取指定类型的视频总数量
请求方式: GET
请求URL: /api/videos/count/type/{typeId}
路径参数:
typeId: 类型ID(必填)
请求示例:
GET /api/videos/count/type/1返回示例:
{
"success": true,
"message": "查询成功",
"data": 1500
}接口说明: 检查指定ID的视频是否存在
请求方式: GET
请求URL: /api/videos/exists/{videoId}
路径参数:
videoId: 视频ID(必填)
请求示例:
GET /api/videos/exists/12345返回示例:
{
"success": true,
"message": "查询成功",
"data": true
}接口说明: 手动触发全量数据爬取,会爬取所有可用的视频数据
请求方式: POST
请求URL: /api/crawler/full
请求示例:
POST /api/crawler/full
Content-Type: application/json返回示例:
{
"success": true,
"message": "全量爬取任务已启动",
"data": {
"taskId": "full_crawl_20240101_100000",
"startTime": "2024-01-01T10:00:00",
"status": "RUNNING"
}
}接口说明: 手动触发增量数据爬取,只爬取最近更新的视频数据
请求方式: POST
请求URL: /api/crawler/incremental
请求示例:
POST /api/crawler/incremental
Content-Type: application/json返回示例:
{
"success": true,
"message": "增量爬取任务已启动",
"data": {
"taskId": "incremental_crawl_20240101_100000",
"startTime": "2024-01-01T10:00:00",
"status": "RUNNING"
}
}接口说明: 获取当前爬虫任务的运行状态和统计信息
请求方式: GET
请求URL: /api/crawler/status
请求示例:
GET /api/crawler/status返回示例:
{
"success": true,
"message": "查询成功",
"data": {
"isRunning": true,
"currentTask": {
"taskId": "incremental_crawl_20240101_100000",
"type": "INCREMENTAL",
"startTime": "2024-01-01T10:00:00",
"status": "RUNNING",
"progress": {
"currentPage": 5,
"totalPages": 10,
"processedCount": 500,
"newCount": 50,
"updatedCount": 30,
"errorCount": 2
}
},
"lastFullCrawl": "2024-01-01T02:00:00",
"lastIncrementalCrawl": "2024-01-01T09:00:00",
"totalVideos": 10000
}
}接口说明: 获取当前爬虫的配置信息
请求方式: GET
请求URL: /api/crawler/config
请求示例:
GET /api/crawler/config返回示例:
{
"success": true,
"message": "查询成功",
"data": {
"maccms": {
"baseUrl": "https://json02.heimuer.xyz/api.php/provide/vod/",
"pageSize": 100,
"requestTimeout": 30000,
"retryTimes": 3,
"retryDelay": 5000
},
"schedule": {
"fullCrawlCron": "0 0 2 * * ?",
"incrementalCrawlCron": "0 0 * * * ?",
"enabled": true
},
"performance": {
"maxConcurrentRequests": 5,
"requestDelay": 1000,
"batchSize": 50
}
}
}当API请求出现错误时,会返回以下格式的响应:
{
"success": false,
"message": "错误描述信息",
"data": null,
"error": {
"code": "ERROR_CODE",
"details": "详细错误信息"
}
}INVALID_PARAMETER: 请求参数无效RESOURCE_NOT_FOUND: 资源不存在CRAWLER_ALREADY_RUNNING: 爬虫任务已在运行中CRAWLER_CONFIG_ERROR: 爬虫配置错误DATABASE_ERROR: 数据库操作错误NETWORK_ERROR: 网络请求错误INTERNAL_SERVER_ERROR: 服务器内部错误
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | Long | 主键ID |
| videoId | Long | 视频ID(来自MacCMS) |
| typeId | Integer | 视频类型ID |
| typeName | String | 视频类型名称 |
| name | String | 视频名称 |
| subName | String | 视频子名称 |
| nameEn | String | 视频英文名 |
| state | Integer | 视频状态(1:正常,0:禁用) |
| pic | String | 视频海报图片URL |
| lang | String | 视频语言 |
| area | String | 视频地区 |
| year | String | 视频年份 |
| note | String | 视频备注信息 |
| actor | String | 视频演员列表(逗号分隔) |
| director | String | 视频导演 |
| des | String | 视频简介描述 |
| tag | String | 视频标签(逗号分隔) |
| className | String | 视频分类名称 |
| vodPlayUrl | String | 视频播放地址(原始格式:1$url1#2$url2) |
| playUrls | Array | 解析后的播放URL列表,包含episode和url字段 |
| vodDownUrl | String | 视频下载地址 |
| vodPlayFrom | String | 视频播放来源 |
| vodServer | String | 视频服务器 |
| vodPlayNote | String | 视频播放备注 |
| duration | String | 视频时长 |
| updateTime | Long | 视频更新时间(时间戳) |
| addTime | Long | 视频添加时间(时间戳) |
| createdAt | String | 数据创建时间(ISO格式) |
| updatedAt | String | 数据更新时间(ISO格式) |
| deleted | Boolean | 是否已删除 |
| 字段名 | 类型 | 说明 |
|---|---|---|
| episode | String | 集数或标识(如:1、2、3等) |
| url | String | 具体的播放URL地址 |
说明: playUrls 字段是对 vodPlayUrl 字段的解析结果。原始的 vodPlayUrl 格式为:1$url1#2$url2#3$url3,系统会自动解析为 playUrls 数组,方便前端直接使用。
- 分页查询: 建议使用合适的页面大小(10-50),避免一次性获取过多数据
- 缓存策略: 系统已实现Redis缓存,相同查询条件的请求会优先从缓存获取数据
- 搜索优化: 使用具体的搜索条件可以获得更精确的结果
- 错误处理: 前端应该根据
success字段判断请求是否成功,并适当处理错误情况 - 爬虫管理: 避免频繁触发爬虫任务,建议检查爬虫状态后再决定是否启动新任务
crawler:
maccms:
base-url: https://json02.heimuer.xyz/api.php/provide/vod/
page-size: 100 # 每页爬取数量
request-timeout: 30000 # 请求超时时间(毫秒)
retry-times: 3 # 重试次数
retry-delay: 5000 # 重试延迟(毫秒)
schedule:
# 全量爬取:每天凌晨2点执行
full-crawl-cron: "0 0 2 * * ?"
# 增量爬取:每小时执行一次
incremental-crawl-cron: "0 0 * * * ?"spring:
jpa:
hibernate:
ddl-auto: update # 自动创建/更新表结构
show-sql: true # 显示SQL语句spring:
data:
redis:
timeout: 5000ms
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接系统默认配置了两个定时任务:
- 全量爬取: 每天凌晨2点执行,爬取所有视频数据
- 增量爬取: 每小时执行一次,只爬取最近更新的数据
可以通过修改配置文件中的cron表达式来调整执行频率。
访问 http://localhost:8080/api/actuator/health 查看应用健康状态
日志文件位置:logs/cms-server.log
可以通过修改 application.yml 中的日志配置来调整日志级别和输出格式。
- 在
VideoRepository中添加查询方法 - 在
VideoService中添加业务逻辑 - 在
VideoController中添加REST接口
- 修改
MacCmsResponseDTO以支持新的数据字段 - 更新
VideoEntity实体类 - 在
CrawlerService中添加新的处理逻辑
A: 修改 application.yml 中的 crawler.schedule 配置项。
A: 可以重启Redis服务,或者调用 VideoService.clearCache() 方法。
A: 修改实体类后,Spring Boot会自动更新表结构(ddl-auto: update)。
A: 查看 logs/cms-server.log 文件,或者控制台输出。
本项目采用 MIT 许可证。