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{