From 171054efbbdffb98a04324086b804d1b0e37a057 Mon Sep 17 00:00:00 2001
From: ShiQi <15883326+shirenan@user.noreply.gitee.com>
Date: Tue, 23 Dec 2025 18:09:56 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80=E4=BA=9Bbu?=
=?UTF-8?q?g=E5=92=8C=E6=B7=BB=E5=8A=A0=E4=BA=86TODO?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
android-app/app/src/main/AndroidManifest.xml | 10 +
.../com/example/livestreaming/AuthHelper.java | 91 +++++
.../livestreaming/CategoryFilterManager.java | 14 +
.../livestreaming/ConversationActivity.java | 5 +
.../livestreaming/EditProfileActivity.java | 34 ++
.../example/livestreaming/FriendsAdapter.java | 10 +
.../LocalNotificationManager.java | 12 +
.../example/livestreaming/LoginActivity.java | 236 ++++++++++++
.../example/livestreaming/MainActivity.java | 161 ++++++--
.../livestreaming/MyFriendsActivity.java | 9 +
.../livestreaming/NearbyUsersAdapter.java | 49 ++-
.../NotificationSettingsActivity.java | 22 ++
.../livestreaming/NotificationsActivity.java | 7 +
.../livestreaming/NotificationsAdapter.java | 5 +
.../example/livestreaming/PlayerActivity.java | 18 +
.../livestreaming/ProfileActivity.java | 98 ++++-
.../livestreaming/RegisterActivity.java | 350 ++++++++++++++++++
.../livestreaming/RoomDetailActivity.java | 18 +
.../example/livestreaming/RoomsAdapter.java | 34 +-
.../example/livestreaming/SearchActivity.java | 17 +
.../SearchSuggestionsAdapter.java | 6 +
.../livestreaming/SettingsPageActivity.java | 88 ++++-
.../com/example/livestreaming/ShareUtils.java | 19 +
.../livestreaming/TabPlaceholderActivity.java | 104 +++++-
.../livestreaming/UnreadMessageManager.java | 5 +
.../UserProfileReadOnlyActivity.java | 21 ++
.../livestreaming/UserWorksAdapter.java | 9 +
.../livestreaming/WaterfallRoomsAdapter.java | 14 +
.../livestreaming/WishTreeActivity.java | 7 +
.../example/livestreaming/net/ApiService.java | 3 +
.../livestreaming/net/RegisterRequest.java | 42 +++
.../com/example/livestreaming/net/Room.java | 14 +-
.../res/drawable/bg_add_button_nearby.xml | 6 +
.../drawable/bg_add_button_nearby_pressed.xml | 16 +
.../src/main/res/drawable/bg_live_badge.xml | 6 +
.../main/res/drawable/bg_nearby_user_card.xml | 6 +
.../drawable/bg_nearby_user_card_shadow.xml | 20 +
.../src/main/res/layout/activity_login.xml | 201 ++++++++++
.../src/main/res/layout/activity_register.xml | 261 +++++++++++++
.../src/main/res/layout/item_nearby_user.xml | 159 +++++---
android-app/项目功能完善度分析.md | 338 ++++++++++-------
android-app/项目进度汇报.md | 349 +++++++++++++++++
.../livestreaming/dto/CreateRoomRequest.java | 17 +
.../livestreaming/dto/RoomResponse.java | 49 +++
.../livestreaming/dto/CreateRoomRequest.class | Bin 0 -> 2403 bytes
.../livestreaming/dto/RoomResponse.class | Bin 0 -> 5364 bytes
46 files changed, 2709 insertions(+), 251 deletions(-)
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/AuthHelper.java
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/RegisterActivity.java
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/RegisterRequest.java
create mode 100644 android-app/app/src/main/res/drawable/bg_add_button_nearby.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_add_button_nearby_pressed.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_live_badge.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_nearby_user_card.xml
create mode 100644 android-app/app/src/main/res/drawable/bg_nearby_user_card_shadow.xml
create mode 100644 android-app/app/src/main/res/layout/activity_login.xml
create mode 100644 android-app/app/src/main/res/layout/activity_register.xml
create mode 100644 android-app/项目进度汇报.md
create mode 100644 java-backend/src/main/java/com/example/livestreaming/dto/CreateRoomRequest.java
create mode 100644 java-backend/src/main/java/com/example/livestreaming/dto/RoomResponse.java
create mode 100644 java-backend/target/classes/com/example/livestreaming/dto/CreateRoomRequest.class
create mode 100644 java-backend/target/classes/com/example/livestreaming/dto/RoomResponse.class
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index c69d4ad9..2e5a8202 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -138,6 +138,16 @@
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="portrait" />
+
+
+
+
{
+ // 跳转到登录页面
+ LoginActivity.start(context);
+ })
+ .show();
+
+ return false;
+ }
+
+ /**
+ * 检查登录状态,如果未登录则提示用户并跳转到登录页面(使用默认提示消息)
+ * @param context 上下文(必须是Activity)
+ * @return true表示已登录,false表示未登录(已跳转到登录页面)
+ */
+ public static boolean requireLogin(Context context) {
+ return requireLogin(context, null);
+ }
+
+ /**
+ * 检查登录状态,如果未登录则显示Toast提示并跳转到登录页面
+ * @param context 上下文(必须是Activity)
+ * @param toastMessage Toast提示消息(可选,如果为null则使用默认消息)
+ * @return true表示已登录,false表示未登录(已跳转到登录页面)
+ */
+ public static boolean requireLoginWithToast(Context context, String toastMessage) {
+ if (isLoggedIn(context)) {
+ return true;
+ }
+
+ // 未登录,显示Toast提示并跳转到登录页面
+ String message = toastMessage != null ? toastMessage : "此功能需要登录后使用";
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ LoginActivity.start(context);
+ return false;
+ }
+
+ /**
+ * 检查登录状态,如果未登录则显示Toast提示并跳转到登录页面(使用默认提示消息)
+ * @param context 上下文(必须是Activity)
+ * @return true表示已登录,false表示未登录(已跳转到登录页面)
+ */
+ public static boolean requireLoginWithToast(Context context) {
+ return requireLoginWithToast(context, null);
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java b/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java
index 12481395..8ebb4af4 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java
@@ -27,6 +27,20 @@ public class CategoryFilterManager {
/**
* 异步筛选房间列表
+ * TODO: 接入后端接口 - 获取房间分类列表
+ * 接口路径: GET /api/rooms/categories
+ * 请求参数: 无
+ * 返回数据格式: ApiResponse>
+ * Category对象应包含: id, name, iconUrl, roomCount等字段
+ * 用于显示分类标签页,分类数据应从后端获取,而不是硬编码
+ * TODO: 接入后端接口 - 按分类获取房间列表
+ * 接口路径: GET /api/rooms?category={categoryId}
+ * 请求参数:
+ * - categoryId: 分类ID(路径参数或查询参数)
+ * - page (可选): 页码
+ * - pageSize (可选): 每页数量
+ * 返回数据格式: ApiResponse>
+ * 筛选逻辑应迁移到后端,前端只负责展示结果
*/
public void filterRoomsAsync(List allRooms, String category, FilterCallback callback) {
if (executorService == null || executorService.isShutdown()) {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
index b01d4106..b4cc188f 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
@@ -188,6 +188,11 @@ public class ConversationActivity extends AppCompatActivity {
}
private void sendMessage() {
+ // 检查登录状态,发送私信需要登录
+ if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
+ return;
+ }
+
// TODO: 接入后端接口 - 发送私信消息
// 接口路径: POST /api/conversations/{conversationId}/messages
// 请求参数:
diff --git a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
index ef0a6e97..b7ba206e 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
@@ -70,6 +70,13 @@ public class EditProfileActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ // 检查登录状态,编辑资料需要登录
+ if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
+ finish();
+ return;
+ }
+
binding = ActivityEditProfileBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
@@ -136,6 +143,24 @@ public class EditProfileActivity extends AppCompatActivity {
});
binding.saveButton.setOnClickListener(v -> {
+ // TODO: 接入后端接口 - 上传头像
+ // 接口路径: POST /api/users/{userId}/avatar
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - avatar: 头像文件(multipart/form-data)
+ // 返回数据格式: ApiResponse<{avatarUrl: string}>
+ // 上传成功后,保存avatarUrl到本地,并更新界面显示
+ // TODO: 接入后端接口 - 更新用户资料
+ // 接口路径: PUT /api/users/{userId}/profile
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - name: 昵称
+ // - bio: 个人签名
+ // - birthday: 生日(格式:yyyy-MM-dd)
+ // - gender: 性别(男/女/保密)
+ // - location: 所在地(格式:省份-城市)
+ // 返回数据格式: ApiResponse
+ // 更新成功后,同步更新本地缓存和界面显示
String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : "";
String bio = binding.inputBio.getText() != null ? binding.inputBio.getText().toString().trim() : "";
String birthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString().trim() : "";
@@ -197,6 +222,15 @@ public class EditProfileActivity extends AppCompatActivity {
}
private Uri persistAvatarToInternalStorage(Uri sourceUri) {
+ // TODO: 接入后端接口 - 上传头像到服务器
+ // 接口路径: POST /api/upload/image
+ // 请求参数:
+ // - file: 图片文件(multipart/form-data)
+ // - model: 模块类型(如"user")
+ // - pid: 分类ID(如7表示前台用户)
+ // 返回数据格式: ApiResponse<{url: string}>
+ // 上传成功后,保存返回的URL到本地,并更新界面显示
+ // 注意:这里目前只是保存到本地,实际应该先上传到服务器,然后保存服务器返回的URL
if (sourceUri == null) return null;
InputStream in = null;
OutputStream out = null;
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FriendsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/FriendsAdapter.java
index 1b4647f3..ed229c6c 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/FriendsAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/FriendsAdapter.java
@@ -48,6 +48,16 @@ public class FriendsAdapter extends ListAdapter {
}
void bind(FriendItem item) {
+ // TODO: 接入后端接口 - 从后端获取好友头像URL
+ // 接口路径: GET /api/user/profile/{friendId}
+ // 请求参数: friendId (路径参数,从FriendItem对象中获取)
+ // 返回数据格式: ApiResponse<{avatarUrl: string}>
+ // 使用Glide加载头像,如果没有则使用默认占位图
+ // TODO: 接入后端接口 - 获取好友在线状态
+ // 接口路径: GET /api/user/status/{friendId}
+ // 请求参数: friendId (路径参数)
+ // 返回数据格式: ApiResponse<{isOnline: boolean, lastActiveTime: timestamp}>
+ // 或者FriendItem对象应包含isOnline字段,直接从item.isOnline()获取
binding.name.setText(item != null && item.getName() != null ? item.getName() : "");
binding.subtitle.setText(item != null && item.getSubtitle() != null ? item.getSubtitle() : "");
diff --git a/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java b/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java
index ddfa9d18..4480f07a 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java
@@ -44,6 +44,18 @@ public class LocalNotificationManager {
/**
* 发送通知
+ * TODO: 接入后端接口 - 同步通知到后端
+ * 接口路径: POST /api/notifications/sync
+ * 请求参数:
+ * - userId: 用户ID(从token中获取)
+ * - notificationId: 通知ID(本地生成的唯一ID)
+ * - type: 通知类型
+ * - title: 通知标题
+ * - content: 通知内容
+ * - timestamp: 通知时间戳
+ * 返回数据格式: ApiResponse<{success: boolean, serverNotificationId: string}>
+ * 用于将本地通知同步到后端,确保多设备间通知状态一致
+ * 注意:本地通知仍应正常发送,同步失败不应影响本地通知显示
*/
public static void sendNotification(Context context, String title, String content, NotificationItem.Type type) {
if (context == null || title == null || content == null) return;
diff --git a/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java b/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java
new file mode 100644
index 00000000..f230d95a
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/LoginActivity.java
@@ -0,0 +1,236 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.livestreaming.databinding.ActivityLoginBinding;
+import com.example.livestreaming.net.ApiClient;
+import com.example.livestreaming.net.ApiResponse;
+import com.example.livestreaming.net.AuthStore;
+import com.example.livestreaming.net.LoginRequest;
+import com.example.livestreaming.net.LoginResponse;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class LoginActivity extends AppCompatActivity {
+
+ private ActivityLoginBinding binding;
+ private boolean isLoggingIn = false;
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, LoginActivity.class);
+ // 不使用CLEAR_TOP和NEW_TASK,保留Activity栈,允许用户返回上一页
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityLoginBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setupUI();
+ }
+
+ private void setupUI() {
+ // 返回按钮点击事件
+ binding.backButton.setOnClickListener(v -> finish());
+
+ // 登录按钮点击事件
+ binding.loginButton.setOnClickListener(v -> performLogin());
+
+ // 注册链接点击事件
+ binding.registerLinkText.setOnClickListener(v -> {
+ RegisterActivity.start(LoginActivity.this);
+ });
+
+ // 忘记密码点击事件
+ binding.forgotPasswordText.setOnClickListener(v -> {
+ Toast.makeText(this, "忘记密码功能待开发", Toast.LENGTH_SHORT).show();
+ });
+
+ // 回车键登录
+ binding.passwordInput.setOnEditorActionListener((v, actionId, event) -> {
+ if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_DONE) {
+ performLogin();
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void performLogin() {
+ // 防止重复提交
+ if (isLoggingIn) return;
+
+ String account = binding.accountInput.getText() != null ?
+ binding.accountInput.getText().toString().trim() : "";
+ String password = binding.passwordInput.getText() != null ?
+ binding.passwordInput.getText().toString().trim() : "";
+
+ // 验证输入
+ if (TextUtils.isEmpty(account)) {
+ binding.accountLayout.setError("请输入账号");
+ binding.accountInput.requestFocus();
+ return;
+ } else {
+ binding.accountLayout.setError(null);
+ }
+
+ if (TextUtils.isEmpty(password)) {
+ binding.passwordLayout.setError("请输入密码");
+ binding.passwordInput.requestFocus();
+ return;
+ } else {
+ binding.passwordLayout.setError(null);
+ }
+
+ // 显示加载状态
+ isLoggingIn = true;
+ binding.loginButton.setEnabled(false);
+ binding.loadingProgress.setVisibility(View.VISIBLE);
+
+ // TODO: 接入后端接口 - 用户登录
+ // 接口路径: POST /api/front/login(ApiService中已定义)
+ // 请求参数: LoginRequest {account: string, password: string}
+ // 返回数据格式: ApiResponse
+ // LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
+ // 登录成功后,保存token到AuthStore,并更新用户信息到本地SharedPreferences
+ ApiClient.getService(getApplicationContext()).login(new LoginRequest(account, password))
+ .enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ isLoggingIn = false;
+ binding.loginButton.setEnabled(true);
+ binding.loadingProgress.setVisibility(View.GONE);
+
+ ApiResponse body = response.body();
+ LoginResponse loginData = body != null ? body.getData() : null;
+
+ // 如果响应不成功或数据无效,检查是否是后端未接入的情况
+ if (!response.isSuccessful() || body == null || !body.isOk() || loginData == null) {
+ // 如果是404、500等错误,可能是后端未接入,使用演示模式
+ if (!response.isSuccessful() && (response.code() == 404 || response.code() == 500 || response.code() == 502 || response.code() == 503)) {
+ // 后端服务未启动或未接入,使用演示模式
+ handleDemoModeLogin(account);
+ return;
+ }
+
+ String errorMsg = "登录失败";
+ if (body != null && !TextUtils.isEmpty(body.getMessage())) {
+ errorMsg = body.getMessage();
+ } else if (!response.isSuccessful()) {
+ errorMsg = "网络错误:" + response.code();
+ }
+ Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 保存token
+ String token = loginData.getToken();
+ if (!TextUtils.isEmpty(token)) {
+ AuthStore.setToken(getApplicationContext(), token);
+ }
+
+ // 保存用户信息到本地(如果LoginResponse包含用户信息)
+ // 注意:这里假设LoginResponse可能包含userId和nickname,如果后端返回了这些字段,需要在这里保存
+ SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
+ // 如果后端返回了nickname,可以在这里保存
+ // prefs.edit().putString("profile_name", loginData.getNickname()).apply();
+
+ // 登录成功,返回上一页(如果是从其他页面跳转过来的)
+ // 如果是直接打开登录页面,则跳转到主页面
+ Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
+
+ // 检查是否有上一个Activity
+ if (isTaskRoot()) {
+ // 如果没有上一个Activity,跳转到主页面
+ Intent intent = new Intent(LoginActivity.this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } else {
+ // 有上一个Activity,直接返回
+ finish();
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ isLoggingIn = false;
+ binding.loginButton.setEnabled(true);
+ binding.loadingProgress.setVisibility(View.GONE);
+
+ // 检测是否是网络连接错误(后端未启动)
+ boolean isNetworkError = false;
+ String errorMsg = "网络错误";
+ if (t != null) {
+ String msg = t.getMessage();
+ if (msg != null) {
+ if (msg.contains("Unable to resolve host") ||
+ msg.contains("timeout") ||
+ msg.contains("Connection refused") ||
+ msg.contains("Failed to connect")) {
+ isNetworkError = true;
+ errorMsg = "无法连接到服务器,已切换到演示模式";
+ } else {
+ errorMsg = "网络错误:" + msg;
+ }
+ }
+ }
+
+ // 如果是网络连接错误(后端未接入),使用演示模式登录
+ if (isNetworkError) {
+ handleDemoModeLogin(account);
+ } else {
+ Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+
+ /**
+ * 处理演示模式登录
+ */
+ private void handleDemoModeLogin(String account) {
+ // 演示模式:允许任意账号密码登录(仅用于开发测试)
+ Toast.makeText(this, "演示模式:后端未接入,使用演示账号登录", Toast.LENGTH_LONG).show();
+
+ // 生成一个演示token(基于账号)
+ String demoToken = "demo_token_" + account.hashCode();
+ AuthStore.setToken(getApplicationContext(), demoToken);
+
+ // 保存用户信息到本地
+ SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
+ prefs.edit()
+ .putString("profile_name", account.length() > 0 ? account : "演示用户")
+ .apply();
+
+ // 登录成功,返回上一页(如果是从其他页面跳转过来的)
+ // 如果是直接打开登录页面,则跳转到主页面
+ Toast.makeText(this, "演示模式登录成功", Toast.LENGTH_SHORT).show();
+
+ // 检查是否有上一个Activity
+ if (isTaskRoot()) {
+ // 如果没有上一个Activity,跳转到主页面
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } else {
+ // 有上一个Activity,直接返回
+ finish();
+ }
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
index dd525d04..221c1c2b 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
@@ -90,6 +90,20 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ // 用户打开APP时不需要强制登录,可以直接使用APP
+ // 只有在使用需要登录的功能时(如加好友、发送弹幕等),才检查登录状态
+ // TODO: 接入后端接口 - 用户登录
+ // 接口路径: POST /api/front/login(ApiService中已定义)
+ // 请求参数: LoginRequest {account: string, password: string}
+ // 返回数据格式: ApiResponse
+ // LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
+ // 登录成功后,保存token到AuthStore,并更新用户信息
+ // TODO: 接入后端接口 - 用户注册
+ // 接口路径: POST /api/front/register(ApiService中已定义)
+ // 请求参数: RegisterRequest {phone: string, password: string, verificationCode: string, nickname: string}
+ // 返回数据格式: ApiResponse
+ // 注册成功后,自动登录并保存token
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
@@ -173,49 +187,81 @@ public class MainActivity extends AppCompatActivity {
private void handleDrawerAction(DrawerCardItem item) {
int action = item.getAction();
if (action == DrawerCardItem.ACTION_PROFILE) {
+ // 检查登录状态,个人主页需要登录
+ if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
+ return;
+ }
ProfileActivity.start(this);
finish();
return;
}
if (action == DrawerCardItem.ACTION_MESSAGES) {
+ // MessagesActivity内部已检查登录,这里直接跳转
MessagesActivity.start(this);
finish();
return;
}
if (action == DrawerCardItem.ACTION_MY_FRIENDS) {
+ // 检查登录状态,我的好友需要登录
+ if (!AuthHelper.requireLogin(this, "查看好友列表需要登录")) {
+ return;
+ }
startActivity(new Intent(this, MyFriendsActivity.class));
return;
}
if (action == DrawerCardItem.ACTION_FISH_POND) {
+ // 检查登录状态,缘池功能需要登录
+ if (!AuthHelper.requireLogin(this, "缘池功能需要登录")) {
+ return;
+ }
startActivity(new Intent(this, FishPondActivity.class));
finish();
return;
}
if (action == DrawerCardItem.ACTION_FOLLOWING) {
+ // 检查登录状态,我的关注需要登录
+ if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) {
+ return;
+ }
FollowingListActivity.start(this);
return;
}
if (action == DrawerCardItem.ACTION_FANS) {
+ // 检查登录状态,我的粉丝需要登录
+ if (!AuthHelper.requireLogin(this, "查看粉丝列表需要登录")) {
+ return;
+ }
FansListActivity.start(this);
return;
}
if (action == DrawerCardItem.ACTION_LIKES) {
+ // 检查登录状态,获赞列表需要登录
+ if (!AuthHelper.requireLogin(this, "查看获赞列表需要登录")) {
+ return;
+ }
LikesListActivity.start(this);
return;
}
if (action == DrawerCardItem.ACTION_HISTORY) {
+ // 检查登录状态,观看历史需要登录
+ if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
+ return;
+ }
WatchHistoryActivity.start(this);
return;
}
if (action == DrawerCardItem.ACTION_SEARCH) {
+ // 搜索功能不需要登录,游客也可以搜索
SearchActivity.start(this);
return;
}
if (action == DrawerCardItem.ACTION_SETTINGS) {
+ // SettingsPageActivity内部已检查登录,这里直接跳转
SettingsPageActivity.start(this, "");
return;
}
if (action == DrawerCardItem.ACTION_HELP) {
+ // 帮助页面不需要登录
SettingsPageActivity.start(this, SettingsPageActivity.PAGE_HELP);
return;
}
@@ -274,6 +320,10 @@ public class MainActivity extends AppCompatActivity {
});
binding.avatarButton.setOnClickListener(v -> {
+ // 检查登录状态,个人主页需要登录
+ if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
+ return;
+ }
ProfileActivity.start(this);
finish();
});
@@ -408,6 +458,10 @@ public class MainActivity extends AppCompatActivity {
return true;
}
if (id == R.id.nav_profile) {
+ // 检查登录状态,个人主页需要登录
+ if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
+ return false;
+ }
ProfileActivity.start(this);
finish();
return true;
@@ -654,6 +708,32 @@ public class MainActivity extends AppCompatActivity {
} else {
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
}
+ } else if (requestCode == REQUEST_LOCATION_PERMISSION) {
+ // 处理位置权限请求结果
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // 权限已授予,刷新附近页面显示数据
+ showNearbyTab();
+ } else {
+ // 权限被拒绝
+ // 检查是否是因为用户选择了"不再询问"
+ // 如果 shouldShowRequestPermissionRationale 返回 false,说明用户可能选择了"不再询问"
+ boolean shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION);
+ if (!shouldShowRationale) {
+ // 用户可能选择了"不再询问",提示去设置中开启
+ new AlertDialog.Builder(this)
+ .setTitle("需要位置权限")
+ .setMessage("您已拒绝位置权限,如需使用附近功能,请在设置中手动开启位置权限。")
+ .setPositiveButton("去设置", (dialog, which) -> {
+ Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(android.net.Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+ // 不切换页面,让用户停留在附近页面
+ // 界面已经显示了需要权限的提示,用户可以再次点击"授权"按钮
+ }
}
}
@@ -816,6 +896,12 @@ public class MainActivity extends AppCompatActivity {
dialog.setOnShowListener(d -> {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
+ // 检查登录状态,创建直播间需要登录
+ if (!AuthHelper.requireLogin(this, "创建直播间需要登录")) {
+ dialog.dismiss();
+ return;
+ }
+
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
@@ -1398,6 +1484,12 @@ public class MainActivity extends AppCompatActivity {
/**
* 初始化顶部标签页数据
+ * TODO: 接入后端接口 - 获取顶部标签页配置(关注/发现/附近)
+ * 接口路径: GET /api/home/tabs
+ * 请求参数: 无(从token中获取userId,可选)
+ * 返回数据格式: ApiResponse>
+ * TabConfig对象应包含: id, name, iconUrl, badgeCount(未读数等)等字段
+ * 用于动态配置顶部标签页,支持个性化显示
*/
private void initializeTopTabData() {
// 初始化关注页面数据(已关注主播的直播)
@@ -1498,16 +1590,7 @@ public class MainActivity extends AppCompatActivity {
* 显示附近页面
*/
private void showNearbyTab() {
- // 检查位置权限
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
- != PackageManager.PERMISSION_GRANTED
- && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
- != PackageManager.PERMISSION_GRANTED) {
- // 请求位置权限
- requestLocationPermission();
- return;
- }
-
+ // 先切换界面布局(无论是否有权限)
// 隐藏分类标签(附近页面不需要分类筛选)
if (binding.categoryTabs != null) {
binding.categoryTabs.setVisibility(View.GONE);
@@ -1526,10 +1609,37 @@ public class MainActivity extends AppCompatActivity {
if (binding.roomsRecyclerView != null) {
binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
- nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
}
+ // 检查位置权限
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED
+ && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // 显示需要权限的提示
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setIcon(R.drawable.ic_search_24);
+ binding.emptyStateView.setTitle("需要位置权限");
+ binding.emptyStateView.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播");
+ binding.emptyStateView.setActionText("授权");
+ binding.emptyStateView.setOnActionClickListener(v -> requestLocationPermission());
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ // 清空列表,因为还没有权限获取数据
+ if (nearbyUsersAdapter != null) {
+ nearbyUsersAdapter.submitList(new ArrayList<>());
+ }
+ // 请求位置权限(但不阻塞界面切换)
+ requestLocationPermission();
+ return;
+ }
+
+ // 有权限,显示附近用户列表
+ if (nearbyUsersAdapter != null) {
+ nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
+ }
+
// 更新空状态
if (nearbyUsers.isEmpty()) {
if (binding.emptyStateView != null) {
@@ -1708,11 +1818,18 @@ public class MainActivity extends AppCompatActivity {
* 请求位置权限
*/
private void requestLocationPermission() {
- if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // 检查是否应该显示权限说明
+ // shouldShowRequestPermissionRationale 返回 true 表示用户之前拒绝过,但还可以再次请求
+ // 返回 false 可能是第一次请求,也可能是用户选择了"不再询问"
+ boolean shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION);
+
+ if (shouldShowRationale) {
+ // 用户之前拒绝过,但还可以再次请求,显示说明后直接请求权限
new AlertDialog.Builder(this)
.setTitle("需要位置权限")
- .setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。")
- .setPositiveButton("确定", (dialog, which) -> {
+ .setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。")
+ .setPositiveButton("授权", (dialog, which) -> {
+ // 直接请求权限
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
@@ -1720,19 +1837,13 @@ public class MainActivity extends AppCompatActivity {
},
REQUEST_LOCATION_PERMISSION);
})
- .setNegativeButton("取消", (dialog, which) -> {
- // 用户拒绝权限,显示提示
- Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
- // 切换回发现页面
- if (binding.topTabs != null) {
- TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
- if (discoverTab != null) {
- discoverTab.select();
- }
- }
- })
+ .setNegativeButton("取消", null)
+ .setCancelable(true)
.show();
} else {
+ // 第一次请求权限,或者用户选择了"不再询问"
+ // 直接请求权限,如果用户选择了"不再询问",系统会静默失败
+ // 我们会在权限回调中处理这种情况
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java
index 875d6441..1bb98cde 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java
@@ -67,6 +67,15 @@ public class MyFriendsActivity extends AppCompatActivity {
}
private void applyFilter(String query) {
+ // TODO: 接入后端接口 - 搜索好友
+ // 接口路径: GET /api/friends/search
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - keyword: 搜索关键词
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // 搜索范围包括:好友昵称、备注、共同关注等
if (query == null || query.trim().isEmpty()) {
adapter.submitList(new ArrayList<>(all));
updateEmptyState(all);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java
index 8880c12f..a0760a5f 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/NearbyUsersAdapter.java
@@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
import com.example.livestreaming.databinding.ItemNearbyUserBinding;
public class NearbyUsersAdapter extends ListAdapter {
@@ -48,29 +49,51 @@ public class NearbyUsersAdapter extends ListAdapter
+ // 目前使用默认占位图
+ Glide.with(binding.avatarImage.getContext())
+ .load((String) null) // 暂时为null,等待后端接口
+ .circleCrop()
+ .placeholder(com.example.livestreaming.R.drawable.ic_account_circle_24)
+ .error(com.example.livestreaming.R.drawable.ic_account_circle_24)
+ .into(binding.avatarImage);
+
+ // 显示/隐藏直播状态徽章
+ if (user.isLive()) {
+ binding.liveBadge.setVisibility(View.VISIBLE);
+ } else {
+ binding.liveBadge.setVisibility(View.GONE);
+ }
// 添加按钮点击事件
binding.addButton.setOnClickListener(v -> {
- if (user == null) return;
- if (onUserClickListener != null) onUserClickListener.onUserClick(user);
+ if (onUserClickListener != null) {
+ onUserClickListener.onUserClick(user);
+ }
});
// 整个item点击也可以触发添加
binding.getRoot().setOnClickListener(v -> {
- if (user == null) return;
- if (onUserClickListener != null) onUserClickListener.onUserClick(user);
+ if (onUserClickListener != null) {
+ onUserClickListener.onUserClick(user);
+ }
});
}
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java
index 7b4bc0a3..59f867ed 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java
@@ -73,6 +73,14 @@ public class NotificationSettingsActivity extends AppCompatActivity {
}
private void refreshItems() {
+ // TODO: 接入后端接口 - 获取用户通知设置
+ // 接口路径: GET /api/users/{userId}/notification/settings
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // 返回数据格式: ApiResponse
+ // NotificationSettings对象应包含: systemEnabled, followEnabled, commentEnabled,
+ // messageEnabled, liveEnabled, dndEnabled, dndStartHour, dndEndHour等字段
+ // 首次加载时从接口获取,后续可从本地缓存读取
List items = new ArrayList<>();
// 系统通知开关
@@ -123,6 +131,20 @@ public class NotificationSettingsActivity extends AppCompatActivity {
.setTitle("免打扰设置")
.setMessage(message)
.setPositiveButton("开启", (dialog, which) -> {
+ // TODO: 接入后端接口 - 更新通知设置
+ // 接口路径: PUT /api/users/{userId}/notification/settings
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - systemEnabled (可选): 系统通知开关
+ // - followEnabled (可选): 关注提醒开关
+ // - commentEnabled (可选): 评论提醒开关
+ // - messageEnabled (可选): 私信提醒开关
+ // - liveEnabled (可选): 开播提醒开关
+ // - dndEnabled (可选): 免打扰开关
+ // - dndStartHour (可选): 免打扰开始时间(小时,0-23)
+ // - dndEndHour (可选): 免打扰结束时间(小时,0-23)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 更新成功后,同步更新本地缓存和界面显示
prefs.edit().putBoolean(KEY_DND_ENABLED, true).apply();
refreshItems();
Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show();
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java
index 874f1182..66f6c4ed 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java
@@ -32,6 +32,13 @@ public class NotificationsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ // 检查登录状态,通知列表需要登录
+ if (!AuthHelper.requireLogin(this, "查看通知需要登录")) {
+ finish();
+ return;
+ }
+
binding = ActivityNotificationsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java
index 0ab98840..01e8c4c4 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java
@@ -110,6 +110,11 @@ public class NotificationsAdapter extends ListAdapter
+ // 如果NotificationItem包含senderId,则调用此接口获取头像;否则使用默认图标
if (binding.avatar == null) return;
String avatarUrl = item.getAvatarUrl();
diff --git a/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java
index 5bd8c95e..8e42427a 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java
@@ -46,6 +46,15 @@ public class PlayerActivity extends AppCompatActivity {
@Override
protected void onStart() {
super.onStart();
+ // TODO: 接入后端接口 - 记录播放开始
+ // 接口路径: POST /api/play/start
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取,可选)
+ // - roomId: 房间ID(从Intent中获取)
+ // - playUrl: 播放地址
+ // - timestamp: 开始播放时间戳
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 用于统计播放数据和用户行为分析
String url = getIntent().getStringExtra(EXTRA_PLAY_URL);
if (url == null || url.trim().isEmpty()) return;
@@ -115,6 +124,15 @@ public class PlayerActivity extends AppCompatActivity {
@Override
protected void onStop() {
super.onStop();
+ // TODO: 接入后端接口 - 记录播放结束
+ // 接口路径: POST /api/play/end
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取,可选)
+ // - roomId: 房间ID(从Intent中获取)
+ // - playDuration: 播放时长(秒)
+ // - timestamp: 结束播放时间戳
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 用于统计播放时长和用户观看行为
releaseExoPlayer();
releaseIjkPlayer();
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
index c10b75a9..6b45f60d 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
@@ -246,7 +246,13 @@ public class ProfileActivity extends AppCompatActivity {
private void setupNavigationClicks() {
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
- binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this));
+ binding.topActionClock.setOnClickListener(v -> {
+ // 检查登录状态,观看历史需要登录
+ if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
+ return;
+ }
+ WatchHistoryActivity.start(this);
+ });
binding.topActionMore.setOnClickListener(v -> TabPlaceholderActivity.start(this, "更多"));
binding.copyIdBtn.setOnClickListener(v -> {
@@ -267,19 +273,53 @@ public class ProfileActivity extends AppCompatActivity {
// - userId: 用户ID(路径参数)
// 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
// 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示
- binding.following.setOnClickListener(v -> FollowingListActivity.start(this));
- binding.followers.setOnClickListener(v -> FansListActivity.start(this));
- binding.likes.setOnClickListener(v -> LikesListActivity.start(this));
+ binding.following.setOnClickListener(v -> {
+ // 检查登录状态,查看关注列表需要登录
+ if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) {
+ return;
+ }
+ FollowingListActivity.start(this);
+ });
+ binding.followers.setOnClickListener(v -> {
+ // 检查登录状态,查看粉丝列表需要登录
+ if (!AuthHelper.requireLogin(this, "查看粉丝列表需要登录")) {
+ return;
+ }
+ FansListActivity.start(this);
+ });
+ binding.likes.setOnClickListener(v -> {
+ // 检查登录状态,查看获赞列表需要登录
+ if (!AuthHelper.requireLogin(this, "查看获赞列表需要登录")) {
+ return;
+ }
+ LikesListActivity.start(this);
+ });
binding.action1.setOnClickListener(v -> TabPlaceholderActivity.start(this, "公园勋章"));
- binding.action2.setOnClickListener(v -> WatchHistoryActivity.start(this));
+ binding.action2.setOnClickListener(v -> {
+ // 检查登录状态,观看历史需要登录
+ if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
+ return;
+ }
+ WatchHistoryActivity.start(this);
+ });
binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class)));
binding.editProfile.setOnClickListener(v -> {
+ // 检查登录状态,编辑资料需要登录
+ if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
+ return;
+ }
Intent intent = new Intent(this, EditProfileActivity.class);
editProfileLauncher.launch(intent);
});
- binding.shareHome.setOnClickListener(v -> showShareProfileDialog());
+ binding.shareHome.setOnClickListener(v -> {
+ // 检查登录状态,分享个人主页需要登录
+ if (!AuthHelper.requireLogin(this, "分享个人主页需要登录")) {
+ return;
+ }
+ showShareProfileDialog();
+ });
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
}
@@ -304,16 +344,60 @@ public class ProfileActivity extends AppCompatActivity {
}
});
- binding.worksPublishBtn.setOnClickListener(v -> Toast.makeText(this, "发布功能待接入", Toast.LENGTH_SHORT).show());
+ // TODO: 接入后端接口 - 发布作品
+ // 接口路径: POST /api/works
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - title: 作品标题
+ // - description: 作品描述(可选)
+ // - coverUrl: 封面图片URL(必填,需要先上传图片)
+ // - videoUrl (可选): 视频URL(如果是视频作品)
+ // - images (可选): 图片URL列表(如果是图片作品)
+ // 返回数据格式: ApiResponse
+ // WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段
+ // 发布成功后,刷新作品列表显示
+ binding.worksPublishBtn.setOnClickListener(v -> {
+ // 检查登录状态,发布作品需要登录
+ if (!AuthHelper.requireLogin(this, "发布作品需要登录")) {
+ return;
+ }
+ Toast.makeText(this, "发布功能待接入", Toast.LENGTH_SHORT).show();
+ });
binding.likedGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
binding.profileEditFromTab.setOnClickListener(v -> {
+ // 检查登录状态,编辑资料需要登录
+ if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
+ return;
+ }
Intent intent = new Intent(this, EditProfileActivity.class);
editProfileLauncher.launch(intent);
});
}
private void showTab(int index) {
+ // TODO: 接入后端接口 - 获取用户作品列表
+ // 接口路径: GET /api/users/{userId}/works
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段
+ // TODO: 接入后端接口 - 获取用户收藏列表
+ // 接口路径: GET /api/users/{userId}/favorites
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // TODO: 接入后端接口 - 获取用户赞过的作品列表
+ // 接口路径: GET /api/users/{userId}/liked
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
// 标签页顺序:0-作品, 1-收藏, 2-赞过
binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE);
binding.tabFavorites.setVisibility(index == 1 ? View.VISIBLE : View.GONE);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RegisterActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RegisterActivity.java
new file mode 100644
index 00000000..f1ca967a
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/RegisterActivity.java
@@ -0,0 +1,350 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.livestreaming.databinding.ActivityRegisterBinding;
+import com.example.livestreaming.net.ApiClient;
+import com.example.livestreaming.net.ApiResponse;
+import com.example.livestreaming.net.AuthStore;
+import com.example.livestreaming.net.LoginResponse;
+import com.example.livestreaming.net.RegisterRequest;
+
+import java.util.regex.Pattern;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class RegisterActivity extends AppCompatActivity {
+
+ private ActivityRegisterBinding binding;
+ private boolean isRegistering = false;
+ private CountDownTimer countDownTimer;
+ private boolean isCodeSending = false;
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, RegisterActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityRegisterBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setupUI();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (countDownTimer != null) {
+ countDownTimer.cancel();
+ }
+ }
+
+ private void setupUI() {
+ // 返回按钮
+ binding.backButton.setOnClickListener(v -> finish());
+
+ // 发送验证码按钮
+ binding.sendCodeButton.setOnClickListener(v -> sendVerificationCode());
+
+ // 注册按钮
+ binding.registerButton.setOnClickListener(v -> performRegister());
+
+ // 登录链接
+ binding.loginLinkText.setOnClickListener(v -> {
+ LoginActivity.start(RegisterActivity.this);
+ finish();
+ });
+
+ // 回车键注册
+ binding.nicknameInput.setOnEditorActionListener((v, actionId, event) -> {
+ if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_DONE) {
+ performRegister();
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void sendVerificationCode() {
+ if (isCodeSending) return;
+
+ String phone = binding.phoneInput.getText() != null ?
+ binding.phoneInput.getText().toString().trim() : "";
+
+ // 验证手机号
+ if (TextUtils.isEmpty(phone)) {
+ binding.phoneLayout.setError("请输入手机号");
+ binding.phoneInput.requestFocus();
+ return;
+ }
+
+ // 简单的手机号格式验证(11位数字)
+ Pattern phonePattern = Pattern.compile("^1[3-9]\\d{9}$");
+ if (!phonePattern.matcher(phone).matches()) {
+ binding.phoneLayout.setError("请输入正确的手机号");
+ binding.phoneInput.requestFocus();
+ return;
+ }
+
+ binding.phoneLayout.setError(null);
+
+ // TODO: 接入后端接口 - 发送手机验证码
+ // 接口路径: POST /api/sms/send
+ // 请求参数:
+ // - phone: 手机号
+ // - type: 验证码类型(register)
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // 发送成功后,开始倒计时,防止重复发送
+ isCodeSending = true;
+ binding.sendCodeButton.setEnabled(false);
+
+ // 模拟发送验证码(实际应该调用后端接口)
+ Toast.makeText(this, "验证码已发送", Toast.LENGTH_SHORT).show();
+
+ // 开始倒计时60秒
+ startCountDown(60);
+
+ // 实际代码应该是:
+ // ApiClient.getService(getApplicationContext()).sendVerificationCode(phone, "register")
+ // .enqueue(new Callback>() {
+ // @Override
+ // public void onResponse(Call> call, Response> response) {
+ // isCodeSending = false;
+ // if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
+ // Toast.makeText(RegisterActivity.this, "验证码已发送", Toast.LENGTH_SHORT).show();
+ // startCountDown(60);
+ // } else {
+ // binding.sendCodeButton.setEnabled(true);
+ // String msg = response.body() != null ? response.body().getMessage() : "发送失败";
+ // Toast.makeText(RegisterActivity.this, msg, Toast.LENGTH_SHORT).show();
+ // }
+ // }
+ //
+ // @Override
+ // public void onFailure(Call> call, Throwable t) {
+ // isCodeSending = false;
+ // binding.sendCodeButton.setEnabled(true);
+ // Toast.makeText(RegisterActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
+ // }
+ // });
+ }
+
+ private void startCountDown(int seconds) {
+ if (countDownTimer != null) {
+ countDownTimer.cancel();
+ }
+
+ countDownTimer = new CountDownTimer(seconds * 1000L, 1000L) {
+ @Override
+ public void onTick(long millisUntilFinished) {
+ int remaining = (int) (millisUntilFinished / 1000);
+ binding.sendCodeButton.setText(remaining + "秒后重发");
+ }
+
+ @Override
+ public void onFinish() {
+ isCodeSending = false;
+ binding.sendCodeButton.setEnabled(true);
+ binding.sendCodeButton.setText("发送验证码");
+ }
+ };
+ countDownTimer.start();
+ }
+
+ private void performRegister() {
+ // 防止重复提交
+ if (isRegistering) return;
+
+ String phone = binding.phoneInput.getText() != null ?
+ binding.phoneInput.getText().toString().trim() : "";
+ String verificationCode = binding.verificationCodeInput.getText() != null ?
+ binding.verificationCodeInput.getText().toString().trim() : "";
+ String password = binding.passwordInput.getText() != null ?
+ binding.passwordInput.getText().toString().trim() : "";
+ String nickname = binding.nicknameInput.getText() != null ?
+ binding.nicknameInput.getText().toString().trim() : "";
+
+ // 验证输入
+ if (TextUtils.isEmpty(phone)) {
+ binding.phoneLayout.setError("请输入手机号");
+ binding.phoneInput.requestFocus();
+ return;
+ } else {
+ binding.phoneLayout.setError(null);
+ }
+
+ Pattern phonePattern = Pattern.compile("^1[3-9]\\d{9}$");
+ if (!phonePattern.matcher(phone).matches()) {
+ binding.phoneLayout.setError("请输入正确的手机号");
+ binding.phoneInput.requestFocus();
+ return;
+ }
+
+ if (TextUtils.isEmpty(verificationCode)) {
+ binding.verificationCodeLayout.setError("请输入验证码");
+ binding.verificationCodeInput.requestFocus();
+ return;
+ } else {
+ binding.verificationCodeLayout.setError(null);
+ }
+
+ if (TextUtils.isEmpty(password)) {
+ binding.passwordLayout.setError("请输入密码");
+ binding.passwordInput.requestFocus();
+ return;
+ } else if (password.length() < 6) {
+ binding.passwordLayout.setError("密码至少6位");
+ binding.passwordInput.requestFocus();
+ return;
+ } else {
+ binding.passwordLayout.setError(null);
+ }
+
+ if (TextUtils.isEmpty(nickname)) {
+ binding.nicknameLayout.setError("请输入昵称");
+ binding.nicknameInput.requestFocus();
+ return;
+ } else {
+ binding.nicknameLayout.setError(null);
+ }
+
+ // 显示加载状态
+ isRegistering = true;
+ binding.registerButton.setEnabled(false);
+ binding.loadingProgress.setVisibility(View.VISIBLE);
+
+ // TODO: 接入后端接口 - 用户注册
+ // 接口路径: POST /api/front/register(ApiService中已定义)
+ // 请求参数: RegisterRequest {phone: string, password: string, verificationCode: string, nickname: string}
+ // 返回数据格式: ApiResponse
+ // LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
+ // 注册成功后,自动登录并保存token,更新用户信息到本地SharedPreferences
+ ApiClient.getService(getApplicationContext()).register(
+ new RegisterRequest(phone, password, verificationCode, nickname))
+ .enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ isRegistering = false;
+ binding.registerButton.setEnabled(true);
+ binding.loadingProgress.setVisibility(View.GONE);
+
+ ApiResponse body = response.body();
+ LoginResponse loginData = body != null ? body.getData() : null;
+
+ if (!response.isSuccessful() || body == null || !body.isOk() || loginData == null) {
+ String errorMsg = "注册失败";
+ if (body != null && !TextUtils.isEmpty(body.getMessage())) {
+ errorMsg = body.getMessage();
+ } else if (!response.isSuccessful()) {
+ errorMsg = "网络错误:" + response.code();
+ }
+ Toast.makeText(RegisterActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // 保存token
+ String token = loginData.getToken();
+ if (!TextUtils.isEmpty(token)) {
+ AuthStore.setToken(getApplicationContext(), token);
+ }
+
+ // 保存用户信息到本地
+ SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
+ prefs.edit()
+ .putString("profile_name", nickname)
+ .apply();
+
+ // 注册成功,自动登录,返回上一页(如果是从其他页面跳转过来的)
+ // 如果是直接打开注册页面,则跳转到主页面
+ Toast.makeText(RegisterActivity.this, "注册成功", Toast.LENGTH_SHORT).show();
+
+ // 检查是否有上一个Activity
+ if (isTaskRoot()) {
+ // 如果没有上一个Activity,跳转到主页面
+ Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } else {
+ // 有上一个Activity,直接返回
+ finish();
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ isRegistering = false;
+ binding.registerButton.setEnabled(true);
+ binding.loadingProgress.setVisibility(View.GONE);
+
+ // 检测是否是网络连接错误(后端未启动)
+ boolean isNetworkError = false;
+ String errorMsg = "网络错误";
+ if (t != null) {
+ String msg = t.getMessage();
+ if (msg != null) {
+ if (msg.contains("Unable to resolve host") ||
+ msg.contains("timeout") ||
+ msg.contains("Connection refused") ||
+ msg.contains("Failed to connect")) {
+ isNetworkError = true;
+ errorMsg = "无法连接到服务器,已切换到演示模式";
+ } else {
+ errorMsg = "网络错误:" + msg;
+ }
+ }
+ }
+
+ // 如果是网络连接错误(后端未接入),使用演示模式注册
+ if (isNetworkError) {
+ // 演示模式:允许任意信息注册(仅用于开发测试)
+ Toast.makeText(RegisterActivity.this, "演示模式:后端未接入,使用演示账号注册", Toast.LENGTH_LONG).show();
+
+ // 生成一个演示token(基于手机号)
+ String demoToken = "demo_token_" + phone.hashCode();
+ AuthStore.setToken(getApplicationContext(), demoToken);
+
+ // 保存用户信息到本地
+ SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
+ prefs.edit()
+ .putString("profile_name", nickname)
+ .apply();
+
+ // 注册成功,自动登录,返回上一页(如果是从其他页面跳转过来的)
+ // 如果是直接打开注册页面,则跳转到主页面
+ Toast.makeText(RegisterActivity.this, "演示模式注册成功", Toast.LENGTH_SHORT).show();
+
+ // 检查是否有上一个Activity
+ if (isTaskRoot()) {
+ // 如果没有上一个Activity,跳转到主页面
+ Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } else {
+ // 有上一个Activity,直接返回
+ finish();
+ }
+ } else {
+ Toast.makeText(RegisterActivity.this, errorMsg, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
index 6187189f..9ddc8a5d 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
@@ -101,6 +101,14 @@ public class RoomDetailActivity extends AppCompatActivity {
setupUI();
setupChat();
+ // TODO: 接入后端接口 - 记录观看历史
+ // 接口路径: POST /api/watch/history
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - roomId: 房间ID
+ // - watchTime: 观看时间(时间戳,可选)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 进入房间时调用,用于记录用户观看历史
// 添加欢迎消息
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
}
@@ -139,6 +147,11 @@ public class RoomDetailActivity extends AppCompatActivity {
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
// 关注成功后,更新按钮状态为"已关注",并禁用按钮
binding.followButton.setOnClickListener(v -> {
+ // 检查登录状态,关注主播需要登录
+ if (!AuthHelper.requireLogin(this, "关注主播需要登录")) {
+ return;
+ }
+
Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
@@ -171,6 +184,11 @@ public class RoomDetailActivity extends AppCompatActivity {
}
private void sendMessage() {
+ // 检查登录状态,发送弹幕需要登录
+ if (!AuthHelper.requireLoginWithToast(this, "发送弹幕需要登录")) {
+ return;
+ }
+
// TODO: 接入后端接口 - 发送直播间弹幕消息
// 接口路径: POST /api/rooms/{roomId}/messages
// 请求参数:
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java
index 1b3ea23a..6de399bd 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java
@@ -72,6 +72,19 @@ public class RoomsAdapter extends ListAdapter {
binding.roomTitle.setText(room != null && room.getTitle() != null ? room.getTitle() : "(Untitled)");
binding.streamerName.setText(room != null && room.getStreamerName() != null ? room.getStreamerName() : "");
+ // TODO: 接入后端接口 - 获取房间点赞数
+ // 接口路径: GET /api/rooms/{roomId}/likes/count
+ // 请求参数: roomId (路径参数)
+ // 返回数据格式: ApiResponse<{likeCount: number}>
+ // 或者Room对象应包含likeCount字段,直接从room.getLikeCount()获取
+ // TODO: 接入后端接口 - 点赞/取消点赞房间
+ // 接口路径: POST /api/rooms/{roomId}/like 或 DELETE /api/rooms/{roomId}/like
+ // 请求参数:
+ // - roomId: 房间ID(路径参数)
+ // - userId: 用户ID(从token中获取)
+ // - action: "like" 或 "unlike"
+ // 返回数据格式: ApiResponse<{success: boolean, likeCount: number, isLiked: boolean}>
+ // 点赞成功后,更新本地点赞数和点赞状态
try {
String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
int h = Math.abs(seed.hashCode());
@@ -114,14 +127,27 @@ public class RoomsAdapter extends ListAdapter {
assetFile = a.coverAssetFiles.get(idx);
}
+ // TODO: 接入后端接口 - 从后端获取房间封面图片URL
+ // 接口路径: GET /api/rooms/{roomId}/cover
+ // 请求参数: roomId (路径参数)
+ // 返回数据格式: ApiResponse<{coverUrl: string}>
+ // 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
+ // 优先使用Room对象中的coverUrl,如果没有则使用默认占位图
Object model;
if (assetFile != null) {
model = "file:///android_asset/img/" + assetFile;
} else {
- String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
- int h = Math.abs(seed.hashCode());
- int imageId = (h % 1000) + 1;
- model = "https://picsum.photos/id/" + imageId + "/600/450";
+ // 优先从Room对象获取封面URL
+ String coverUrl = room != null ? room.getCoverUrl() : null;
+ if (coverUrl != null && !coverUrl.trim().isEmpty()) {
+ model = coverUrl;
+ } else {
+ // 降级方案:使用占位图或随机图片
+ String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
+ int h = Math.abs(seed.hashCode());
+ int imageId = (h % 1000) + 1;
+ model = "https://picsum.photos/id/" + imageId + "/600/450";
+ }
}
Glide.with(binding.coverImage)
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
index a03ecbc8..4354ae75 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
@@ -107,6 +107,23 @@ public class SearchActivity extends AppCompatActivity {
}
private void applyFilter(String q) {
+ // TODO: 接入后端接口 - 实时搜索建议(当用户输入时)
+ // 接口路径: GET /api/search/suggestions
+ // 请求参数:
+ // - keyword: 搜索关键词(必填)
+ // - limit (可选): 返回数量限制,默认10
+ // 返回数据格式: ApiResponse>
+ // SearchSuggestion对象应包含: type (room/user), id, title, subtitle, avatarUrl等字段
+ // 用于在用户输入时显示搜索建议,提升搜索体验
+ // TODO: 接入后端接口 - 搜索功能(当用户提交搜索时)
+ // 接口路径: GET /api/search
+ // 请求参数:
+ // - keyword: 搜索关键词(必填)
+ // - type (可选): 搜索类型(room/user/all),默认all
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse<{rooms: Room[], users: User[], total: number}>
+ // 搜索逻辑应迁移到后端,前端只负责展示结果
String query = q != null ? q.trim() : "";
if (query.isEmpty()) {
adapter.submitList(new ArrayList<>(all));
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java
index 911068b1..eb97e3ba 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/SearchSuggestionsAdapter.java
@@ -48,6 +48,12 @@ public class SearchSuggestionsAdapter extends ListAdapter
+ // 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
+ // 如果搜索建议包含封面图片,使用Glide加载;否则使用默认占位图
String title = room != null && room.getTitle() != null ? room.getTitle() : "";
String sub = room != null && room.getStreamerName() != null ? room.getStreamerName() : "";
binding.title.setText(title);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
index 9e004676..63afa67a 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
@@ -45,12 +45,22 @@ public class SettingsPageActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ // 检查登录状态,设置页面需要登录(除了服务器设置等通用设置)
+ page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
+ if (page != null && !PAGE_SERVER.equals(page) && !PAGE_ABOUT.equals(page) && !PAGE_HELP.equals(page)) {
+ if (!AuthHelper.requireLogin(this, "此设置需要登录")) {
+ finish();
+ return;
+ }
+ }
+
binding = ActivitySettingsPageBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.backButton.setOnClickListener(v -> finish());
- page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
+ // page已在onCreate开始处获取
if (page == null) page = "";
String title = resolveTitle(page);
@@ -478,6 +488,14 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void showChangePasswordDialog() {
+ // TODO: 接入后端接口 - 修改密码
+ // 接口路径: POST /api/users/{userId}/password/change
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - oldPassword: 旧密码
+ // - newPassword: 新密码
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // 修改成功后,提示用户并关闭对话框
EditText oldPassword = new EditText(this);
oldPassword.setHint("请输入旧密码");
oldPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
@@ -503,6 +521,20 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void showBindPhoneDialog() {
+ // TODO: 接入后端接口 - 绑定手机号
+ // 接口路径: POST /api/users/{userId}/phone/bind
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - phone: 手机号
+ // - verificationCode: 验证码(需要先调用发送验证码接口)
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // TODO: 接入后端接口 - 发送手机验证码
+ // 接口路径: POST /api/sms/send
+ // 请求参数:
+ // - phone: 手机号
+ // - type: 验证码类型(bind/change等)
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // 绑定成功后,提示用户并关闭对话框
EditText phoneInput = new EditText(this);
phoneInput.setHint("请输入手机号");
phoneInput.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
@@ -518,6 +550,19 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void showDeviceManagementDialog() {
+ // TODO: 接入后端接口 - 获取登录设备列表
+ // 接口路径: GET /api/users/{userId}/devices
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // 返回数据格式: ApiResponse>
+ // DeviceInfo对象应包含: deviceId, deviceName, deviceType, loginTime, lastActiveTime, isCurrent等字段
+ // TODO: 接入后端接口 - 退出指定设备登录
+ // 接口路径: DELETE /api/users/{userId}/devices/{deviceId}
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - deviceId: 设备ID(路径参数)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 退出成功后,从设备列表中移除该设备
new AlertDialog.Builder(this)
.setTitle("登录设备管理")
.setMessage("当前登录设备:\n• 本设备(当前)\n\n功能开发中...")
@@ -526,6 +571,26 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void showBlacklistDialog() {
+ // TODO: 接入后端接口 - 获取黑名单列表
+ // 接口路径: GET /api/users/{userId}/blacklist
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // User对象应包含: id, name, avatarUrl, addToBlacklistTime等字段
+ // TODO: 接入后端接口 - 添加用户到黑名单
+ // 接口路径: POST /api/users/{userId}/blacklist
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - targetUserId: 要屏蔽的用户ID
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // TODO: 接入后端接口 - 从黑名单移除用户
+ // 接口路径: DELETE /api/users/{userId}/blacklist/{targetUserId}
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - targetUserId: 要解除屏蔽的用户ID(路径参数)
+ // 返回数据格式: ApiResponse<{success: boolean}>
new AlertDialog.Builder(this)
.setTitle("黑名单管理")
.setMessage("黑名单功能允许您屏蔽不想看到的用户。\n\n" +
@@ -655,6 +720,18 @@ public class SettingsPageActivity extends AppCompatActivity {
"A: 请联系客服或通过绑定的手机号找回密码。\n\n" +
"Q7: 如何举报不良内容?\n" +
"A: 在直播间或用户主页可以举报违规内容,我们会及时处理。\n\n" +
+ // TODO: 接入后端接口 - 举报功能
+ // 接口路径: POST /api/report
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - targetType: 举报类型(room/user/message/work等)
+ // - targetId: 被举报对象ID
+ // - reason: 举报原因(可选)
+ // - description: 举报描述(可选)
+ // - images (可选): 举报截图URL列表
+ // 返回数据格式: ApiResponse<{success: boolean, reportId: string}>
+ // 举报成功后,提示用户并记录举报信息
+ //
"更多问题请联系客服。";
new AlertDialog.Builder(this)
@@ -665,6 +742,15 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void showFeedbackDialog() {
+ // TODO: 接入后端接口 - 提交意见反馈
+ // 接口路径: POST /api/feedback
+ // 请求参数:
+ // - userId: 用户ID(从token中获取,可选)
+ // - content: 反馈内容(必填)
+ // - contact: 联系方式(可选,邮箱或手机号)
+ // - images (可选): 反馈图片URL列表
+ // 返回数据格式: ApiResponse<{success: boolean, feedbackId: string}>
+ // 提交成功后,提示用户并关闭对话框
EditText feedbackInput = new EditText(this);
feedbackInput.setHint("请输入您的意见或建议");
feedbackInput.setMinLines(5);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java b/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java
index 3c49cee8..6028d08d 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java
@@ -11,6 +11,11 @@ public class ShareUtils {
/**
* 生成个人主页分享链接
+ * TODO: 接入后端接口 - 获取个人主页分享链接
+ * 接口路径: GET /api/share/profile/{userId}
+ * 请求参数: userId (路径参数)
+ * 返回数据格式: ApiResponse<{shareLink: string, shareCode: string}>
+ * 后端可以生成短链接、二维码等,并记录分享统计
*/
public static String generateProfileShareLink(String userId) {
return "https://livestreaming.com/profile/" + userId;
@@ -18,6 +23,11 @@ public class ShareUtils {
/**
* 生成直播间分享链接
+ * TODO: 接入后端接口 - 获取直播间分享链接
+ * 接口路径: GET /api/share/room/{roomId}
+ * 请求参数: roomId (路径参数)
+ * 返回数据格式: ApiResponse<{shareLink: string, shareCode: string, qrCodeUrl: string}>
+ * 后端可以生成短链接、二维码等,并记录分享统计
*/
public static String generateRoomShareLink(String roomId) {
return "https://livestreaming.com/room/" + roomId;
@@ -25,6 +35,15 @@ public class ShareUtils {
/**
* 分享链接(使用系统分享菜单)
+ * TODO: 接入后端接口 - 记录分享行为
+ * 接口路径: POST /api/share/record
+ * 请求参数:
+ * - userId: 当前用户ID(从token中获取,可选)
+ * - shareType: 分享类型(profile/room/work等)
+ * - targetId: 目标ID(用户ID/房间ID/作品ID等)
+ * - sharePlatform: 分享平台(wechat/weibo/qq/system等,可选)
+ * 返回数据格式: ApiResponse<{success: boolean}>
+ * 用于统计分享数据和用户行为分析
*/
public static void shareLink(Context context, String link, String title, String text) {
if (context == null || link == null) return;
diff --git a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
index f312625f..541c0c85 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
@@ -142,7 +142,21 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.discoverShortcutLive.setOnClickListener(v -> Toast.makeText(this, "进入直播模块", Toast.LENGTH_SHORT).show());
binding.discoverShortcutNearby.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
+ // TODO: 接入后端接口 - 获取榜单数据
+ // 接口路径: GET /api/rankings
+ // 请求参数:
+ // - type (可选): 榜单类型(hot/new/rich等)
+ // - category (可选): 分类筛选
+ // 返回数据格式: ApiResponse>
+ // RankingItem对象应包含: rank, roomId, title, streamerName, viewerCount, likeCount等字段
binding.discoverShortcutRank.setOnClickListener(v -> Toast.makeText(this, "榜单功能待接入", Toast.LENGTH_SHORT).show());
+ // TODO: 接入后端接口 - 获取话题列表
+ // 接口路径: GET /api/topics
+ // 请求参数:
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // TopicItem对象应包含: id, title, coverUrl, participantCount, postCount, hotScore等字段
binding.discoverShortcutTopic.setOnClickListener(v -> Toast.makeText(this, "话题功能待接入", Toast.LENGTH_SHORT).show());
}
@@ -180,6 +194,13 @@ public class TabPlaceholderActivity extends AppCompatActivity {
}
private void applyDiscoverFilter(String q) {
+ // TODO: 接入后端接口 - 发现页搜索建议
+ // 接口路径: GET /api/discover/search/suggestions
+ // 请求参数:
+ // - keyword: 搜索关键词(必填)
+ // - limit (可选): 返回数量限制,默认10
+ // 返回数据格式: ApiResponse>
+ // 用于在发现页搜索框输入时显示实时搜索建议
String query = q != null ? q.trim() : "";
if (TextUtils.isEmpty(query)) {
binding.discoverSuggestionsRecyclerView.setVisibility(View.GONE);
@@ -227,6 +248,13 @@ public class TabPlaceholderActivity extends AppCompatActivity {
ensureFollowRoomsAdapter();
+ // TODO: 接入后端接口 - 获取关注主播的直播间列表(TabPlaceholderActivity中的关注页面)
+ // 接口路径: GET /api/following/rooms
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
// 初始化数据
followAllRooms.clear();
followAllRooms.addAll(buildFollowDemoRooms(16));
@@ -327,6 +355,14 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.nearbyRecyclerView.setLayoutManager(layoutManager);
binding.nearbyRecyclerView.setAdapter(adapter);
+ // TODO: 接入后端接口 - 获取附近用户列表(TabPlaceholderActivity中的附近页面)
+ // 接口路径: GET /api/users/nearby
+ // 请求参数:
+ // - latitude: 当前用户纬度(必填)
+ // - longitude: 当前用户经度(必填)
+ // - radius (可选): 搜索半径(单位:米,默认5000)
+ // - page (可选): 页码
+ // 返回数据格式: ApiResponse>
adapter.submitList(buildNearbyDemoUsers(18));
}
@@ -407,6 +443,12 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.parkBadgeRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
binding.parkBadgeRecyclerView.setAdapter(badgesAdapter);
+ // TODO: 接入后端接口 - 获取用户勋章列表
+ // 接口路径: GET /api/users/{userId}/badges
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // 返回数据格式: ApiResponse>
+ // BadgeItem对象应包含: id, name, desc, iconUrl, isAchieved, isLocked, achieveTime等字段
badgesAdapter.submitList(buildDemoBadges());
}
@@ -548,14 +590,21 @@ public class TabPlaceholderActivity extends AppCompatActivity {
}
private void showLogoutConfirm() {
+ // TODO: 接入后端接口 - 退出登录
+ // 接口路径: POST /api/logout 或 GET /api/logout
+ // 请求参数: 无(从token中获取userId)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 退出成功后,清除本地token和用户信息,跳转到登录页面
new AlertDialog.Builder(this)
.setTitle("退出登录")
.setMessage("确定要退出登录吗?")
.setNegativeButton("取消", null)
.setPositiveButton("退出", (d, w) -> {
- Intent intent = new Intent(this, MainActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
+ // 清除token
+ com.example.livestreaming.net.AuthStore.setToken(getApplicationContext(), null);
+
+ // 跳转到登录页面
+ LoginActivity.start(TabPlaceholderActivity.this);
finish();
})
.show();
@@ -572,9 +621,34 @@ public class TabPlaceholderActivity extends AppCompatActivity {
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
+ // TODO: 接入后端接口 - 刷新定位并获取附近数据
+ // 接口路径: POST /api/users/{userId}/location/update
+ // 请求参数:
+ // - userId: 用户ID(从token中获取)
+ // - latitude: 当前用户纬度
+ // - longitude: 当前用户经度
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 更新成功后,重新获取附近用户、附近直播、热门地点等数据
binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
binding.nearbyEntryUsers.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
+ // TODO: 接入后端接口 - 获取附近直播列表
+ // 接口路径: GET /api/rooms/nearby
+ // 请求参数:
+ // - latitude: 当前用户纬度(必填)
+ // - longitude: 当前用户经度(必填)
+ // - radius (可选): 搜索半径(单位:米,默认10000)
+ // - page (可选): 页码
+ // 返回数据格式: ApiResponse>
+ // 返回距离用户最近的正在直播的房间列表
binding.nearbyEntryLive.setOnClickListener(v -> Toast.makeText(this, "附近直播(待接入)", Toast.LENGTH_SHORT).show());
+ // TODO: 接入后端接口 - 获取热门地点列表
+ // 接口路径: GET /api/locations/hot
+ // 请求参数:
+ // - latitude: 当前用户纬度(可选,用于排序)
+ // - longitude: 当前用户经度(可选,用于排序)
+ // - page (可选): 页码
+ // 返回数据格式: ApiResponse>
+ // LocationItem对象应包含: id, name, address, latitude, longitude, distance, liveCount, userCount等字段
binding.nearbyEntryPlaces.setOnClickListener(v -> Toast.makeText(this, "热门地点(待接入)", Toast.LENGTH_SHORT).show());
}
@@ -597,12 +671,36 @@ public class TabPlaceholderActivity extends AppCompatActivity {
addFriendAdapter = new NearbyUsersAdapter(user -> {
if (user == null) return;
+
+ // 检查登录状态,加好友需要登录
+ if (!AuthHelper.requireLogin(this, "加好友需要登录")) {
+ return;
+ }
+
+ // TODO: 接入后端接口 - 发送好友请求
+ // 接口路径: POST /api/friends/request
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - targetUserId: 目标用户ID
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // 发送成功后,提示用户并更新按钮状态
Toast.makeText(this, "已发送好友请求:" + user.getName(), Toast.LENGTH_SHORT).show();
});
binding.addFriendRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.addFriendRecyclerView.setAdapter(addFriendAdapter);
+ // TODO: 接入后端接口 - 获取推荐好友列表(加好友页面)
+ // 接口路径: GET /api/friends/recommend
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - latitude (可选): 当前用户纬度(用于推荐附近的人)
+ // - longitude (可选): 当前用户经度(用于推荐附近的人)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // User对象应包含: id, name, avatarUrl, bio, location, distance, mutualFriendsCount等字段
+ // 推荐算法:基于共同好友、地理位置、兴趣标签等
addFriendAllUsers.clear();
addFriendAllUsers.addAll(buildNearbyDemoUsers(18));
addFriendAdapter.submitList(new ArrayList<>(addFriendAllUsers));
diff --git a/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java b/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java
index f86d9d52..ba303d90 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/UnreadMessageManager.java
@@ -13,6 +13,11 @@ public class UnreadMessageManager {
/**
* 获取总未读消息数量
+ * TODO: 接入后端接口 - 从后端获取未读消息总数
+ * 接口路径: GET /api/messages/unread/count
+ * 请求参数: 无(从token中获取userId)
+ * 返回数据格式: ApiResponse<{unreadCount: number}>
+ * 建议:在应用启动时和进入后台后重新进入时调用此接口更新未读数量
*/
public static int getUnreadCount(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java
index 73546026..089d2926 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java
@@ -132,6 +132,27 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
}
private void showTab(int index) {
+ // TODO: 接入后端接口 - 获取其他用户的作品列表
+ // 接口路径: GET /api/users/{userId}/works
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // TODO: 接入后端接口 - 获取其他用户的收藏列表
+ // 接口路径: GET /api/users/{userId}/favorites
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // TODO: 接入后端接口 - 获取其他用户赞过的作品列表
+ // 接口路径: GET /api/users/{userId}/liked
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
if (binding == null) return;
// 标签页顺序:0-作品, 1-收藏, 2-赞过
binding.worksRecycler.setVisibility(index == 0 ? android.view.View.VISIBLE : android.view.View.GONE);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/UserWorksAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/UserWorksAdapter.java
index 6319f67c..d827e907 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/UserWorksAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/UserWorksAdapter.java
@@ -15,6 +15,15 @@ public class UserWorksAdapter extends RecyclerView.Adapter
private final List items = new ArrayList<>();
+ // TODO: 接入后端接口 - 加载用户作品数据
+ // 接口路径: GET /api/users/{userId}/works
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // WorkItem对象应包含: id, coverUrl, title, likeCount, viewCount, publishTime等字段
+ // 注意:当前使用Integer列表(drawable资源ID)作为临时方案,应改为使用WorkItem对象列表
public void submitList(List list) {
items.clear();
if (list != null) items.addAll(list);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java
index da443aa6..f8d2db31 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java
@@ -67,6 +67,20 @@ public class WaterfallRoomsAdapter extends ListAdapter
+ // 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
+ // TODO: 接入后端接口 - 从后端获取主播头像URL
+ // 接口路径: GET /api/user/profile/{streamerId}
+ // 请求参数: streamerId (路径参数,从Room对象中获取streamerId)
+ // 返回数据格式: ApiResponse<{avatarUrl: string}>
+ // TODO: 接入后端接口 - 获取房间观看人数
+ // 接口路径: GET /api/rooms/{roomId}/viewers/count
+ // 请求参数: roomId (路径参数)
+ // 返回数据格式: ApiResponse<{viewerCount: number}>
+ // 或者Room对象应包含viewerCount字段,直接从room.getViewerCount()获取
if (room == null) return;
// 设置标题
diff --git a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
index 2bc269e3..7edb8589 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
@@ -92,6 +92,13 @@ public class WishTreeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ // TODO: 接入后端接口 - 获取愿望树相关数据
+ // 接口路径: GET /api/wish_tree/info
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取,可选)
+ // 返回数据格式: ApiResponse
+ // WishTreeInfo对象应包含: totalWishes, todayWishes, userWishCount, nextResetTime等字段
+ // 用于显示愿望树统计信息和倒计时
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java
index 7f79cb20..daeffc2e 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java
@@ -13,6 +13,9 @@ public interface ApiService {
@POST("api/front/login")
Call> login(@Body LoginRequest body);
+ @POST("api/front/register")
+ Call> register(@Body RegisterRequest body);
+
@GET("api/rooms")
Call>> getRooms();
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/RegisterRequest.java b/android-app/app/src/main/java/com/example/livestreaming/net/RegisterRequest.java
new file mode 100644
index 00000000..4f528def
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/RegisterRequest.java
@@ -0,0 +1,42 @@
+package com.example.livestreaming.net;
+
+import com.google.gson.annotations.SerializedName;
+
+public class RegisterRequest {
+
+ @SerializedName("phone")
+ private final String phone;
+
+ @SerializedName("password")
+ private final String password;
+
+ @SerializedName("verificationCode")
+ private final String verificationCode;
+
+ @SerializedName("nickname")
+ private final String nickname;
+
+ public RegisterRequest(String phone, String password, String verificationCode, String nickname) {
+ this.phone = phone;
+ this.password = password;
+ this.verificationCode = verificationCode;
+ this.nickname = nickname;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getVerificationCode() {
+ return verificationCode;
+ }
+
+ public String getNickname() {
+ return nickname;
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java
index 56b7aee4..b9616f7a 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java
@@ -27,6 +27,9 @@ public class Room {
@SerializedName("viewerCount")
private int viewerCount;
+ @SerializedName("coverUrl")
+ private String coverUrl;
+
@SerializedName("streamUrls")
private StreamUrls streamUrls;
@@ -72,6 +75,10 @@ public class Room {
this.viewerCount = viewerCount;
}
+ public void setCoverUrl(String coverUrl) {
+ this.coverUrl = coverUrl;
+ }
+
public String getId() {
return id;
}
@@ -104,6 +111,10 @@ public class Room {
return viewerCount;
}
+ public String getCoverUrl() {
+ return coverUrl;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -116,11 +127,12 @@ public class Room {
&& Objects.equals(streamerName, room.streamerName)
&& Objects.equals(type, room.type)
&& Objects.equals(streamKey, room.streamKey)
+ && Objects.equals(coverUrl, room.coverUrl)
&& Objects.equals(streamUrls, room.streamUrls);
}
@Override
public int hashCode() {
- return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, streamUrls);
+ return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, coverUrl, streamUrls);
}
}
diff --git a/android-app/app/src/main/res/drawable/bg_add_button_nearby.xml b/android-app/app/src/main/res/drawable/bg_add_button_nearby.xml
new file mode 100644
index 00000000..20b80c7b
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_add_button_nearby.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_add_button_nearby_pressed.xml b/android-app/app/src/main/res/drawable/bg_add_button_nearby_pressed.xml
new file mode 100644
index 00000000..151eca3c
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_add_button_nearby_pressed.xml
@@ -0,0 +1,16 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_live_badge.xml b/android-app/app/src/main/res/drawable/bg_live_badge.xml
new file mode 100644
index 00000000..8b7263c6
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_live_badge.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_nearby_user_card.xml b/android-app/app/src/main/res/drawable/bg_nearby_user_card.xml
new file mode 100644
index 00000000..f72d4149
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_nearby_user_card.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_nearby_user_card_shadow.xml b/android-app/app/src/main/res/drawable/bg_nearby_user_card_shadow.xml
new file mode 100644
index 00000000..e665f483
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_nearby_user_card_shadow.xml
@@ -0,0 +1,20 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_login.xml b/android-app/app/src/main/res/layout/activity_login.xml
new file mode 100644
index 00000000..51f4490c
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_register.xml b/android-app/app/src/main/res/layout/activity_register.xml
new file mode 100644
index 00000000..352d5b51
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_register.xml
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/item_nearby_user.xml b/android-app/app/src/main/res/layout/item_nearby_user.xml
index c6d0bd9a..7292747c 100644
--- a/android-app/app/src/main/res/layout/item_nearby_user.xml
+++ b/android-app/app/src/main/res/layout/item_nearby_user.xml
@@ -1,78 +1,127 @@
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:background="@drawable/bg_nearby_user_card_shadow"
+ android:elevation="2dp"
+ android:padding="16dp">
-
+
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
-
+
+
+
+
+
+
+
+
+ app:layout_constraintStart_toEndOf="@id/avatarContainer"
+ app:layout_constraintTop_toTopOf="@id/avatarContainer">
-
+
+
+
+
+
+
+
+
+
+
-
-
+ app:layout_constraintTop_toTopOf="@id/avatarContainer" />
diff --git a/android-app/项目功能完善度分析.md b/android-app/项目功能完善度分析.md
index 83d32e86..b1ecd452 100644
--- a/android-app/项目功能完善度分析.md
+++ b/android-app/项目功能完善度分析.md
@@ -41,7 +41,7 @@
## ✅ 已完善功能
### 1. 主界面 (MainActivity)
-**完成度**: 90%
+**完成度**: 95%
- ✅ 瀑布流布局展示直播间
- ✅ 下拉刷新
@@ -50,16 +50,19 @@
- ✅ 推流地址显示和复制
- ✅ 搜索框UI
- ✅ 语音搜索功能
-- ✅ 分类标签UI
+- ✅ 分类标签UI和筛选功能(CategoryFilterManager)
+- ✅ 顶部标签页功能(关注/发现/附近)
- ✅ 底部导航栏
- ✅ 侧边栏菜单
+- ✅ 空状态和错误处理
**待完善**:
-- ⚠️ 分类筛选功能(UI已准备好,但筛选逻辑使用演示数据)
-- ⚠️ 顶部标签页(关注/发现/附近)跳转到占位页面
+- ⚠️ 分类筛选使用本地数据筛选(等待后端API)
+- ⚠️ 顶部标签页数据使用演示数据(关注列表、发现推荐、附近用户)
+- ⚠️ 附近用户功能需要位置权限,数据为模拟数据
### 2. 直播间详情 (RoomDetailActivity)
-**完成度**: 85%
+**完成度**: 90%
- ✅ 直播流播放(HLS/FLV)
- ✅ 全屏播放
@@ -69,12 +72,13 @@
- ✅ 观看人数显示
- ✅ 自动重连机制
- ✅ 低延迟播放配置
+- ✅ 分享功能(系统分享菜单)
**待完善**:
- ⚠️ 聊天功能使用模拟数据,未连接真实WebSocket
- ⚠️ 礼物打赏功能未实现
-- ⚠️ 分享功能未实现
- ⚠️ 弹幕功能未实现
+- ⚠️ 分享链接为模拟数据(等待后端生成真实分享链接)
### 3. 个人资料 (ProfileActivity)
**完成度**: 90%
@@ -86,11 +90,13 @@
- ✅ 作品/收藏/赞过标签页
- ✅ 关注/粉丝/获赞统计
- ✅ 主页链接复制
+- ✅ 作品列表基础UI(UserWorksAdapter)
**待完善**:
- ⚠️ 作品发布功能(显示"待接入")
- ⚠️ 头像上传功能(仅支持本地资源)
-- ⚠️ 作品列表使用演示数据
+- ⚠️ 作品列表使用演示数据(Integer列表)
+- ⚠️ 作品详情页面未实现
### 4. 编辑资料 (EditProfileActivity)
**完成度**: 95%
@@ -108,45 +114,53 @@
- ⚠️ 数据同步到后端API
### 5. 消息功能 (MessagesActivity)
-**完成度**: 80%
+**完成度**: 85%
- ✅ 会话列表展示
- ✅ 未读消息徽章
- ✅ 滑动删除/标记已读
- ✅ 会话详情页面
- ✅ 消息发送(本地)
+- ✅ 会话搜索功能(按会话标题和消息内容搜索)
+- ✅ 空状态显示
**待完善**:
- ⚠️ 所有消息数据使用演示数据
-- ⚠️ 未连接真实消息服务器
+- ⚠️ 未连接真实消息服务器(WebSocket)
- ⚠️ 消息推送功能未实现
- ⚠️ 图片/语音消息未实现
### 6. 会话详情 (ConversationActivity)
-**完成度**: 75%
+**完成度**: 90%
- ✅ 消息列表展示
- ✅ 消息发送
-- ✅ 自动滚动到底部
+- ✅ 自动滚动到底部(使用smoothScrollToPosition)
- ✅ 未读消息标记
+- ✅ 消息状态显示(发送中、已发送、已读)
+- ✅ 消息长按菜单(复制、删除)
+- ✅ 内存泄漏防护(onDestroy中清理Handler)
+- ✅ DiffUtil使用messageId优化性能
**待完善**:
- ⚠️ 消息数据使用演示数据
-- ⚠️ 未实现实时消息接收
-- ⚠️ 消息类型单一(仅文本)
+- ⚠️ 未实现实时消息接收(WebSocket)
+- ⚠️ 消息类型单一(仅文本,图片/语音消息未实现)
### 7. 搜索功能 (SearchActivity)
-**完成度**: 60%
+**完成度**: 70%
- ✅ 搜索界面UI
- ✅ 实时搜索过滤
- ✅ 搜索结果展示
+- ✅ 空状态显示(无搜索结果时显示提示)
**待完善**:
- ⚠️ 仅支持本地数据过滤
- ⚠️ 未连接后端搜索API
- ⚠️ 搜索历史未实现
- ⚠️ 热门搜索未实现
+- ⚠️ 搜索建议(自动补全)未实现
### 8. 缘池功能 (FishPondActivity)
**完成度**: 85%
@@ -174,42 +188,47 @@
### 10. 其他已实现页面
- ✅ 观看历史 (WatchHistoryActivity) - 基础UI
-- ✅ 我的好友 (MyFriendsActivity) - 基础UI
+- ✅ 我的好友 (MyFriendsActivity) - 基础UI,支持空状态
- ✅ 粉丝列表 (FansListActivity) - 基础UI
- ✅ 关注列表 (FollowingListActivity) - 基础UI
- ✅ 获赞列表 (LikesListActivity) - 基础UI
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
+- ✅ 登录页面 (LoginActivity) - 完整实现
+- ✅ 注册页面 (RegisterActivity) - 完整实现
+- ✅ 通知列表 (NotificationsActivity) - 完整实现,支持分类筛选
+- ✅ 通知设置 (NotificationSettingsActivity) - 完整实现
---
## ⚠️ 部分完善功能
### 1. 网络层集成
-**完成度**: 60%
+**完成度**: 70%
- ✅ Retrofit + OkHttp 配置完成
- ✅ API 接口定义完成
-- ✅ 基础错误处理
+- ✅ 基础错误处理(ErrorHandler工具类)
- ✅ 模拟器/真机网络地址配置
+- ✅ 网络请求生命周期管理(NetworkRequestManager)
+- ✅ 统一错误提示和重试机制
**待完善**:
- ⚠️ API 调用失败时大量使用演示数据
-- ⚠️ 缺少统一的错误处理机制
- ⚠️ 缺少请求重试机制
- ⚠️ 缺少网络状态监听
- ⚠️ 缺少请求缓存策略
-### 2. 数据持久化
-**完成度**: 50%
+### 2. 数据存储(前端)
+**完成度**: 60%
-- ✅ SharedPreferences 用于个人资料
-- ✅ 本地数据缓存
+- ✅ SharedPreferences 用于个人资料和配置
+- ✅ 本地数据缓存(内存缓存)
+- ✅ 分类筛选条件记忆(SharedPreferences)
**待完善**:
-- ⚠️ 未使用 Room 数据库
-- ⚠️ 未实现离线数据支持
-- ⚠️ 未实现数据同步机制
+- ⚠️ 未实现离线数据支持(当前仅使用内存缓存)
+- ⚠️ 未实现数据同步机制(等待后端)
### 3. 图片处理
**完成度**: 70%
@@ -230,12 +249,12 @@
### 1. 后端API集成
**问题**: 虽然定义了API接口,但很多功能在API失败时回退到演示数据
-**需要完善**:
-- [ ] 完善所有API接口的错误处理
-- [ ] 实现API请求的加载状态管理
-- [ ] 实现API数据的本地缓存
-- [ ] 实现数据同步机制
-- [ ] 添加API版本管理
+**需要完善**(前端部分):
+- [ ] 完善所有API接口的错误处理(前端错误提示)
+- [ ] 实现API请求的加载状态管理(前端UI)
+- [ ] 添加API版本管理(前端配置)
+
+**注意**: API数据的本地缓存和数据同步机制需要等待后端支持,前端仅负责UI展示和错误处理。
### 2. 实时通信
**问题**: 聊天、弹幕等功能使用模拟数据
@@ -276,20 +295,30 @@
- [ ] 添加位置权限处理
### 6. 占位页面功能
-**以下功能跳转到 `TabPlaceholderActivity`,需要实现**:
+**状态**: 部分功能已实现基础UI,但核心功能未实现
-- [ ] 语音匹配 (VoiceMatchActivity)
-- [ ] 心动信号 (HeartbeatSignalActivity)
-- [ ] 在线处对象 (OnlineDatingActivity)
-- [ ] 找人玩游戏 (FindGameActivity)
-- [ ] 一起KTV (KTVTogetherActivity)
-- [ ] 你画我猜 (DrawGuessActivity)
-- [ ] 和平精英 (PeaceEliteActivity)
-- [ ] 桌子游 (TableGamesActivity)
-- [ ] 公园勋章
-- [ ] 分享主页(部分实现,仅复制链接)
-- [ ] 加好友
-- [ ] 定位/发现
+**已实现基础UI的Activity**:
+- ✅ 语音匹配 (VoiceMatchActivity) - 基础页面
+- ✅ 心动信号 (HeartbeatSignalActivity) - 基础页面
+- ✅ 在线处对象 (OnlineDatingActivity) - 基础页面
+- ✅ 找人玩游戏 (FindGameActivity) - 基础页面
+- ✅ 一起KTV (KTVTogetherActivity) - 基础页面
+- ✅ 你画我猜 (DrawGuessActivity) - 基础页面
+- ✅ 和平精英 (PeaceEliteActivity) - 基础页面
+- ✅ 桌子游 (TableGamesActivity) - 基础页面
+
+**TabPlaceholderActivity中的占位功能**:
+- ⚠️ 公园勋章 - 显示占位页面
+- ⚠️ 加好友 - 显示附近用户列表(演示数据)
+- ⚠️ 定位/发现 - 显示占位页面
+- ⚠️ 关注页面(TabPlaceholderActivity中) - 显示关注列表(演示数据)
+- ⚠️ 附近页面(TabPlaceholderActivity中) - 显示附近用户(演示数据)
+- ⚠️ 发现页面(TabPlaceholderActivity中) - 显示推荐内容(演示数据)
+
+**需要完善**:
+- [ ] 实现各功能的核心逻辑
+- [ ] 接入后端API获取真实数据
+- [ ] 实现游戏匹配、语音匹配等功能
### 7. 作品功能
**问题**: 个人资料中的"作品"标签页未实现
@@ -384,11 +413,12 @@
- [ ] 引入依赖注入框架(Hilt/Dagger)
- [ ] 模块化拆分
-### 2. 数据层优化
-- [ ] 引入 Room 数据库
-- [ ] 实现 Repository 模式
-- [ ] 添加数据同步机制
-- [ ] 实现离线数据支持
+### 2. 数据层优化(前端)
+- [ ] 实现 Repository 模式(本地数据源)
+- [ ] 优化内存缓存策略
+- [ ] 实现请求去重机制
+
+**注意**: Room数据库和离线数据支持暂不实现,当前使用SharedPreferences和内存缓存。
### 3. UI/UX 优化
- [ ] 统一设计语言和组件库
@@ -439,48 +469,29 @@
- [x] 修复动画未取消问题
- [x] 完善网络请求取消机制
- [x] 优化播放器资源释放
-- [x] 添加 LeakCanary 检测
-**实际工作量**: 1.5天
**完成内容**:
- 为所有使用Handler的Activity添加onDestroy方法,清理Runnable防止内存泄漏
- 完善动画生命周期管理,确保动画在Activity销毁时正确取消
-- 创建NetworkRequestManager和NetworkUtils,实现生命周期感知的网络请求管理
+- 创建`NetworkRequestManager`工具类,实现生命周期感知的网络请求管理,在Activity销毁时自动取消所有请求
- 验证ExoPlayer资源释放逻辑正确(已有实现)
-- 集成LeakCanary内存泄漏检测工具
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
**为什么重要**: 提升用户体验一致性
-- [x] 创建统一的加载状态组件
+- [x] 创建统一的加载状态管理器
- [x] 实现骨架屏(Skeleton Screen)
- [x] 统一所有页面的加载提示
-- [x] 添加加载动画
**完成内容**:
-- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
-- 实现了 `SkeletonView` 和 `SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
-- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持LoadingView、ProgressBar、骨架屏)
-- 更新了所有主要Activity使用统一的加载状态:
- - **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
- - **RoomDetailActivity**: 使用LoadingView显示加载状态
- - **SearchActivity**: 搜索时显示"搜索中..."加载状态
+- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持骨架屏、下拉刷新等)
+- 实现了骨架屏适配器(`SkeletonAdapter`),在RecyclerView中显示占位效果
+- 更新了主要Activity使用统一的加载状态:
+ - **MainActivity**: 在列表为空时使用骨架屏显示加载状态
+ - **SearchActivity**: 搜索时显示加载状态
- **MessagesActivity**: 加载消息列表时显示加载状态
-- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
-#### 4. **数据持久化(本地)** ⭐⭐⭐
-**为什么重要**: 支持离线使用,提升用户体验
-
-- [ ] 引入 Room 数据库
-- [ ] 缓存直播间列表到本地
-- [ ] 缓存用户信息
-- [ ] 实现搜索历史存储
-- [ ] 实现观看历史存储
-- [ ] 实现消息记录缓存
-
-**预计工作量**: 3-5天
-
---
### 🟡 第二优先级(功能完善与架构优化)
@@ -501,7 +512,7 @@
- [x] 完善本地数据筛选逻辑
- [x] 实现筛选条件记忆(SharedPreferences)
-- [x] 优化筛选性能
+- [x] 优化筛选性能(异步筛选)
- [x] 添加筛选动画效果
**完成内容**:
@@ -511,55 +522,88 @@
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
- 在Activity恢复时自动恢复上次选中的分类标签
+- 支持在关注、发现、附近三个标签页中分别筛选
-**预计工作量**: 1-2天
+**待完善**:
+- ⚠️ 等待后端API支持按分类获取房间列表
-#### 7. **顶部标签页功能(前端实现)** ⭐⭐
+**预计工作量**: 1-2天(已完成)
+
+#### 7. **顶部标签页功能(前端实现)** ⭐⭐ ✅ 已完成
**为什么重要**: 完善主界面功能
-- [ ] 实现关注页面(使用本地数据)
-- [ ] 实现发现页面(推荐算法前端实现)
-- [ ] 实现附近页面(使用模拟位置数据)
-- [ ] 添加位置权限申请(即使后端未就绪)
+- [x] 实现关注页面(使用本地数据)
+- [x] 实现发现页面(推荐算法前端实现)
+- [x] 实现附近页面(使用模拟位置数据)
+- [x] 添加位置权限申请(即使后端未就绪)
-**预计工作量**: 3-4天
+**完成内容**:
+- 实现了 `showFollowTab()`、`showDiscoverTab()`、`showNearbyTab()` 三个方法
+- 创建了 `NearbyUser` 数据模型和 `NearbyUsersAdapter` 适配器
+- 创建了 `LocationDataManager` 位置数据管理器
+- 实现了位置权限申请和权限处理逻辑
+- 关注页面显示已关注主播的直播列表
+- 发现页面显示推荐内容,支持分类筛选
+- 附近页面显示附近用户列表,支持空状态显示
+- 所有页面都支持搜索功能(在对应数据源中搜索)
-#### 8. **搜索功能增强** ⭐
+**待完善**:
+- ⚠️ 数据使用演示数据,等待后端API
+- ⚠️ 附近用户数据为模拟数据,需要真实位置服务
+
+**预计工作量**: 3-4天(已完成)
+
+#### 8. **搜索功能增强** ⭐ ⚠️ 部分完成
**为什么重要**: 提升搜索体验
+- [x] 实时搜索过滤(SearchActivity已实现)
+- [x] 空状态显示(SearchActivity已实现)
- [ ] 实现搜索历史(本地存储)
- [ ] 实现热门搜索(模拟数据)
-- [ ] 优化搜索性能
-- [ ] 添加搜索建议
+- [ ] 优化搜索性能(防抖、缓存)
+- [ ] 添加搜索建议(自动补全)
-**预计工作量**: 2-3天
+**已完成内容**:
+- ✅ SearchActivity已实现实时搜索过滤功能
+- ✅ SearchActivity已实现空状态显示(无搜索结果时显示提示)
+- ✅ 支持从MainActivity传递搜索关键词到SearchActivity
-#### 9. **消息功能完善(前端)** ⭐⭐ ⚠️ 待实现
+**待实现内容**:
+- ⚠️ 搜索历史功能(使用SharedPreferences存储最近搜索记录)
+- ⚠️ 热门搜索功能(显示热门搜索关键词)
+- ⚠️ 搜索性能优化(防抖处理,避免频繁过滤)
+- ⚠️ 搜索建议功能(输入时显示自动补全建议)
+
+**预计工作量**: 2-3天(部分完成,剩余1-2天)
+
+#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
**为什么重要**: 完善核心社交功能
- [x] 完善消息列表UI(会话列表)
-- [ ] 实现消息状态显示(发送中、已发送、已读)
-- [ ] 优化消息列表性能(使用messageId)
-- [ ] 添加消息操作(复制、删除等)
-- [ ] 实现消息搜索(本地)
+- [x] 实现消息状态显示(发送中、已发送、已读)
+- [x] 优化消息列表性能(使用messageId)
+- [x] 添加消息操作(复制、删除等)
+- [x] 实现消息搜索(本地)
**已完成内容**:
- ✅ 会话列表展示(MessagesActivity)
- ✅ 滑动删除/标记已读功能(MessagesActivity)
- ✅ 消息发送和列表展示(ConversationActivity)
- ✅ 未读消息徽章管理
+- ✅ ChatMessage已添加MessageStatus枚举(SENDING, SENT, READ)
+- ✅ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)- ConversationMessagesAdapter已实现
+- ✅ 消息长按菜单,支持复制和删除操作 - ConversationActivity已实现
+- ✅ DiffUtil已使用messageId作为唯一标识,提升列表更新性能 - ConversationMessagesAdapter已优化
+- ✅ MessagesActivity已实现搜索功能,支持按会话标题和消息内容搜索
+- ✅ 内存泄漏防护已实现(onDestroy中清理Handler延迟任务)- ConversationActivity已实现
+- ✅ 滚动行为已优化(使用smoothScrollToPosition)- ConversationActivity已实现
-**待实现内容**:
-- ⚠️ 为ChatMessage添加MessageStatus枚举(发送中、已发送、已读)
-- ⚠️ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
-- ⚠️ 实现消息长按菜单,支持复制和删除操作
-- ⚠️ 优化DiffUtil,使用messageId作为唯一标识,提升列表更新性能(当前使用timestamp)
-- ⚠️ 在MessagesActivity中添加搜索功能,支持按会话标题和消息内容搜索
-- ⚠️ 优化消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
-- ⚠️ 添加内存泄漏防护(onDestroy中清理延迟任务)
-- ⚠️ 优化滚动行为(使用smoothScrollToPosition替代scrollToPosition)
+**待完善内容**:
+- ⚠️ 消息预览功能(处理图片/语音消息类型)- 当前仅支持文本消息
+- ⚠️ 图片/语音消息发送和接收功能
+- ⚠️ 实时消息接收(WebSocket集成,等待后端)
-**预计工作量**: 3-4天
+**预计工作量**: 3-4天(已完成)
---
@@ -609,15 +653,30 @@
### 🔵 第四优先级(功能扩展)
-#### 14. **作品功能(前端UI)** ⭐
+#### 14. **作品功能(前端UI)** ⭐ ⚠️ 部分完成
**为什么重要**: 完善个人中心功能
-- [ ] 实现作品列表UI
+- [x] 作品列表UI(ProfileActivity已实现基础UI)
+- [x] UserWorksAdapter已创建(使用演示数据)
- [ ] 实现作品发布UI(数据仅本地存储)
- [ ] 实现作品详情页面
- [ ] 实现作品编辑UI
+- [ ] 作品数据模型(WorkItem)
-**预计工作量**: 3-4天
+**已完成内容**:
+- ✅ ProfileActivity中已实现作品标签页UI
+- ✅ UserWorksAdapter已创建,支持显示作品列表
+- ✅ 作品列表使用GridLayoutManager(3列布局)
+- ✅ 支持作品/收藏/赞过三个标签页切换
+
+**待实现内容**:
+- ⚠️ 作品发布功能(当前显示"待接入"提示)
+- ⚠️ 作品详情页面(点击作品查看详情)
+- ⚠️ 作品编辑和删除功能
+- ⚠️ 作品数据模型(WorkItem),当前使用Integer列表(drawable资源ID)作为临时方案
+- ⚠️ 作品数据持久化(本地存储,等待后端API)
+
+**预计工作量**: 3-4天(部分完成,剩余2-3天)
#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
**为什么重要**: 提升传播能力
@@ -698,17 +757,6 @@
- [ ] 重构重复代码
**预计工作量**: 持续进行
-
-#### 20. **测试** ⭐
-**为什么重要**: 保证代码质量
-
-- [ ] 编写单元测试(工具类)
-- [ ] 编写 UI 测试(关键流程)
-- [ ] 性能测试
-- [ ] 兼容性测试
-
-**预计工作量**: 持续进行
-
---
### 📋 暂不实现(需等待后端)
@@ -722,22 +770,23 @@
- ❌ 支付功能(等待后端和支付SDK)
- ❌ 推流功能(如需要,等待推流SDK集成)
+**注意**: 数据持久化(Room数据库)相关功能暂不实现,当前使用SharedPreferences存储简单配置数据。
+
---
## 📅 开发时间估算
-### 第一阶段(核心修复)- 约 8-13 天
+### 第一阶段(核心修复)- 约 5-8 天
- 空状态和错误处理
- 生命周期和内存管理
- 统一加载状态
-- 数据持久化
-### 第二阶段(架构优化)- 约 12-18 天
+### 第二阶段(架构优化)- 约 8-12 天
- 前端架构优化
-- 分类筛选功能
-- 顶部标签页功能
-- 搜索功能增强
-- 消息功能完善
+- ~~分类筛选功能~~ ✅ 已完成
+- ~~顶部标签页功能~~ ✅ 已完成
+- 搜索功能增强(部分完成,剩余搜索历史和热门搜索)
+- ~~消息功能完善~~ ✅ 已完成
### 第三阶段(体验增强)- 约 9-13 天
- 引导页面
@@ -745,18 +794,20 @@
- 深色模式
- 多屏幕适配
-### 第四阶段(功能扩展)- 约 9-13 天
+### 第四阶段(功能扩展)- 约 3-6 天
- 作品功能
-- 分享功能
-- 通知功能
-- 设置页面完善
+- ~~分享功能~~ ✅ 已完成
+- ~~通知功能~~ ✅ 已完成
+- ~~设置页面完善~~ ✅ 已完成
### 第五阶段(优化提升)- 持续进行
- 性能优化
- 代码质量
- 测试
-**总计**: 约 38-57 个工作日(约 2-3 个月,按单人开发估算)
+**总计**: 约 25-39 个工作日(约 1.5-2 个月,按单人开发估算)
+
+**注意**: 部分功能已完成(分类筛选、顶部标签页、分享、通知、设置、消息功能完善),实际开发时间会相应减少。当前已完成约 60-70% 的前端功能开发。
---
@@ -765,9 +816,9 @@
如果想快速看到效果,建议按以下顺序:
1. **第1周**: 空状态 + 错误处理 + 内存泄漏修复
-2. **第2周**: 统一加载状态 + 数据持久化(Room)
-3. **第3周**: 架构优化(MVVM)+ 分类筛选
-4. **第4周**: 顶部标签页 + 搜索增强
+2. **第2周**: 统一加载状态 + 分类筛选
+3. **第3周**: 架构优化(MVVM)+ 顶部标签页
+4. **第4周**: 搜索增强 + 消息功能完善
这样可以在一个月内完成核心功能的前端完善。
@@ -776,18 +827,20 @@
## 📝 总结(前端开发视角)
### 当前状态
-- **整体完成度**: 约 60-70%(前端UI层面)
-- **核心UI功能**: 基本完成,但缺少完善
-- **用户体验**: 良好,但需要优化细节
-- **代码质量**: 中等,需要重构和优化
-- **数据层**: 仅使用 SharedPreferences,需要引入 Room
+- **整体完成度**: 约 80-85%(前端UI层面)
+- **核心UI功能**: 基本完成,大部分功能已实现前端逻辑
+- **用户体验**: 良好,已实现空状态、错误处理、加载状态等
+- **代码质量**: 中等,已实现部分工具类和统一管理
+- **数据层**: 使用 SharedPreferences 存储配置,内存缓存存储临时数据
+- **已完成功能**: 主界面(含顶部标签页、分类筛选)、直播间详情、个人资料、消息(含状态显示、长按菜单、搜索)、搜索(含实时过滤、空状态)、分享、通知、设置等
+- **最近完成**: 消息功能完善(状态显示、长按菜单、性能优化、搜索功能)
### 前端可独立完成的工作
1. ✅ **UI/UX 完善**: 空状态、错误处理、加载状态
2. ✅ **架构优化**: MVVM、Repository 模式、代码重构
-3. ✅ **数据持久化**: Room 数据库、本地缓存
-4. ✅ **功能完善**: 分类筛选、搜索增强、消息功能
-5. ✅ **体验增强**: 引导页、动画、深色模式
+3. ✅ **功能完善**: 分类筛选、搜索增强、消息功能
+4. ✅ **体验增强**: 引导页、动画、深色模式
+5. ✅ **工具类**: 分享功能、通知功能、设置页面、缓存管理
### 需要等待后端的功能
1. ❌ **API 集成**: 等待后端接口定义
@@ -798,9 +851,9 @@
### 前端开发建议
1. **立即开始**: 空状态处理、内存泄漏修复、统一加载状态
2. **第一周**: 完成基础架构和用户体验修复
-3. **第二周**: 引入 Room 数据库,实现本地数据持久化
-4. **第三周**: 架构优化,引入 MVVM 模式
-5. **第四周**: 功能完善,实现分类筛选、搜索增强等
+3. **第二周**: 架构优化,引入 MVVM 模式
+4. **第三周**: 功能完善,实现分类筛选、搜索增强等
+5. **第四周**: 体验增强,实现引导页、动画优化等
### 开发原则
- ✅ **优先前端可独立完成的工作**
@@ -808,6 +861,7 @@
- ✅ **保持代码结构便于后续对接后端**
- ✅ **注重用户体验和代码质量**
- ⏸️ **等待后端接口的功能先做UI,数据用模拟**
+- ⏸️ **暂不实现数据持久化(Room数据库),使用SharedPreferences存储简单配置**
---
diff --git a/android-app/项目进度汇报.md b/android-app/项目进度汇报.md
new file mode 100644
index 00000000..ad02bcbb
--- /dev/null
+++ b/android-app/项目进度汇报.md
@@ -0,0 +1,349 @@
+# Android 直播应用 - 项目进度汇报
+
+> **汇报时间**: 2024年
+> **项目名称**: Android 直播应用
+> **开发阶段**: 前端功能完善阶段
+
+---
+
+## 📊 项目整体进度
+
+### 完成度概览
+- **整体完成度**: **80-85%**(前端UI层面)
+- **核心功能完成度**: **85-90%**
+- **代码质量**: 中等,已实现统一工具类和错误处理机制
+
+### 主要成果
+✅ **已完成核心功能模块**: 主界面、直播间、个人中心、消息、搜索、分享、通知、设置等
+✅ **已完成基础架构**: 空状态处理、错误处理、加载状态管理、内存泄漏防护
+✅ **已完成用户体验优化**: 统一UI组件、搜索功能、消息状态显示等
+
+---
+
+## ✅ 本周/近期完成的工作
+
+### 1. 消息功能完善 ⭐⭐⭐
+**状态**: ✅ 已完成
+
+**完成内容**:
+- ✅ 实现消息状态显示(发送中、已发送、已读)
+ - 在消息气泡旁显示状态图标(时钟、单勾、双勾)
+ - 支持状态自动更新(发送中 → 已发送 → 已读)
+- ✅ 实现消息长按菜单
+ - 支持复制消息内容
+ - 支持删除消息(带确认对话框)
+- ✅ 优化消息列表性能
+ - DiffUtil使用messageId作为唯一标识,提升列表更新性能
+ - 使用smoothScrollToPosition优化滚动体验
+- ✅ 实现会话搜索功能
+ - 支持按会话标题搜索
+ - 支持按消息内容搜索
+ - 实时过滤,响应迅速
+- ✅ 内存泄漏防护
+ - 在onDestroy中清理Handler延迟任务
+ - 确保Activity销毁时资源正确释放
+
+**技术亮点**:
+- 使用ListAdapter + DiffUtil实现高效的列表更新
+- 消息状态通过Handler模拟真实发送流程
+- 搜索功能支持实时过滤,用户体验流畅
+
+---
+
+### 2. 搜索功能增强 ⭐⭐
+**状态**: ⚠️ 部分完成(70%)
+
+**完成内容**:
+- ✅ 实时搜索过滤功能
+ - 输入关键词即时显示搜索结果
+ - 支持按房间标题和主播名称搜索
+- ✅ 空状态显示
+ - 无搜索结果时显示友好提示
+ - 支持从MainActivity传递搜索关键词
+
+**待完善**:
+- ⚠️ 搜索历史功能(预计1天)
+- ⚠️ 热门搜索功能(预计1天)
+- ⚠️ 搜索建议/自动补全(预计1天)
+
+---
+
+### 3. 作品功能基础搭建 ⭐
+**状态**: ⚠️ 部分完成(30%)
+
+**完成内容**:
+- ✅ 作品列表UI已实现
+ - ProfileActivity中作品标签页UI完整
+ - UserWorksAdapter已创建
+ - 支持3列网格布局展示
+
+**待完善**:
+- ⚠️ 作品发布功能(预计2天)
+- ⚠️ 作品详情页面(预计1天)
+- ⚠️ 作品数据模型完善(预计1天)
+
+---
+
+## 📈 已完成的核心功能模块
+
+### 1. 主界面 (MainActivity) - 95%
+- ✅ 瀑布流布局展示直播间
+- ✅ 下拉刷新和自动轮询(15秒间隔)
+- ✅ 创建直播间功能
+- ✅ 搜索框和语音搜索
+- ✅ 分类标签筛选(CategoryFilterManager)
+- ✅ 顶部标签页(关注/发现/附近)
+- ✅ 底部导航栏和侧边栏菜单
+
+### 2. 直播间详情 (RoomDetailActivity) - 90%
+- ✅ 直播流播放(HLS/FLV)
+- ✅ 全屏播放和横竖屏切换
+- ✅ 聊天功能(模拟)
+- ✅ 关注按钮和观看人数显示
+- ✅ 自动重连机制
+- ✅ 分享功能
+
+### 3. 个人资料 (ProfileActivity) - 90%
+- ✅ 个人资料展示和编辑
+- ✅ 头像查看(大图预览)
+- ✅ 标签显示(地区、性别、年龄、星座)
+- ✅ 作品/收藏/赞过标签页
+- ✅ 关注/粉丝/获赞统计
+- ✅ 主页链接复制
+
+### 4. 消息功能 (MessagesActivity + ConversationActivity) - 90%
+- ✅ 会话列表展示
+- ✅ 未读消息徽章管理
+- ✅ 滑动删除/标记已读
+- ✅ 消息发送和状态显示
+- ✅ 消息长按菜单(复制、删除)
+- ✅ 会话搜索功能
+- ✅ 内存泄漏防护
+
+### 5. 搜索功能 (SearchActivity) - 70%
+- ✅ 搜索界面UI
+- ✅ 实时搜索过滤
+- ✅ 搜索结果展示
+- ✅ 空状态显示
+
+### 6. 其他已完成功能
+- ✅ 分享功能(ShareUtils工具类)
+- ✅ 通知功能(NotificationsActivity + NotificationSettingsActivity)
+- ✅ 设置页面(SettingsPageActivity)
+- ✅ 登录/注册页面
+- ✅ 用户资料查看(UserProfileReadOnlyActivity)
+
+---
+
+## 🛠️ 技术架构与工具类
+
+### 已实现的工具类
+1. **ErrorHandler** - 统一错误处理和提示
+2. **EmptyStateView** - 统一空状态组件
+3. **LoadingStateManager** - 统一加载状态管理
+4. **NetworkRequestManager** - 生命周期感知的网络请求管理
+5. **CategoryFilterManager** - 分类筛选管理
+6. **LocationDataManager** - 位置数据管理
+7. **ShareUtils** - 分享功能工具类
+8. **LocalNotificationManager** - 本地通知管理
+9. **CacheManager** - 缓存管理
+10. **UnreadMessageManager** - 未读消息管理
+
+### 数据存储
+- ✅ SharedPreferences - 用于个人资料和配置存储
+- ✅ 内存缓存 - 用于临时数据缓存
+- ✅ 网络层 - Retrofit + OkHttp 已配置完成
+
+---
+
+## ⚠️ 待完善功能(优先级排序)
+
+### 🔴 高优先级(1-2周内完成)
+
+#### 1. 搜索功能完善
+- [ ] 搜索历史(本地存储)
+- [ ] 热门搜索(模拟数据)
+- [ ] 搜索建议(自动补全)
+- **预计工作量**: 2-3天
+
+#### 2. 作品功能完善
+- [ ] 作品发布UI
+- [ ] 作品详情页面
+- [ ] 作品数据模型
+- **预计工作量**: 3-4天
+
+#### 3. 前端架构优化
+- [ ] 引入 MVVM 架构(ViewModel + LiveData)
+- [ ] 实现 Repository 模式
+- [ ] 提取公共基类 Activity
+- **预计工作量**: 5-7天
+
+---
+
+### 🟡 中优先级(2-4周内完成)
+
+#### 4. 用户体验增强
+- [ ] 引导页面和帮助
+- [ ] 过渡动画和交互优化
+- [ ] 深色模式支持
+- [ ] 多屏幕适配
+- **预计工作量**: 9-13天
+
+---
+
+### 🟢 低优先级(持续优化)
+
+#### 5. 性能优化
+- [ ] 图片加载优化(Glide配置)
+- [ ] 列表滚动性能优化
+- [ ] 请求去重机制
+- [ ] 内存优化
+- **预计工作量**: 3-4天
+
+#### 6. 代码质量提升
+- [ ] 提取硬编码字符串到资源文件
+- [ ] 添加代码注释
+- [ ] 统一代码风格
+- [ ] 重构重复代码
+- **预计工作量**: 持续进行
+
+---
+
+## 📋 等待后端支持的功能
+
+以下功能需要后端API支持,建议后端开发完成后再实现:
+
+- ❌ 后端API完整集成(等待后端接口)
+- ❌ 实时通信(WebSocket,等待后端)
+- ❌ 真实数据同步(等待后端)
+- ❌ 用户登录/注册(等待后端)
+- ❌ 支付功能(等待后端和支付SDK)
+- ❌ 推流功能(如需要,等待推流SDK集成)
+
+**当前策略**: 使用演示数据模拟后端接口,保持代码结构便于后续对接。
+
+---
+
+## 📅 下一步工作计划
+
+### 本周计划(优先级排序)
+
+1. **搜索功能完善**(2-3天)
+ - 实现搜索历史(SharedPreferences存储)
+ - 实现热门搜索(模拟数据)
+ - 添加搜索建议功能
+
+2. **作品功能完善**(3-4天)
+ - 实现作品发布UI
+ - 实现作品详情页面
+ - 完善作品数据模型
+
+3. **代码优化**(持续)
+ - 修复已知问题
+ - 优化代码结构
+ - 添加必要注释
+
+### 下周计划
+
+1. **前端架构优化**(5-7天)
+ - 引入 MVVM 架构
+ - 实现 Repository 模式
+ - 提取公共基类
+
+2. **用户体验增强**(开始)
+ - 引导页面
+ - 过渡动画优化
+
+---
+
+## 📊 统计数据
+
+### 代码统计
+- **Activity数量**: 30+ 个
+- **布局文件**: 50+ 个
+- **工具类**: 10+ 个
+- **适配器**: 15+ 个
+
+### 功能统计
+- **已完成功能模块**: 8+ 个核心模块
+- **已完成工具类**: 10+ 个
+- **已完成页面**: 25+ 个页面
+
+### 完成度统计
+- **整体完成度**: 80-85%
+- **核心功能完成度**: 85-90%
+- **UI完成度**: 90%+
+- **功能完成度**: 75-80%
+
+---
+
+## 🎯 项目亮点
+
+### 1. 完善的用户体验
+- ✅ 统一的空状态处理
+- ✅ 统一的错误提示机制
+- ✅ 统一的加载状态管理
+- ✅ 友好的用户交互反馈
+
+### 2. 良好的代码架构
+- ✅ 工具类统一管理
+- ✅ 生命周期感知的网络请求
+- ✅ 内存泄漏防护
+- ✅ 性能优化(DiffUtil、smoothScroll等)
+
+### 3. 完整的功能实现
+- ✅ 消息功能完善(状态显示、长按菜单、搜索)
+- ✅ 搜索功能基础实现
+- ✅ 分享、通知、设置等辅助功能
+
+### 4. 可扩展性
+- ✅ 代码结构便于对接后端API
+- ✅ 使用演示数据模拟,便于测试
+- ✅ 预留接口便于后续扩展
+
+---
+
+## ⚠️ 风险与挑战
+
+### 1. 后端依赖
+- **风险**: 部分功能需要等待后端API支持
+- **应对**: 使用演示数据模拟,保持代码结构便于对接
+
+### 2. 数据持久化
+- **风险**: 当前仅使用SharedPreferences和内存缓存
+- **应对**: 等待后端API后实现完整的数据同步机制
+
+### 3. 实时通信
+- **风险**: WebSocket功能需要后端支持
+- **应对**: 当前使用模拟数据,等待后端WebSocket服务
+
+---
+
+## 💡 总结
+
+### 当前状态
+项目前端开发进展顺利,**整体完成度达到80-85%**。核心功能模块基本完成,用户体验良好,代码质量持续提升。
+
+### 主要成就
+1. ✅ **消息功能完善** - 实现了完整的消息状态显示、长按菜单、搜索等功能
+2. ✅ **搜索功能基础** - 实现了实时搜索过滤和空状态显示
+3. ✅ **工具类体系** - 建立了完善的工具类体系,提升代码复用性
+4. ✅ **用户体验** - 统一了空状态、错误处理、加载状态等用户体验
+
+### 下一步重点
+1. 完善搜索功能(搜索历史、热门搜索)
+2. 完善作品功能(发布、详情页面)
+3. 前端架构优化(MVVM、Repository模式)
+
+### 预计完成时间
+- **搜索功能完善**: 2-3天
+- **作品功能完善**: 3-4天
+- **前端架构优化**: 5-7天
+- **总计**: 约10-14个工作日(2-3周)
+
+---
+
+**汇报人**: [您的姓名]
+**日期**: 2024年
+**版本**: v1.0
+
diff --git a/java-backend/src/main/java/com/example/livestreaming/dto/CreateRoomRequest.java b/java-backend/src/main/java/com/example/livestreaming/dto/CreateRoomRequest.java
new file mode 100644
index 00000000..f2c464ca
--- /dev/null
+++ b/java-backend/src/main/java/com/example/livestreaming/dto/CreateRoomRequest.java
@@ -0,0 +1,17 @@
+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;
+
+ @NotBlank(message = "直播类型不能为空")
+ private String type;
+}
diff --git a/java-backend/src/main/java/com/example/livestreaming/dto/RoomResponse.java b/java-backend/src/main/java/com/example/livestreaming/dto/RoomResponse.java
new file mode 100644
index 00000000..04a381d9
--- /dev/null
+++ b/java-backend/src/main/java/com/example/livestreaming/dto/RoomResponse.java
@@ -0,0 +1,49 @@
+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 type;
+ 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.setType(room.getType());
+ 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;
+ }
+}
diff --git a/java-backend/target/classes/com/example/livestreaming/dto/CreateRoomRequest.class b/java-backend/target/classes/com/example/livestreaming/dto/CreateRoomRequest.class
new file mode 100644
index 0000000000000000000000000000000000000000..c1bd224daa209e8fc518ae99da80a0c4a95fe1fd
GIT binary patch
literal 2403
zcmeHH*=`dt6unN;CY=TnLKmPcQ`Ur~LlI9Dkf0P)DHPO}O7P|+R^xPL96U}0Uxow{
zyz>qG06&2^o}mkBNP{%uiHEUuJ@+0T+vnV`KR$jYq6c&*PZ>sDQw5%Q$AhjfJYV)j
zWVGOc47a_OQQk5FBi5A)*2UW%e[sNCUw?)e-^FB%=uG>pcKG`?U|sqg>WFd8^Z
zjONyRp^<@D4f`^Z4PQJ8LuI&;DvTI8^)+Q418oPX`9MSw-v&}KVIuSz4-n?e`!1%!
zwlJH?R#Vm5pv@|EUj+@d<2@6h&|u#JY;IY#M4nDCD(&-I(OT&Oon-9Wt5Azb`F+uH
zaUosl$K>3uR$v=M_grTpL3Nwla(5fT-K#Ll<*udoKk46J!4q!y@JDf|Ow&c0p;<=b
z_hl%}Lq^lp!`i8BK^}D(inU(Q5PFm2?X99!llxm-OB?@M%$c@?bQkM?X&*?j1O>wx
zFj*q)0-a|xKU{#&AFN@epO_%pGeP;eFm2U($u;nd&>&U9--8>V(&opN<8U$ek@Va7
z6B94h-Y{Yc9)reLY#wZ_Oj~FywRzOG9%7WO)>g4#RATRVVXHt3jJ^*)MC_u?ws2+8
zRob{6zQgsv$8#nQq$R#v_`^Fp6T8i0xCqisL(|$&J>3*frIpCcp*Fc-yA0%3x3rgN
zo^Va*x>?H57>y&!BeHV=bB3G&YmJ0K=j!G7RrzZ
NX=Uj4o=_Kwegb1&{uclM
literal 0
HcmV?d00001
diff --git a/java-backend/target/classes/com/example/livestreaming/dto/RoomResponse.class b/java-backend/target/classes/com/example/livestreaming/dto/RoomResponse.class
new file mode 100644
index 0000000000000000000000000000000000000000..7d791c16bfb46753eb8a06c77f495bee4940e34d
GIT binary patch
literal 5364
zcmeHJTX!2Z5FR2*eH`@ZCXaJ#%^!I_TZ+R72W)sdp%a&N`TX7t{yrtSMe
zSq*OWrz>IEmJVz-OBZP|N0;de;(E*R9d(D%LjLsT3w3Ox=JVpH2KbI%a_mTDWp_
z*v==N$BCbZ;N^#4%~tm4r>QBU?H~tSd$DYrM!wn86qKe987+-Y6vz`uhgcC(!x34f
zG=|};T>qb;tciR9Tr~}4{Wx7g4Z_lMD+A67m-cCEs*e;h*G=TZvvOA=2y<#0()bD|
z(EXni`e*1w|DSOpg<(j%@%(}2gJ~#E_!G}pwLS;^afb}dz#%g{VpK*e`7`>XO(pp+
zhqfm46Py)R8)Y4HCcrtMz;!(jtRhsKOHD&|(ANBlt`Z0UiV#LPk9wNEVX=+T$8UII
zugxvT>wwiX{T6$5QSn4wROF^m7;Y@%exTaxH?&YdJYL$v>2LTUK^ev<7XL{$_X1`!f5#B&tn}vZ*
zCkSxQ_2b5incPDCzR7hp7tj*n_M_C#Qj8LmL@!3Eo=o>-rYEyKIY~J)Z;GakoayOj
zdvcEEp}#~KdW9|kZvmraSV=%;|D=l#=U@AsF8yuRzJYHNdmvh*H;snq9ms_QSrAKT
zZ=t=7b`_1$yYwC)^XThk1xOA5Dd06o2){f8K55`{;VimFA7D14;uvV9vC%$?6z$_k
z(LRY3Z4wcG-YJY9x9jv-glIP+_4M;d(QeX8gr1h^3hw=|w-D>Ug>g?oKe_~Xl4kKN
zZbc8AGQhW^2TmK{JJADY4Dc7x17{8Jm$Vx7;Y}Lguc#6=@RR{wi#`-H26#Ps;8_EF
zmo}mf#hd}YN7bl-zXoue?ne(?GdtdlG{