Merge branch 'master' of http://115.190.64.57:8000/xiaozhang/zhibo into IM-gift
# Conflicts: # Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java # android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java # android-app/app/src/main/res/layout/dialog_make_wish.xml # android-app/app/src/main/res/layout/dialog_view_wish.xml
This commit is contained in:
commit
c2e2684f52
304
.kiro/specs/wishtree-mobile/requirements.md
Normal file
304
.kiro/specs/wishtree-mobile/requirements.md
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
# 许愿树移动端设计规范
|
||||||
|
|
||||||
|
> 版本:V2.1
|
||||||
|
> 更新日期:2025-12-31
|
||||||
|
> 设计范围:Android移动端许愿树功能完善
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、设计原则
|
||||||
|
|
||||||
|
1. **不修改后端**:复用现有API,不做任何后端改动
|
||||||
|
2. **不修改管理端**:管理端功能保持不变
|
||||||
|
3. **功能完善**:确保所有功能逻辑正确
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、现有后端API(不可修改)
|
||||||
|
|
||||||
|
| 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 获取节日列表 | GET | `/api/front/wishtree/festivals` | 获取所有启用的节日 |
|
||||||
|
| 获取当前节日 | GET | `/api/front/wishtree/festivals/current` | 获取当前进行中的节日 |
|
||||||
|
| 获取心愿列表 | GET | `/api/front/wishtree/wishes` | 分页获取心愿列表 |
|
||||||
|
| 获取我的心愿 | GET | `/api/front/wishtree/wishes/my` | 获取当前用户的心愿 |
|
||||||
|
| 发布心愿 | POST | `/api/front/wishtree/wishes` | 发布新心愿 |
|
||||||
|
| 获取心愿详情 | GET | `/api/front/wishtree/wishes/{id}` | 获取心愿详情 |
|
||||||
|
| 删除心愿 | DELETE | `/api/front/wishtree/wishes/{id}` | 删除我的心愿 |
|
||||||
|
| 点赞心愿 | POST | `/api/front/wishtree/wishes/{id}/like` | 点赞 |
|
||||||
|
| 取消点赞 | DELETE | `/api/front/wishtree/wishes/{id}/like` | 取消点赞 |
|
||||||
|
| 获取评论 | GET | `/api/front/wishtree/wishes/{id}/comments` | 获取评论列表 |
|
||||||
|
| 发表评论 | POST | `/api/front/wishtree/wishes/{id}/comments` | 发表评论 |
|
||||||
|
| 获取背景素材 | GET | `/api/front/wishtree/backgrounds` | 获取背景素材列表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、用户故事与验收标准
|
||||||
|
|
||||||
|
### US-001: 查看许愿树主页
|
||||||
|
|
||||||
|
**作为** 用户
|
||||||
|
**我想要** 进入许愿树页面查看许愿树和我的心愿
|
||||||
|
**以便于** 了解当前活动和管理我的心愿
|
||||||
|
|
||||||
|
**验收标准:**
|
||||||
|
- [x] 页面显示许愿树背景图
|
||||||
|
- [x] 显示当前节日活动横幅(调用 `/festivals/current`)
|
||||||
|
- [x] 显示我的心愿卡片(调用 `/wishes/my`,最多7个)
|
||||||
|
- [x] 显示"添加心愿"按钮
|
||||||
|
- [x] 底部导航栏正确高亮"许愿树"选项
|
||||||
|
- [x] 显示倒计时
|
||||||
|
- [x] 显示祈愿值统计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### US-002: 发布心愿
|
||||||
|
|
||||||
|
**作为** 已登录用户
|
||||||
|
**我想要** 发布一个新的心愿
|
||||||
|
**以便于** 表达我的愿望
|
||||||
|
|
||||||
|
**验收标准:**
|
||||||
|
- [x] 点击"添加心愿"或空白卡片弹出许愿对话框
|
||||||
|
- [x] 输入框限制50字,显示字数统计
|
||||||
|
- [x] 未登录时提示登录并跳转登录页
|
||||||
|
- [x] 调用 `POST /wishes` 发布心愿
|
||||||
|
- [x] 提交成功后显示成功动画
|
||||||
|
- [x] 心愿列表自动刷新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### US-003: 查看我的心愿
|
||||||
|
|
||||||
|
**作为** 用户
|
||||||
|
**我想要** 查看我已发布的心愿详情
|
||||||
|
**以便于** 回顾我的愿望
|
||||||
|
|
||||||
|
**验收标准:**
|
||||||
|
- [x] 点击心愿卡片弹出详情对话框
|
||||||
|
- [x] 显示心愿内容、点赞数、评论数
|
||||||
|
- [x] 提供"删除心愿"和"愿望达成"操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### US-004: 删除心愿
|
||||||
|
|
||||||
|
**作为** 用户
|
||||||
|
**我想要** 删除我不想要的心愿
|
||||||
|
**以便于** 管理我的心愿列表
|
||||||
|
|
||||||
|
**验收标准:**
|
||||||
|
- [x] 点击"删除心愿"弹出确认对话框
|
||||||
|
- [x] 确认后调用 `DELETE /wishes/{id}` 删除
|
||||||
|
- [x] 删除成功后刷新列表
|
||||||
|
- [x] 显示删除成功提示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### US-005: 愿望达成
|
||||||
|
|
||||||
|
**作为** 用户
|
||||||
|
**我想要** 标记我的愿望已达成
|
||||||
|
**以便于** 庆祝愿望实现
|
||||||
|
|
||||||
|
**验收标准:**
|
||||||
|
- [x] 点击"愿望达成"弹出确认对话框
|
||||||
|
- [x] 确认后调用 `DELETE /wishes/{id}` 删除心愿
|
||||||
|
- [x] 显示庆祝动画(持续2秒)
|
||||||
|
- [x] 刷新心愿列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### US-006: 查看节日活动
|
||||||
|
|
||||||
|
**作为** 用户
|
||||||
|
**我想要** 看到当前进行中的节日活动
|
||||||
|
**以便于** 参与节日许愿
|
||||||
|
|
||||||
|
**验收标准:**
|
||||||
|
- [x] 页面顶部显示活动横幅
|
||||||
|
- [x] 横幅显示节日名称
|
||||||
|
- [x] 显示倒计时
|
||||||
|
- [x] 无活动时显示默认文案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、移动端文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
android-app/app/src/main/java/com/example/livestreaming/
|
||||||
|
├── WishTreeActivity.java # 许愿树主页面
|
||||||
|
└── net/
|
||||||
|
├── ApiService.java # API接口定义
|
||||||
|
├── WishtreeRequest.java # 请求类
|
||||||
|
└── WishtreeResponse.java # 响应类
|
||||||
|
|
||||||
|
android-app/app/src/main/res/layout/
|
||||||
|
├── activity_wish_tree.xml # 主页面布局
|
||||||
|
├── dialog_make_wish.xml # 许愿对话框
|
||||||
|
├── dialog_view_wish.xml # 查看心愿对话框
|
||||||
|
├── dialog_wish_success.xml # 成功提示对话框
|
||||||
|
└── dialog_wish_complete.xml # 愿望达成对话框
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、数据模型
|
||||||
|
|
||||||
|
### 5.1 WishtreeResponse.java
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class WishtreeResponse {
|
||||||
|
// 节日信息
|
||||||
|
public static class Festival {
|
||||||
|
public int id;
|
||||||
|
public String name;
|
||||||
|
public String icon;
|
||||||
|
public String startDate;
|
||||||
|
public String endDate;
|
||||||
|
public int isLunar;
|
||||||
|
public String themeColor;
|
||||||
|
public int status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心愿信息
|
||||||
|
public static class Wish {
|
||||||
|
public long id;
|
||||||
|
public int uid;
|
||||||
|
public String nickname;
|
||||||
|
public String avatar;
|
||||||
|
public int festivalId;
|
||||||
|
public String festivalName;
|
||||||
|
public String festivalIcon;
|
||||||
|
public String content;
|
||||||
|
public int backgroundId;
|
||||||
|
public String backgroundImage;
|
||||||
|
public int status; // 0待审核 1通过 2拒绝
|
||||||
|
public int likeCount;
|
||||||
|
public int commentCount;
|
||||||
|
public Boolean isLiked;
|
||||||
|
public String createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
public static class WishPage {
|
||||||
|
public List<Wish> list;
|
||||||
|
public int total;
|
||||||
|
public int pageNum;
|
||||||
|
public int pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 WishtreeRequest.java
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class WishtreeRequest {
|
||||||
|
// 发布心愿请求
|
||||||
|
public static class PublishWish {
|
||||||
|
public Integer festivalId;
|
||||||
|
public String content;
|
||||||
|
public Integer backgroundId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、核心交互流程
|
||||||
|
|
||||||
|
### 6.1 页面加载
|
||||||
|
|
||||||
|
```
|
||||||
|
onCreate()
|
||||||
|
├── 初始化ApiService
|
||||||
|
├── 启动横幅倒计时
|
||||||
|
├── 设置底部导航
|
||||||
|
├── 设置心愿卡片点击事件
|
||||||
|
├── loadCurrentFestival() → 更新横幅文字
|
||||||
|
└── loadMyWishes() → 更新心愿卡片、祈愿值
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 发布心愿
|
||||||
|
|
||||||
|
```
|
||||||
|
点击"添加心愿"
|
||||||
|
├── checkLogin() → 未登录跳转登录页
|
||||||
|
├── showMakeWishInputDialog()
|
||||||
|
├── 输入内容(≤50字)
|
||||||
|
├── publishWish() → POST /wishes
|
||||||
|
├── 成功 → showSuccessDialog() → loadMyWishes()
|
||||||
|
└── 失败 → Toast提示
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 删除/达成心愿
|
||||||
|
|
||||||
|
```
|
||||||
|
点击心愿卡片
|
||||||
|
├── showViewWishDialog()
|
||||||
|
├── 点击"删除心愿"
|
||||||
|
│ ├── 确认对话框
|
||||||
|
│ ├── DELETE /wishes/{id}
|
||||||
|
│ └── 成功 → Toast → loadMyWishes()
|
||||||
|
└── 点击"愿望达成"
|
||||||
|
├── 确认对话框
|
||||||
|
├── DELETE /wishes/{id}
|
||||||
|
└── 成功 → 庆祝动画 → loadMyWishes()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、错误处理
|
||||||
|
|
||||||
|
| 场景 | 处理方式 |
|
||||||
|
|------|----------|
|
||||||
|
| 网络错误 | Toast "网络错误" |
|
||||||
|
| 业务错误 | Toast 显示后端返回的message |
|
||||||
|
| 未登录 | 跳转LoginActivity |
|
||||||
|
| 空数据 | 显示空列表,卡片显示空白 |
|
||||||
|
| 内容为空 | Toast "请输入心愿内容" |
|
||||||
|
| 内容超长 | Toast "心愿内容不能超过50字" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、实现任务清单
|
||||||
|
|
||||||
|
### Phase 1: 数据层 ✅
|
||||||
|
- [x] WishtreeResponse.java - 响应类
|
||||||
|
- [x] WishtreeRequest.java - 请求类
|
||||||
|
- [x] ApiService.java - 接口定义
|
||||||
|
|
||||||
|
### Phase 2: UI层 ✅
|
||||||
|
- [x] activity_wish_tree.xml - 主页面
|
||||||
|
- [x] dialog_make_wish.xml - 许愿对话框
|
||||||
|
- [x] dialog_view_wish.xml - 查看心愿对话框
|
||||||
|
- [x] dialog_wish_success.xml - 成功提示
|
||||||
|
- [x] dialog_wish_complete.xml - 愿望达成
|
||||||
|
|
||||||
|
### Phase 3: 逻辑层 ✅
|
||||||
|
- [x] WishTreeActivity.java - 核心逻辑
|
||||||
|
- [x] 页面加载与数据刷新
|
||||||
|
- [x] 发布心愿功能
|
||||||
|
- [x] 删除心愿功能
|
||||||
|
- [x] 愿望达成功能
|
||||||
|
- [x] 登录检查
|
||||||
|
- [x] 错误处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、测试要点
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
- [x] 页面正常加载
|
||||||
|
- [x] 节日信息正确显示
|
||||||
|
- [x] 心愿卡片正确显示(最多7个)
|
||||||
|
- [x] 发布心愿成功
|
||||||
|
- [x] 删除心愿成功
|
||||||
|
- [x] 愿望达成功能正常
|
||||||
|
- [x] 底部导航切换正常
|
||||||
|
|
||||||
|
### 异常测试
|
||||||
|
- [x] 网络断开时的提示
|
||||||
|
- [x] 未登录时跳转登录页
|
||||||
|
- [x] 空数据时的显示
|
||||||
|
- [x] 心愿内容校验(空、超长)
|
||||||
|
|
@ -68,14 +68,6 @@ export function wishTreeNodeListApi(params) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 节点详情
|
|
||||||
export function wishTreeNodeInfoApi(id) {
|
|
||||||
return request({
|
|
||||||
url: `/admin/wish/tree/node/info/${id}`,
|
|
||||||
method: 'get',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增节点
|
// 新增节点
|
||||||
export function wishTreeNodeSaveApi(data) {
|
export function wishTreeNodeSaveApi(data) {
|
||||||
return request({
|
return request({
|
||||||
|
|
@ -112,7 +104,7 @@ export function wishTreeNodeStatusApi(id) {
|
||||||
|
|
||||||
// ========== 用户留言管理 ==========
|
// ========== 用户留言管理 ==========
|
||||||
|
|
||||||
// 留言列表(按许愿树查询)
|
// 留言列表
|
||||||
export function wishTreeMessageListApi(params) {
|
export function wishTreeMessageListApi(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/wish/tree/message/list',
|
url: '/admin/wish/tree/message/list',
|
||||||
|
|
@ -121,14 +113,6 @@ export function wishTreeMessageListApi(params) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 留言详情
|
|
||||||
export function wishTreeMessageInfoApi(id) {
|
|
||||||
return request({
|
|
||||||
url: `/admin/wish/tree/message/info/${id}`,
|
|
||||||
method: 'get',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除留言
|
// 删除留言
|
||||||
export function wishTreeMessageDeleteApi(id) {
|
export function wishTreeMessageDeleteApi(id) {
|
||||||
return request({
|
return request({
|
||||||
|
|
@ -144,3 +128,107 @@ export function wishTreeMessageStatusApi(id) {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 节日管理 ==========
|
||||||
|
|
||||||
|
// 节日列表
|
||||||
|
export function festivalList(params) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/festival/list',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存节日
|
||||||
|
export function festivalSave(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/festival/save',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除节日 - 后端使用DELETE方法
|
||||||
|
export function festivalDelete(id) {
|
||||||
|
return request({
|
||||||
|
url: `/admin/wishtree/festival/delete/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改节日状态 - 后端接收RequestBody
|
||||||
|
export function festivalStatus(id, status) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/festival/status',
|
||||||
|
method: 'post',
|
||||||
|
data: { id, status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 心愿管理 ==========
|
||||||
|
|
||||||
|
// 心愿列表 - 后端使用POST方法
|
||||||
|
export function wishList(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/wish/list',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核心愿
|
||||||
|
export function wishAudit(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/wish/audit',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除心愿 - 后端使用DELETE方法
|
||||||
|
export function wishDelete(id) {
|
||||||
|
return request({
|
||||||
|
url: `/admin/wishtree/wish/delete/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 背景管理 ==========
|
||||||
|
|
||||||
|
// 背景列表
|
||||||
|
export function backgroundList(params) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/background/list',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存背景
|
||||||
|
export function backgroundSave(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/background/save',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除背景 - 后端使用DELETE方法
|
||||||
|
export function backgroundDelete(id) {
|
||||||
|
return request({
|
||||||
|
url: `/admin/wishtree/background/delete/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 统计 ==========
|
||||||
|
|
||||||
|
// 获取统计数据
|
||||||
|
export function getStatistics(params) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/wishtree/statistics',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
Zhibo/admin/src/assets/imgs/logo-lefttop.png
Normal file
BIN
Zhibo/admin/src/assets/imgs/logo-lefttop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
BIN
Zhibo/admin/src/assets/imgs/logo-original.png
Normal file
BIN
Zhibo/admin/src/assets/imgs/logo-original.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
BIN
Zhibo/admin/src/assets/imgs/logo-square.png
Normal file
BIN
Zhibo/admin/src/assets/imgs/logo-square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
|
|
@ -4,21 +4,20 @@
|
||||||
v-if="$store.state.themeConfig.themeConfig.layout !== 'columns' && !$store.state.themeConfig.themeConfig.isCollapse"
|
v-if="$store.state.themeConfig.themeConfig.layout !== 'columns' && !$store.state.themeConfig.themeConfig.isCollapse"
|
||||||
@click="onThemeConfigChange"
|
@click="onThemeConfigChange"
|
||||||
>
|
>
|
||||||
<img v-if="platMerLoginInfo" class="layout-logo-medium-img" :src="platMerLoginInfo.siteLogoSquare" />
|
<!-- 直接使用本地Logo图片,替换Logo只需修改 logo-square.png 文件 -->
|
||||||
|
<img class="layout-logo-medium-img" src="@/assets/imgs/logo-square.png" />
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
|
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
|
||||||
<img v-if="platMerLoginInfo" class="layout-logo-size-img" :src="platMerLoginInfo.siteLogoLeftTop" />
|
<!-- 直接使用本地Logo图片,替换Logo只需修改 logo-lefttop.png 文件 -->
|
||||||
|
<img class="layout-logo-size-img" src="@/assets/imgs/logo-lefttop.png" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
export default {
|
export default {
|
||||||
name: 'layoutLogo',
|
name: 'layoutLogo',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {};
|
||||||
platMerLoginInfo: JSON.parse(Cookies.get('logoInfo')), //登录后的信息
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// 获取布局配置信息
|
// 获取布局配置信息
|
||||||
|
|
|
||||||
|
|
@ -13,28 +13,53 @@ const wishtreeManageRouter = {
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'festival',
|
path: 'festival',
|
||||||
component: () => import('@/views/wishtree/festival/index'),
|
component: () => import('@/views/wishTree/festival/index'),
|
||||||
name: 'WishtreeFestival',
|
name: 'WishtreeFestival',
|
||||||
meta: { title: '节日管理' },
|
meta: { title: '节日管理' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'wish',
|
path: 'wish',
|
||||||
component: () => import('@/views/wishtree/wish/index'),
|
component: () => import('@/views/wishTree/wish/index'),
|
||||||
name: 'WishtreeWish',
|
name: 'WishtreeWish',
|
||||||
meta: { title: '心愿管理' },
|
meta: { title: '心愿管理' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'background',
|
path: 'background',
|
||||||
component: () => import('@/views/wishtree/background/index'),
|
component: () => import('@/views/wishTree/background/index'),
|
||||||
name: 'WishtreeBackground',
|
name: 'WishtreeBackground',
|
||||||
meta: { title: '背景素材' },
|
meta: { title: '背景素材' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
component: () => import('@/views/wishtree/statistics/index'),
|
component: () => import('@/views/wishTree/statistics/index'),
|
||||||
name: 'WishtreeStatistics',
|
name: 'WishtreeStatistics',
|
||||||
meta: { title: '数据统计' },
|
meta: { title: '数据统计' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tree',
|
||||||
|
component: () => import('@/views/wishTree/tree/index'),
|
||||||
|
name: 'WishTreeList',
|
||||||
|
meta: { title: '许愿树列表' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tree/detail/:id',
|
||||||
|
component: () => import('@/views/wishTree/tree/detail'),
|
||||||
|
name: 'WishTreeDetail',
|
||||||
|
meta: { title: '许愿树详情' },
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'node',
|
||||||
|
component: () => import('@/views/wishTree/node/index'),
|
||||||
|
name: 'WishTreeNode',
|
||||||
|
meta: { title: '节点管理' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'message',
|
||||||
|
component: () => import('@/views/wishTree/message/index'),
|
||||||
|
name: 'WishTreeMessage',
|
||||||
|
meta: { title: '留言管理' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ const VUE_APP_WS_URL =
|
||||||
const SettingMer = {
|
const SettingMer = {
|
||||||
// 服务器地址
|
// 服务器地址
|
||||||
httpUrl: VUE_APP_API_URL,
|
httpUrl: VUE_APP_API_URL,
|
||||||
// 接口请求地址
|
// 接口请求地址 - 不再重复添加/api
|
||||||
apiBaseURL: VUE_APP_API_URL + '/api/',
|
apiBaseURL: VUE_APP_API_URL,
|
||||||
// socket连接
|
// socket连接
|
||||||
wsSocketUrl: VUE_APP_WS_URL,
|
wsSocketUrl: VUE_APP_WS_URL,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeMessage',
|
name: 'WishTreeMessage',
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishTree';
|
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishtree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeNode',
|
name: 'WishTreeNode',
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeDetail',
|
name: 'WishTreeDetail',
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishTree';
|
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishtree';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WishTreeList',
|
name: 'WishTreeList',
|
||||||
|
|
@ -159,7 +159,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleViewMessages(row) {
|
handleViewMessages(row) {
|
||||||
this.$router.push({ path: `/wishTree/tree/detail/${row.id}` });
|
this.$router.push({ path: `/wishtree/tree/detail/${row.id}` });
|
||||||
},
|
},
|
||||||
addNode() {
|
addNode() {
|
||||||
this.formData.nodes.push({ title: '', description: '', open_time: null, sort: 0, status: 1 });
|
this.formData.nodes.push({ title: '', description: '', open_time: null, sort: 0, status: 1 });
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async handleStatusChange(row) {
|
async handleStatusChange(row) {
|
||||||
await festivalStatus({ id: row.id, status: row.status });
|
await festivalStatus(row.id, row.status);
|
||||||
this.$message.success('状态更新成功');
|
this.$message.success('状态更新成功');
|
||||||
},
|
},
|
||||||
handleDelete(row) {
|
handleDelete(row) {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ module.exports = {
|
||||||
warnings: false,
|
warnings: false,
|
||||||
errors: true,
|
errors: true,
|
||||||
},
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:30001',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
configureWebpack: {
|
configureWebpack: {
|
||||||
// provide the app's title in webpack's name field, so that
|
// provide the app's title in webpack's name field, so that
|
||||||
|
|
|
||||||
|
|
@ -35,29 +35,30 @@ public class LiveStatusSyncTask {
|
||||||
@Value("${SRS_API_URL:http://127.0.0.1:1985}")
|
@Value("${SRS_API_URL:http://127.0.0.1:1985}")
|
||||||
private String srsApiUrl;
|
private String srsApiUrl;
|
||||||
|
|
||||||
@Value("${live.status.sync.enabled:true}")
|
|
||||||
private boolean syncEnabled;
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步直播状态(每 30 秒执行一次)
|
* 同步直播状态(每 30 秒执行一次,减少频率)
|
||||||
* 生产环境建议保留此任务,确保直播状态与实际推流状态一致
|
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 30000) // 改为30秒,降低频率
|
@Scheduled(fixedRate = 30000)
|
||||||
public void syncLiveStatus() {
|
public void syncLiveStatus() {
|
||||||
if (!syncEnabled) {
|
|
||||||
logger.debug("直播状态同步已禁用");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
Set<String> liveStreamKeys = fetchLiveStreamKeysFromSrs();
|
Set<String> liveStreamKeys = fetchLiveStreamKeysFromSrs();
|
||||||
|
// 只有成功获取到 SRS 数据时才更新状态
|
||||||
|
// 如果 SRS 查询失败(返回空集合),不修改现有状态
|
||||||
|
if (liveStreamKeys != null && !liveStreamKeys.isEmpty()) {
|
||||||
updateLiveStatus(liveStreamKeys);
|
updateLiveStatus(liveStreamKeys);
|
||||||
|
} else {
|
||||||
|
logger.debug("SRS 返回空流列表,保持现有直播状态不变");
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("LiveStatusSyncTask 执行失败: {}", e.getMessage());
|
logger.error("LiveStatusSyncTask 执行失败: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 标记是否成功连接过 SRS
|
||||||
|
private boolean srsConnected = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 SRS API 获取当前正在推流的 streamKey 列表
|
* 从 SRS API 获取当前正在推流的 streamKey 列表
|
||||||
*/
|
*/
|
||||||
|
|
@ -71,6 +72,7 @@ public class LiveStatusSyncTask {
|
||||||
conn.setReadTimeout(3000);
|
conn.setReadTimeout(3000);
|
||||||
|
|
||||||
if (conn.getResponseCode() == 200) {
|
if (conn.getResponseCode() == 200) {
|
||||||
|
srsConnected = true;
|
||||||
JsonNode root = objectMapper.readTree(conn.getInputStream());
|
JsonNode root = objectMapper.readTree(conn.getInputStream());
|
||||||
JsonNode streams = root.get("streams");
|
JsonNode streams = root.get("streams");
|
||||||
if (streams != null && streams.isArray()) {
|
if (streams != null && streams.isArray()) {
|
||||||
|
|
@ -88,15 +90,18 @@ public class LiveStatusSyncTask {
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("查询 SRS API 失败: {}", e.getMessage());
|
logger.warn("查询 SRS API 失败: {}", e.getMessage());
|
||||||
|
// 返回 null 表示查询失败,不应该修改状态
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return streamKeys;
|
return streamKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新数据库中的直播状态
|
* 更新数据库中的直播状态
|
||||||
* 增加容错:只有连续多次检测到断流才更新为未开播
|
|
||||||
*/
|
*/
|
||||||
private void updateLiveStatus(Set<String> liveStreamKeys) {
|
private void updateLiveStatus(Set<String> liveStreamKeys) {
|
||||||
|
if (liveStreamKeys == null) return;
|
||||||
|
|
||||||
List<LiveRoom> allRooms = liveRoomService.list(new LambdaQueryWrapper<>());
|
List<LiveRoom> allRooms = liveRoomService.list(new LambdaQueryWrapper<>());
|
||||||
for (LiveRoom room : allRooms) {
|
for (LiveRoom room : allRooms) {
|
||||||
String streamKey = room.getStreamKey();
|
String streamKey = room.getStreamKey();
|
||||||
|
|
@ -105,18 +110,9 @@ public class LiveStatusSyncTask {
|
||||||
int currentStatus = room.getIsLive() == null ? 0 : room.getIsLive();
|
int currentStatus = room.getIsLive() == null ? 0 : room.getIsLive();
|
||||||
|
|
||||||
if (newStatus != currentStatus) {
|
if (newStatus != currentStatus) {
|
||||||
// 如果检测到推流开始,立即更新为直播中
|
|
||||||
if (newStatus == 1) {
|
|
||||||
room.setIsLive(newStatus);
|
room.setIsLive(newStatus);
|
||||||
liveRoomService.updateById(room);
|
liveRoomService.updateById(room);
|
||||||
logger.info("直播状态更新: {} -> 直播中", room.getTitle());
|
logger.info("直播状态更新: {} -> {}", room.getTitle(), shouldBeLive ? "直播中" : "未开播");
|
||||||
}
|
|
||||||
// 如果检测到推流结束,也立即更新(SRS回调已经处理了,这里是兜底)
|
|
||||||
else if (currentStatus == 1) {
|
|
||||||
room.setIsLive(0);
|
|
||||||
liveRoomService.updateById(room);
|
|
||||||
logger.info("直播状态更新: {} -> 未开播(定时任务检测到断流)", room.getTitle());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ server:
|
||||||
max-threads: 1000 # 最大线程数量 默认200
|
max-threads: 1000 # 最大线程数量 默认200
|
||||||
min-spare-threads: 30 # 初始化启动线程数量
|
min-spare-threads: 30 # 初始化启动线程数量
|
||||||
|
|
||||||
|
# ============ 直播流服务器配置 ============
|
||||||
|
# 直播流始终使用远程SRS服务器
|
||||||
|
LIVE_PUBLIC_SRS_HOST: 1.15.149.240
|
||||||
|
LIVE_PUBLIC_SRS_RTMP_PORT: 25002
|
||||||
|
LIVE_PUBLIC_SRS_HTTP_PORT: 25003
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
# 配置的环境
|
# 配置的环境
|
||||||
|
|
|
||||||
|
|
@ -296,8 +296,9 @@ public class WishtreeServiceImpl implements WishtreeService {
|
||||||
WishtreeStatisticsVO vo = new WishtreeStatisticsVO();
|
WishtreeStatisticsVO vo = new WishtreeStatisticsVO();
|
||||||
|
|
||||||
// 总心愿数
|
// 总心愿数
|
||||||
vo.setTotalWishes(Long.valueOf(wishDao.selectCount(new LambdaQueryWrapper<WishtreeWish>()
|
Integer totalWishes = wishDao.selectCount(new LambdaQueryWrapper<WishtreeWish>()
|
||||||
.eq(WishtreeWish::getIsDelete, 0))));
|
.eq(WishtreeWish::getIsDelete, 0));
|
||||||
|
vo.setTotalWishes(totalWishes != null ? totalWishes.longValue() : 0L);
|
||||||
|
|
||||||
// 今日新增
|
// 今日新增
|
||||||
vo.setTodayWishes(wishDao.countTodayWishes());
|
vo.setTodayWishes(wishDao.countTodayWishes());
|
||||||
|
|
@ -306,11 +307,13 @@ public class WishtreeServiceImpl implements WishtreeService {
|
||||||
vo.setPendingWishes(wishDao.countPendingWishes());
|
vo.setPendingWishes(wishDao.countPendingWishes());
|
||||||
|
|
||||||
// 总点赞数
|
// 总点赞数
|
||||||
vo.setTotalLikes(Long.valueOf(likeDao.selectCount(null)));
|
Integer totalLikes = likeDao.selectCount(null);
|
||||||
|
vo.setTotalLikes(totalLikes != null ? totalLikes.longValue() : 0L);
|
||||||
|
|
||||||
// 总评论数
|
// 总评论数
|
||||||
vo.setTotalComments(Long.valueOf(commentDao.selectCount(new LambdaQueryWrapper<WishtreeComment>()
|
Integer totalComments = commentDao.selectCount(new LambdaQueryWrapper<WishtreeComment>()
|
||||||
.eq(WishtreeComment::getIsDelete, 0))));
|
.eq(WishtreeComment::getIsDelete, 0));
|
||||||
|
vo.setTotalComments(totalComments != null ? totalComments.longValue() : 0L);
|
||||||
|
|
||||||
// 心愿趋势
|
// 心愿趋势
|
||||||
vo.setWishTrend(wishDao.selectWishTrend(days));
|
vo.setWishTrend(wishDao.selectWishTrend(days));
|
||||||
|
|
|
||||||
98
Zhibo/zhibo-h/sql/wishtree_quick_setup.sql
Normal file
98
Zhibo/zhibo-h/sql/wishtree_quick_setup.sql
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
-- ============================================
|
||||||
|
-- 许愿树快速建表脚本
|
||||||
|
-- 执行此脚本创建许愿树所需的所有表
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 1. 节日表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_wishtree_festival` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '节日ID',
|
||||||
|
`name` varchar(50) NOT NULL COMMENT '节日名称',
|
||||||
|
`icon` varchar(255) DEFAULT '' COMMENT '节日图标',
|
||||||
|
`start_date` varchar(20) DEFAULT '' COMMENT '开始日期',
|
||||||
|
`end_date` varchar(20) DEFAULT '' COMMENT '结束日期',
|
||||||
|
`is_lunar` tinyint DEFAULT 0 COMMENT '是否农历',
|
||||||
|
`theme_color` varchar(20) DEFAULT '#FF6B6B' COMMENT '主题色',
|
||||||
|
`sort` int DEFAULT 0 COMMENT '排序',
|
||||||
|
`status` tinyint DEFAULT 1 COMMENT '状态 0禁用 1启用',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树节日表';
|
||||||
|
|
||||||
|
-- 2. 心愿表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_wishtree_wish` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '心愿ID',
|
||||||
|
`uid` int NOT NULL COMMENT '用户ID',
|
||||||
|
`festival_id` int DEFAULT 0 COMMENT '节日ID',
|
||||||
|
`content` varchar(500) NOT NULL COMMENT '心愿内容',
|
||||||
|
`background_id` int DEFAULT 0 COMMENT '背景ID',
|
||||||
|
`status` tinyint DEFAULT 1 COMMENT '状态 0待审核 1通过 2拒绝',
|
||||||
|
`audit_type` tinyint DEFAULT 0 COMMENT '审核方式 0自动 1人工',
|
||||||
|
`audit_remark` varchar(255) DEFAULT '' COMMENT '审核备注',
|
||||||
|
`like_count` int DEFAULT 0 COMMENT '点赞数',
|
||||||
|
`comment_count` int DEFAULT 0 COMMENT '评论数',
|
||||||
|
`is_delete` tinyint DEFAULT 0 COMMENT '是否删除',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_uid` (`uid`),
|
||||||
|
KEY `idx_festival` (`festival_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树心愿表';
|
||||||
|
|
||||||
|
-- 3. 点赞表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_wishtree_like` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`uid` int NOT NULL COMMENT '用户ID',
|
||||||
|
`wish_id` bigint NOT NULL COMMENT '心愿ID',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_uid_wish` (`uid`, `wish_id`),
|
||||||
|
KEY `idx_wish` (`wish_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树点赞表';
|
||||||
|
|
||||||
|
-- 4. 评论表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_wishtree_comment` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`wish_id` bigint NOT NULL COMMENT '心愿ID',
|
||||||
|
`uid` int NOT NULL COMMENT '用户ID',
|
||||||
|
`content` varchar(255) NOT NULL COMMENT '评论内容',
|
||||||
|
`parent_id` bigint DEFAULT 0 COMMENT '父评论ID',
|
||||||
|
`status` tinyint DEFAULT 1 COMMENT '状态',
|
||||||
|
`is_delete` tinyint DEFAULT 0 COMMENT '是否删除',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_wish` (`wish_id`),
|
||||||
|
KEY `idx_uid` (`uid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树评论表';
|
||||||
|
|
||||||
|
-- 5. 背景素材表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_wishtree_background` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(50) NOT NULL COMMENT '背景名称',
|
||||||
|
`image` varchar(255) NOT NULL COMMENT '背景图片',
|
||||||
|
`festival_id` int DEFAULT 0 COMMENT '关联节日ID',
|
||||||
|
`sort` int DEFAULT 0 COMMENT '排序',
|
||||||
|
`status` tinyint DEFAULT 1 COMMENT '状态',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树背景素材表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 初始化节日数据
|
||||||
|
-- ============================================
|
||||||
|
INSERT IGNORE INTO `eb_wishtree_festival` (`id`, `name`, `icon`, `start_date`, `end_date`, `is_lunar`, `theme_color`, `sort`, `status`) VALUES
|
||||||
|
(1, '元旦', '🎉', '12-31', '01-03', 0, '#FF6B6B', 100, 1),
|
||||||
|
(2, '春节', '🧧', '除夕', '正月十五', 1, '#E74C3C', 99, 1),
|
||||||
|
(3, '情人节', '💕', '02-13', '02-15', 0, '#FF69B4', 98, 1),
|
||||||
|
(4, '生日祝福', '🎂', '全年', '全年', 0, '#FF69B4', 50, 1),
|
||||||
|
(5, '日常祝福', '✨', '全年', '全年', 0, '#9B59B6', 49, 1);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 验证表是否创建成功
|
||||||
|
-- ============================================
|
||||||
|
SELECT 'eb_wishtree_festival' AS table_name, COUNT(*) AS count FROM eb_wishtree_festival;
|
||||||
|
SELECT 'eb_wishtree_wish' AS table_name, COUNT(*) AS count FROM eb_wishtree_wish;
|
||||||
|
SELECT 'eb_wishtree_like' AS table_name, COUNT(*) AS count FROM eb_wishtree_like;
|
||||||
|
SELECT 'eb_wishtree_comment' AS table_name, COUNT(*) AS count FROM eb_wishtree_comment;
|
||||||
|
SELECT 'eb_wishtree_background' AS table_name, COUNT(*) AS count FROM eb_wishtree_background;
|
||||||
|
|
@ -31,16 +31,25 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 主API地址(普通业务功能)============
|
||||||
val apiBaseUrlEmulator = (localProps.getProperty("api.base_url_emulator")
|
val apiBaseUrlEmulator = (localProps.getProperty("api.base_url_emulator")
|
||||||
?: "http://10.0.2.2:8081/").trim()
|
?: "http://10.0.2.2:8081/").trim()
|
||||||
|
|
||||||
val apiBaseUrlDevice = (localProps.getProperty("api.base_url_device")
|
val apiBaseUrlDevice = (localProps.getProperty("api.base_url_device")
|
||||||
?: "http://192.168.1.164:8081/").trim()
|
?: "http://192.168.1.164:8081/").trim()
|
||||||
|
|
||||||
// 模拟器使用服务器地址
|
|
||||||
buildConfigField("String", "API_BASE_URL_EMULATOR", "\"$apiBaseUrlEmulator\"")
|
buildConfigField("String", "API_BASE_URL_EMULATOR", "\"$apiBaseUrlEmulator\"")
|
||||||
// 真机使用服务器地址
|
|
||||||
buildConfigField("String", "API_BASE_URL_DEVICE", "\"$apiBaseUrlDevice\"")
|
buildConfigField("String", "API_BASE_URL_DEVICE", "\"$apiBaseUrlDevice\"")
|
||||||
|
|
||||||
|
// ============ 直播/通话服务地址(始终远程)============
|
||||||
|
val liveServerHost = (localProps.getProperty("live.server_host") ?: "1.15.149.240").trim()
|
||||||
|
val liveServerPort = (localProps.getProperty("live.server_port") ?: "8083").trim()
|
||||||
|
val turnServerHost = (localProps.getProperty("turn.server_host") ?: "1.15.149.240").trim()
|
||||||
|
val turnServerPort = (localProps.getProperty("turn.server_port") ?: "3478").trim()
|
||||||
|
|
||||||
|
buildConfigField("String", "LIVE_SERVER_HOST", "\"$liveServerHost\"")
|
||||||
|
buildConfigField("String", "LIVE_SERVER_PORT", "\"$liveServerPort\"")
|
||||||
|
buildConfigField("String", "TURN_SERVER_HOST", "\"$turnServerHost\"")
|
||||||
|
buildConfigField("String", "TURN_SERVER_PORT", "\"$turnServerPort\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ public class LiveStreamingApplication extends Application {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 如果用户已登录,连接通话信令服务器
|
* 如果用户已登录,连接通话信令服务器
|
||||||
|
* 延迟执行,避免启动时阻塞
|
||||||
*/
|
*/
|
||||||
public void connectCallSignalingIfLoggedIn() {
|
public void connectCallSignalingIfLoggedIn() {
|
||||||
String userId = AuthStore.getUserId(this);
|
String userId = AuthStore.getUserId(this);
|
||||||
|
|
@ -48,8 +49,15 @@ public class LiveStreamingApplication extends Application {
|
||||||
try {
|
try {
|
||||||
int uid = (int) Double.parseDouble(userId);
|
int uid = (int) Double.parseDouble(userId);
|
||||||
if (uid > 0) {
|
if (uid > 0) {
|
||||||
Log.d(TAG, "用户已登录,连接通话信令服务器,userId: " + uid);
|
Log.d(TAG, "用户已登录,延迟连接通话信令服务器,userId: " + uid);
|
||||||
|
// 延迟3秒连接,避免启动时阻塞
|
||||||
|
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
try {
|
||||||
CallManager.getInstance(this).connect(uid);
|
CallManager.getInstance(this).connect(uid);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "连接通话信令服务器失败", e);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
Log.e(TAG, "解析用户ID失败: " + userId, e);
|
Log.e(TAG, "解析用户ID失败: " + userId, e);
|
||||||
|
|
@ -64,7 +72,11 @@ public class LiveStreamingApplication extends Application {
|
||||||
*/
|
*/
|
||||||
public void onUserLoggedIn(int userId) {
|
public void onUserLoggedIn(int userId) {
|
||||||
Log.d(TAG, "用户登录成功,连接通话信令服务器,userId: " + userId);
|
Log.d(TAG, "用户登录成功,连接通话信令服务器,userId: " + userId);
|
||||||
|
try {
|
||||||
CallManager.getInstance(this).connect(userId);
|
CallManager.getInstance(this).connect(userId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "连接通话信令服务器失败", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,18 @@ public class MainActivity extends AppCompatActivity {
|
||||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
// 强制设置正确的 API 地址
|
// 清除之前缓存的自定义API地址,使用BuildConfig中的配置
|
||||||
ApiClient.setCustomBaseUrl(getApplicationContext(), "http://192.168.1.164:8081/");
|
ApiClient.clearCustomBaseUrl(getApplicationContext());
|
||||||
ApiClient.getService(getApplicationContext());
|
ApiClient.getService(getApplicationContext());
|
||||||
|
|
||||||
|
// 调试:打印当前使用的 API 地址
|
||||||
|
String currentApiUrl = ApiClient.getCurrentBaseUrl(getApplicationContext());
|
||||||
|
Log.d(TAG, "========== API 配置 ==========");
|
||||||
|
Log.d(TAG, "当前 API 地址: " + currentApiUrl);
|
||||||
|
Log.d(TAG, "BuildConfig EMULATOR: " + com.example.livestreaming.BuildConfig.API_BASE_URL_EMULATOR);
|
||||||
|
Log.d(TAG, "BuildConfig DEVICE: " + com.example.livestreaming.BuildConfig.API_BASE_URL_DEVICE);
|
||||||
|
Log.d(TAG, "==============================");
|
||||||
|
|
||||||
// 立即显示缓存数据,提升启动速度
|
// 立即显示缓存数据,提升启动速度
|
||||||
setupRecyclerView();
|
setupRecyclerView();
|
||||||
setupUI();
|
setupUI();
|
||||||
|
|
|
||||||
|
|
@ -78,33 +78,36 @@ public class PlayerActivity extends AppCompatActivity {
|
||||||
releaseExoPlayer();
|
releaseExoPlayer();
|
||||||
triedAltUrl = false;
|
triedAltUrl = false;
|
||||||
|
|
||||||
// 创建低延迟播放器配置
|
// 优化缓冲配置,平衡延迟和流畅度
|
||||||
ExoPlayer exo = new ExoPlayer.Builder(this)
|
androidx.media3.exoplayer.DefaultLoadControl loadControl =
|
||||||
.setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
||||||
// 减少缓冲区大小,降低延迟
|
|
||||||
.setBufferDurationsMs(
|
.setBufferDurationsMs(
|
||||||
1000, // 最小缓冲时长 1秒
|
3000, // 最小缓冲 3秒
|
||||||
3000, // 最大缓冲时长 3秒
|
15000, // 最大缓冲 15秒
|
||||||
500, // 播放缓冲时长 0.5秒
|
1500, // 播放前缓冲 1.5秒
|
||||||
1000 // 播放后缓冲时长 1秒
|
3000 // 重新缓冲 3秒
|
||||||
)
|
)
|
||||||
.build())
|
.setPrioritizeTimeOverSizeThresholds(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 设置播放器视图
|
ExoPlayer exo = new ExoPlayer.Builder(this)
|
||||||
binding.playerView.setPlayer(exo);
|
.setLoadControl(loadControl)
|
||||||
|
.build();
|
||||||
|
|
||||||
// 启用低延迟模式
|
binding.playerView.setPlayer(exo);
|
||||||
binding.playerView.setUseController(true);
|
binding.playerView.setUseController(true);
|
||||||
binding.playerView.setControllerAutoShow(false);
|
binding.playerView.setControllerAutoShow(false);
|
||||||
|
|
||||||
String computedAltUrl = altUrl;
|
String computedAltUrl = altUrl;
|
||||||
if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) computedAltUrl = getAltHlsUrl(url);
|
if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) {
|
||||||
|
computedAltUrl = getAltHlsUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
String finalComputedAltUrl = computedAltUrl;
|
String finalComputedAltUrl = computedAltUrl;
|
||||||
exo.addListener(new Player.Listener() {
|
exo.addListener(new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(PlaybackException error) {
|
public void onPlayerError(PlaybackException error) {
|
||||||
|
android.util.Log.e("PlayerActivity", "播放错误: " + error.getMessage());
|
||||||
if (triedAltUrl) return;
|
if (triedAltUrl) return;
|
||||||
if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return;
|
if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return;
|
||||||
triedAltUrl = true;
|
triedAltUrl = true;
|
||||||
|
|
@ -115,6 +118,7 @@ public class PlayerActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
android.util.Log.d("PlayerActivity", "开始播放: " + url);
|
||||||
exo.setMediaItem(MediaItem.fromUri(url));
|
exo.setMediaItem(MediaItem.fromUri(url));
|
||||||
exo.prepare();
|
exo.prepare();
|
||||||
exo.setPlayWhenReady(true);
|
exo.setPlayWhenReady(true);
|
||||||
|
|
@ -138,47 +142,16 @@ public class PlayerActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
||||||
ensureIjkLibsLoaded();
|
// 禁用 IjkPlayer,直接使用 HLS 播放(IjkPlayer 在某些设备上会崩溃)
|
||||||
releaseExoPlayer();
|
android.util.Log.d("PlayerActivity", "FLV流转为HLS播放: " + flvUrl);
|
||||||
releaseIjkPlayer();
|
|
||||||
|
|
||||||
ijkUrl = flvUrl;
|
// 将 FLV 地址转换为 HLS 地址
|
||||||
ijkFallbackHlsUrl = fallbackHlsUrl;
|
String hlsUrl = fallbackHlsUrl;
|
||||||
ijkFallbackTried = false;
|
if (hlsUrl == null || hlsUrl.trim().isEmpty()) {
|
||||||
|
hlsUrl = flvUrl.replace(".flv", ".m3u8");
|
||||||
if (binding != null) {
|
|
||||||
binding.playerView.setVisibility(android.view.View.GONE);
|
|
||||||
binding.flvTextureView.setVisibility(android.view.View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureView view = binding.flvTextureView;
|
startHls(hlsUrl, null);
|
||||||
TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() {
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureAvailable(android.graphics.SurfaceTexture surfaceTexture, int width, int height) {
|
|
||||||
ijkSurface = new Surface(surfaceTexture);
|
|
||||||
prepareIjk(flvUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureSizeChanged(android.graphics.SurfaceTexture surface, int width, int height) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSurfaceTextureDestroyed(android.graphics.SurfaceTexture surface) {
|
|
||||||
releaseIjkPlayer();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureUpdated(android.graphics.SurfaceTexture surface) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view.setSurfaceTextureListener(listener);
|
|
||||||
if (view.isAvailable() && view.getSurfaceTexture() != null) {
|
|
||||||
ijkSurface = new Surface(view.getSurfaceTexture());
|
|
||||||
prepareIjk(flvUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareIjk(String url) {
|
private void prepareIjk(String url) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
|
||||||
|
import com.example.livestreaming.call.WebRTCConfig;
|
||||||
import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding;
|
import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding;
|
||||||
import com.example.livestreaming.net.ApiClient;
|
import com.example.livestreaming.net.ApiClient;
|
||||||
import com.example.livestreaming.net.ApiResponse;
|
import com.example.livestreaming.net.ApiResponse;
|
||||||
|
|
@ -105,22 +106,25 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
private WebSocket onlineCountWebSocket;
|
private WebSocket onlineCountWebSocket;
|
||||||
private OkHttpClient onlineCountWsClient;
|
private OkHttpClient onlineCountWsClient;
|
||||||
|
|
||||||
// 动态获取WebSocket URL
|
// 动态获取WebSocket URL - 直播服务使用远程服务器
|
||||||
private String getWsChatBaseUrl() {
|
private String getWsChatBaseUrl() {
|
||||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
try {
|
||||||
if (baseUrl == null || baseUrl.isEmpty()) {
|
// 直播弹幕WebSocket使用远程服务器
|
||||||
baseUrl = "http://192.168.1.164:8081/";
|
return WebRTCConfig.getLiveWsUrl() + "ws/live/chat/";
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("RoomDetail", "获取WsChatBaseUrl失败", e);
|
||||||
|
return "ws://1.15.149.240:8083/ws/live/chat/";
|
||||||
}
|
}
|
||||||
// 将 http:// 转换为 ws://
|
|
||||||
return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/chat/";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getWsOnlineBaseUrl() {
|
private String getWsOnlineBaseUrl() {
|
||||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
try {
|
||||||
if (baseUrl == null || baseUrl.isEmpty()) {
|
// 直播在线人数WebSocket使用远程服务器
|
||||||
baseUrl = "http://192.168.1.164:8081/";
|
return WebRTCConfig.getLiveWsUrl() + "ws/live/";
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("RoomDetail", "获取WsOnlineBaseUrl失败", e);
|
||||||
|
return "ws://1.15.149.240:8083/ws/live/";
|
||||||
}
|
}
|
||||||
return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket 心跳检测 - 弹幕
|
// WebSocket 心跳检测 - 弹幕
|
||||||
|
|
@ -150,6 +154,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
try {
|
||||||
// 隐藏ActionBar,使用自定义顶部栏
|
// 隐藏ActionBar,使用自定义顶部栏
|
||||||
if (getSupportActionBar() != null) {
|
if (getSupportActionBar() != null) {
|
||||||
getSupportActionBar().hide();
|
getSupportActionBar().hide();
|
||||||
|
|
@ -161,14 +166,25 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
ApiClient.getService(getApplicationContext());
|
ApiClient.getService(getApplicationContext());
|
||||||
|
|
||||||
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
|
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
|
||||||
|
if (TextUtils.isEmpty(roomId)) {
|
||||||
|
Toast.makeText(this, "直播间ID无效", Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
triedAltUrl = false;
|
triedAltUrl = false;
|
||||||
|
|
||||||
setupUI();
|
setupUI();
|
||||||
setupChat();
|
setupChat();
|
||||||
setupGifts();
|
setupGifts();
|
||||||
|
|
||||||
// 记录观看历史
|
// 记录观看历史(异步,不阻塞)
|
||||||
recordWatchHistory();
|
recordWatchHistory();
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("RoomDetail", "onCreate异常: " + e.getMessage(), e);
|
||||||
|
Toast.makeText(this, "加载直播间失败", Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupUI() {
|
private void setupUI() {
|
||||||
|
|
@ -196,13 +212,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
// 全屏按钮
|
// 全屏按钮
|
||||||
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
|
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
|
||||||
|
|
||||||
// 观众列表按钮
|
|
||||||
binding.topViewerLayout.setOnClickListener(v -> showViewerListDialog());
|
|
||||||
|
|
||||||
// 关注按钮
|
// 关注按钮
|
||||||
binding.followButton.setOnClickListener(v -> {
|
binding.followButton.setOnClickListener(v -> {
|
||||||
// 检查登录状态,关注主播需要登录
|
// 检查登录状态,关注主播需要登录
|
||||||
if (!AuthHelper.requireLogin(this, "关注/取消关注需要登录")) {
|
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,12 +224,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据当前关注状态决定是关注还是取消关注
|
// 调用后端接口关注主播
|
||||||
if (room.getIsFollowing()) {
|
|
||||||
unfollowStreamerBackend();
|
|
||||||
} else {
|
|
||||||
followStreamerBackend();
|
followStreamerBackend();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 分享按钮
|
// 分享按钮
|
||||||
|
|
@ -751,6 +760,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
* 记录观看历史
|
* 记录观看历史
|
||||||
*/
|
*/
|
||||||
private void recordWatchHistory() {
|
private void recordWatchHistory() {
|
||||||
|
try {
|
||||||
if (!AuthHelper.isLoggedIn(this)) {
|
if (!AuthHelper.isLoggedIn(this)) {
|
||||||
return; // 未登录用户不记录
|
return; // 未登录用户不记录
|
||||||
}
|
}
|
||||||
|
|
@ -771,16 +781,18 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
|
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
|
||||||
Response<ApiResponse<java.util.Map<String, Object>>> response) {
|
Response<ApiResponse<java.util.Map<String, Object>>> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
// 忽略结果,接口可能不存在
|
||||||
android.util.Log.d("RoomDetail", "观看历史记录成功");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
|
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
|
||||||
android.util.Log.e("RoomDetail", "观看历史记录失败: " + t.getMessage());
|
// 忽略错误,接口可能不存在
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略所有异常,不影响直播观看
|
||||||
|
android.util.Log.w("RoomDetail", "记录观看历史失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -903,6 +915,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindRoom(Room r) {
|
private void bindRoom(Room r) {
|
||||||
|
try {
|
||||||
String title = r.getTitle() != null ? r.getTitle() : "直播间";
|
String title = r.getTitle() != null ? r.getTitle() : "直播间";
|
||||||
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
|
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
|
||||||
|
|
||||||
|
|
@ -913,9 +926,6 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
binding.roomTitle.setText(title);
|
binding.roomTitle.setText(title);
|
||||||
binding.streamerName.setText(streamer);
|
binding.streamerName.setText(streamer);
|
||||||
|
|
||||||
// 更新关注按钮状态
|
|
||||||
updateFollowButtonState(r.getIsFollowing());
|
|
||||||
|
|
||||||
// 设置直播状态
|
// 设置直播状态
|
||||||
if (r.isLive()) {
|
if (r.isLive()) {
|
||||||
binding.liveTag.setVisibility(View.VISIBLE);
|
binding.liveTag.setVisibility(View.VISIBLE);
|
||||||
|
|
@ -950,20 +960,17 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("RoomDetail", "bindRoom异常: " + e.getMessage(), e);
|
||||||
|
// 不要因为绑定失败就退出,显示错误状态
|
||||||
|
if (binding != null && binding.offlineLayout != null) {
|
||||||
|
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新关注按钮状态
|
|
||||||
*/
|
|
||||||
private void updateFollowButtonState(boolean isFollowing) {
|
|
||||||
if (isFollowing) {
|
|
||||||
binding.followButton.setText("已关注");
|
|
||||||
} else {
|
|
||||||
binding.followButton.setText("关注");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensurePlayer(String url, String fallbackHlsUrl) {
|
private void ensurePlayer(String url, String fallbackHlsUrl) {
|
||||||
|
try {
|
||||||
if (TextUtils.isEmpty(url)) return;
|
if (TextUtils.isEmpty(url)) return;
|
||||||
|
|
||||||
if (url.endsWith(".flv")) {
|
if (url.endsWith(".flv")) {
|
||||||
|
|
@ -982,7 +989,17 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
startHls(url, null);
|
startHls(url, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("RoomDetail", "ensurePlayer异常: " + e.getMessage(), e);
|
||||||
|
// 播放器初始化失败,显示离线状态
|
||||||
|
if (binding != null && binding.offlineLayout != null) {
|
||||||
|
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复显示连接消息
|
||||||
|
private boolean hasShownConnectedMessage = false;
|
||||||
|
|
||||||
private void startHls(String url, @Nullable String altUrl) {
|
private void startHls(String url, @Nullable String altUrl) {
|
||||||
releaseIjkPlayer();
|
releaseIjkPlayer();
|
||||||
|
|
@ -993,46 +1010,95 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
|
|
||||||
releaseExoPlayer();
|
releaseExoPlayer();
|
||||||
triedAltUrl = false;
|
triedAltUrl = false;
|
||||||
|
hasShownConnectedMessage = false; // 重置连接消息标志
|
||||||
|
|
||||||
ExoPlayer exo = new ExoPlayer.Builder(this)
|
// 优化缓冲配置 - 增大缓冲区以减少卡顿
|
||||||
.setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
// HLS 直播通常有 2-3 秒的切片延迟,需要足够的缓冲
|
||||||
|
androidx.media3.exoplayer.DefaultLoadControl loadControl =
|
||||||
|
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
||||||
.setBufferDurationsMs(
|
.setBufferDurationsMs(
|
||||||
1000,
|
10000, // 最小缓冲 10秒(保证流畅播放)
|
||||||
3000,
|
30000, // 最大缓冲 30秒(足够应对网络波动)
|
||||||
500,
|
5000, // 播放前缓冲 5秒(确保有足够数据再开始)
|
||||||
1000
|
10000 // 重新缓冲 10秒(卡顿后充分缓冲再继续)
|
||||||
)
|
)
|
||||||
.build())
|
.setPrioritizeTimeOverSizeThresholds(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// 创建播放器
|
||||||
|
ExoPlayer exo = new ExoPlayer.Builder(this)
|
||||||
|
.setLoadControl(loadControl)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 设置播放器视图
|
||||||
binding.playerView.setPlayer(exo);
|
binding.playerView.setPlayer(exo);
|
||||||
|
binding.playerView.setUseController(true);
|
||||||
|
binding.playerView.setControllerAutoShow(false);
|
||||||
|
|
||||||
String computedAltUrl = altUrl;
|
String computedAltUrl = altUrl;
|
||||||
if (TextUtils.isEmpty(computedAltUrl)) computedAltUrl = getAltHlsUrl(url);
|
if (TextUtils.isEmpty(computedAltUrl)) computedAltUrl = getAltHlsUrl(url);
|
||||||
|
|
||||||
String finalComputedAltUrl = computedAltUrl;
|
String finalComputedAltUrl = computedAltUrl;
|
||||||
|
final String finalUrl = url;
|
||||||
|
|
||||||
exo.addListener(new Player.Listener() {
|
exo.addListener(new Player.Listener() {
|
||||||
|
private int retryCount = 0;
|
||||||
|
private static final int MAX_RETRY = 3;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(PlaybackException error) {
|
public void onPlayerError(PlaybackException error) {
|
||||||
if (triedAltUrl || TextUtils.isEmpty(finalComputedAltUrl)) {
|
android.util.Log.e("ExoPlayer", "播放错误: " + error.getMessage());
|
||||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
|
||||||
return;
|
// 先尝试备用地址
|
||||||
}
|
if (!triedAltUrl && !TextUtils.isEmpty(finalComputedAltUrl)) {
|
||||||
triedAltUrl = true;
|
triedAltUrl = true;
|
||||||
|
android.util.Log.d("ExoPlayer", "尝试备用地址: " + finalComputedAltUrl);
|
||||||
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
|
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
|
||||||
exo.prepare();
|
exo.prepare();
|
||||||
exo.setPlayWhenReady(true);
|
exo.setPlayWhenReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动重试
|
||||||
|
if (retryCount < MAX_RETRY) {
|
||||||
|
retryCount++;
|
||||||
|
android.util.Log.d("ExoPlayer", "自动重试 " + retryCount + "/" + MAX_RETRY);
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
if (!isFinishing() && !isDestroyed()) {
|
||||||
|
exo.setMediaItem(MediaItem.fromUri(finalUrl));
|
||||||
|
exo.prepare();
|
||||||
|
exo.setPlayWhenReady(true);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试失败,显示离线状态
|
||||||
|
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
if (!isFinishing() && !isDestroyed()) {
|
||||||
|
fetchRoom();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackStateChanged(int playbackState) {
|
public void onPlaybackStateChanged(int playbackState) {
|
||||||
if (playbackState == Player.STATE_READY) {
|
if (playbackState == Player.STATE_READY) {
|
||||||
binding.offlineLayout.setVisibility(View.GONE);
|
binding.offlineLayout.setVisibility(View.GONE);
|
||||||
|
retryCount = 0;
|
||||||
|
// 只显示一次连接消息
|
||||||
|
if (!hasShownConnectedMessage) {
|
||||||
|
hasShownConnectedMessage = true;
|
||||||
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
|
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
|
||||||
}
|
}
|
||||||
|
} else if (playbackState == Player.STATE_BUFFERING) {
|
||||||
|
android.util.Log.d("ExoPlayer", "正在缓冲...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
android.util.Log.d("ExoPlayer", "开始播放: " + url);
|
||||||
exo.setMediaItem(MediaItem.fromUri(url));
|
exo.setMediaItem(MediaItem.fromUri(url));
|
||||||
exo.prepare();
|
exo.prepare();
|
||||||
exo.setPlayWhenReady(true);
|
exo.setPlayWhenReady(true);
|
||||||
|
|
@ -1040,47 +1106,16 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
||||||
ensureIjkLibsLoaded();
|
android.util.Log.d("RoomDetail", "开始播放FLV流: " + flvUrl);
|
||||||
releaseExoPlayer();
|
|
||||||
releaseIjkPlayer();
|
|
||||||
|
|
||||||
ijkUrl = flvUrl;
|
// 直接使用 HLS 播放,避免 IjkPlayer 崩溃问题
|
||||||
ijkFallbackHlsUrl = fallbackHlsUrl;
|
// HLS 虽然延迟稍高,但稳定性更好
|
||||||
ijkFallbackTried = false;
|
String hlsUrl = fallbackHlsUrl;
|
||||||
|
if (TextUtils.isEmpty(hlsUrl)) {
|
||||||
if (binding != null) {
|
hlsUrl = flvUrl.replace(".flv", ".m3u8");
|
||||||
binding.playerView.setVisibility(View.GONE);
|
|
||||||
binding.flvTextureView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureView view = binding.flvTextureView;
|
|
||||||
TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() {
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureAvailable(@NonNull android.graphics.SurfaceTexture surfaceTexture, int width, int height) {
|
|
||||||
ijkSurface = new Surface(surfaceTexture);
|
|
||||||
prepareIjk(flvUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureSizeChanged(@NonNull android.graphics.SurfaceTexture surface, int width, int height) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSurfaceTextureDestroyed(@NonNull android.graphics.SurfaceTexture surface) {
|
|
||||||
releaseIjkPlayer();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSurfaceTextureUpdated(@NonNull android.graphics.SurfaceTexture surface) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view.setSurfaceTextureListener(listener);
|
|
||||||
if (view.isAvailable() && view.getSurfaceTexture() != null) {
|
|
||||||
ijkSurface = new Surface(view.getSurfaceTexture());
|
|
||||||
prepareIjk(flvUrl);
|
|
||||||
}
|
}
|
||||||
|
android.util.Log.d("RoomDetail", "使用 HLS 播放: " + hlsUrl);
|
||||||
|
startHls(hlsUrl, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareIjk(String url) {
|
private void prepareIjk(String url) {
|
||||||
|
|
@ -1088,33 +1123,36 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
|
|
||||||
IjkMediaPlayer p = new IjkMediaPlayer();
|
IjkMediaPlayer p = new IjkMediaPlayer();
|
||||||
// 优化缓冲设置,减少卡顿
|
// 优化缓冲设置,减少卡顿
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1); // 开启缓冲
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000); // 100ms分析时长
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240); // 10KB探测大小
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5); // 允许丢帧
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000); // 3秒缓存
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50); // 最小缓冲帧数
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0); // 关闭无限缓冲
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1); // 断线重连
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1);
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5); // 最大重连延迟5秒
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5);
|
||||||
|
|
||||||
p.setOnPreparedListener(mp -> {
|
p.setOnPreparedListener(mp -> {
|
||||||
binding.offlineLayout.setVisibility(View.GONE);
|
binding.offlineLayout.setVisibility(View.GONE);
|
||||||
mp.start();
|
mp.start();
|
||||||
|
// 只显示一次连接消息
|
||||||
|
if (!hasShownConnectedMessage) {
|
||||||
|
hasShownConnectedMessage = true;
|
||||||
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
|
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
|
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
|
||||||
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
|
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
|
||||||
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
||||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||||
// 5秒后尝试重新连接
|
|
||||||
handler.postDelayed(() -> {
|
handler.postDelayed(() -> {
|
||||||
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
|
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
|
||||||
fetchRoom(); // 重新获取房间信息并播放
|
fetchRoom();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1154,14 +1192,34 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean ijkLibLoadFailed = false;
|
||||||
|
|
||||||
private static void ensureIjkLibsLoaded() {
|
private static void ensureIjkLibsLoaded() {
|
||||||
if (ijkLibLoaded) return;
|
if (ijkLibLoaded || ijkLibLoadFailed) return;
|
||||||
try {
|
try {
|
||||||
|
// 检查设备 CPU 架构
|
||||||
|
String[] abis = android.os.Build.SUPPORTED_ABIS;
|
||||||
|
boolean supported = false;
|
||||||
|
for (String abi : abis) {
|
||||||
|
if ("armeabi-v7a".equals(abi) || "arm64-v8a".equals(abi)) {
|
||||||
|
supported = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!supported) {
|
||||||
|
android.util.Log.w("IjkPlayer", "设备 CPU 架构不支持 IjkPlayer: " + java.util.Arrays.toString(abis));
|
||||||
|
ijkLibLoadFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IjkMediaPlayer.loadLibrariesOnce(null);
|
IjkMediaPlayer.loadLibrariesOnce(null);
|
||||||
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
|
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
ijkLibLoaded = true;
|
ijkLibLoaded = true;
|
||||||
|
android.util.Log.d("IjkPlayer", "IjkPlayer 库加载成功");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
android.util.Log.e("IjkPlayer", "加载IjkPlayer库失败: " + e.getMessage());
|
||||||
|
ijkLibLoadFailed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releasePlayer() {
|
private void releasePlayer() {
|
||||||
|
|
@ -1756,49 +1814,16 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
*/
|
*/
|
||||||
private void followStreamerBackend() {
|
private void followStreamerBackend() {
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查主播ID是否存在
|
|
||||||
if (room.getStreamerId() == null) {
|
|
||||||
Toast.makeText(this, "主播信息不可用", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前登录用户ID
|
|
||||||
String currentUserIdStr = AuthStore.getUserId(this);
|
|
||||||
android.util.Log.d("RoomDetail", "followStreamerBackend: currentUserIdStr = " + currentUserIdStr);
|
|
||||||
|
|
||||||
if (currentUserIdStr == null || currentUserIdStr.trim().isEmpty()) {
|
|
||||||
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证用户ID格式
|
|
||||||
try {
|
|
||||||
int currentUserId = Integer.parseInt(currentUserIdStr.trim());
|
|
||||||
int streamerId = room.getStreamerId();
|
|
||||||
|
|
||||||
android.util.Log.d("RoomDetail", "followStreamerBackend: currentUserId = " + currentUserId + ", streamerId = " + streamerId);
|
|
||||||
|
|
||||||
// 不能关注自己
|
|
||||||
if (currentUserId == streamerId) {
|
|
||||||
Toast.makeText(this, "不能关注自己", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
android.util.Log.e("RoomDetail", "用户ID格式错误: currentUserIdStr = '" + currentUserIdStr + "'", e);
|
|
||||||
Toast.makeText(this, "用户ID格式错误,请重新登录", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||||
|
|
||||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||||
body.put("userId", room.getStreamerId()); // 使用主播的用户ID
|
body.put("streamerId", roomId); // 使用房间ID作为主播ID
|
||||||
|
body.put("action", "follow");
|
||||||
|
|
||||||
Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(body);
|
Call<ApiResponse<Map<String, Object>>> call = apiService.followStreamer(body);
|
||||||
|
|
||||||
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1806,26 +1831,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
Response<ApiResponse<Map<String, Object>>> response) {
|
Response<ApiResponse<Map<String, Object>>> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
||||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
if (apiResponse.getCode() == 200) {
|
||||||
Map<String, Object> data = apiResponse.getData();
|
Toast.makeText(RoomDetailActivity.this, "已关注主播", Toast.LENGTH_SHORT).show();
|
||||||
Boolean success = (Boolean) data.get("success");
|
|
||||||
String message = (String) data.get("message");
|
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(success)) {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
message != null ? message : "已关注主播",
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
// 更新UI和状态
|
|
||||||
binding.followButton.setText("已关注");
|
binding.followButton.setText("已关注");
|
||||||
if (room != null) {
|
binding.followButton.setEnabled(false);
|
||||||
room.setIsFollowing(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
message != null ? message : "关注失败",
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
Toast.makeText(RoomDetailActivity.this,
|
||||||
"关注失败: " + apiResponse.getMessage(),
|
"关注失败: " + apiResponse.getMessage(),
|
||||||
|
|
@ -1847,164 +1856,6 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消关注主播
|
|
||||||
*/
|
|
||||||
private void unfollowStreamerBackend() {
|
|
||||||
if (room == null) {
|
|
||||||
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查主播ID是否存在
|
|
||||||
if (room.getStreamerId() == null) {
|
|
||||||
Toast.makeText(this, "主播信息不可用", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
|
||||||
|
|
||||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
|
||||||
body.put("userId", room.getStreamerId()); // 使用主播的用户ID
|
|
||||||
|
|
||||||
Call<ApiResponse<Map<String, Object>>> call = apiService.unfollowUser(body);
|
|
||||||
|
|
||||||
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
|
|
||||||
Response<ApiResponse<Map<String, Object>>> response) {
|
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
|
||||||
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
|
||||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
|
||||||
Map<String, Object> data = apiResponse.getData();
|
|
||||||
Boolean success = (Boolean) data.get("success");
|
|
||||||
String message = (String) data.get("message");
|
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(success)) {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
message != null ? message : "已取消关注",
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
// 更新UI和状态
|
|
||||||
binding.followButton.setText("关注");
|
|
||||||
if (room != null) {
|
|
||||||
room.setIsFollowing(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
message != null ? message : "取消关注失败",
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
"取消关注失败: " + apiResponse.getMessage(),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
"取消关注失败",
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
|
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
|
||||||
"网络错误: " + t.getMessage(),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示观众列表对话框
|
|
||||||
*/
|
|
||||||
private void showViewerListDialog() {
|
|
||||||
if (room == null || TextUtils.isEmpty(roomId)) {
|
|
||||||
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建对话框
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
|
|
||||||
builder.setTitle("在线观众 (" + binding.topViewerCount.getText() + ")");
|
|
||||||
|
|
||||||
// 显示加载中
|
|
||||||
View loadingView = getLayoutInflater().inflate(android.R.layout.simple_list_item_1, null);
|
|
||||||
((android.widget.TextView) loadingView.findViewById(android.R.id.text1)).setText("加载中...");
|
|
||||||
builder.setView(loadingView);
|
|
||||||
|
|
||||||
androidx.appcompat.app.AlertDialog dialog = builder.create();
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
// 调用后端接口获取观众列表
|
|
||||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
|
||||||
Call<ApiResponse<Map<String, Object>>> call = apiService.getRoomOnlineUsers(roomId);
|
|
||||||
|
|
||||||
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
|
|
||||||
Response<ApiResponse<Map<String, Object>>> response) {
|
|
||||||
if (!isFinishing() && dialog.isShowing()) {
|
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
|
||||||
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
|
||||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
|
||||||
Map<String, Object> data = apiResponse.getData();
|
|
||||||
|
|
||||||
// 获取观众列表
|
|
||||||
Object usersObj = data.get("users");
|
|
||||||
if (usersObj instanceof List) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Map<String, Object>> users = (List<Map<String, Object>>) usersObj;
|
|
||||||
|
|
||||||
if (users.isEmpty()) {
|
|
||||||
// 没有观众
|
|
||||||
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
|
|
||||||
.setText("暂无观众");
|
|
||||||
} else {
|
|
||||||
// 创建观众列表视图
|
|
||||||
String[] userNames = new String[users.size()];
|
|
||||||
for (int i = 0; i < users.size(); i++) {
|
|
||||||
Map<String, Object> user = users.get(i);
|
|
||||||
String nickname = user.get("nickname") != null ?
|
|
||||||
user.get("nickname").toString() : "匿名用户";
|
|
||||||
userNames[i] = nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
new MaterialAlertDialogBuilder(RoomDetailActivity.this)
|
|
||||||
.setTitle("在线观众 (" + users.size() + ")")
|
|
||||||
.setItems(userNames, null)
|
|
||||||
.setPositiveButton("关闭", null)
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
|
|
||||||
.setText("数据格式错误");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
|
|
||||||
.setText("获取失败: " + apiResponse.getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
|
|
||||||
.setText("获取失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
|
|
||||||
if (!isFinishing() && dialog.isShowing()) {
|
|
||||||
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
|
|
||||||
.setText("网络错误: " + t.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始直播
|
* 开始直播
|
||||||
* 接口: POST /api/front/live/room/{id}/start
|
* 接口: POST /api/front/live/room/{id}/start
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package com.example.livestreaming;
|
package com.example.livestreaming;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
@ -16,24 +16,48 @@ import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.example.livestreaming.databinding.ActivityWishTreeBinding;
|
import com.example.livestreaming.databinding.ActivityWishTreeBinding;
|
||||||
|
import com.example.livestreaming.net.ApiClient;
|
||||||
|
import com.example.livestreaming.net.ApiResponse;
|
||||||
|
import com.example.livestreaming.net.AuthStore;
|
||||||
|
import com.example.livestreaming.net.WishtreeRequest;
|
||||||
|
import com.example.livestreaming.net.WishtreeResponse;
|
||||||
|
import com.example.livestreaming.net.ApiService;
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 许愿树页面
|
||||||
|
* 数据通过API实时同步到服务端
|
||||||
|
*/
|
||||||
public class WishTreeActivity extends AppCompatActivity {
|
public class WishTreeActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private ActivityWishTreeBinding binding;
|
private ActivityWishTreeBinding binding;
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
private Runnable timerRunnable;
|
private Runnable timerRunnable;
|
||||||
private SharedPreferences prefs;
|
private ApiService apiService;
|
||||||
private String[] wishes = new String[7];
|
|
||||||
|
// 心愿数据(从服务端获取)
|
||||||
|
private List<WishtreeResponse.Wish> myWishes = new ArrayList<>();
|
||||||
|
// 当前节日
|
||||||
|
private WishtreeResponse.Festival currentFestival;
|
||||||
|
// 是否正在加载
|
||||||
|
private boolean isLoading = false;
|
||||||
|
|
||||||
public static void start(Context context) {
|
public static void start(Context context) {
|
||||||
context.startActivity(new Intent(context, WishTreeActivity.class));
|
context.startActivity(new Intent(context, WishTreeActivity.class));
|
||||||
|
|
@ -45,36 +69,112 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
|
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
prefs = getSharedPreferences("wishes", MODE_PRIVATE);
|
apiService = ApiClient.getService(this);
|
||||||
loadWishes();
|
|
||||||
|
|
||||||
startBannerCountdown();
|
startBannerCountdown();
|
||||||
setupBottomNav();
|
setupBottomNav();
|
||||||
setupWishCards();
|
setupWishCards();
|
||||||
setupMakeWishButton();
|
setupMakeWishButton();
|
||||||
|
setupTopBarButtons();
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
loadCurrentFestival();
|
||||||
|
loadMyWishes();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadWishes() {
|
/**
|
||||||
for (int i = 0; i < 7; i++) {
|
* 设置顶部栏按钮
|
||||||
wishes[i] = prefs.getString("wish_" + i, "");
|
*/
|
||||||
|
private void setupTopBarButtons() {
|
||||||
|
// 搜索按钮
|
||||||
|
binding.searchButton.setOnClickListener(v -> {
|
||||||
|
SearchActivity.start(this);
|
||||||
|
});
|
||||||
|
// 通知按钮
|
||||||
|
binding.notifyButton.setOnClickListener(v -> {
|
||||||
|
NotificationsActivity.start(this);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已登录
|
||||||
|
*/
|
||||||
|
private boolean checkLogin() {
|
||||||
|
String token = AuthStore.getToken(this);
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||||
|
startActivity(new Intent(this, LoginActivity.class));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载当前节日
|
||||||
|
*/
|
||||||
|
private void loadCurrentFestival() {
|
||||||
|
apiService.getCurrentFestival().enqueue(new Callback<ApiResponse<WishtreeResponse.Festival>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.Festival>> call,
|
||||||
|
@NonNull Response<ApiResponse<WishtreeResponse.Festival>> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
|
||||||
|
currentFestival = response.body().getData();
|
||||||
|
updateBannerText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.Festival>> call, @NonNull Throwable t) {
|
||||||
|
// 使用默认文案
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新横幅文字
|
||||||
|
*/
|
||||||
|
private void updateBannerText() {
|
||||||
|
if (currentFestival != null && binding != null) {
|
||||||
|
String text = currentFestival.name + "许愿树 | 许下心愿,愿望成真";
|
||||||
|
binding.bannerText.setText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从服务端加载我的心愿
|
||||||
|
*/
|
||||||
|
private void loadMyWishes() {
|
||||||
|
apiService.getMyWishes(1, 10).enqueue(new Callback<ApiResponse<WishtreeResponse.WishPage>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call,
|
||||||
|
@NonNull Response<ApiResponse<WishtreeResponse.WishPage>> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
|
||||||
|
myWishes = response.body().getData().list;
|
||||||
|
if (myWishes == null) myWishes = new ArrayList<>();
|
||||||
updateWishCards();
|
updateWishCards();
|
||||||
|
updateWishCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveWish(int index, String wish) {
|
@Override
|
||||||
wishes[index] = wish;
|
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.WishPage>> call, @NonNull Throwable t) {
|
||||||
prefs.edit().putString("wish_" + index, wish).apply();
|
Toast.makeText(WishTreeActivity.this, "加载心愿失败", Toast.LENGTH_SHORT).show();
|
||||||
updateWishCards();
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新心愿卡片显示
|
||||||
|
*/
|
||||||
private void updateWishCards() {
|
private void updateWishCards() {
|
||||||
TextView[] cards = {
|
TextView[] cards = {
|
||||||
binding.wishCard1, binding.wishCard2, binding.wishCard3,
|
binding.wishCard1, binding.wishCard2, binding.wishCard3,
|
||||||
binding.wishCard4, binding.wishCard5, binding.wishCard6, binding.wishCard7
|
binding.wishCard4, binding.wishCard5, binding.wishCard6, binding.wishCard7
|
||||||
};
|
};
|
||||||
for (int i = 0; i < cards.length; i++) {
|
for (int i = 0; i < cards.length; i++) {
|
||||||
if (wishes[i] != null && !wishes[i].isEmpty()) {
|
if (i < myWishes.size() && myWishes.get(i) != null) {
|
||||||
String text = wishes[i].length() > 8 ? wishes[i].substring(0, 8) + "..." : wishes[i];
|
String content = myWishes.get(i).content;
|
||||||
|
String text = content.length() > 8 ? content.substring(0, 8) + "..." : content;
|
||||||
cards[i].setText(text);
|
cards[i].setText(text);
|
||||||
} else {
|
} else {
|
||||||
cards[i].setText("");
|
cards[i].setText("");
|
||||||
|
|
@ -82,6 +182,17 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新祈愿值显示
|
||||||
|
*/
|
||||||
|
private void updateWishCount() {
|
||||||
|
int count = myWishes != null ? myWishes.size() : 0;
|
||||||
|
binding.tvWishCount.setText("祈愿值:" + count + "/100");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置心愿卡片点击事件
|
||||||
|
*/
|
||||||
private void setupWishCards() {
|
private void setupWishCards() {
|
||||||
TextView[] cards = {
|
TextView[] cards = {
|
||||||
binding.wishCard1, binding.wishCard2, binding.wishCard3,
|
binding.wishCard1, binding.wishCard2, binding.wishCard3,
|
||||||
|
|
@ -91,29 +202,36 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
final int index = i;
|
final int index = i;
|
||||||
cards[i].setOnClickListener(v -> onWishCardClick(index));
|
cards[i].setOnClickListener(v -> onWishCardClick(index));
|
||||||
}
|
}
|
||||||
binding.addWishCard.setOnClickListener(v -> showMakeWishInputDialog(-1));
|
binding.addWishCard.setOnClickListener(v -> showMakeWishInputDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 心愿卡片点击
|
||||||
|
*/
|
||||||
private void onWishCardClick(int index) {
|
private void onWishCardClick(int index) {
|
||||||
if (wishes[index] != null && !wishes[index].isEmpty()) {
|
if (index < myWishes.size() && myWishes.get(index) != null) {
|
||||||
showViewWishDialog(index);
|
showViewWishDialog(index);
|
||||||
} else {
|
} else {
|
||||||
showMakeWishInputDialog(index);
|
showMakeWishInputDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showMakeWishInputDialog(int index) {
|
/**
|
||||||
|
* 显示许愿输入对话框
|
||||||
|
*/
|
||||||
|
private void showMakeWishInputDialog() {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!checkLogin()) return;
|
||||||
|
|
||||||
Dialog dialog = new Dialog(this);
|
Dialog dialog = new Dialog(this);
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
dialog.setContentView(R.layout.dialog_make_wish);
|
dialog.setContentView(R.layout.dialog_make_wish);
|
||||||
if (dialog.getWindow() != null) {
|
if (dialog.getWindow() != null) {
|
||||||
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||||
// 设置对话框宽度为屏幕宽度的85%
|
|
||||||
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.85);
|
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.85);
|
||||||
dialog.getWindow().setLayout(width, android.view.WindowManager.LayoutParams.WRAP_CONTENT);
|
dialog.getWindow().setLayout(width, android.view.WindowManager.LayoutParams.WRAP_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
EditText input = dialog.findViewById(R.id.editWish);
|
EditText input = dialog.findViewById(R.id.editWish);
|
||||||
TextView tvCharCount = dialog.findViewById(R.id.tvCharCount);
|
TextView tvCharCount = dialog.findViewById(R.id.tvCharCount);
|
||||||
View btnCancel = dialog.findViewById(R.id.btnCancel);
|
View btnCancel = dialog.findViewById(R.id.btnCancel);
|
||||||
|
|
@ -136,14 +254,11 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
btnMakeWish.setOnClickListener(v -> {
|
btnMakeWish.setOnClickListener(v -> {
|
||||||
String wish = input.getText().toString().trim();
|
String wish = input.getText().toString().trim();
|
||||||
if (!wish.isEmpty()) {
|
if (!wish.isEmpty()) {
|
||||||
int saveIndex = index >= 0 ? index : findEmptySlot();
|
if (wish.length() > 50) {
|
||||||
if (saveIndex >= 0) {
|
Toast.makeText(this, "心愿内容不能超过50字", Toast.LENGTH_SHORT).show();
|
||||||
saveWish(saveIndex, wish);
|
return;
|
||||||
dialog.dismiss();
|
|
||||||
showSuccessDialog();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "心愿牌已满", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
}
|
||||||
|
publishWish(wish, dialog);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "请输入心愿内容", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "请输入心愿内容", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
@ -152,13 +267,42 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int findEmptySlot() {
|
/**
|
||||||
for (int i = 0; i < wishes.length; i++) {
|
* 发布心愿到服务端
|
||||||
if (wishes[i] == null || wishes[i].isEmpty()) return i;
|
*/
|
||||||
|
private void publishWish(String content, Dialog dialog) {
|
||||||
|
Integer festivalId = currentFestival != null ? currentFestival.id : null;
|
||||||
|
WishtreeRequest.PublishWish request = new WishtreeRequest.PublishWish(festivalId, content, null);
|
||||||
|
|
||||||
|
apiService.publishWish(request).enqueue(new Callback<ApiResponse<WishtreeResponse.Wish>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse<WishtreeResponse.Wish>> call,
|
||||||
|
@NonNull Response<ApiResponse<WishtreeResponse.Wish>> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
if (response.body().getCode() == 200) {
|
||||||
|
dialog.dismiss();
|
||||||
|
showSuccessDialog();
|
||||||
|
loadMyWishes(); // 重新加载心愿列表
|
||||||
|
} else {
|
||||||
|
Toast.makeText(WishTreeActivity.this,
|
||||||
|
response.body().getMessage() != null ? response.body().getMessage() : "发布失败",
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(WishTreeActivity.this, "发布失败", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse<WishtreeResponse.Wish>> call, @NonNull Throwable t) {
|
||||||
|
Toast.makeText(WishTreeActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示成功对话框
|
||||||
|
*/
|
||||||
private void showSuccessDialog() {
|
private void showSuccessDialog() {
|
||||||
Dialog dialog = new Dialog(this);
|
Dialog dialog = new Dialog(this);
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
@ -170,12 +314,20 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
handler.postDelayed(dialog::dismiss, 1500);
|
handler.postDelayed(dialog::dismiss, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示查看心愿对话框
|
||||||
|
*/
|
||||||
private void showViewWishDialog(int index) {
|
private void showViewWishDialog(int index) {
|
||||||
|
if (index >= myWishes.size()) return;
|
||||||
|
WishtreeResponse.Wish wish = myWishes.get(index);
|
||||||
|
|
||||||
Dialog dialog = new Dialog(this);
|
Dialog dialog = new Dialog(this);
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
dialog.setContentView(R.layout.dialog_view_wish);
|
dialog.setContentView(R.layout.dialog_view_wish);
|
||||||
if (dialog.getWindow() != null) {
|
if (dialog.getWindow() != null) {
|
||||||
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||||
|
int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.85);
|
||||||
|
dialog.getWindow().setLayout(width, android.view.WindowManager.LayoutParams.WRAP_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextView tvContent = dialog.findViewById(R.id.tvWishContent);
|
TextView tvContent = dialog.findViewById(R.id.tvWishContent);
|
||||||
|
|
@ -183,27 +335,94 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
TextView btnDelete = dialog.findViewById(R.id.btnDeleteWish);
|
TextView btnDelete = dialog.findViewById(R.id.btnDeleteWish);
|
||||||
TextView btnComplete = dialog.findViewById(R.id.btnCompleteWish);
|
TextView btnComplete = dialog.findViewById(R.id.btnCompleteWish);
|
||||||
|
|
||||||
tvContent.setText(wishes[index]);
|
tvContent.setText(wish.content);
|
||||||
|
|
||||||
|
// 显示点赞数和评论数
|
||||||
|
TextView tvLikeCount = dialog.findViewById(R.id.tvLikeCount);
|
||||||
|
TextView tvCommentCount = dialog.findViewById(R.id.tvCommentCount);
|
||||||
|
if (tvLikeCount != null) {
|
||||||
|
tvLikeCount.setText("❤ " + wish.likeCount);
|
||||||
|
}
|
||||||
|
if (tvCommentCount != null) {
|
||||||
|
tvCommentCount.setText("💬 " + wish.commentCount);
|
||||||
|
}
|
||||||
|
|
||||||
btnClose.setOnClickListener(v -> dialog.dismiss());
|
btnClose.setOnClickListener(v -> dialog.dismiss());
|
||||||
|
|
||||||
|
// 删除心愿 - 添加确认对话框
|
||||||
btnDelete.setOnClickListener(v -> {
|
btnDelete.setOnClickListener(v -> {
|
||||||
saveWish(index, "");
|
new AlertDialog.Builder(this)
|
||||||
Toast.makeText(this, "心愿已删除", Toast.LENGTH_SHORT).show();
|
.setTitle("确认删除")
|
||||||
dialog.dismiss();
|
.setMessage("确定要删除这个心愿吗?")
|
||||||
|
.setPositiveButton("删除", (d, w) -> {
|
||||||
|
deleteWish(wish.id, dialog, false);
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 愿望达成 - 添加确认对话框
|
||||||
btnComplete.setOnClickListener(v -> {
|
btnComplete.setOnClickListener(v -> {
|
||||||
saveWish(index, "");
|
new AlertDialog.Builder(this)
|
||||||
Toast.makeText(this, "恭喜愿望达成!", Toast.LENGTH_SHORT).show();
|
.setTitle("愿望达成")
|
||||||
dialog.dismiss();
|
.setMessage("恭喜!确认愿望已经达成了吗?")
|
||||||
|
.setPositiveButton("确认", (d, w) -> {
|
||||||
|
deleteWish(wish.id, dialog, true);
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除心愿
|
||||||
|
* @param wishId 心愿ID
|
||||||
|
* @param dialog 对话框
|
||||||
|
* @param isComplete 是否是愿望达成
|
||||||
|
*/
|
||||||
|
private void deleteWish(long wishId, Dialog dialog, boolean isComplete) {
|
||||||
|
apiService.deleteMyWish(wishId).enqueue(new Callback<ApiResponse<String>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse<String>> call,
|
||||||
|
@NonNull Response<ApiResponse<String>> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null && response.body().getCode() == 200) {
|
||||||
|
dialog.dismiss();
|
||||||
|
if (isComplete) {
|
||||||
|
showCompleteSuccessDialog();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(WishTreeActivity.this, "心愿已删除", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
loadMyWishes(); // 重新加载
|
||||||
|
} else {
|
||||||
|
Toast.makeText(WishTreeActivity.this, "操作失败", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse<String>> call, @NonNull Throwable t) {
|
||||||
|
Toast.makeText(WishTreeActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示愿望达成成功对话框
|
||||||
|
*/
|
||||||
|
private void showCompleteSuccessDialog() {
|
||||||
|
Dialog dialog = new Dialog(this);
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
dialog.setContentView(R.layout.dialog_wish_complete);
|
||||||
|
if (dialog.getWindow() != null) {
|
||||||
|
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||||
|
}
|
||||||
|
dialog.show();
|
||||||
|
handler.postDelayed(dialog::dismiss, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
private void setupMakeWishButton() {
|
private void setupMakeWishButton() {
|
||||||
binding.btnMakeWish.setOnClickListener(v -> showMakeWishInputDialog(-1));
|
binding.btnMakeWish.setOnClickListener(v -> showMakeWishInputDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupBottomNav() {
|
private void setupBottomNav() {
|
||||||
|
|
@ -295,5 +514,7 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
|
binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
|
||||||
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
||||||
}
|
}
|
||||||
|
// 每次返回页面时刷新数据
|
||||||
|
loadMyWishes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接信令服务器
|
* 连接信令服务器
|
||||||
|
* 通话服务始终使用远程服务器地址
|
||||||
*/
|
*/
|
||||||
public void connect(int userId) {
|
public void connect(int userId) {
|
||||||
Log.d(TAG, "connect() called, userId: " + userId);
|
Log.d(TAG, "connect() called, userId: " + userId);
|
||||||
|
|
@ -88,8 +89,9 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
Log.d(TAG, "已经连接,跳过");
|
Log.d(TAG, "已经连接,跳过");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String baseUrl = ApiClient.getCurrentBaseUrl(context);
|
// 通话服务使用独立的远程服务器地址
|
||||||
Log.d(TAG, "连接信令服务器,baseUrl: " + baseUrl);
|
String baseUrl = WebRTCConfig.getLiveServerUrl();
|
||||||
|
Log.d(TAG, "连接信令服务器(远程),baseUrl: " + baseUrl);
|
||||||
signalingClient = new CallSignalingClient(baseUrl, userId);
|
signalingClient = new CallSignalingClient(baseUrl, userId);
|
||||||
signalingClient.setListener(this);
|
signalingClient.setListener(this);
|
||||||
signalingClient.connect();
|
signalingClient.connect();
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
package com.example.livestreaming.call;
|
package com.example.livestreaming.call;
|
||||||
|
|
||||||
|
import com.example.livestreaming.BuildConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebRTC 配置
|
* WebRTC 配置
|
||||||
* 部署时修改这里的服务器地址
|
* 直播/通话服务始终连接远程服务器
|
||||||
|
* 配置在 local.properties 中修改
|
||||||
*/
|
*/
|
||||||
public class WebRTCConfig {
|
public class WebRTCConfig {
|
||||||
|
|
||||||
// ============ STUN 服务器 ============
|
// ============ STUN 服务器 ============
|
||||||
// STUN 用于获取设备的公网IP,帮助建立P2P连接
|
|
||||||
public static final String[] STUN_SERVERS = {
|
public static final String[] STUN_SERVERS = {
|
||||||
"stun:stun.l.google.com:19302", // Google STUN(国内可能不稳定)
|
"stun:stun.l.google.com:19302",
|
||||||
"stun:stun.qq.com:3478", // 腾讯 STUN(国内推荐)
|
"stun:stun.qq.com:3478",
|
||||||
"stun:stun.miwifi.com:3478" // 小米 STUN(国内推荐)
|
"stun:stun.miwifi.com:3478"
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============ TURN 服务器 ============
|
// ============ TURN 服务器(从BuildConfig读取)============
|
||||||
// TURN 用于在P2P连接失败时进行中继转发
|
public static final String TURN_SERVER_URL = "turn:" + BuildConfig.TURN_SERVER_HOST + ":" + BuildConfig.TURN_SERVER_PORT;
|
||||||
|
|
||||||
// 你的服务器TURN地址
|
|
||||||
public static final String TURN_SERVER_URL = "turn:1.15.149.240:3478";
|
|
||||||
|
|
||||||
// TURN 服务器用户名
|
// TURN 服务器用户名
|
||||||
public static final String TURN_USERNAME = "turnuser";
|
public static final String TURN_USERNAME = "turnuser";
|
||||||
|
|
@ -26,27 +25,12 @@ public class WebRTCConfig {
|
||||||
// TURN 服务器密码
|
// TURN 服务器密码
|
||||||
public static final String TURN_PASSWORD = "TurnPass123456";
|
public static final String TURN_PASSWORD = "TurnPass123456";
|
||||||
|
|
||||||
// ============ 使用说明 ============
|
// ============ 直播/通话服务地址 ============
|
||||||
/*
|
public static String getLiveServerUrl() {
|
||||||
* 局域网测试:
|
return "http://" + BuildConfig.LIVE_SERVER_HOST + ":" + BuildConfig.LIVE_SERVER_PORT + "/";
|
||||||
* - 不需要修改,当前配置即可使用
|
}
|
||||||
*
|
|
||||||
* 部署到公网服务器:
|
public static String getLiveWsUrl() {
|
||||||
* 1. 在服务器安装 coturn (TURN服务器)
|
return "ws://" + BuildConfig.LIVE_SERVER_HOST + ":" + BuildConfig.LIVE_SERVER_PORT + "/";
|
||||||
* 2. 修改上面的配置:
|
}
|
||||||
* TURN_SERVER_URL = "turn:你的服务器IP:3478"
|
|
||||||
* TURN_USERNAME = "你设置的用户名"
|
|
||||||
* TURN_PASSWORD = "你设置的密码"
|
|
||||||
*
|
|
||||||
* 宝塔安装 coturn 步骤:
|
|
||||||
* 1. SSH执行: yum install -y coturn (CentOS) 或 apt install -y coturn (Ubuntu)
|
|
||||||
* 2. 编辑 /etc/turnserver.conf:
|
|
||||||
* listening-port=3478
|
|
||||||
* external-ip=你的公网IP
|
|
||||||
* realm=你的公网IP
|
|
||||||
* lt-cred-mech
|
|
||||||
* user=用户名:密码
|
|
||||||
* 3. 宝塔放行端口: 3478(TCP/UDP), 49152-65535(UDP)
|
|
||||||
* 4. 启动: systemctl start coturn
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -420,4 +420,88 @@ public interface ApiService {
|
||||||
|
|
||||||
@POST("api/front/live/rooms/{roomId}/broadcast/online")
|
@POST("api/front/live/rooms/{roomId}/broadcast/online")
|
||||||
Call<ApiResponse<Map<String, Object>>> broadcastOnlineCount(@Path("roomId") String roomId);
|
Call<ApiResponse<Map<String, Object>>> broadcastOnlineCount(@Path("roomId") String roomId);
|
||||||
|
|
||||||
|
// ==================== 许愿树接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节日列表
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/festivals")
|
||||||
|
Call<ApiResponse<List<WishtreeResponse.Festival>>> getWishtreeFestivals();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前进行中的节日
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/festivals/current")
|
||||||
|
Call<ApiResponse<WishtreeResponse.Festival>> getCurrentFestival();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取心愿列表
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/wishes")
|
||||||
|
Call<ApiResponse<WishtreeResponse.WishPage>> getWishtreeWishes(
|
||||||
|
@Query("festivalId") Integer festivalId,
|
||||||
|
@Query("page") int page,
|
||||||
|
@Query("limit") int limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的心愿列表
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/wishes/my")
|
||||||
|
Call<ApiResponse<WishtreeResponse.WishPage>> getMyWishes(
|
||||||
|
@Query("page") int page,
|
||||||
|
@Query("limit") int limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布心愿
|
||||||
|
*/
|
||||||
|
@POST("api/front/wishtree/wishes")
|
||||||
|
Call<ApiResponse<WishtreeResponse.Wish>> publishWish(@Body WishtreeRequest.PublishWish body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取心愿详情
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/wishes/{id}")
|
||||||
|
Call<ApiResponse<WishtreeResponse.Wish>> getWishDetail(@Path("id") long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除我的心愿
|
||||||
|
*/
|
||||||
|
@DELETE("api/front/wishtree/wishes/{id}")
|
||||||
|
Call<ApiResponse<String>> deleteMyWish(@Path("id") long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点赞心愿
|
||||||
|
*/
|
||||||
|
@POST("api/front/wishtree/wishes/{id}/like")
|
||||||
|
Call<ApiResponse<String>> likeWish(@Path("id") long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消点赞
|
||||||
|
*/
|
||||||
|
@DELETE("api/front/wishtree/wishes/{id}/like")
|
||||||
|
Call<ApiResponse<String>> unlikeWish(@Path("id") long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论列表
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/wishes/{id}/comments")
|
||||||
|
Call<ApiResponse<WishtreeResponse.CommentPage>> getWishComments(
|
||||||
|
@Path("id") long wishId,
|
||||||
|
@Query("page") int page,
|
||||||
|
@Query("limit") int limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发表评论
|
||||||
|
*/
|
||||||
|
@POST("api/front/wishtree/wishes/{id}/comments")
|
||||||
|
Call<ApiResponse<WishtreeResponse.Comment>> addWishComment(
|
||||||
|
@Path("id") long wishId,
|
||||||
|
@Body WishtreeRequest.AddComment body);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取背景素材列表
|
||||||
|
*/
|
||||||
|
@GET("api/front/wishtree/backgrounds")
|
||||||
|
Call<ApiResponse<List<WishtreeResponse.Background>>> getWishtreeBackgrounds();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.example.livestreaming.net;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 许愿树相关请求类
|
||||||
|
*/
|
||||||
|
public class WishtreeRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布心愿请求
|
||||||
|
*/
|
||||||
|
public static class PublishWish {
|
||||||
|
@SerializedName("festivalId")
|
||||||
|
public Integer festivalId;
|
||||||
|
@SerializedName("content")
|
||||||
|
public String content;
|
||||||
|
@SerializedName("backgroundId")
|
||||||
|
public Integer backgroundId;
|
||||||
|
|
||||||
|
public PublishWish(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublishWish(Integer festivalId, String content, Integer backgroundId) {
|
||||||
|
this.festivalId = festivalId;
|
||||||
|
this.content = content;
|
||||||
|
this.backgroundId = backgroundId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发表评论请求
|
||||||
|
*/
|
||||||
|
public static class AddComment {
|
||||||
|
@SerializedName("content")
|
||||||
|
public String content;
|
||||||
|
@SerializedName("parentId")
|
||||||
|
public Long parentId;
|
||||||
|
|
||||||
|
public AddComment(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddComment(String content, Long parentId) {
|
||||||
|
this.content = content;
|
||||||
|
this.parentId = parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package com.example.livestreaming.net;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 许愿树相关响应类
|
||||||
|
*/
|
||||||
|
public class WishtreeResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节日信息
|
||||||
|
*/
|
||||||
|
public static class Festival {
|
||||||
|
@SerializedName("id")
|
||||||
|
public int id;
|
||||||
|
@SerializedName("name")
|
||||||
|
public String name;
|
||||||
|
@SerializedName("icon")
|
||||||
|
public String icon;
|
||||||
|
@SerializedName("startDate")
|
||||||
|
public String startDate;
|
||||||
|
@SerializedName("endDate")
|
||||||
|
public String endDate;
|
||||||
|
@SerializedName("isLunar")
|
||||||
|
public int isLunar;
|
||||||
|
@SerializedName("themeColor")
|
||||||
|
public String themeColor;
|
||||||
|
@SerializedName("status")
|
||||||
|
public int status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 心愿信息
|
||||||
|
*/
|
||||||
|
public static class Wish {
|
||||||
|
@SerializedName("id")
|
||||||
|
public long id;
|
||||||
|
@SerializedName("uid")
|
||||||
|
public int uid;
|
||||||
|
@SerializedName("nickname")
|
||||||
|
public String nickname;
|
||||||
|
@SerializedName("avatar")
|
||||||
|
public String avatar;
|
||||||
|
@SerializedName("festivalId")
|
||||||
|
public int festivalId;
|
||||||
|
@SerializedName("festivalName")
|
||||||
|
public String festivalName;
|
||||||
|
@SerializedName("festivalIcon")
|
||||||
|
public String festivalIcon;
|
||||||
|
@SerializedName("content")
|
||||||
|
public String content;
|
||||||
|
@SerializedName("backgroundId")
|
||||||
|
public int backgroundId;
|
||||||
|
@SerializedName("backgroundImage")
|
||||||
|
public String backgroundImage;
|
||||||
|
@SerializedName("status")
|
||||||
|
public int status;
|
||||||
|
@SerializedName("likeCount")
|
||||||
|
public int likeCount;
|
||||||
|
@SerializedName("commentCount")
|
||||||
|
public int commentCount;
|
||||||
|
@SerializedName("isLiked")
|
||||||
|
public Boolean isLiked;
|
||||||
|
@SerializedName("createTime")
|
||||||
|
public String createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景素材
|
||||||
|
*/
|
||||||
|
public static class Background {
|
||||||
|
@SerializedName("id")
|
||||||
|
public int id;
|
||||||
|
@SerializedName("name")
|
||||||
|
public String name;
|
||||||
|
@SerializedName("image")
|
||||||
|
public String image;
|
||||||
|
@SerializedName("festivalId")
|
||||||
|
public int festivalId;
|
||||||
|
@SerializedName("status")
|
||||||
|
public int status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论信息
|
||||||
|
*/
|
||||||
|
public static class Comment {
|
||||||
|
@SerializedName("id")
|
||||||
|
public long id;
|
||||||
|
@SerializedName("wishId")
|
||||||
|
public long wishId;
|
||||||
|
@SerializedName("uid")
|
||||||
|
public int uid;
|
||||||
|
@SerializedName("nickname")
|
||||||
|
public String nickname;
|
||||||
|
@SerializedName("avatar")
|
||||||
|
public String avatar;
|
||||||
|
@SerializedName("content")
|
||||||
|
public String content;
|
||||||
|
@SerializedName("parentId")
|
||||||
|
public long parentId;
|
||||||
|
@SerializedName("status")
|
||||||
|
public int status;
|
||||||
|
@SerializedName("createTime")
|
||||||
|
public String createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页数据
|
||||||
|
*/
|
||||||
|
public static class WishPage {
|
||||||
|
@SerializedName("list")
|
||||||
|
public List<Wish> list;
|
||||||
|
@SerializedName("total")
|
||||||
|
public int total;
|
||||||
|
@SerializedName("pageNum")
|
||||||
|
public int pageNum;
|
||||||
|
@SerializedName("pageSize")
|
||||||
|
public int pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论分页数据
|
||||||
|
*/
|
||||||
|
public static class CommentPage {
|
||||||
|
@SerializedName("list")
|
||||||
|
public List<Comment> list;
|
||||||
|
@SerializedName("total")
|
||||||
|
public int total;
|
||||||
|
@SerializedName("pageNum")
|
||||||
|
public int pageNum;
|
||||||
|
@SerializedName("pageSize")
|
||||||
|
public int pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
android-app/app/src/main/res/drawable/app_logo.png
Normal file
BIN
android-app/app/src/main/res/drawable/app_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
6
android-app/app/src/main/res/drawable/bg_btn_cancel.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_btn_cancel.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#F0F0F0" />
|
||||||
|
<corners android:radius="22dp" />
|
||||||
|
</shape>
|
||||||
6
android-app/app/src/main/res/drawable/bg_btn_primary.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_btn_primary.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FF6B9D" />
|
||||||
|
<corners android:radius="22dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
</shape>
|
||||||
7
android-app/app/src/main/res/drawable/bg_edit_text.xml
Normal file
7
android-app/app/src/main/res/drawable/bg_edit_text.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#F5F5F5" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#E0E0E0" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#FFF8E1" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke android:width="1dp" android:color="#FFE082" />
|
||||||
|
</shape>
|
||||||
|
|
@ -3,46 +3,47 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="24dp"
|
android:background="@drawable/bg_dialog_rounded"
|
||||||
android:background="@android:color/white">
|
android:padding="20dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="许下你的愿望"
|
android:text="许下你的愿望"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
|
android:textColor="#333333"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textColor="@android:color/black"
|
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/editWish"
|
android:id="@+id/editWish"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="120dp"
|
android:layout_height="wrap_content"
|
||||||
android:hint="写下你的愿望..."
|
android:hint="写下你的愿望..."
|
||||||
|
android:textColorHint="#999999"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:minLines="3"
|
||||||
android:maxLength="50"
|
android:maxLength="50"
|
||||||
android:gravity="top|start"
|
android:gravity="top"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="@android:drawable/edit_text"
|
android:background="@drawable/bg_edit_text"
|
||||||
android:textSize="14sp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvCharCount"
|
android:id="@+id/tvCharCount"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="0/50"
|
android:text="0/50"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:textColor="@android:color/darker_gray"
|
android:textColor="#999999"
|
||||||
android:layout_gravity="end"
|
android:gravity="end"
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal">
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/btnCancel"
|
android:id="@+id/btnCancel"
|
||||||
|
|
@ -50,10 +51,10 @@
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="取消"
|
android:text="取消"
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/darker_gray"
|
android:textColor="#666666"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:gravity="center"
|
||||||
|
android:background="@drawable/bg_btn_cancel"
|
||||||
android:layout_marginEnd="8dp" />
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -62,10 +63,10 @@
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="许愿"
|
android:text="许愿"
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="#FFFFFF"
|
||||||
android:background="#FF6B9D"
|
android:gravity="center"
|
||||||
|
android:background="@drawable/bg_btn_primary"
|
||||||
android:layout_marginStart="8dp" />
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="24dp"
|
android:background="@drawable/bg_dialog_rounded"
|
||||||
android:background="@android:color/white">
|
android:padding="20dp">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
@ -14,20 +14,19 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="我的愿望"
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="我的心愿"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
android:textColor="#333333"
|
||||||
android:textColor="@android:color/black"
|
android:textStyle="bold" />
|
||||||
android:layout_centerInParent="true" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/btnClose"
|
android:id="@+id/btnClose"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_centerVertical="true"
|
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless" />
|
android:contentDescription="关闭" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|
@ -35,29 +34,56 @@
|
||||||
android:id="@+id/tvWishContent"
|
android:id="@+id/tvWishContent"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="100dp"
|
android:minHeight="80dp"
|
||||||
|
android:text=""
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#333333"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:textSize="14sp"
|
android:background="@drawable/bg_edit_text"
|
||||||
android:textColor="@android:color/black"
|
android:layout_marginBottom="12dp" />
|
||||||
android:background="@android:drawable/edit_text"
|
|
||||||
android:layout_marginBottom="16dp" />
|
|
||||||
|
|
||||||
|
<!-- 点赞数和评论数 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center">
|
android:layout_marginBottom="16dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLikeCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="❤ 0"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#FF6B6B"
|
||||||
|
android:layout_marginEnd="20dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCommentCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="💬 0"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#666666" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/btnDeleteWish"
|
android:id="@+id/btnDeleteWish"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="删除"
|
android:text="删除心愿"
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/holo_red_dark"
|
android:textColor="#FF6B6B"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:gravity="center"
|
||||||
|
android:background="@drawable/bg_btn_cancel"
|
||||||
android:layout_marginEnd="8dp" />
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -65,11 +91,11 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="完成"
|
android:text="愿望达成"
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="#FFFFFF"
|
||||||
android:background="#FF6B9D"
|
android:gravity="center"
|
||||||
|
android:background="@drawable/bg_btn_primary"
|
||||||
android:layout_marginStart="8dp" />
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
||||||
33
android-app/app/src/main/res/layout/dialog_wish_complete.xml
Normal file
33
android-app/app/src/main/res/layout/dialog_wish_complete.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/bg_dialog_rounded"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="🎉"
|
||||||
|
android:textSize="48sp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="恭喜愿望达成!"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textColor="#FF6D00"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="愿你心想事成,万事如意"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#666666" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
# 使用 Android Studio 自带的 JDK 17
|
# JDK配置由Android Studio自动管理
|
||||||
org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr
|
# 请在Android Studio中编译:Build -> Rebuild Project
|
||||||
|
|
||||||
systemProp.gradle.wrapperUser=myuser
|
systemProp.gradle.wrapperUser=myuser
|
||||||
systemProp.gradle.wrapperPassword=mypassword
|
systemProp.gradle.wrapperPassword=mypassword
|
||||||
537
android-app/移动端许愿树功能设计文档.md
Normal file
537
android-app/移动端许愿树功能设计文档.md
Normal file
|
|
@ -0,0 +1,537 @@
|
||||||
|
# 许愿树移动端功能设计文档
|
||||||
|
|
||||||
|
> 版本:V1.0
|
||||||
|
> 更新日期:2025-12-31
|
||||||
|
> 设计范围:Android移动端许愿树功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、功能概述
|
||||||
|
|
||||||
|
### 1.1 核心特性
|
||||||
|
|
||||||
|
- **服务端实时存储**:心愿数据通过API实时同步到服务端
|
||||||
|
- **节日主题**:支持不同节日的许愿活动
|
||||||
|
- **社交互动**:支持点赞、评论功能
|
||||||
|
- **多端同步**:数据云端存储,多设备共享
|
||||||
|
|
||||||
|
### 1.2 数据流架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ API请求 ┌─────────────────┐
|
||||||
|
│ WishTreeActivity│ ◄──────────────► │ 后端服务 │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ Retrofit │ MyBatis
|
||||||
|
▼ ▼
|
||||||
|
ApiService MySQL数据库
|
||||||
|
(许愿树接口) (eb_wishtree_*)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、页面设计
|
||||||
|
|
||||||
|
### 2.1 主页面布局 (activity_wish_tree.xml)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ [许愿树标题] [🔍] [🔔] │ ← 顶部栏
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 🎉 元旦许愿树 | 许下心愿,愿望成真 [12:30 05:00] │ │ ← 活动横幅
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ [心愿1] [心愿2] [心愿3] │
|
||||||
|
│ │
|
||||||
|
│ [心愿4] [心愿+] [心愿5] │ ← 许愿树区域
|
||||||
|
│ │
|
||||||
|
│ [心愿6] [心愿7] │
|
||||||
|
│ │
|
||||||
|
│ 元 │
|
||||||
|
│ 旦 │
|
||||||
|
│ 许 │
|
||||||
|
│ 愿 │
|
||||||
|
│ 树 │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ 祈愿值:3/100 │ ← 底部区域
|
||||||
|
│ [ 前往祈愿 ] │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ [首页] [缘池] [许愿树] [消息] [我的] │ ← 底部导航
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 许愿对话框 (dialog_make_wish.xml)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 许下心愿 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 请输入您的心愿... │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
│ 0/50 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ [取消] [许愿] │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 查看心愿对话框 (dialog_view_wish.xml)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 我的心愿 [✕] │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 新年快乐,万事如意! │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
│ ❤ 128 💬 32 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ [删除心愿] [愿望达成] │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 成功提示对话框 (dialog_wish_success.xml)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ✨ │
|
||||||
|
│ 许愿成功! │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、API接口设计
|
||||||
|
|
||||||
|
### 3.1 接口列表
|
||||||
|
|
||||||
|
| 接口 | 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 获取节日列表 | GET | `/api/front/wishtree/festivals` | 获取所有启用的节日 |
|
||||||
|
| 获取当前节日 | GET | `/api/front/wishtree/festivals/current` | 获取当前进行中的节日 |
|
||||||
|
| 获取心愿列表 | GET | `/api/front/wishtree/wishes` | 分页获取心愿列表 |
|
||||||
|
| 获取我的心愿 | GET | `/api/front/wishtree/wishes/my` | 获取当前用户的心愿 |
|
||||||
|
| 发布心愿 | POST | `/api/front/wishtree/wishes` | 发布新心愿 |
|
||||||
|
| 获取心愿详情 | GET | `/api/front/wishtree/wishes/{id}` | 获取心愿详情 |
|
||||||
|
| 删除心愿 | DELETE | `/api/front/wishtree/wishes/{id}` | 删除我的心愿 |
|
||||||
|
| 点赞心愿 | POST | `/api/front/wishtree/wishes/{id}/like` | 点赞 |
|
||||||
|
| 取消点赞 | DELETE | `/api/front/wishtree/wishes/{id}/like` | 取消点赞 |
|
||||||
|
| 获取评论 | GET | `/api/front/wishtree/wishes/{id}/comments` | 获取评论列表 |
|
||||||
|
| 发表评论 | POST | `/api/front/wishtree/wishes/{id}/comments` | 发表评论 |
|
||||||
|
| 获取背景素材 | GET | `/api/front/wishtree/backgrounds` | 获取背景素材列表 |
|
||||||
|
|
||||||
|
### 3.2 请求/响应示例
|
||||||
|
|
||||||
|
#### 发布心愿
|
||||||
|
|
||||||
|
```json
|
||||||
|
// POST /api/front/wishtree/wishes
|
||||||
|
// Request:
|
||||||
|
{
|
||||||
|
"festivalId": 1,
|
||||||
|
"content": "新年快乐,万事如意!",
|
||||||
|
"backgroundId": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 10001,
|
||||||
|
"uid": 1001,
|
||||||
|
"festivalId": 1,
|
||||||
|
"content": "新年快乐,万事如意!",
|
||||||
|
"status": 1,
|
||||||
|
"likeCount": 0,
|
||||||
|
"commentCount": 0,
|
||||||
|
"createTime": "2025-12-31 12:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取我的心愿列表
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/front/wishtree/wishes/my?page=1&limit=10
|
||||||
|
// Response:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 10001,
|
||||||
|
"uid": 1001,
|
||||||
|
"nickname": "用户昵称",
|
||||||
|
"avatar": "https://xxx/avatar.jpg",
|
||||||
|
"festivalId": 1,
|
||||||
|
"festivalName": "元旦",
|
||||||
|
"festivalIcon": "🎉",
|
||||||
|
"content": "新年快乐,万事如意!",
|
||||||
|
"backgroundImage": "https://xxx/bg1.jpg",
|
||||||
|
"status": 1,
|
||||||
|
"likeCount": 128,
|
||||||
|
"commentCount": 32,
|
||||||
|
"isLiked": false,
|
||||||
|
"createTime": "2025-12-31 12:00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 5,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、代码结构
|
||||||
|
|
||||||
|
### 4.1 文件清单
|
||||||
|
|
||||||
|
```
|
||||||
|
android-app/app/src/main/java/com/example/livestreaming/
|
||||||
|
├── WishTreeActivity.java # 许愿树主页面
|
||||||
|
└── net/
|
||||||
|
├── ApiService.java # API接口定义(含许愿树接口)
|
||||||
|
├── WishtreeRequest.java # 请求类
|
||||||
|
│ ├── PublishWish # 发布心愿请求
|
||||||
|
│ └── AddComment # 发表评论请求
|
||||||
|
└── WishtreeResponse.java # 响应类
|
||||||
|
├── Festival # 节日信息
|
||||||
|
├── Wish # 心愿信息
|
||||||
|
├── Background # 背景素材
|
||||||
|
├── Comment # 评论信息
|
||||||
|
├── WishPage # 心愿分页数据
|
||||||
|
└── CommentPage # 评论分页数据
|
||||||
|
|
||||||
|
android-app/app/src/main/res/layout/
|
||||||
|
├── activity_wish_tree.xml # 主页面布局
|
||||||
|
├── dialog_make_wish.xml # 许愿对话框
|
||||||
|
├── dialog_view_wish.xml # 查看心愿对话框
|
||||||
|
└── dialog_wish_success.xml # 成功提示对话框
|
||||||
|
|
||||||
|
android-app/app/src/main/res/drawable/
|
||||||
|
├── bg_wish_tree_banner.xml # 横幅背景
|
||||||
|
├── bg_wish_tree_timer.xml # 倒计时背景
|
||||||
|
├── bg_wish_card.xml # 心愿卡片背景
|
||||||
|
├── bg_wish_card_add.xml # 添加心愿卡片背景
|
||||||
|
├── bg_trunk_text.xml # 树干文字背景
|
||||||
|
├── bg_dialog_rounded.xml # 对话框圆角背景
|
||||||
|
├── bg_edit_text.xml # 输入框背景
|
||||||
|
├── bg_btn_primary.xml # 主按钮背景
|
||||||
|
└── bg_btn_cancel.xml # 取消按钮背景
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 核心类设计
|
||||||
|
|
||||||
|
#### WishTreeActivity.java
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class WishTreeActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ActivityWishTreeBinding binding;
|
||||||
|
private ApiService apiService;
|
||||||
|
private List<WishtreeResponse.Wish> myWishes;
|
||||||
|
private WishtreeResponse.Festival currentFestival;
|
||||||
|
|
||||||
|
// 生命周期方法
|
||||||
|
onCreate() // 初始化页面、加载数据
|
||||||
|
onResume() // 刷新数据
|
||||||
|
onStart() // 启动倒计时
|
||||||
|
onStop() // 停止倒计时
|
||||||
|
|
||||||
|
// 数据加载
|
||||||
|
loadCurrentFestival() // 加载当前节日
|
||||||
|
loadMyWishes() // 加载我的心愿
|
||||||
|
|
||||||
|
// UI更新
|
||||||
|
updateBannerText() // 更新横幅文字
|
||||||
|
updateWishCards() // 更新心愿卡片
|
||||||
|
updateWishCount() // 更新祈愿值
|
||||||
|
updateBannerTimer() // 更新倒计时
|
||||||
|
|
||||||
|
// 用户交互
|
||||||
|
showMakeWishInputDialog() // 显示许愿对话框
|
||||||
|
showViewWishDialog() // 显示查看心愿对话框
|
||||||
|
showSuccessDialog() // 显示成功提示
|
||||||
|
|
||||||
|
// API调用
|
||||||
|
publishWish() // 发布心愿
|
||||||
|
deleteWish() // 删除心愿
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WishtreeRequest.java
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class WishtreeRequest {
|
||||||
|
|
||||||
|
// 发布心愿请求
|
||||||
|
public static class PublishWish {
|
||||||
|
public Integer festivalId;
|
||||||
|
public String content;
|
||||||
|
public Integer backgroundId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发表评论请求
|
||||||
|
public static class AddComment {
|
||||||
|
public String content;
|
||||||
|
public Long parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WishtreeResponse.java
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class WishtreeResponse {
|
||||||
|
|
||||||
|
// 节日信息
|
||||||
|
public static class Festival {
|
||||||
|
public int id;
|
||||||
|
public String name;
|
||||||
|
public String icon;
|
||||||
|
public String startDate;
|
||||||
|
public String endDate;
|
||||||
|
public String themeColor;
|
||||||
|
public int status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心愿信息
|
||||||
|
public static class Wish {
|
||||||
|
public long id;
|
||||||
|
public int uid;
|
||||||
|
public String nickname;
|
||||||
|
public String avatar;
|
||||||
|
public int festivalId;
|
||||||
|
public String festivalName;
|
||||||
|
public String festivalIcon;
|
||||||
|
public String content;
|
||||||
|
public String backgroundImage;
|
||||||
|
public int status;
|
||||||
|
public int likeCount;
|
||||||
|
public int commentCount;
|
||||||
|
public Boolean isLiked;
|
||||||
|
public String createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景素材
|
||||||
|
public static class Background {
|
||||||
|
public int id;
|
||||||
|
public String name;
|
||||||
|
public String image;
|
||||||
|
public int festivalId;
|
||||||
|
public int status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论信息
|
||||||
|
public static class Comment {
|
||||||
|
public long id;
|
||||||
|
public long wishId;
|
||||||
|
public int uid;
|
||||||
|
public String nickname;
|
||||||
|
public String avatar;
|
||||||
|
public String content;
|
||||||
|
public long parentId;
|
||||||
|
public String createTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、交互流程
|
||||||
|
|
||||||
|
### 5.1 页面加载流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ onCreate │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ 1. 初始化ApiService │
|
||||||
|
│ 2. 设置底部导航 │
|
||||||
|
│ 3. 设置心愿卡片点击事件 │
|
||||||
|
│ 4. 启动横幅倒计时 │
|
||||||
|
│ 5. 加载当前节日 (loadCurrentFestival) │
|
||||||
|
│ 6. 加载我的心愿 (loadMyWishes) │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 发布心愿流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ 点击许愿按钮 │ ──► │ 显示许愿对话框│ ──► │ 输入心愿内容 │
|
||||||
|
└─────────────┘ └─────────────┘ └──────┬──────┘
|
||||||
|
│
|
||||||
|
┌───────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ 点击许愿按钮 │ ──► │ 调用API发布 │ ──► │ 显示成功提示 │
|
||||||
|
└─────────────┘ └─────────────┘ └──────┬──────┘
|
||||||
|
│
|
||||||
|
┌───────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ 重新加载心愿列表 │
|
||||||
|
│ 更新心愿卡片显示 │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 查看/删除心愿流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐
|
||||||
|
│ 点击心愿卡片 │ ──► │ 显示心愿详情 │
|
||||||
|
└─────────────┘ └──────┬──────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||||
|
│ 点击关闭 │ │ 点击删除 │ │ 愿望达成 │
|
||||||
|
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||||
|
│ 关闭对话框 │ │ 调用API删除│ │ 调用API删除│
|
||||||
|
└───────────┘ │ 刷新列表 │ │ 显示祝贺 │
|
||||||
|
└───────────┘ └───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、状态管理
|
||||||
|
|
||||||
|
### 6.1 心愿状态
|
||||||
|
|
||||||
|
| 状态值 | 说明 | 显示 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 0 | 待审核 | 审核中 |
|
||||||
|
| 1 | 已通过 | 正常显示 |
|
||||||
|
| 2 | 已拒绝 | 不显示 |
|
||||||
|
|
||||||
|
### 6.2 页面状态
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 心愿数据
|
||||||
|
private List<WishtreeResponse.Wish> myWishes = new ArrayList<>();
|
||||||
|
|
||||||
|
// 当前节日
|
||||||
|
private WishtreeResponse.Festival currentFestival;
|
||||||
|
|
||||||
|
// 状态更新时机
|
||||||
|
- onCreate: 初始加载
|
||||||
|
- onResume: 每次返回页面刷新
|
||||||
|
- 发布/删除心愿后: 重新加载
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、错误处理
|
||||||
|
|
||||||
|
### 7.1 网络错误
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<...> call, Throwable t) {
|
||||||
|
Toast.makeText(context, "网络错误", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 业务错误
|
||||||
|
|
||||||
|
```java
|
||||||
|
if (response.body().getCode() != 200) {
|
||||||
|
String msg = response.body().getMessage();
|
||||||
|
Toast.makeText(context, msg != null ? msg : "操作失败", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 空数据处理
|
||||||
|
|
||||||
|
```java
|
||||||
|
if (myWishes == null) {
|
||||||
|
myWishes = new ArrayList<>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、后续扩展
|
||||||
|
|
||||||
|
### 8.1 待实现功能
|
||||||
|
|
||||||
|
- [ ] 心愿列表页面(查看所有用户心愿)
|
||||||
|
- [ ] 点赞功能
|
||||||
|
- [ ] 评论功能
|
||||||
|
- [ ] 背景素材选择
|
||||||
|
- [ ] 节日主题切换
|
||||||
|
- [ ] 心愿分享功能
|
||||||
|
|
||||||
|
### 8.2 优化方向
|
||||||
|
|
||||||
|
- 添加加载状态指示器
|
||||||
|
- 添加下拉刷新
|
||||||
|
- 添加心愿卡片动画效果
|
||||||
|
- 支持心愿图片上传
|
||||||
|
- 添加本地缓存减少网络请求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、数据库表结构
|
||||||
|
|
||||||
|
### 9.1 相关表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 节日表
|
||||||
|
eb_wishtree_festival (id, name, icon, start_date, end_date, theme_color, status, ...)
|
||||||
|
|
||||||
|
-- 心愿表
|
||||||
|
eb_wishtree_wish (id, uid, festival_id, content, background_id, status, like_count, comment_count, ...)
|
||||||
|
|
||||||
|
-- 点赞表
|
||||||
|
eb_wishtree_like (id, uid, wish_id, create_time)
|
||||||
|
|
||||||
|
-- 评论表
|
||||||
|
eb_wishtree_comment (id, wish_id, uid, content, parent_id, status, ...)
|
||||||
|
|
||||||
|
-- 背景素材表
|
||||||
|
eb_wishtree_background (id, name, image, festival_id, status, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、测试要点
|
||||||
|
|
||||||
|
### 10.1 功能测试
|
||||||
|
|
||||||
|
- [ ] 页面正常加载
|
||||||
|
- [ ] 节日信息正确显示
|
||||||
|
- [ ] 心愿卡片正确显示
|
||||||
|
- [ ] 发布心愿成功
|
||||||
|
- [ ] 删除心愿成功
|
||||||
|
- [ ] 愿望达成功能正常
|
||||||
|
- [ ] 底部导航切换正常
|
||||||
|
|
||||||
|
### 10.2 异常测试
|
||||||
|
|
||||||
|
- [ ] 网络断开时的提示
|
||||||
|
- [ ] 未登录时的处理
|
||||||
|
- [ ] 空数据时的显示
|
||||||
|
- [ ] 心愿内容为空时的校验
|
||||||
60
friend_tables.sql
Normal file
60
friend_tables.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
-- 好友系统数据库表
|
||||||
|
-- 请在服务器数据库中执行此脚本
|
||||||
|
|
||||||
|
-- 1. 好友关系表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_friend` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||||
|
`friend_id` int(11) NOT NULL COMMENT '好友ID',
|
||||||
|
`remark` varchar(50) DEFAULT NULL COMMENT '好友备注',
|
||||||
|
`status` int(11) NOT NULL DEFAULT 1 COMMENT '状态:1-正常 0-已删除',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_friend_id` (`friend_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表';
|
||||||
|
|
||||||
|
-- 2. 好友请求表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_friend_request` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`from_user_id` int(11) NOT NULL COMMENT '发起请求的用户ID',
|
||||||
|
`to_user_id` int(11) NOT NULL COMMENT '接收请求的用户ID',
|
||||||
|
`message` varchar(200) DEFAULT NULL COMMENT '验证消息',
|
||||||
|
`status` int(11) NOT NULL DEFAULT 0 COMMENT '状态:0-待处理 1-已接受 2-已拒绝',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '处理时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_from_user` (`from_user_id`),
|
||||||
|
KEY `idx_to_user` (`to_user_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友请求表';
|
||||||
|
|
||||||
|
-- 3. 用户黑名单表
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_user_blacklist` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||||
|
`blocked_user_id` int(11) NOT NULL COMMENT '被拉黑的用户ID',
|
||||||
|
`reason` varchar(200) DEFAULT NULL COMMENT '拉黑原因',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_user_blocked` (`user_id`, `blocked_user_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_blocked_user_id` (`blocked_user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户黑名单表';
|
||||||
|
|
||||||
|
-- 4. 私聊会话表(如果不存在)
|
||||||
|
CREATE TABLE IF NOT EXISTS `eb_conversation` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`user1_id` int(11) NOT NULL COMMENT '用户1 ID(较小的ID)',
|
||||||
|
`user2_id` int(11) NOT NULL COMMENT '用户2 ID(较大的ID)',
|
||||||
|
`last_message_id` bigint(20) DEFAULT NULL COMMENT '最后一条消息ID',
|
||||||
|
`last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_users` (`user1_id`, `user2_id`),
|
||||||
|
KEY `idx_user1` (`user1_id`),
|
||||||
|
KEY `idx_user2` (`user2_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私聊会话表';
|
||||||
177
环境配置指南.md
Normal file
177
环境配置指南.md
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
# 环境配置指南
|
||||||
|
|
||||||
|
本项目采用分离配置架构:
|
||||||
|
- **普通业务功能**(用户、消息、商城等)→ 可切换本地/远程
|
||||||
|
- **直播/通话服务** → 始终连接远程服务器
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、APP 配置
|
||||||
|
|
||||||
|
### 配置文件位置
|
||||||
|
`android-app/local.properties`
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ============ 主API地址(普通业务功能)============
|
||||||
|
# 本地开发时使用本地地址
|
||||||
|
api.base_url_emulator=http://10.0.2.2:8081/
|
||||||
|
api.base_url_device=http://192.168.1.164:8081/
|
||||||
|
|
||||||
|
# ============ 直播/通话服务地址(始终远程)============
|
||||||
|
live.server_host=1.15.149.240
|
||||||
|
live.server_port=8083
|
||||||
|
turn.server_host=1.15.149.240
|
||||||
|
turn.server_port=3478
|
||||||
|
```
|
||||||
|
|
||||||
|
### 切换环境
|
||||||
|
|
||||||
|
#### 本地开发(默认)
|
||||||
|
```properties
|
||||||
|
api.base_url_emulator=http://10.0.2.2:8081/
|
||||||
|
api.base_url_device=http://192.168.1.164:8081/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 全部连接远程
|
||||||
|
```properties
|
||||||
|
api.base_url_emulator=http://1.15.149.240:8083/
|
||||||
|
api.base_url_device=http://1.15.149.240:8083/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改后需要重新编译
|
||||||
|
```bash
|
||||||
|
cd android-app
|
||||||
|
.\gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、前端管理端配置
|
||||||
|
|
||||||
|
### 配置文件位置
|
||||||
|
- 开发环境:`Zhibo/admin/.env.development`
|
||||||
|
- 生产环境:`Zhibo/admin/.env.production`
|
||||||
|
|
||||||
|
### 开发环境(本地)
|
||||||
|
```env
|
||||||
|
ENV = 'development'
|
||||||
|
VUE_APP_BASE_API = 'http://127.0.0.1:30001'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境(远程)
|
||||||
|
```env
|
||||||
|
ENV = 'production'
|
||||||
|
VUE_APP_BASE_API = ''
|
||||||
|
```
|
||||||
|
> 生产环境使用空字符串,由Nginx代理到后端
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、后端配置
|
||||||
|
|
||||||
|
### 配置文件位置
|
||||||
|
`Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml`
|
||||||
|
|
||||||
|
### 关键配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 8081 # 本地开发端口
|
||||||
|
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://1.15.149.240:3306/zhibo # 数据库地址
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1 # Redis地址
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务器启动脚本
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
JAR_PATH="/www/wwwroot/1.15.149.240_30002/Jar"
|
||||||
|
|
||||||
|
# Front API (带SRS配置)
|
||||||
|
nohup java -Xms512m -Xmx1024m -jar ${JAR_PATH}/Crmeb-front.jar \
|
||||||
|
--server.port=8083 \
|
||||||
|
--spring.redis.host=127.0.0.1 \
|
||||||
|
--LIVE_PUBLIC_SRS_HOST=1.15.149.240 \
|
||||||
|
--LIVE_PUBLIC_SRS_RTMP_PORT=25002 \
|
||||||
|
--LIVE_PUBLIC_SRS_HTTP_PORT=25003 \
|
||||||
|
> ${JAR_PATH}/logs/front.log 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、服务架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 远程服务器 1.15.149.240 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Front API (8083) │
|
||||||
|
│ ├── WebSocket: /ws/call (通话信令) ←── APP直连 │
|
||||||
|
│ ├── WebSocket: /ws/live/* (直播弹幕) ←── APP直连 │
|
||||||
|
│ └── REST API: /api/front/* (可选) │
|
||||||
|
│ │
|
||||||
|
│ SRS流媒体服务器 │
|
||||||
|
│ ├── RTMP: 25002 │
|
||||||
|
│ └── HTTP-FLV: 25003 │
|
||||||
|
│ │
|
||||||
|
│ TURN服务器: 3478 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
▲
|
||||||
|
直播/通话服务 │
|
||||||
|
─────────────────────┼─────────────────────
|
||||||
|
普通业务功能 │
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 本地开发环境 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Front API (8081) │
|
||||||
|
│ └── REST API: /api/front/* ←── APP连接 │
|
||||||
|
│ │
|
||||||
|
│ Admin API (30001) │
|
||||||
|
│ └── REST API: /api/admin/* ←── 管理前端连接 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、快速切换命令
|
||||||
|
|
||||||
|
### APP切换到全远程
|
||||||
|
编辑 `android-app/local.properties`:
|
||||||
|
```properties
|
||||||
|
api.base_url_device=http://1.15.149.240:8083/
|
||||||
|
```
|
||||||
|
|
||||||
|
### APP切换到本地开发
|
||||||
|
编辑 `android-app/local.properties`:
|
||||||
|
```properties
|
||||||
|
api.base_url_device=http://192.168.1.164:8081/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端切换
|
||||||
|
```bash
|
||||||
|
# 开发模式(连本地)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 生产构建(连远程)
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、端口清单
|
||||||
|
|
||||||
|
| 服务 | 本地端口 | 远程端口 | 说明 |
|
||||||
|
|------|----------|----------|------|
|
||||||
|
| Front API | 8081 | 8083 | APP主API |
|
||||||
|
| Admin API | 30001 | 30003 | 管理后台API |
|
||||||
|
| SRS RTMP | - | 25002 | 直播推流 |
|
||||||
|
| SRS HTTP | - | 25003 | 直播播放 |
|
||||||
|
| TURN | - | 3478 | 视频通话中继 |
|
||||||
|
| MySQL | - | 3306 | 数据库 |
|
||||||
|
| Redis | 6379 | 6379 | 缓存 |
|
||||||
141
环境配置汇总.md
Normal file
141
环境配置汇总.md
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# 环境配置汇总
|
||||||
|
|
||||||
|
## 一、Android APP 配置
|
||||||
|
|
||||||
|
### 1. 主配置文件:`android-app/local.properties`
|
||||||
|
|
||||||
|
```properties
|
||||||
|
# ============ API 服务器地址配置 ============
|
||||||
|
# 主API地址 - 模拟器使用
|
||||||
|
api.base_url_emulator=http://1.15.149.240:8083/
|
||||||
|
# 主API地址 - 真机使用
|
||||||
|
api.base_url_device=http://1.15.149.240:8083/
|
||||||
|
|
||||||
|
# ============ 直播/通话服务地址配置 ============
|
||||||
|
# 直播/通话 WebSocket 服务地址
|
||||||
|
live.server_host=1.15.149.240
|
||||||
|
live.server_port=8083
|
||||||
|
# TURN 服务器地址(视频通话中继)
|
||||||
|
turn.server_host=1.15.149.240
|
||||||
|
turn.server_port=3478
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- `api.base_url_emulator` - 模拟器运行时使用的API地址
|
||||||
|
- `api.base_url_device` - 真机运行时使用的API地址
|
||||||
|
- `live.server_host/port` - 直播和通话WebSocket服务地址
|
||||||
|
- `turn.server_host/port` - WebRTC TURN中继服务器地址
|
||||||
|
|
||||||
|
### 2. 相关代码文件(无需修改,自动读取local.properties)
|
||||||
|
|
||||||
|
| 文件 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `app/build.gradle.kts` | 读取local.properties生成BuildConfig |
|
||||||
|
| `ApiConfig.java` | 提供统一的API地址获取方法 |
|
||||||
|
| `ApiClient.java` | 网络请求客户端,自动选择模拟器/真机地址 |
|
||||||
|
| `WebRTCConfig.java` | WebRTC/TURN服务器配置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、前端 Admin 配置
|
||||||
|
|
||||||
|
### 1. 开发环境:`Zhibo/admin/.env.development`
|
||||||
|
|
||||||
|
```properties
|
||||||
|
ENV = 'development'
|
||||||
|
# 本地开发时连接本地后端
|
||||||
|
VUE_APP_BASE_API = 'http://127.0.0.1:30001'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 生产环境:`Zhibo/admin/.env.production`
|
||||||
|
|
||||||
|
```properties
|
||||||
|
ENV = 'production'
|
||||||
|
# 生产环境使用相对路径,由Nginx代理
|
||||||
|
VUE_APP_BASE_API = ''
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 预发布环境:`Zhibo/admin/.env.staging`
|
||||||
|
|
||||||
|
```properties
|
||||||
|
ENV = 'production'
|
||||||
|
VUE_APP_BASE_API = 'http://192.168.31.35:2500'
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 开发环境:`npm run dev` 使用 `.env.development`
|
||||||
|
- 生产打包:`npm run build:prod` 使用 `.env.production`
|
||||||
|
- 预发布打包:`npm run build:stage` 使用 `.env.staging`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、环境切换指南
|
||||||
|
|
||||||
|
### 切换到生产环境(服务器 1.15.149.240)
|
||||||
|
|
||||||
|
**Android APP**:
|
||||||
|
```properties
|
||||||
|
# android-app/local.properties
|
||||||
|
api.base_url_emulator=http://1.15.149.240:8083/
|
||||||
|
api.base_url_device=http://1.15.149.240:8083/
|
||||||
|
live.server_host=1.15.149.240
|
||||||
|
live.server_port=8083
|
||||||
|
turn.server_host=1.15.149.240
|
||||||
|
turn.server_port=3478
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端 Admin**:
|
||||||
|
```properties
|
||||||
|
# Zhibo/admin/.env.production
|
||||||
|
VUE_APP_BASE_API = ''
|
||||||
|
```
|
||||||
|
然后执行 `npm run build:prod`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 切换到本地开发环境
|
||||||
|
|
||||||
|
**Android APP**:
|
||||||
|
```properties
|
||||||
|
# android-app/local.properties
|
||||||
|
api.base_url_emulator=http://10.0.2.2:8081/
|
||||||
|
api.base_url_device=http://192.168.x.x:8081/
|
||||||
|
live.server_host=1.15.149.240
|
||||||
|
live.server_port=8083
|
||||||
|
turn.server_host=1.15.149.240
|
||||||
|
turn.server_port=3478
|
||||||
|
```
|
||||||
|
> 注意:直播/通话服务建议始终使用远程服务器
|
||||||
|
|
||||||
|
**前端 Admin**:
|
||||||
|
```properties
|
||||||
|
# Zhibo/admin/.env.development
|
||||||
|
VUE_APP_BASE_API = 'http://127.0.0.1:30001'
|
||||||
|
```
|
||||||
|
然后执行 `npm run dev`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、服务端口说明
|
||||||
|
|
||||||
|
| 服务 | 端口 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Admin API | 30003 | 管理后台API |
|
||||||
|
| Front API | 8083 | 前端/APP API |
|
||||||
|
| Admin 前端 | 30002 | 管理后台网页 |
|
||||||
|
| TURN 服务 | 3478 | WebRTC中继 |
|
||||||
|
| SRS RTMP | 25002 | 直播推流 |
|
||||||
|
| SRS HTTP | 25003 | 直播拉流 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、修改后需要的操作
|
||||||
|
|
||||||
|
### Android APP
|
||||||
|
1. 修改 `local.properties`
|
||||||
|
2. 重新编译:`./gradlew assembleRelease`
|
||||||
|
|
||||||
|
### 前端 Admin
|
||||||
|
1. 修改对应的 `.env.*` 文件
|
||||||
|
2. 重新打包:`npm run build:prod`
|
||||||
|
3. 上传 `dist` 目录到服务器
|
||||||
Loading…
Reference in New Issue
Block a user