# Design Document ## Overview 本设计文档描述了在Android直播应用中添加"热门"Tab功能的技术实现方案。该功能将在"发现"页面的分类标签栏(categoryTabs)中添加一个"热门"Tab,位于最左侧("推荐"Tab左边),用于展示后台管理员标记为热门的作品内容。 ## Architecture ### 系统架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ MainActivity │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ topTabs: 关注 | 发现 | 附近 │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ categoryTabs: 热门 | 推荐 | 直播 | 视频 | 音乐 | 游戏│ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ RecyclerView (WorksAdapter) │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ 作品1 │ │ 作品2 │ │ 作品3 │ │ │ │ │ │ 🔥热门 │ │ 🔥热门 │ │ 🔥热门 │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ ApiService ↓ GET /api/works/hot?page=1&pageSize=20 ↓ 后端服务器 ↓ 返回 is_hot=1 的作品列表 ``` ### 数据流 1. **用户操作** → 点击"热门"Tab 2. **MainActivity** → 调用 `loadHotWorks(page)` 方法 3. **ApiService** → 发送 GET 请求到 `/api/works/hot` 4. **后端服务器** → 查询 `is_hot=1` 的作品,按 `hot_time` 降序排序 5. **ApiService** → 接收 `PageResponse` 数据 6. **WorksAdapter** → 渲染作品列表 7. **UI** → 显示热门作品,每个作品卡片显示🔥标识 ## Components and Interfaces ### 1. MainActivity 修改 #### 新增成员变量 ```java // 热门作品相关 private final List hotWorks = new ArrayList<>(); // 热门作品列表 private int currentHotWorksPage = 1; // 当前热门作品页码 private boolean isLoadingHotWorks = false; // 是否正在加载热门作品 private boolean hasMoreHotWorks = true; // 是否还有更多热门作品 ``` #### 新增方法 ```java /** * 加载热门作品列表 * @param page 页码(从1开始) */ private void loadHotWorks(int page) { if (isLoadingHotWorks) return; isLoadingHotWorks = true; // 显示加载状态 if (page == 1) { if (hotWorks.isEmpty()) { LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6); } } ApiClient.getService(getApplicationContext()) .getHotWorks(page, 20) .enqueue(new Callback>>() { @Override public void onResponse(Call>> call, Response>> response) { isLoadingHotWorks = false; binding.swipeRefresh.setRefreshing(false); if (response.isSuccessful() && response.body() != null && response.body().isOk()) { PageResponse pageData = response.body().getData(); if (pageData != null && pageData.getList() != null) { if (page == 1) { hotWorks.clear(); } hotWorks.addAll(pageData.getList()); // 检查是否还有更多数据 hasMoreHotWorks = pageData.getList().size() >= 20; // 更新UI updateHotWorksUI(); } } else { showErrorState("加载失败,请重试"); } } @Override public void onFailure(Call>> call, Throwable t) { isLoadingHotWorks = false; binding.swipeRefresh.setRefreshing(false); showErrorState("网络错误,请重试"); } }); } /** * 更新热门作品UI */ private void updateHotWorksUI() { if (hotWorks.isEmpty()) { showEmptyState("暂无热门作品"); } else { hideEmptyState(); hideErrorState(); // 转换为FeedItem列表 List feedItems = new ArrayList<>(); for (WorksResponse work : hotWorks) { feedItems.add(FeedItem.fromWorks(work)); } adapter.submitList(feedItems); } } /** * 刷新热门作品 */ private void refreshHotWorks() { currentHotWorksPage = 1; hasMoreHotWorks = true; loadHotWorks(1); } /** * 加载更多热门作品 */ private void loadMoreHotWorks() { if (!isLoadingHotWorks && hasMoreHotWorks) { currentHotWorksPage++; loadHotWorks(currentHotWorksPage); } } ``` #### 修改 categoryTabs 初始化 在 `activity_main.xml` 中的 `categoryTabs` 添加"热门"Tab: ```xml ... ``` #### 修改 categoryTabs 监听器 ```java binding.categoryTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { if (tab == null) return; CharSequence title = tab.getText(); currentCategory = title != null ? title.toString() : "热门"; // 保存选中的分类 CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory); // 根据分类加载数据 if ("热门".equals(currentCategory)) { // 加载热门作品 refreshHotWorks(); } else { // 应用其他分类筛选 applyCategoryFilterWithAnimation(currentCategory); } } @Override public void onTabUnselected(TabLayout.Tab tab) {} @Override public void onTabReselected(TabLayout.Tab tab) { if (tab == null) return; CharSequence title = tab.getText(); currentCategory = title != null ? title.toString() : "热门"; if ("热门".equals(currentCategory)) { // 刷新热门作品 refreshHotWorks(); } else { applyCategoryFilterWithAnimation(currentCategory); } } }); ``` #### 修改下拉刷新逻辑 ```java binding.swipeRefresh.setOnRefreshListener(() -> { if ("热门".equals(currentCategory)) { refreshHotWorks(); } else { fetchDiscoverRooms(); } }); ``` #### 修改滚动加载更多逻辑 ```java binding.roomsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy <= 0) return; RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); if (!(lm instanceof GridLayoutManager)) return; GridLayoutManager glm = (GridLayoutManager) lm; int total = glm.getItemCount(); int lastVisible = glm.findLastVisibleItemPosition(); if (total <= 0) return; if (lastVisible >= total - 4) { if ("热门".equals(currentCategory)) { loadMoreHotWorks(); } else { // 其他分类的加载逻辑 } } } }); ``` ### 2. WorksAdapter 修改 #### 添加热门标识显示 在 `item_works.xml` 布局中添加热门标识: ```xml ``` #### 修改 ViewHolder 的 bind 方法 ```java public void bind(WorksResponse works, OnWorksClickListener listener) { if (works == null) return; // ... 现有代码 ... // 显示热门标识 ImageView hotBadge = itemView.findViewById(R.id.hotBadge); if (hotBadge != null) { Boolean isHot = works.getIsHot(); if (isHot != null && isHot) { hotBadge.setVisibility(View.VISIBLE); } else { hotBadge.setVisibility(View.GONE); } } // ... 现有代码 ... } ``` ### 3. WorksResponse 数据模型 确保 `WorksResponse` 类包含 `isHot` 字段: ```java public class WorksResponse { private Long id; private String title; private String coverUrl; private String videoUrl; private String type; private Integer userId; private String userName; private String authorName; private Integer likeCount; private Boolean isLiked; private Boolean isHot; // 新增:是否热门 private String hotTime; // 新增:设置热门的时间 // Getters and Setters public Boolean getIsHot() { return isHot; } public void setIsHot(Boolean isHot) { this.isHot = isHot; } public String getHotTime() { return hotTime; } public void setHotTime(String hotTime) { this.hotTime = hotTime; } } ``` ### 4. ApiService 接口 已存在的接口: ```java /** * 获取热门作品列表 */ @GET("api/works/hot") Call>> getHotWorks( @Query("page") int page, @Query("pageSize") int pageSize); ``` ## Data Models ### WorksResponse ```java public class WorksResponse { private Long id; // 作品ID private String title; // 作品标题 private String coverUrl; // 封面图片URL private String videoUrl; // 视频URL private String type; // 作品类型(VIDEO/IMAGE) private Integer userId; // 用户ID private String userName; // 用户名 private String authorName; // 作者名 private Integer likeCount; // 点赞数 private Boolean isLiked; // 当前用户是否已点赞 private Boolean isHot; // 是否热门 private String hotTime; // 设置热门的时间 } ``` ### PageResponse ```java public class PageResponse { private List list; // 数据列表 private Integer total; // 总数 private Integer page; // 当前页码 private Integer pageSize; // 每页大小 private Integer totalPage; // 总页数 } ``` ### ApiResponse ```java public class ApiResponse { private Integer code; // 响应码(200表示成功) private String message; // 响应消息 private T data; // 响应数据 public boolean isOk() { return code != null && code == 200; } } ``` ## Data Models ### 数据库字段(后端) ```sql -- eb_works 表 ALTER TABLE `eb_works` ADD COLUMN `is_hot` tinyint DEFAULT 0 COMMENT '是否热门:1-是 0-否' AFTER `status`, ADD COLUMN `hot_time` datetime DEFAULT NULL COMMENT '设置热门的时间' AFTER `is_hot`, ADD INDEX `idx_is_hot` (`is_hot`); ``` ### API 请求参数 ``` GET /api/works/hot?page=1&pageSize=20 ``` ### API 响应格式 ```json { "code": 200, "message": "success", "data": { "list": [ { "id": 1, "title": "精彩作品", "coverUrl": "https://example.com/cover.jpg", "videoUrl": "https://example.com/video.mp4", "type": "VIDEO", "userId": 10, "userName": "用户A", "authorName": "作者A", "likeCount": 100, "isLiked": false, "isHot": true, "hotTime": "2026-01-08 10:00:00" } ], "total": 50, "page": 1, "pageSize": 20, "totalPage": 3 } } ``` ## Error Handling ### 错误场景处理 1. **网络错误** - 显示错误提示:"网络错误,请重试" - 提供重试按钮 - 保留已加载的数据 2. **API 返回错误** - 显示服务器返回的错误消息 - 提供重试按钮 3. **空数据** - 显示空状态视图:"暂无热门作品" - 提供刷新按钮 4. **加载超时** - 设置合理的超时时间(30秒) - 显示超时提示 - 提供重试选项 ### 错误处理代码 ```java @Override public void onFailure(Call>> call, Throwable t) { isLoadingHotWorks = false; binding.swipeRefresh.setRefreshing(false); String errorMsg = "网络错误"; if (t != null) { if (t.getMessage() != null && t.getMessage().contains("Unable to resolve host")) { errorMsg = "无法连接服务器,请检查网络"; } else if (t.getMessage() != null && t.getMessage().contains("timeout")) { errorMsg = "连接超时,请重试"; } else if (t.getMessage() != null && t.getMessage().contains("Connection refused")) { errorMsg = "连接被拒绝,请确保服务器已启动"; } } showErrorState(errorMsg); } ``` ## Testing Strategy ### Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* #### Property 1: 热门Tab仅显示作品内容 *For any* 热门Tab中显示的数据项,该数据项应该是WorksResponse类型,不应包含Room(直播间)类型的数据 **Validates: Requirements 2.3** #### Property 2: 热门作品显示热门标识 *For any* 在热门Tab中显示的作品,如果该作品的isHot字段为true,则作品卡片上应该显示热门图标(hotBadge可见) **Validates: Requirements 3.1** ### Unit Tests 1. **MainActivity 单元测试** - 测试"热门"Tab在categoryTabs的索引为0(最左侧)**Validates: Requirements 1.1, 1.2** - 测试categoryTabs包含6个Tab,顺序为:热门、推荐、直播、视频、音乐、游戏 **Validates: Requirements 1.4** - 测试进入"发现"Tab时默认选中"热门"Tab **Validates: Requirements 1.3** - 测试点击"热门"Tab调用loadHotWorks方法 **Validates: Requirements 2.1** - 测试loadHotWorks方法调用ApiService.getHotWorks接口 **Validates: Requirements 2.1** - 测试接收到数据后WorksAdapter被正确更新 **Validates: Requirements 2.2** - 测试加载中显示骨架屏或加载动画 **Validates: Requirements 2.4** - 测试加载失败显示错误提示 **Validates: Requirements 2.5** - 测试空数据显示"暂无热门作品" **Validates: Requirements 2.6** - 测试下拉刷新触发refreshHotWorks方法 **Validates: Requirements 4.1** - 测试刷新时SwipeRefreshLayout.isRefreshing为true **Validates: Requirements 4.2** - 测试刷新完成后isRefreshing为false且列表更新 **Validates: Requirements 4.3** - 测试刷新失败显示错误提示且保留原数据 **Validates: Requirements 4.4** - 测试滚动到底部触发loadMoreHotWorks方法 **Validates: Requirements 5.1** - 测试加载更多时显示底部加载指示器 **Validates: Requirements 5.2** - 测试hasMoreHotWorks为false时显示"没有更多内容" **Validates: Requirements 5.3** - 测试加载更多失败显示错误提示 **Validates: Requirements 5.4** - 测试切换Tab后hotWorks列表数据保留 **Validates: Requirements 6.1** - 测试返回热门Tab后列表数据和滚动位置恢复 **Validates: Requirements 6.2** - 测试停留超过5分钟后返回触发自动刷新 **Validates: Requirements 6.3** 2. **WorksAdapter 单元测试** - 测试isHot=true时hotBadge可见 **Validates: Requirements 3.1** - 测试isHot=false时hotBadge不可见 **Validates: Requirements 3.1** - 测试点击作品触发onWorksClick回调 **Validates: Requirements 3.3** - 测试点击作品启动WorkDetailActivity **Validates: Requirements 3.3** 3. **API 接口测试** - 测试getHotWorks接口返回PageResponse - 测试page和pageSize参数正确传递 - 测试错误响应正确处理 ### Integration Tests 1. **端到端测试** - 测试从点击"热门"Tab到显示作品列表的完整流程 - 测试下拉刷新功能 - 测试上拉加载更多功能 - 测试点击作品跳转到详情页 2. **UI 测试** - 测试"热门"Tab在最左侧显示 - 测试热门标识🔥正确显示 - 测试加载动画正确显示 - 测试空状态和错误状态正确显示 ### 测试数据准备 ```sql -- 准备测试数据:设置一些作品为热门 UPDATE eb_works SET is_hot = 1, hot_time = NOW() WHERE id IN (1, 2, 3, 4, 5); ``` ### 测试用例 | 测试用例 | 输入 | 预期输出 | |---------|------|---------| | 加载热门作品 | page=1, pageSize=20 | 返回20条热门作品 | | 空数据 | 数据库无热门作品 | 显示"暂无热门作品" | | 网络错误 | 网络断开 | 显示"网络错误,请重试" | | 下拉刷新 | 下拉列表 | 重新加载第1页数据 | | 上拉加载更多 | 滚动到底部 | 加载下一页数据 | | 点击作品 | 点击作品卡片 | 跳转到作品详情页 |