chore: remove java-backend test project

This commit is contained in:
xiao12feng@outlook.com 2025-12-21 16:18:22 +08:00
parent a1c6996fdc
commit cc690e5c60
30 changed files with 0 additions and 792 deletions

View File

@ -1,126 +0,0 @@
# 直播系统 Java 后端
基于 Spring Boot 3.2 + JPA + H2/MySQL 的直播系统后端服务。
## 技术栈
- Java 17
- Spring Boot 3.2
- Spring Data JPA
- H2 Database (开发) / MySQL (生产)
- Lombok
## 快速开始
### 1. 使用 IDEA 打开项目
1. 打开 IntelliJ IDEA
2. 选择 `File` -> `Open`
3. 选择 `java-backend` 文件夹
4. 等待 Maven 依赖下载完成
### 2. 运行项目
方式一:直接运行
- 找到 `LivestreamingApplication.java`
- 右键点击 -> `Run 'LivestreamingApplication'`
方式二Maven 命令
```bash
cd java-backend
mvn spring-boot:run
```
### 3. 访问服务
- API 地址: http://localhost:3001/api
- H2 控制台: http://localhost:3001/h2-console
- JDBC URL: `jdbc:h2:file:./data/livestream`
- 用户名: `sa`
- 密码: (空)
## API 接口
### 直播间接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/rooms | 获取所有直播间 |
| GET | /api/rooms/{id} | 获取单个直播间 |
| POST | /api/rooms | 创建直播间 |
| DELETE | /api/rooms/{id} | 删除直播间 |
### 创建直播间请求示例
```json
POST /api/rooms
{
"title": "我的直播间",
"streamerName": "主播名称"
}
```
### 响应示例
```json
{
"success": true,
"data": {
"id": "uuid",
"title": "我的直播间",
"streamerName": "主播名称",
"streamKey": "uuid",
"isLive": false,
"viewerCount": 0,
"streamUrls": {
"rtmp": "rtmp://localhost:1935/live/uuid",
"flv": "http://localhost:8080/live/uuid.flv",
"hls": "http://localhost:8080/live/uuid/index.m3u8"
}
}
}
```
## 切换到 MySQL
1. 安装 MySQL 并创建数据库:
```sql
CREATE DATABASE livestream CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
2. 修改 `application.yml`
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/livestream?useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: your_password
```
## 项目结构
```
java-backend/
├── src/main/java/com/example/livestreaming/
│ ├── LivestreamingApplication.java # 启动类
│ ├── config/ # 配置类
│ ├── controller/ # 控制器
│ ├── dto/ # 数据传输对象
│ ├── entity/ # 实体类
│ ├── exception/ # 异常处理
│ ├── repository/ # 数据访问层
│ └── service/ # 业务逻辑层
├── src/main/resources/
│ └── application.yml # 配置文件
└── pom.xml # Maven 配置
```
## Android App 配置
确保 Android App 的 API 地址指向此服务:
```kotlin
// build.gradle.kts
buildConfigField("String", "API_BASE_URL_DEVICE", "\"http://你的电脑IP:3001/api/\"")
```

Binary file not shown.

View File

@ -1,88 +0,0 @@
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>livestreaming-backend</artifactId>
<version>1.0.0</version>
<name>livestreaming-backend</name>
<description>直播系统后端服务</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- H2 Database (开发测试用,可选) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,17 +0,0 @@
package com.example.livestreaming;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LivestreamingApplication {
public static void main(String[] args) {
SpringApplication.run(LivestreamingApplication.class, args);
System.out.println("========================================");
System.out.println(" 直播系统后端服务启动成功!");
System.out.println(" API地址: http://localhost:3001/api");
System.out.println(" H2控制台: http://localhost:3001/h2-console");
System.out.println("========================================");
}
}

View File

@ -1,18 +0,0 @@
package com.example.livestreaming.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}

View File

@ -1,20 +0,0 @@
package com.example.livestreaming.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Map;
@RestController
public class HealthController {
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
return ResponseEntity.ok(Map.of(
"status", "ok",
"timestamp", LocalDateTime.now().toString()
));
}
}

View File

@ -1,64 +0,0 @@
package com.example.livestreaming.controller;
import com.example.livestreaming.dto.ApiResponse;
import com.example.livestreaming.dto.CreateRoomRequest;
import com.example.livestreaming.dto.RoomResponse;
import com.example.livestreaming.service.RoomService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/rooms")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class RoomController {
private final RoomService roomService;
/**
* 获取所有直播间
* GET /api/rooms
*/
@GetMapping
public ResponseEntity<ApiResponse<List<RoomResponse>>> getAllRooms() {
List<RoomResponse> rooms = roomService.getAllRooms();
return ResponseEntity.ok(ApiResponse.success(rooms));
}
/**
* 获取单个直播间
* GET /api/rooms/{id}
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<RoomResponse>> getRoomById(@PathVariable String id) {
return roomService.getRoomById(id)
.map(room -> ResponseEntity.ok(ApiResponse.success(room)))
.orElse(ResponseEntity.notFound().build());
}
/**
* 创建直播间
* POST /api/rooms
*/
@PostMapping
public ResponseEntity<ApiResponse<RoomResponse>> createRoom(@Valid @RequestBody CreateRoomRequest request) {
RoomResponse room = roomService.createRoom(request);
return ResponseEntity.ok(ApiResponse.success(room, "直播间创建成功"));
}
/**
* 删除直播间
* DELETE /api/rooms/{id}
*/
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> deleteRoom(@PathVariable String id) {
if (roomService.deleteRoom(id)) {
return ResponseEntity.ok(ApiResponse.success(null, "删除成功"));
}
return ResponseEntity.notFound().build();
}
}

View File

@ -1,84 +0,0 @@
package com.example.livestreaming.controller;
import com.example.livestreaming.service.RoomService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* SRS/NodeMediaServer 回调接口
* 用于接收推流开始/结束的通知
*/
@Slf4j
@RestController
@RequestMapping("/api/srs")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class SrsCallbackController {
private final RoomService roomService;
/**
* 推流开始回调
* POST /api/srs/on_publish
*/
@PostMapping("/on_publish")
public ResponseEntity<Map<String, Object>> onPublish(@RequestBody Map<String, Object> body) {
String stream = (String) body.get("stream");
log.info("推流开始: stream={}", stream);
if (stream != null) {
roomService.setLiveStatus(stream, true);
}
return ResponseEntity.ok(successResponse());
}
/**
* 推流结束回调
* POST /api/srs/on_unpublish
*/
@PostMapping("/on_unpublish")
public ResponseEntity<Map<String, Object>> onUnpublish(@RequestBody Map<String, Object> body) {
String stream = (String) body.get("stream");
log.info("推流结束: stream={}", stream);
if (stream != null) {
roomService.setLiveStatus(stream, false);
}
return ResponseEntity.ok(successResponse());
}
/**
* 播放开始回调
* POST /api/srs/on_play
*/
@PostMapping("/on_play")
public ResponseEntity<Map<String, Object>> onPlay(@RequestBody Map<String, Object> body) {
String stream = (String) body.get("stream");
log.info("观众进入: stream={}", stream);
return ResponseEntity.ok(successResponse());
}
/**
* 播放结束回调
* POST /api/srs/on_stop
*/
@PostMapping("/on_stop")
public ResponseEntity<Map<String, Object>> onStop(@RequestBody Map<String, Object> body) {
String stream = (String) body.get("stream");
log.info("观众离开: stream={}", stream);
return ResponseEntity.ok(successResponse());
}
private Map<String, Object> successResponse() {
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
return result;
}
}

View File

@ -1,27 +0,0 @@
package com.example.livestreaming.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private T data;
private String message;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null);
}
public static <T> ApiResponse<T> success(T data, String message) {
return new ApiResponse<>(true, data, message);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, null, message);
}
}

View File

@ -1,14 +0,0 @@
package com.example.livestreaming.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class CreateRoomRequest {
@NotBlank(message = "标题不能为空")
private String title;
@NotBlank(message = "主播名称不能为空")
private String streamerName;
}

View File

@ -1,47 +0,0 @@
package com.example.livestreaming.dto;
import com.example.livestreaming.entity.Room;
import lombok.Data;
@Data
public class RoomResponse {
private String id;
private String title;
private String streamerName;
private String streamKey;
private boolean isLive;
private int viewerCount;
private String createdAt;
private String startedAt;
private StreamUrls streamUrls;
@Data
public static class StreamUrls {
private String rtmp;
private String flv;
private String hls;
}
public static RoomResponse fromEntity(Room room, String rtmpHost, int rtmpPort, String httpHost, int httpPort) {
RoomResponse response = new RoomResponse();
response.setId(room.getId());
response.setTitle(room.getTitle());
response.setStreamerName(room.getStreamerName());
response.setStreamKey(room.getStreamKey());
response.setLive(room.isLive());
response.setViewerCount(room.getViewerCount());
response.setCreatedAt(room.getCreatedAt() != null ? room.getCreatedAt().toString() : null);
response.setStartedAt(room.getStartedAt() != null ? room.getStartedAt().toString() : null);
// 构建流地址
StreamUrls urls = new StreamUrls();
String streamKey = room.getStreamKey();
urls.setRtmp(String.format("rtmp://%s:%d/live/%s", rtmpHost, rtmpPort, streamKey));
urls.setFlv(String.format("http://%s:%d/live/%s.flv", httpHost, httpPort, streamKey));
urls.setHls(String.format("http://%s:%d/live/%s/index.m3u8", httpHost, httpPort, streamKey));
response.setStreamUrls(urls);
return response;
}
}

View File

@ -1,53 +0,0 @@
package com.example.livestreaming.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "rooms")
public class Room {
@Id
private String id;
@Column(nullable = false)
private String title;
@Column(name = "streamer_name", nullable = false)
private String streamerName;
@Column(name = "stream_key", unique = true, nullable = false)
private String streamKey;
@Column(name = "is_live")
private boolean isLive = false;
@Column(name = "viewer_count")
private int viewerCount = 0;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "started_at")
private LocalDateTime startedAt;
@PrePersist
public void prePersist() {
if (id == null) {
id = UUID.randomUUID().toString();
}
if (streamKey == null) {
streamKey = id;
}
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
}
}

View File

@ -1,32 +0,0 @@
package com.example.livestreaming.exception;
import com.example.livestreaming.dto.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(ApiResponse.error(message));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
log.error("服务器错误", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("服务器内部错误"));
}
}

View File

@ -1,13 +0,0 @@
package com.example.livestreaming.repository;
import com.example.livestreaming.entity.Room;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoomRepository extends JpaRepository<Room, String> {
Optional<Room> findByStreamKey(String streamKey);
}

View File

@ -1,89 +0,0 @@
package com.example.livestreaming.service;
import com.example.livestreaming.dto.CreateRoomRequest;
import com.example.livestreaming.dto.RoomResponse;
import com.example.livestreaming.entity.Room;
import com.example.livestreaming.repository.RoomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class RoomService {
private final RoomRepository roomRepository;
@Value("${livestream.rtmp.host:localhost}")
private String rtmpHost;
@Value("${livestream.rtmp.port:1935}")
private int rtmpPort;
@Value("${livestream.http.host:localhost}")
private String httpHost;
@Value("${livestream.http.port:8080}")
private int httpPort;
/**
* 获取所有直播间
*/
public List<RoomResponse> getAllRooms() {
return roomRepository.findAll().stream()
.map(room -> RoomResponse.fromEntity(room, rtmpHost, rtmpPort, httpHost, httpPort))
.collect(Collectors.toList());
}
/**
* 根据ID获取直播间
*/
public Optional<RoomResponse> getRoomById(String id) {
return roomRepository.findById(id)
.map(room -> RoomResponse.fromEntity(room, rtmpHost, rtmpPort, httpHost, httpPort));
}
/**
* 创建直播间
*/
@Transactional
public RoomResponse createRoom(CreateRoomRequest request) {
Room room = new Room();
room.setTitle(request.getTitle());
room.setStreamerName(request.getStreamerName());
Room saved = roomRepository.save(room);
return RoomResponse.fromEntity(saved, rtmpHost, rtmpPort, httpHost, httpPort);
}
/**
* 更新直播状态供SRS回调使用
*/
@Transactional
public Optional<Room> setLiveStatus(String streamKey, boolean isLive) {
return roomRepository.findByStreamKey(streamKey)
.map(room -> {
room.setLive(isLive);
room.setStartedAt(isLive ? LocalDateTime.now() : null);
return roomRepository.save(room);
});
}
/**
* 删除直播间
*/
@Transactional
public boolean deleteRoom(String id) {
if (roomRepository.existsById(id)) {
roomRepository.deleteById(id);
return true;
}
return false;
}
}

View File

@ -1,50 +0,0 @@
server:
port: 3001
spring:
application:
name: livestreaming-backend
# 数据库配置 - 默认使用H2内存数据库开发测试
# 如需使用MySQL请取消下方注释并配置
datasource:
# H2 内存数据库(开发测试用,无需安装)
url: jdbc:h2:file:./data/livestream;AUTO_SERVER=TRUE
driver-class-name: org.h2.Driver
username: sa
password:
# MySQL 配置(生产环境使用)
# url: jdbc:mysql://localhost:3306/livestream?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: your_password
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
# H2 控制台(开发时可访问 http://localhost:3001/h2-console
h2:
console:
enabled: true
path: /h2-console
# 直播服务配置
livestream:
rtmp:
host: localhost
port: 1935
http:
host: localhost
port: 8080
# 日志配置
logging:
level:
com.example.livestreaming: DEBUG
org.hibernate.SQL: DEBUG

View File

@ -1,50 +0,0 @@
server:
port: 3001
spring:
application:
name: livestreaming-backend
# 数据库配置 - 默认使用H2内存数据库开发测试
# 如需使用MySQL请取消下方注释并配置
datasource:
# H2 内存数据库(开发测试用,无需安装)
url: jdbc:h2:file:./data/livestream;AUTO_SERVER=TRUE
driver-class-name: org.h2.Driver
username: sa
password:
# MySQL 配置(生产环境使用)
# url: jdbc:mysql://localhost:3306/livestream?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: your_password
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
# H2 控制台(开发时可访问 http://localhost:3001/h2-console
h2:
console:
enabled: true
path: /h2-console
# 直播服务配置
livestream:
rtmp:
host: localhost
port: 1935
http:
host: localhost
port: 8080
# 日志配置
logging:
level:
com.example.livestreaming: DEBUG
org.hibernate.SQL: DEBUG