Merge IM-gift branch: add virtual currency and recharge features
This commit is contained in:
commit
d234b91836
167
0-待完成接入接口.md
167
0-待完成接入接口.md
|
|
@ -1,167 +0,0 @@
|
|||
# 待完成接入接口清单
|
||||
|
||||
> 更新时间: 2024-12-30
|
||||
|
||||
## 📊 总体情况
|
||||
|
||||
| 类别 | 后端接口数 | Android已接入 | 待接入 |
|
||||
|------|-----------|--------------|--------|
|
||||
| 总计 | 131 | ~85 | ~46 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已接入模块 (无需处理)
|
||||
|
||||
| 模块 | 接口数 | 状态 |
|
||||
|------|--------|------|
|
||||
| 用户认证 | 4 | ✅ 全部接入 |
|
||||
| 用户信息 | 2 | ✅ 全部接入 |
|
||||
| 直播间 | 6 | ✅ 全部接入 |
|
||||
| 直播弹幕 | 2 | ✅ 全部接入 |
|
||||
| 礼物打赏 | 5 | ✅ 全部接入 |
|
||||
| 私聊会话 | 8 | ✅ 全部接入 |
|
||||
| 好友管理 | 9 | ✅ 全部接入 |
|
||||
| 文件上传 | 2 | ✅ 全部接入 |
|
||||
| 在线状态 | 5 | ✅ 全部接入 |
|
||||
| 离线消息 | 3 | ✅ 全部接入 |
|
||||
| 消息表情回应 | 4 | ✅ 全部接入 |
|
||||
| 关注功能 | 7 | ✅ 全部接入 |
|
||||
| 作品管理 | 13 | ✅ 全部接入 |
|
||||
| 搜索功能 | 9 | ✅ 全部接入 |
|
||||
| 支付集成 | 4 | ✅ 全部接入 |
|
||||
| 通话功能 | 10 | ✅ 全部接入 |
|
||||
|
||||
---
|
||||
|
||||
## ❌ 待接入模块
|
||||
|
||||
### 1. 群组管理 (10个接口) - 🔴 高优先级
|
||||
|
||||
后端已完成,Android端未定义接口。
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 创建群组 | `POST /api/front/groups/create` | 创建新群组 |
|
||||
| 群组列表 | `GET /api/front/groups/list` | 获取我的群组 |
|
||||
| 群组详情 | `GET /api/front/groups/{groupId}` | 获取群组信息 |
|
||||
| 更新群组 | `PUT /api/front/groups/{groupId}` | 修改群组信息 |
|
||||
| 解散群组 | `DELETE /api/front/groups/{groupId}` | 解散群组 |
|
||||
| 添加成员 | `POST /api/front/groups/{groupId}/members` | 邀请成员 |
|
||||
| 移除成员 | `DELETE /api/front/groups/{groupId}/members/{userId}` | 踢出成员 |
|
||||
| 成员列表 | `GET /api/front/groups/{groupId}/members` | 获取成员 |
|
||||
| 退出群组 | `POST /api/front/groups/{groupId}/leave` | 主动退群 |
|
||||
| 转让群主 | `POST /api/front/groups/{groupId}/transfer` | 转让群主 |
|
||||
|
||||
---
|
||||
|
||||
### 2. 群组消息 (4个接口) - 🔴 高优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 发送群消息 | `POST /api/front/groups/{groupId}/messages` | 发送消息 |
|
||||
| 群消息历史 | `GET /api/front/groups/{groupId}/messages` | 获取历史 |
|
||||
| 撤回消息 | `DELETE /api/front/groups/{groupId}/messages/{messageId}` | 撤回消息 |
|
||||
| 转发消息 | `POST /api/front/groups/{groupId}/messages/{messageId}/forward` | 转发消息 |
|
||||
|
||||
---
|
||||
|
||||
### 3. 消息搜索 (3个接口) - 🟡 中优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 搜索会话 | `GET /api/front/messages/search/conversations` | 搜索会话 |
|
||||
| 搜索消息 | `GET /api/front/messages/search/messages` | 搜索消息内容 |
|
||||
| 全局搜索 | `GET /api/front/messages/search/global` | 全局搜索 |
|
||||
|
||||
---
|
||||
|
||||
### 4. 评论功能 (8个接口) - 🟡 中优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 发布评论 | `POST /api/front/works/comment/publish` | 发布评论 |
|
||||
| 评论列表 | `GET /api/front/works/comment/list/{worksId}` | 获取评论 |
|
||||
| 点赞评论 | `POST /api/front/works/comment/like/{commentId}` | 点赞 |
|
||||
| 取消点赞 | `POST /api/front/works/comment/unlike/{commentId}` | 取消点赞 |
|
||||
| 删除评论 | `POST /api/front/works/comment/delete/{commentId}` | 删除评论 |
|
||||
| 回复列表 | `GET /api/front/works/comment/reply/list/{commentId}` | 获取回复 |
|
||||
| 评论详情 | `GET /api/front/works/comment/detail/{commentId}` | 评论详情 |
|
||||
| 检查点赞 | `GET /api/front/works/comment/check-liked/{commentId}` | 检查状态 |
|
||||
|
||||
---
|
||||
|
||||
### 5. 通知推送 (9个接口) - 🟡 中优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 通知列表 | `GET /api/front/notification/list` | 获取通知 |
|
||||
| 未读数量 | `GET /api/front/notification/unread-count` | 未读数 |
|
||||
| 标记已读 | `POST /api/front/notification/mark-read/{id}` | 单条已读 |
|
||||
| 全部已读 | `POST /api/front/notification/mark-all-read` | 全部已读 |
|
||||
| 注册FCM | `POST /api/front/notification/fcm/register` | 注册推送 |
|
||||
| 移除FCM | `POST /api/front/notification/fcm/remove` | 移除推送 |
|
||||
| 删除通知 | `DELETE /api/front/notification/{id}` | 删除单条 |
|
||||
| 清空通知 | `DELETE /api/front/notification/clear-all` | 清空全部 |
|
||||
| 按类型统计 | `GET /api/front/notification/unread-count-by-type` | 分类统计 |
|
||||
|
||||
---
|
||||
|
||||
### 6. 分类管理 (7个接口) - 🟢 低优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 直播间分类 | `GET /api/front/category/live-room` | 直播分类 |
|
||||
| 作品分类 | `GET /api/front/category/work` | 作品分类 |
|
||||
| 分类列表 | `GET /api/front/category/list` | 全部分类 |
|
||||
| 分类详情 | `GET /api/front/category/{id}` | 分类详情 |
|
||||
| 分类统计 | `GET /api/front/category/statistics` | 统计数据 |
|
||||
| 热门分类 | `GET /api/front/category/hot` | 热门分类 |
|
||||
| 子分类 | `GET /api/front/category/{parentId}/children` | 子分类 |
|
||||
|
||||
---
|
||||
|
||||
### 7. 文件上传补充 (3个接口) - 🟢 低优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 通用图片上传 | `POST /api/upload/image` | 通用图片 |
|
||||
| 通用文件上传 | `POST /api/upload/file` | 通用文件 |
|
||||
| 语音上传 | `POST /api/upload/chat/voice` | 语音消息 |
|
||||
|
||||
---
|
||||
|
||||
### 8. 直播间补充 (4个接口) - 🟢 低优先级
|
||||
|
||||
| 接口 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 开始直播 | `POST /api/front/live/room/{id}/start` | 开播 |
|
||||
| 结束直播 | `POST /api/front/live/room/{id}/stop` | 停播 |
|
||||
| 观众列表 | `GET /api/rooms/{roomId}/viewers` | 观众列表 |
|
||||
| 手动广播人数 | `POST /api/live/online/broadcast/{roomId}` | 广播人数 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 接入优先级建议
|
||||
|
||||
### 第一优先级 (核心社交)
|
||||
1. **群组管理** - 10个接口
|
||||
2. **群组消息** - 4个接口
|
||||
|
||||
### 第二优先级 (内容互动)
|
||||
3. **评论功能** - 8个接口
|
||||
4. **通知推送** - 9个接口
|
||||
|
||||
### 第三优先级 (辅助功能)
|
||||
5. **消息搜索** - 3个接口
|
||||
6. **分类管理** - 7个接口
|
||||
7. **文件上传补充** - 3个接口
|
||||
8. **直播间补充** - 4个接口
|
||||
|
||||
---
|
||||
|
||||
## 📝 备注
|
||||
|
||||
- 后端接口已全部完成 (131个)
|
||||
- Android端已接入约85个接口
|
||||
- 待接入约46个接口
|
||||
- 核心功能已基本完成,待接入的主要是群组和辅助功能
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
本文档详细列出了Android应用中**真实调用的所有接口**的请求参数和响应参数示例。
|
||||
|
||||
**文档版本**: v1.0
|
||||
**更新时间**: 2024-12-30
|
||||
**接口总数**: 73个真实调用的接口
|
||||
**更新时间**: 2024-12-31
|
||||
**接口总数**: 74个真实调用的接口
|
||||
**基础URL**: `http://your-server:port`
|
||||
|
||||
---
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
11. [搜索功能模块](#11-搜索功能模块)
|
||||
12. [观看历史模块](#12-观看历史模块)
|
||||
13. [分类管理模块](#13-分类管理模块)
|
||||
14. [直播类型模块](#14-直播类型模块)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -292,7 +293,7 @@ Authorization: Bearer <token>
|
|||
{
|
||||
"title": "我的直播间",
|
||||
"streamerName": "主播昵称",
|
||||
"type": "video",
|
||||
"type": "game",
|
||||
"categoryId": 1,
|
||||
"description": "直播间描述",
|
||||
"coverImage": "https://example.com/cover.jpg",
|
||||
|
|
@ -306,13 +307,25 @@ Authorization: Bearer <token>
|
|||
|--------|------|------|------|
|
||||
| title | String | 是 | 直播间标题 |
|
||||
| streamerName | String | 是 | 主播名称 |
|
||||
| type | String | 否 | 直播类型,默认video |
|
||||
| type | String | 否 | 直播类型编码(game/talent/outdoor/music/food/chat),默认game |
|
||||
| categoryId | Integer | 否 | 分类ID |
|
||||
| description | String | 否 | 直播间描述 |
|
||||
| coverImage | String | 否 | 封面图片URL |
|
||||
| tags | String | 否 | 标签,逗号分隔 |
|
||||
| notice | String | 否 | 直播间公告 |
|
||||
|
||||
**重要说明**:
|
||||
- `type` 参数应使用类型编码(code),而不是类型名称(name)
|
||||
- 可用的类型编码:
|
||||
- `game` - 游戏
|
||||
- `talent` - 才艺
|
||||
- `outdoor` - 户外
|
||||
- `music` - 音乐
|
||||
- `food` - 美食
|
||||
- `chat` - 聊天
|
||||
- 前端应先调用 `GET /api/front/live/public/types` 获取类型列表
|
||||
- 如果type参数为空,后端会使用默认值 `game`
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
|
|
@ -321,6 +334,7 @@ Authorization: Bearer <token>
|
|||
"data": {
|
||||
"id": "10",
|
||||
"title": "我的直播间",
|
||||
"type": "game",
|
||||
"streamKey": "live_stream_key_456",
|
||||
"streamUrls": {
|
||||
"rtmp": "rtmp://server:25002/live/stream_key_456",
|
||||
|
|
@ -379,7 +393,7 @@ Authorization: Bearer <token>
|
|||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Integer | 是 | 直播间ID |
|
||||
| id | String | 是 | 直播间ID |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
|
|
@ -405,7 +419,7 @@ Authorization: Bearer <token>
|
|||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Integer | 是 | 直播间ID |
|
||||
| id | String | 是 | 直播间ID |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
|
|
@ -469,6 +483,11 @@ Authorization: Bearer <token>
|
|||
|--------|------|------|------|
|
||||
| roomId | String | 是 | 直播间ID |
|
||||
|
||||
**重要说明**:
|
||||
- 此接口路径不包含 `/front` 前缀
|
||||
- 完整路径为: `POST /api/live/online/broadcast/{roomId}`
|
||||
- 用于向WebSocket客户端广播直播间在线人数更新
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
|
|
@ -1558,6 +1577,7 @@ Authorization: Bearer <token>
|
|||
{
|
||||
"title": "我的作品标题",
|
||||
"description": "作品描述内容",
|
||||
"type": "VIDEO",
|
||||
"coverUrl": "https://example.com/cover.jpg",
|
||||
"videoUrl": "https://example.com/video.mp4",
|
||||
"categoryId": 1,
|
||||
|
|
@ -1603,6 +1623,7 @@ Authorization: Bearer <token>
|
|||
{
|
||||
"id": 10001,
|
||||
"title": "修改后的标题",
|
||||
"type": "VIDEO",
|
||||
"description": "修改后的描述",
|
||||
"coverUrl": "https://example.com/new-cover.jpg",
|
||||
"categoryId": 2,
|
||||
|
|
@ -1616,6 +1637,7 @@ Authorization: Bearer <token>
|
|||
|--------|------|------|------|
|
||||
| id | Long | 是 | 作品ID |
|
||||
| title | String | 否 | 作品标题 |
|
||||
| type | String | 否 | 作品类型 |
|
||||
| description | String | 否 | 作品描述 |
|
||||
| coverUrl | String | 否 | 封面图片URL |
|
||||
| categoryId | Integer | 否 | 分类ID |
|
||||
|
|
@ -2538,6 +2560,90 @@ Authorization: Bearer <token>
|
|||
|
||||
---
|
||||
|
||||
## 14. 直播类型模块
|
||||
|
||||
### 14.1 获取直播类型列表
|
||||
|
||||
**接口地址**: `GET /api/front/live/public/types`
|
||||
|
||||
**请求参数**: 无
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "游戏",
|
||||
"code": "game",
|
||||
"icon": "",
|
||||
"description": "游戏直播",
|
||||
"sort": 100
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "才艺",
|
||||
"code": "talent",
|
||||
"icon": "",
|
||||
"description": "才艺表演",
|
||||
"sort": 90
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "户外",
|
||||
"code": "outdoor",
|
||||
"icon": "",
|
||||
"description": "户外直播",
|
||||
"sort": 80
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "音乐",
|
||||
"code": "music",
|
||||
"icon": "",
|
||||
"description": "音乐直播",
|
||||
"sort": 70
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "美食",
|
||||
"code": "food",
|
||||
"icon": "",
|
||||
"description": "美食直播",
|
||||
"sort": 60
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "聊天",
|
||||
"code": "chat",
|
||||
"icon": "",
|
||||
"description": "聊天互动",
|
||||
"sort": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明**:
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | Integer | 类型ID |
|
||||
| name | String | 类型名称(显示用) |
|
||||
| code | String | 类型编码(提交用) |
|
||||
| icon | String | 类型图标URL |
|
||||
| description | String | 类型描述 |
|
||||
| sort | Integer | 排序值,越大越靠前 |
|
||||
|
||||
**使用说明**:
|
||||
- 此接口用于获取创建直播间时可选的类型列表
|
||||
- 前端在创建直播间时,应使用 `code` 字段的值提交给后端
|
||||
- 类型列表按 `sort` 字段降序排列
|
||||
- 只返回状态为启用的类型
|
||||
|
||||
---
|
||||
|
||||
## 📝 附录
|
||||
|
||||
### A. 通用响应格式
|
||||
|
|
|
|||
|
|
@ -1,340 +0,0 @@
|
|||
# Android应用接口真实调用情况分析报告
|
||||
|
||||
## 📊 分析概述
|
||||
|
||||
本报告详细分析了Android应用中对后端接口的**真实调用情况**,区分了"已定义但未使用"和"已实际调用"的接口。
|
||||
|
||||
**分析时间**: 2024年12月30日
|
||||
**分析范围**: android-app 和 java-backend 全部代码
|
||||
**分析方法**: 代码静态分析 + 接口定义对比
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已真实调用的接口(共 45+ 个)
|
||||
|
||||
### 1. 用户认证模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/login` | LoginActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/register` | LoginActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/sendCode` | LoginActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/logout` | SettingsPageActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/user` | ProfileActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 2. 直播间模块 (85% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/live/public/rooms` | MainActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/rooms` | MainActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/live/public/rooms/{id}` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/live/rooms/{id}` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/follow` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/room/{id}/start` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/room/{id}/stop` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/live/online/broadcast/{roomId}` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/live/public/rooms/{roomId}/messages` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/rooms/{roomId}/gift` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
|
||||
**未调用接口**:
|
||||
- ❌ `GET /api/front/live/public/rooms/{roomId}/viewers/count` - 已定义但未使用
|
||||
- ❌ `GET /api/front/live/rooms/{roomId}/viewers` - 已定义但未使用
|
||||
|
||||
### 3. 礼物打赏模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/gift/list` | RoomDetailActivity.java:1190 | ✅ 真实调用 |
|
||||
| `GET /api/front/gift/balance` | RoomDetailActivity.java:1588 | ✅ 真实调用 |
|
||||
| `POST /api/front/gift/send` | RoomDetailActivity.java:1636 | ✅ 真实调用 |
|
||||
| `GET /api/front/gift/recharge/options` | RoomDetailActivity.java:1387 | ✅ 真实调用 |
|
||||
| `POST /api/front/gift/recharge/create` | RoomDetailActivity.java:1460 | ✅ 真实调用 |
|
||||
| `POST /api/front/pay/payment` | RoomDetailActivity.java:1537 | ✅ 真实调用 |
|
||||
|
||||
### 4. 私聊会话模块 (90% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/conversations` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/conversations/search` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/conversations/with/{otherUserId}` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/conversations/{id}/read` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/conversations/{id}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/conversations/{id}/messages` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/conversations/{id}/messages` | ChatActivity.java | ✅ 真实调用 |
|
||||
|
||||
**未调用接口**:
|
||||
- ❌ `DELETE /api/front/conversations/messages/{id}` - 已定义但未使用(删除单条消息功能未实现)
|
||||
|
||||
### 5. 好友管理模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/friends` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/friends/{friendId}` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/block/{friendId}` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/unblock/{friendId}` | BlockedListActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/friends/blocked` | BlockedListActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/users/search` | AddFriendActivity.java:62 | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/request` | AddFriendActivity.java:142 | ✅ 真实调用 |
|
||||
| `GET /api/front/friends/requests` | FriendRequestsActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/requests/{requestId}/handle` | FriendRequestsActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 6. 关注功能模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/follow/follow` | UserProfileReadOnlyActivity.java:141 | ✅ 真实调用 |
|
||||
| `POST /api/front/follow/unfollow` | UserProfileReadOnlyActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/status/{userId}` | UserProfileReadOnlyActivity.java:105 | ✅ 真实调用 |
|
||||
| `POST /api/front/follow/status/batch` | FishPondActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/following` | FollowingListActivity.java:62 | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/followers` | FansListActivity.java:62 | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/stats` | ProfileActivity.java:684 | ✅ 真实调用 |
|
||||
|
||||
### 7. 作品管理模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/works/publish` | PublishWorkActivity.java:766 | ✅ 真实调用 |
|
||||
| `POST /api/front/works/update` | EditWorkActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/delete/{worksId}` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/detail/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/search` | SearchActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/user/{userId}` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/like/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/unlike/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/collect/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/uncollect/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/my/liked` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/my/collected` | ProfileActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 8. 文件上传模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/user/upload/image` | PublishWorkActivity.java:642 | ✅ 真实调用 |
|
||||
| `POST /api/front/upload/work/video` | PublishWorkActivity.java:691 | ✅ 真实调用 |
|
||||
|
||||
### 9. 在线状态模块 (80% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/online/status/{userId}` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/online/status/batch` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/online/room/{roomId}/count` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/online/room/{roomId}/users` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
|
||||
**未调用接口**:
|
||||
- ❌ `GET /api/front/online/stats` - 已定义但未使用(连接统计功能未实现)
|
||||
|
||||
### 10. 离线消息模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/online/offline/count/{userId}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/online/offline/messages/{userId}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/online/offline/messages/{userId}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 11. 搜索功能模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/search/live-rooms` | SearchActivity.java:156 | ✅ 真实调用 |
|
||||
| `GET /api/front/search/hot` | SearchActivity.java:257 | ✅ 真实调用 |
|
||||
| `GET /api/front/search/suggestions` | SearchActivity.java:286 | ✅ 真实调用 |
|
||||
| `GET /api/front/search/history` | SearchActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/search/history` | SearchActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 12. 观看历史模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/watch/history` | RoomDetailActivity.java:744 | ✅ 真实调用 |
|
||||
|
||||
### 13. 分类管理模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/category/live-room` | MainActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/category/work` | PublishWorkActivity.java | ✅ 真实调用 |
|
||||
|
||||
---
|
||||
|
||||
## ❌ 已定义但未真实调用的接口(共 8 个)
|
||||
|
||||
### 1. 直播间模块
|
||||
|
||||
```java
|
||||
// ApiService.java 中已定义,但在代码中未找到调用
|
||||
❌ GET /api/front/live/public/rooms/{roomId}/viewers/count
|
||||
- 功能: 获取观众数量
|
||||
- 原因: 使用 WebSocket 实时推送在线人数,不需要轮询
|
||||
|
||||
❌ GET /api/front/live/rooms/{roomId}/viewers
|
||||
- 功能: 获取观众列表
|
||||
- 原因: 功能未实现,UI中没有显示观众列表的入口
|
||||
```
|
||||
|
||||
### 2. 私聊模块
|
||||
|
||||
```java
|
||||
❌ DELETE /api/front/conversations/messages/{id}
|
||||
- 功能: 删除单条消息
|
||||
- 原因: UI中只实现了删除整个会话,未实现删除单条消息
|
||||
```
|
||||
|
||||
### 3. 在线状态模块
|
||||
|
||||
```java
|
||||
❌ GET /api/front/online/stats
|
||||
- 功能: 获取连接统计信息
|
||||
- 原因: 管理功能,前端应用不需要
|
||||
```
|
||||
|
||||
### 4. 消息表情回应模块(整个模块未使用)
|
||||
|
||||
```java
|
||||
❌ POST /api/front/messages/reactions/add
|
||||
❌ DELETE /api/front/messages/reactions/remove
|
||||
❌ GET /api/front/messages/{messageId}/reactions
|
||||
❌ GET /api/front/messages/{messageId}/reactions/users
|
||||
- 功能: 消息表情回应(类似微信的点赞、爱心等)
|
||||
- 原因: 功能未实现,UI设计中没有这个功能
|
||||
```
|
||||
|
||||
### 5. 搜索模块
|
||||
|
||||
```java
|
||||
❌ GET /api/front/search/users
|
||||
- 功能: 全局搜索用户
|
||||
- 原因: 使用了 /api/front/users/search 替代
|
||||
|
||||
❌ GET /api/front/search/works
|
||||
- 功能: 全局搜索作品
|
||||
- 原因: 使用了 /api/front/works/search 替代
|
||||
|
||||
❌ GET /api/front/search/all
|
||||
- 功能: 综合搜索
|
||||
- 原因: 功能未实现,当前只支持分类搜索
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 统计数据
|
||||
|
||||
### 整体调用率
|
||||
|
||||
| 模块 | 已定义接口数 | 真实调用数 | 调用率 |
|
||||
|------|------------|-----------|--------|
|
||||
| 用户认证 | 5 | 5 | 100% |
|
||||
| 直播间 | 12 | 10 | 83% |
|
||||
| 礼物打赏 | 6 | 6 | 100% |
|
||||
| 私聊会话 | 8 | 7 | 88% |
|
||||
| 好友管理 | 9 | 9 | 100% |
|
||||
| 关注功能 | 7 | 7 | 100% |
|
||||
| 作品管理 | 12 | 12 | 100% |
|
||||
| 文件上传 | 2 | 2 | 100% |
|
||||
| 在线状态 | 5 | 4 | 80% |
|
||||
| 离线消息 | 3 | 3 | 100% |
|
||||
| 搜索功能 | 8 | 5 | 63% |
|
||||
| 观看历史 | 1 | 1 | 100% |
|
||||
| 分类管理 | 2 | 2 | 100% |
|
||||
| 消息表情 | 4 | 0 | 0% |
|
||||
| **总计** | **84** | **73** | **87%** |
|
||||
|
||||
### 关键发现
|
||||
|
||||
1. **高调用率模块** (100%):
|
||||
- ✅ 用户认证、礼物打赏、好友管理、关注功能、作品管理
|
||||
- ✅ 文件上传、离线消息、观看历史、分类管理
|
||||
|
||||
2. **中等调用率模块** (80-90%):
|
||||
- ⚠️ 直播间模块 (83%) - 部分接口被 WebSocket 替代
|
||||
- ⚠️ 私聊会话 (88%) - 删除单条消息功能未实现
|
||||
- ⚠️ 在线状态 (80%) - 统计接口未使用
|
||||
|
||||
3. **低调用率模块** (< 80%):
|
||||
- ⚠️ 搜索功能 (63%) - 部分接口有替代方案
|
||||
- ❌ 消息表情 (0%) - 功能完全未实现
|
||||
|
||||
---
|
||||
|
||||
## 🔍 WebSocket 实时通信使用情况
|
||||
|
||||
### 已实现的 WebSocket 连接
|
||||
|
||||
| WebSocket | 用途 | 调用位置 | 状态 |
|
||||
|-----------|------|---------|------|
|
||||
| `ws://*/ws/live/chat/{roomId}` | 直播间弹幕 | RoomDetailActivity.java:connectChatWebSocket() | ✅ 真实使用 |
|
||||
| `ws://*/ws/live/{roomId}` | 在线人数推送 | RoomDetailActivity.java:connectOnlineCountWebSocket() | ✅ 真实使用 |
|
||||
| `ws://*/ws/private/chat` | 私聊消息 | ChatActivity.java:connectWebSocket() | ✅ 真实使用 |
|
||||
| `ws://*/ws/online/status` | 在线状态推送 | MyFriendsActivity.java:connectWebSocket() | ✅ 真实使用 |
|
||||
|
||||
**WebSocket 特性**:
|
||||
- ✅ 心跳检测机制 (30秒间隔)
|
||||
- ✅ 自动重连机制 (最多5次)
|
||||
- ✅ 连接状态管理
|
||||
- ✅ 错误处理和降级
|
||||
|
||||
---
|
||||
|
||||
## 💡 结论与建议
|
||||
|
||||
### 结论
|
||||
|
||||
1. **整体调用率很高**: 87% 的接口都在真实使用中
|
||||
2. **核心功能完整**: 用户认证、直播、私聊、好友、关注、作品等核心模块100%调用
|
||||
3. **WebSocket 替代 HTTP**: 部分实时功能使用 WebSocket 替代了 HTTP 轮询
|
||||
4. **未使用接口有原因**: 大部分未使用接口是因为功能未实现或有更好的替代方案
|
||||
|
||||
### 建议
|
||||
|
||||
#### 1. 可以删除的接口定义(降低维护成本)
|
||||
|
||||
```java
|
||||
// ApiService.java 中可以删除以下接口定义
|
||||
- GET /api/front/live/public/rooms/{roomId}/viewers/count (被 WebSocket 替代)
|
||||
- GET /api/front/online/stats (前端不需要)
|
||||
- 整个消息表情回应模块 (功能未实现)
|
||||
```
|
||||
|
||||
#### 2. 建议实现的功能
|
||||
|
||||
```java
|
||||
// 提升用户体验的功能
|
||||
✨ DELETE /api/front/conversations/messages/{id}
|
||||
- 实现删除单条消息功能
|
||||
- 增加长按消息的操作菜单
|
||||
|
||||
✨ GET /api/front/search/all
|
||||
- 实现综合搜索功能
|
||||
- 一次搜索返回用户、直播间、作品等多种结果
|
||||
```
|
||||
|
||||
#### 3. 代码优化建议
|
||||
|
||||
```java
|
||||
// 统一搜索接口
|
||||
- 将 /api/front/users/search 和 /api/front/search/users 合并
|
||||
- 将 /api/front/works/search 和 /api/front/search/works 合并
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 验证方法
|
||||
|
||||
本报告通过以下方法验证接口调用情况:
|
||||
|
||||
1. **静态代码分析**: 搜索 `apiService.` 关键字,找到所有 Retrofit 接口调用
|
||||
2. **文件定位**: 记录每个接口调用的具体文件和行号
|
||||
3. **对比分析**: 将 ApiService.java 中定义的接口与实际调用进行对比
|
||||
4. **WebSocket 分析**: 检查 WebSocket 连接代码,确认实时通信功能
|
||||
5. **交叉验证**: 检查后端 Controller 代码,确认接口实现情况
|
||||
|
||||
---
|
||||
|
||||
**报告生成时间**: 2024-12-30
|
||||
**分析工具**: 代码静态分析 + 人工审查
|
||||
**可信度**: ⭐⭐⭐⭐⭐ (非常高)
|
||||
|
|
@ -84,7 +84,11 @@
|
|||
:preview-src-list="[scope.row.image]"
|
||||
style="width: 50px; height: 50px"
|
||||
fit="cover"
|
||||
/>
|
||||
>
|
||||
<div slot="error" class="image-slot">
|
||||
<i class="el-icon-picture-outline"></i>
|
||||
</div>
|
||||
</el-image>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="礼物名称" align="center" prop="name" />
|
||||
|
|
@ -126,7 +130,8 @@
|
|||
:total="total"
|
||||
:page.sync="queryParams.page"
|
||||
:limit.sync="queryParams.limit"
|
||||
@pagination="getList"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
|
|
@ -190,6 +195,7 @@
|
|||
|
||||
<script>
|
||||
import { giftListApi, giftAddApi, giftUpdateApi, giftDeleteApi, giftStatusApi } from '@/api/gift';
|
||||
import { fileImageApi } from '@/api/systemSetting';
|
||||
import DataPagination from '@/components/common/DataPagination';
|
||||
|
||||
export default {
|
||||
|
|
@ -240,27 +246,37 @@ export default {
|
|||
getList() {
|
||||
this.loading = true;
|
||||
giftListApi(this.queryParams).then(response => {
|
||||
const data = response.data || {};
|
||||
this.giftList = data.list || [];
|
||||
this.total = data.total || 0;
|
||||
console.log('礼物列表响应:', response);
|
||||
|
||||
// axios 拦截器已经返回了 res.data,所以 response 就是 CommonPage 对象
|
||||
// response = { list: [], total: 0, page: 1, limit: 10, totalPage: 1 }
|
||||
const list = response.list || [];
|
||||
|
||||
// 确保 status 字段是数字类型(1 或 0)
|
||||
this.giftList = list.map(item => ({
|
||||
...item,
|
||||
status: Number(item.status) // 确保是数字类型
|
||||
}));
|
||||
this.total = response.total || 0;
|
||||
|
||||
console.log('礼物列表数据:', this.giftList);
|
||||
console.log('总数:', this.total);
|
||||
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
}).catch(error => {
|
||||
console.error('获取礼物列表失败:', error);
|
||||
this.$message.error('获取礼物列表失败');
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
getStatistics() {
|
||||
// 计算统计数据
|
||||
giftListApi({ page: 1, limit: 9999 }).then(response => {
|
||||
console.log('统计数据响应:', response);
|
||||
const data = response.data || {};
|
||||
const list = data.list || [];
|
||||
console.log('礼物列表:', list);
|
||||
console.log('列表长度:', list.length);
|
||||
const list = response.list || [];
|
||||
this.statistics.total = list.length;
|
||||
this.statistics.enabled = list.filter(item => item.status === true).length;
|
||||
this.statistics.disabled = list.filter(item => item.status === false).length;
|
||||
this.statistics.totalValue = list.reduce((sum, item) => sum + (parseFloat(item.diamondPrice) || 0), 0);
|
||||
console.log('统计结果:', this.statistics);
|
||||
this.statistics.enabled = list.filter(item => item.status === 1 || item.status === true).length;
|
||||
this.statistics.disabled = list.filter(item => item.status === 0 || item.status === false).length;
|
||||
this.statistics.totalValue = list.reduce((sum, item) => sum + (parseFloat(item.diamondPrice) || 0), 0).toFixed(2);
|
||||
}).catch(error => {
|
||||
console.error('获取统计数据失败:', error);
|
||||
});
|
||||
|
|
@ -369,11 +385,36 @@ export default {
|
|||
});
|
||||
},
|
||||
handleUpload(param) {
|
||||
// 这里实现图片上传逻辑
|
||||
const formData = new FormData();
|
||||
formData.append('file', param.file);
|
||||
// 调用上传接口
|
||||
this.$message.info('图片上传功能需要配置上传接口');
|
||||
formData.append('multipart', param.file); // 后端参数名是 multipart
|
||||
|
||||
// 显示上传中提示
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '上传中...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
fileImageApi(formData, { model: 'gift', pid: 0 })
|
||||
.then(response => {
|
||||
loading.close();
|
||||
console.log('上传响应:', response);
|
||||
|
||||
// 根据响应格式设置图片地址
|
||||
if (response) {
|
||||
// response 可能是 FileResultVo 对象或直接是 URL
|
||||
this.form.image = response.url || response.fileUrl || response;
|
||||
this.$message.success('上传成功');
|
||||
} else {
|
||||
this.$message.error('上传失败,返回数据格式错误');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.close();
|
||||
console.error('上传失败:', error);
|
||||
this.$message.error('上传失败: ' + (error.message || '未知错误'));
|
||||
});
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const isImage = file.type.indexOf('image/') === 0;
|
||||
|
|
@ -385,6 +426,57 @@ export default {
|
|||
this.$message.error('上传图片大小不能超过 2MB!');
|
||||
}
|
||||
return isImage && isLt2M;
|
||||
},
|
||||
getImageUrl(url) {
|
||||
if (!url) {
|
||||
console.log('图片URL为空');
|
||||
return '';
|
||||
}
|
||||
|
||||
console.log('原始图片URL:', url);
|
||||
console.log('URL类型:', typeof url);
|
||||
|
||||
// 转换为字符串(防止是对象)
|
||||
let urlStr = String(url).trim();
|
||||
console.log('转换后的URL:', urlStr);
|
||||
|
||||
// 修复:如果URL被重复拼接,提取正确的路径
|
||||
// 例如:http://domain/http://domain/crmebimage/... -> crmebimage/...
|
||||
if (urlStr.indexOf('http://') > 0 || urlStr.indexOf('https://') > 0) {
|
||||
console.log('检测到重复拼接的URL,尝试修复');
|
||||
// 找到第二个 http:// 或 https:// 的位置
|
||||
const secondHttpIndex = urlStr.indexOf('http://', 1);
|
||||
const secondHttpsIndex = urlStr.indexOf('https://', 1);
|
||||
|
||||
if (secondHttpIndex > 0) {
|
||||
urlStr = urlStr.substring(secondHttpIndex);
|
||||
console.log('修复后的URL:', urlStr);
|
||||
} else if (secondHttpsIndex > 0) {
|
||||
urlStr = urlStr.substring(secondHttpsIndex);
|
||||
console.log('修复后的URL:', urlStr);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已经是完整的 URL(http:// 或 https://),直接返回
|
||||
if (urlStr.indexOf('http://') === 0 || urlStr.indexOf('https://') === 0) {
|
||||
console.log('检测到完整URL,直接返回');
|
||||
return urlStr;
|
||||
}
|
||||
|
||||
// 如果是相对路径,拼接服务器地址
|
||||
// 移除开头的 / 避免重复
|
||||
let path = urlStr;
|
||||
if (path.charAt(0) === '/') {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
// 使用当前页面的协议和主机名
|
||||
const baseUrl = window.location.origin;
|
||||
const fullUrl = baseUrl + '/' + path;
|
||||
|
||||
console.log('拼接后的完整URL:', fullUrl);
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -448,4 +540,15 @@ export default {
|
|||
height: 120px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeMessage',
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishtree';
|
||||
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeNode',
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishtree';
|
||||
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeDetail',
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishtree';
|
||||
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishTreeList',
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishtree';
|
||||
import { backgroundList, backgroundSave, backgroundDelete, festivalList } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishtreeBackground',
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishtree';
|
||||
import { festivalList, festivalSave, festivalDelete, festivalStatus } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishtreeFestival',
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getStatistics } from '@/api/wishtree';
|
||||
import { getStatistics } from '@/api/wishTree';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishtree';
|
||||
import { wishList, wishAudit, wishDelete, festivalList } from '@/api/wishTree';
|
||||
|
||||
export default {
|
||||
name: 'WishtreeWish',
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ module.exports = {
|
|||
'/api': {
|
||||
target: 'http://localhost:30001',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/crmebimage': {
|
||||
target: 'http://localhost:30001',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/file': {
|
||||
target: 'http://localhost:30001',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -64,6 +72,9 @@ module.exports = {
|
|||
alias: {
|
||||
'@': resolve('src'),
|
||||
},
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
// 在Windows上强制不区分大小写
|
||||
symlinks: false,
|
||||
},
|
||||
},
|
||||
chainWebpack(config) {
|
||||
|
|
|
|||
|
|
@ -92,13 +92,26 @@ public class WebConfig implements WebMvcConfigurer {
|
|||
registry.addResourceHandler("/webjars/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
|
||||
/** 本地文件上传路径 */
|
||||
registry.addResourceHandler(UploadConstants.UPLOAD_FILE_KEYWORD + "/**")
|
||||
.addResourceLocations("file:" + crmebConfig.getImagePath() + "/" + UploadConstants.UPLOAD_FILE_KEYWORD + "/");
|
||||
|
||||
registry.addResourceHandler(UploadConstants.UPLOAD_AFTER_FILE_KEYWORD + "/**")
|
||||
.addResourceLocations("file:" +crmebConfig.getImagePath() + "/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD + "/" );
|
||||
|
||||
/** 本地文件上传路径 - 使用jar包所在目录 */
|
||||
String uploadPath = crmebConfig.getAbsoluteImagePath();
|
||||
|
||||
// 添加 image 路径映射(修复图片上传显示问题)
|
||||
registry.addResourceHandler("/image/**")
|
||||
.addResourceLocations("file:" + uploadPath + "image" + File.separator);
|
||||
|
||||
// 添加 video 路径映射
|
||||
registry.addResourceHandler("/video/**")
|
||||
.addResourceLocations("file:" + uploadPath + "video" + File.separator);
|
||||
|
||||
// 添加 voice 路径映射
|
||||
registry.addResourceHandler("/voice/**")
|
||||
.addResourceLocations("file:" + uploadPath + "voice" + File.separator);
|
||||
|
||||
registry.addResourceHandler("/crmebimage/**")
|
||||
.addResourceLocations("file:" + uploadPath + "crmebimage" + File.separator);
|
||||
|
||||
registry.addResourceHandler("/file/**")
|
||||
.addResourceLocations("file:" + uploadPath + "file" + File.separator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
|||
|
|
@ -346,6 +346,8 @@ public class GiftAdminController {
|
|||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
|
||||
|
||||
log.info("礼物列表查询 - 参数: name={}, status={}, page={}, limit={}", name, status, page, limit);
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT id, name, image, diamond_price as diamondPrice, intimacy, status, ");
|
||||
sql.append("is_heartbeat as isHeartbeat, buy_type as buyType, belong, remark, ");
|
||||
|
|
@ -375,23 +377,32 @@ public class GiftAdminController {
|
|||
countParams.add(status);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY sort ASC, id DESC ");
|
||||
sql.append(" ORDER BY sort ASC, id ASC ");
|
||||
|
||||
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class, countParams.toArray());
|
||||
if (total == null) {
|
||||
total = 0L;
|
||||
}
|
||||
|
||||
int offset = (page - 1) * limit;
|
||||
sql.append(" LIMIT ? OFFSET ? ");
|
||||
params.add(limit);
|
||||
params.add(offset);
|
||||
|
||||
log.info("礼物列表查询 - SQL: {}", sql.toString());
|
||||
log.info("礼物列表查询 - 参数: {}", params);
|
||||
|
||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 打印日志用于调试
|
||||
log.info("礼物列表查询 - 总数: {}, 当前页: {}, 每页: {}, offset: {}, 列表大小: {}", total, page, limit, offset, list.size());
|
||||
|
||||
CommonPage<Map<String, Object>> result = new CommonPage<>();
|
||||
result.setList(list);
|
||||
result.setTotal(total != null ? total : 0L);
|
||||
result.setTotal(total);
|
||||
result.setPage(page);
|
||||
result.setLimit(limit);
|
||||
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
|
||||
result.setTotalPage((int) Math.ceil((double) total / limit));
|
||||
|
||||
return CommonResult.success(result);
|
||||
}
|
||||
|
|
@ -403,14 +414,19 @@ public class GiftAdminController {
|
|||
@PostMapping("/add")
|
||||
public CommonResult<String> addGift(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
log.info("添加礼物 - 请求参数: {}", request);
|
||||
|
||||
String name = (String) request.get("name");
|
||||
String image = (String) request.get("image");
|
||||
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
||||
Integer intimacy = request.get("intimacy") != null ? Integer.parseInt(request.get("intimacy").toString()) : 0;
|
||||
Integer level = request.get("level") != null ? Integer.parseInt(request.get("level").toString()) : 1;
|
||||
Integer isHeartbeat = request.get("isHeartbeat") != null ? Integer.parseInt(request.get("isHeartbeat").toString()) : 0;
|
||||
|
||||
// 处理布尔值或整数值
|
||||
Integer isHeartbeat = convertToInteger(request.get("isHeartbeat"), 0);
|
||||
Integer sort = request.get("sort") != null ? Integer.parseInt(request.get("sort").toString()) : 0;
|
||||
Integer status = request.get("status") != null ? Integer.parseInt(request.get("status").toString()) : 1;
|
||||
Integer status = convertToInteger(request.get("status"), 1);
|
||||
|
||||
String remark = (String) request.get("remark");
|
||||
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
||||
String belong = request.get("belong") != null ? (String) request.get("belong") : "平台";
|
||||
|
|
@ -420,6 +436,7 @@ public class GiftAdminController {
|
|||
jdbcTemplate.update(sql, name, image, diamondPrice, intimacy, status, isHeartbeat,
|
||||
buyType, belong, remark, level, sort);
|
||||
|
||||
log.info("添加礼物成功 - 名称: {}", name);
|
||||
return CommonResult.success("添加成功");
|
||||
} catch (Exception e) {
|
||||
log.error("添加礼物失败", e);
|
||||
|
|
@ -434,15 +451,20 @@ public class GiftAdminController {
|
|||
@PostMapping("/update")
|
||||
public CommonResult<String> updateGift(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
log.info("更新礼物 - 请求参数: {}", request);
|
||||
|
||||
Integer id = Integer.parseInt(request.get("id").toString());
|
||||
String name = (String) request.get("name");
|
||||
String image = (String) request.get("image");
|
||||
BigDecimal diamondPrice = new BigDecimal(request.get("diamondPrice").toString());
|
||||
Integer intimacy = request.get("intimacy") != null ? Integer.parseInt(request.get("intimacy").toString()) : 0;
|
||||
Integer level = request.get("level") != null ? Integer.parseInt(request.get("level").toString()) : 1;
|
||||
Integer isHeartbeat = request.get("isHeartbeat") != null ? Integer.parseInt(request.get("isHeartbeat").toString()) : 0;
|
||||
|
||||
// 处理布尔值或整数值
|
||||
Integer isHeartbeat = convertToInteger(request.get("isHeartbeat"), 0);
|
||||
Integer sort = request.get("sort") != null ? Integer.parseInt(request.get("sort").toString()) : 0;
|
||||
Integer status = request.get("status") != null ? Integer.parseInt(request.get("status").toString()) : 1;
|
||||
Integer status = convertToInteger(request.get("status"), 1);
|
||||
|
||||
String remark = (String) request.get("remark");
|
||||
String buyType = request.get("buyType") != null ? (String) request.get("buyType") : "钻石";
|
||||
String belong = request.get("belong") != null ? (String) request.get("belong") : "平台";
|
||||
|
|
@ -452,12 +474,42 @@ public class GiftAdminController {
|
|||
jdbcTemplate.update(sql, name, image, diamondPrice, intimacy, status, isHeartbeat,
|
||||
buyType, belong, remark, level, sort, id);
|
||||
|
||||
log.info("更新礼物成功 - ID: {}", id);
|
||||
return CommonResult.success("更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新礼物失败", e);
|
||||
return CommonResult.failed("更新失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为整数(支持布尔值、字符串、整数)
|
||||
*/
|
||||
private Integer convertToInteger(Object value, Integer defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 如果是布尔值
|
||||
if (value instanceof Boolean) {
|
||||
return ((Boolean) value) ? 1 : 0;
|
||||
}
|
||||
|
||||
// 如果是字符串
|
||||
String strValue = value.toString().toLowerCase();
|
||||
if ("true".equals(strValue)) {
|
||||
return 1;
|
||||
} else if ("false".equals(strValue)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 尝试解析为整数
|
||||
try {
|
||||
return Integer.parseInt(strValue);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼物
|
||||
|
|
@ -490,12 +542,15 @@ public class GiftAdminController {
|
|||
@PostMapping("/status")
|
||||
public CommonResult<String> updateGiftStatus(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
log.info("更新礼物状态 - 请求参数: {}", request);
|
||||
|
||||
Integer id = Integer.parseInt(request.get("id").toString());
|
||||
Integer status = Integer.parseInt(request.get("status").toString());
|
||||
Integer status = convertToInteger(request.get("status"), 1);
|
||||
|
||||
String sql = "UPDATE eb_gift SET status = ? WHERE id = ?";
|
||||
jdbcTemplate.update(sql, status, id);
|
||||
|
||||
log.info("更新礼物状态成功 - ID: {}, 状态: {}", id, status);
|
||||
return CommonResult.success("状态更新成功");
|
||||
} catch (Exception e) {
|
||||
log.error("更新礼物状态失败", e);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,18 @@ public class ResponseRouter {
|
|||
return data;
|
||||
}
|
||||
|
||||
// 排除上传接口,上传接口返回的URL不需要添加前缀
|
||||
// 因为前端会直接保存到数据库,下次查询时会再次处理
|
||||
if (path.contains("/upload/image") || path.contains("/upload/file")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
//根据需要处理返回值 && !data.contains("data:image/png;base64")
|
||||
// 如果数据已经包含完整的URL(http://或https://),则不处理
|
||||
if (data.contains("http://") || data.contains("https://")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if ((data.contains(UploadConstants.UPLOAD_FILE_KEYWORD + "/"))
|
||||
|| data.contains(UploadConstants.DOWNLOAD_FILE_KEYWORD) || data.contains(UploadConstants.UPLOAD_AFTER_FILE_KEYWORD)) {
|
||||
if (data.contains(UploadConstants.DOWNLOAD_FILE_KEYWORD + "/" + UploadConstants.UPLOAD_MODEL_PATH_EXCEL)) {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ crmeb:
|
|||
wechat-api-url: #请求微信接口中专服务器
|
||||
wechat-js-api-debug: false #微信js api系列是否开启调试模式
|
||||
wechat-js-api-beta: true #微信js api是否是beta版本
|
||||
asyncConfig: true #是否同步config表数据到redis
|
||||
asyncConfig: false #是否同步config表数据到redis - 改为false,直接从数据库读取
|
||||
asyncWeChatProgramTempList: false #是否同步小程序公共模板库
|
||||
imagePath: /Users/23730/Desktop/projectr/single_java/ # 服务器图片路径配置 斜杠结尾
|
||||
imagePath: upload/ # 服务器图片路径配置 - jar包同级目录的upload文件夹
|
||||
demoSite: true # 是否演示站点 所有手机号码都会掩码
|
||||
activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位,生产环境适当5-10分钟即可
|
||||
ignored: #安全路径白名单
|
||||
|
|
|
|||
|
|
@ -107,6 +107,67 @@ public class CrmebConfig {
|
|||
return imagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片存储的绝对路径
|
||||
* 如果配置的是相对路径,则转换为jar包所在目录的绝对路径
|
||||
*/
|
||||
public String getAbsoluteImagePath() {
|
||||
if (imagePath == null || imagePath.isEmpty()) {
|
||||
String result = getJarDirectory() + "/upload/";
|
||||
System.out.println("图片路径(默认): " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果是绝对路径(Windows: C:/ 或 Linux: /),直接返回
|
||||
if (imagePath.startsWith("/") || imagePath.matches("^[a-zA-Z]:.*")) {
|
||||
System.out.println("图片路径(绝对): " + imagePath);
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// 相对路径,转换为jar包所在目录的绝对路径
|
||||
String absolutePath = getJarDirectory() + "/" + imagePath;
|
||||
// 确保以斜杠结尾
|
||||
if (!absolutePath.endsWith("/") && !absolutePath.endsWith("\\")) {
|
||||
absolutePath += "/";
|
||||
}
|
||||
System.out.println("图片路径(相对转绝对): " + absolutePath);
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取jar包所在目录
|
||||
*/
|
||||
private String getJarDirectory() {
|
||||
try {
|
||||
String path = CrmebConfig.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
// 解码URL编码的路径
|
||||
path = java.net.URLDecoder.decode(path, "UTF-8");
|
||||
|
||||
System.out.println("=== 调试信息 ===");
|
||||
System.out.println("原始路径: " + path);
|
||||
|
||||
// 如果是jar包,去掉jar文件名
|
||||
if (path.endsWith(".jar")) {
|
||||
path = path.substring(0, path.lastIndexOf("/"));
|
||||
System.out.println("去掉jar文件名后: " + path);
|
||||
}
|
||||
// 去掉开头的斜杠(Windows)
|
||||
if (path.startsWith("/") && path.matches("^/[a-zA-Z]:.*")) {
|
||||
path = path.substring(1);
|
||||
System.out.println("去掉开头斜杠后: " + path);
|
||||
}
|
||||
|
||||
System.out.println("最终jar目录: " + path);
|
||||
System.out.println("===============");
|
||||
|
||||
return path;
|
||||
} catch (Exception e) {
|
||||
System.out.println("获取jar目录失败,使用user.dir: " + System.getProperty("user.dir"));
|
||||
// 如果获取失败,使用当前工作目录
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
}
|
||||
|
||||
public void setImagePath(String imagePath) {
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ package com.zbkj.common.constants;
|
|||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
public class Constants {
|
||||
public static final long TOKEN_EXPRESS_MINUTES = (60 * 24); //3小时
|
||||
// Token有效期:30天(43200分钟)
|
||||
// 采用滑动过期策略:每次访问自动刷新过期时间
|
||||
// 只有用户主动退出登录时才会删除Token
|
||||
public static final long TOKEN_EXPRESS_MINUTES = (60 * 24 * 30); // 30天
|
||||
|
||||
public static final int HTTPSTATUS_CODE_SUCCESS = 200;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.zbkj.common.model.friend;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
|
|
@ -55,11 +57,13 @@ public class FriendRequest implements Serializable {
|
|||
private Integer status = 0;
|
||||
|
||||
@ApiModelProperty(value = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date createTime;
|
||||
|
||||
@ApiModelProperty(value = "处理时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
@Column(name = "update_time")
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date updateTime;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.zbkj.common.model.live;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 直播类型实体类
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@Entity
|
||||
@Table(name = "eb_live_type")
|
||||
@TableName("eb_live_type")
|
||||
@ApiModel(value = "LiveType对象", description = "直播类型")
|
||||
public class LiveType implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty(value = "主键ID")
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
@ApiModelProperty(value = "类型名称")
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@ApiModelProperty(value = "类型编码")
|
||||
@Column(name = "code")
|
||||
private String code;
|
||||
|
||||
@ApiModelProperty(value = "类型图标")
|
||||
@Column(name = "icon")
|
||||
private String icon;
|
||||
|
||||
@ApiModelProperty(value = "类型描述")
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@ApiModelProperty(value = "排序(越大越靠前)")
|
||||
@Column(name = "sort")
|
||||
private Integer sort;
|
||||
|
||||
@ApiModelProperty(value = "状态 0=禁用 1=启用")
|
||||
@Column(name = "status")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "创建时间")
|
||||
@TableField("create_time")
|
||||
@Column(name = "create_time")
|
||||
private Date createTime;
|
||||
|
||||
@ApiModelProperty(value = "更新时间")
|
||||
@TableField("update_time")
|
||||
@Column(name = "update_time")
|
||||
private Date updateTime;
|
||||
}
|
||||
|
|
@ -73,6 +73,9 @@ public class User implements Serializable {
|
|||
@ApiModelProperty(value = "用户头像")
|
||||
private String avatar;
|
||||
|
||||
@ApiModelProperty(value = "个人签名")
|
||||
private String bio;
|
||||
|
||||
@ApiModelProperty(value = "手机号码")
|
||||
private String phone;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.zbkj.common.request;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户批量操作Request
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@ApiModel(value="UserBatchOperationRequest对象", description="用户批量操作")
|
||||
public class UserBatchOperationRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@ApiModelProperty(value = "用户ID列表", required = true)
|
||||
@NotEmpty(message = "用户ID列表不能为空")
|
||||
private List<Integer> uids;
|
||||
|
||||
@ApiModelProperty(value = "操作类型:1-启用,2-禁用,3-删除,4-设置分组,5-设置标签", required = true)
|
||||
@NotNull(message = "操作类型不能为空")
|
||||
private Integer operationType;
|
||||
|
||||
@ApiModelProperty(value = "分组ID(操作类型为4时必填)")
|
||||
private String groupId;
|
||||
|
||||
@ApiModelProperty(value = "标签ID(操作类型为5时必填)")
|
||||
private String tagId;
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.zbkj.common.request;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户创建Request
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@ApiModel(value="UserCreateRequest对象", description="创建用户")
|
||||
public class UserCreateRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@ApiModelProperty(value = "用户账号", required = true)
|
||||
@NotBlank(message = "请填写用户账号")
|
||||
@Length(max = 32, message = "用户账号不能超过32个字符")
|
||||
private String account;
|
||||
|
||||
@ApiModelProperty(value = "用户密码", required = true)
|
||||
@NotBlank(message = "请填写用户密码")
|
||||
@Length(min = 6, max = 32, message = "密码长度为6-32个字符")
|
||||
private String pwd;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称", required = true)
|
||||
@NotBlank(message = "请填写用户昵称")
|
||||
@Length(max = 255, message = "用户昵称不能超过255个字符")
|
||||
private String nickname;
|
||||
|
||||
@ApiModelProperty(value = "手机号码")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码格式不正确")
|
||||
private String phone;
|
||||
|
||||
@ApiModelProperty(value = "用户头像")
|
||||
@Length(max = 256, message = "用户头像不能超过256个字符")
|
||||
private String avatar;
|
||||
|
||||
@ApiModelProperty(value = "真实姓名")
|
||||
@Length(max = 25, message = "真实姓名不能超过25个字符")
|
||||
private String realName;
|
||||
|
||||
@ApiModelProperty(value = "生日")
|
||||
private String birthday;
|
||||
|
||||
@ApiModelProperty(value = "性别,0未知,1男,2女,3保密")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "用户备注")
|
||||
@Length(max = 255, message = "用户备注不能超过255个字符")
|
||||
private String mark;
|
||||
|
||||
@ApiModelProperty(value = "用户分组id")
|
||||
private String groupId;
|
||||
|
||||
@ApiModelProperty(value = "标签id")
|
||||
private String tagId;
|
||||
|
||||
@ApiModelProperty(value = "推广员id")
|
||||
private Integer spreadUid;
|
||||
|
||||
@ApiModelProperty(value = "是否为推广员,0否,1是")
|
||||
private Integer isPromoter;
|
||||
|
||||
@ApiModelProperty(value = "用户等级")
|
||||
private Integer level;
|
||||
|
||||
@ApiModelProperty(value = "状态,1正常,0禁用")
|
||||
private Integer status;
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.zbkj.common.request;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用户查询Request
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@ApiModel(value="UserQueryRequest对象", description="用户查询条件")
|
||||
public class UserQueryRequest implements Serializable {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@ApiModelProperty(value = "用户账号")
|
||||
private String account;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称")
|
||||
private String nickname;
|
||||
|
||||
@ApiModelProperty(value = "手机号码")
|
||||
private String phone;
|
||||
|
||||
@ApiModelProperty(value = "用户分组id")
|
||||
private String groupId;
|
||||
|
||||
@ApiModelProperty(value = "标签id")
|
||||
private String tagId;
|
||||
|
||||
@ApiModelProperty(value = "用户等级")
|
||||
private Integer level;
|
||||
|
||||
@ApiModelProperty(value = "状态,1正常,0禁用")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "是否为推广员,0否,1是")
|
||||
private Integer isPromoter;
|
||||
|
||||
@ApiModelProperty(value = "推广员id")
|
||||
private Integer spreadUid;
|
||||
|
||||
@ApiModelProperty(value = "性别,0未知,1男,2女,3保密")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "用户类型")
|
||||
private String userType;
|
||||
|
||||
@ApiModelProperty(value = "关键字搜索(账号/昵称/手机号)")
|
||||
private String keywords;
|
||||
|
||||
@ApiModelProperty(value = "开始时间")
|
||||
private String startTime;
|
||||
|
||||
@ApiModelProperty(value = "结束时间")
|
||||
private String endTime;
|
||||
|
||||
@ApiModelProperty(value = "排序字段")
|
||||
private String orderBy;
|
||||
|
||||
@ApiModelProperty(value = "排序方式,asc升序,desc降序")
|
||||
private String orderType;
|
||||
}
|
||||
|
|
@ -34,6 +34,9 @@ public class SendGiftResponse implements Serializable {
|
|||
@ApiModelProperty(value = "剩余钻石数")
|
||||
private BigDecimal remainingDiamond;
|
||||
|
||||
@ApiModelProperty(value = "新余额(剩余金币数,兼容字段)")
|
||||
private BigDecimal newBalance;
|
||||
|
||||
@ApiModelProperty(value = "增加的亲密度")
|
||||
private Integer intimacy;
|
||||
|
||||
|
|
|
|||
|
|
@ -88,4 +88,19 @@ public class UserCenterResponse implements Serializable {
|
|||
|
||||
@ApiModelProperty(value = "用户收藏数量")
|
||||
private Integer collectCount;
|
||||
|
||||
@ApiModelProperty(value = "个人签名")
|
||||
private String bio;
|
||||
|
||||
@ApiModelProperty(value = "生日")
|
||||
private String birthday;
|
||||
|
||||
@ApiModelProperty(value = "性别(0未知,1男,2女,3保密)")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "详细地址/所在地")
|
||||
private String addres;
|
||||
|
||||
@ApiModelProperty(value = "真实姓名")
|
||||
private String realName;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
package com.zbkj.common.response;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户详情Response
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@ApiModel(value="UserDetailResponse对象", description="用户详情")
|
||||
public class UserDetailResponse implements Serializable {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@ApiModelProperty(value = "用户id")
|
||||
private Integer uid;
|
||||
|
||||
@ApiModelProperty(value = "用户账号")
|
||||
private String account;
|
||||
|
||||
@ApiModelProperty(value = "用户昵称")
|
||||
private String nickname;
|
||||
|
||||
@ApiModelProperty(value = "用户头像")
|
||||
private String avatar;
|
||||
|
||||
@ApiModelProperty(value = "手机号码")
|
||||
private String phone;
|
||||
|
||||
@ApiModelProperty(value = "真实姓名")
|
||||
private String realName;
|
||||
|
||||
@ApiModelProperty(value = "生日")
|
||||
private String birthday;
|
||||
|
||||
@ApiModelProperty(value = "身份证号码")
|
||||
private String cardId;
|
||||
|
||||
@ApiModelProperty(value = "性别,0未知,1男,2女,3保密")
|
||||
private Integer sex;
|
||||
|
||||
@ApiModelProperty(value = "用户备注")
|
||||
private String mark;
|
||||
|
||||
@ApiModelProperty(value = "合伙人id")
|
||||
private Integer partnerId;
|
||||
|
||||
@ApiModelProperty(value = "用户分组id")
|
||||
private String groupId;
|
||||
|
||||
@ApiModelProperty(value = "标签id")
|
||||
private String tagId;
|
||||
|
||||
@ApiModelProperty(value = "用户余额")
|
||||
private BigDecimal nowMoney;
|
||||
|
||||
@ApiModelProperty(value = "佣金金额")
|
||||
private BigDecimal brokeragePrice;
|
||||
|
||||
@ApiModelProperty(value = "用户剩余积分")
|
||||
private Integer integral;
|
||||
|
||||
@ApiModelProperty(value = "用户剩余经验")
|
||||
private Integer experience;
|
||||
|
||||
@ApiModelProperty(value = "连续签到天数")
|
||||
private Integer signNum;
|
||||
|
||||
@ApiModelProperty(value = "状态,1正常,0禁用")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "用户等级")
|
||||
private Integer level;
|
||||
|
||||
@ApiModelProperty(value = "推广员id")
|
||||
private Integer spreadUid;
|
||||
|
||||
@ApiModelProperty(value = "推广员昵称")
|
||||
private String spreadNickname;
|
||||
|
||||
@ApiModelProperty(value = "推广员关联时间")
|
||||
private Date spreadTime;
|
||||
|
||||
@ApiModelProperty(value = "用户类型")
|
||||
private String userType;
|
||||
|
||||
@ApiModelProperty(value = "是否为推广员,0否,1是")
|
||||
private Integer isPromoter;
|
||||
|
||||
@ApiModelProperty(value = "用户购买次数")
|
||||
private Integer payCount;
|
||||
|
||||
@ApiModelProperty(value = "下级人数")
|
||||
private Integer spreadCount;
|
||||
|
||||
@ApiModelProperty(value = "详细地址")
|
||||
private String addres;
|
||||
|
||||
@ApiModelProperty(value = "登陆类型")
|
||||
private String loginType;
|
||||
|
||||
@ApiModelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
@ApiModelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
@ApiModelProperty(value = "最后一次登录时间")
|
||||
private Date lastLoginTime;
|
||||
|
||||
@ApiModelProperty(value = "推广等级记录")
|
||||
private String path;
|
||||
|
||||
@ApiModelProperty(value = "是否关注公众号,0否,1是")
|
||||
private Integer subscribe;
|
||||
|
||||
@ApiModelProperty(value = "关注公众号时间")
|
||||
private Date subscribeTime;
|
||||
|
||||
@ApiModelProperty(value = "国家")
|
||||
private String country;
|
||||
|
||||
@ApiModelProperty(value = "成为分销员时间")
|
||||
private Date promoterTime;
|
||||
|
||||
@ApiModelProperty(value = "是否注销,0未注销,1已注销")
|
||||
private Integer isLogoff;
|
||||
|
||||
@ApiModelProperty(value = "注销时间")
|
||||
private Date logoffTime;
|
||||
|
||||
@ApiModelProperty(value = "扩展字段1")
|
||||
private String extField1;
|
||||
|
||||
@ApiModelProperty(value = "扩展字段2")
|
||||
private String extField2;
|
||||
|
||||
@ApiModelProperty(value = "扩展字段3")
|
||||
private String extField3;
|
||||
|
||||
@ApiModelProperty(value = "扩展字段4")
|
||||
private Integer extField4;
|
||||
|
||||
@ApiModelProperty(value = "扩展字段5")
|
||||
private BigDecimal extField5;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.zbkj.common.response;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 用户统计Response
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@ApiModel(value="UserStatisticsResponse对象", description="用户统计数据")
|
||||
public class UserStatisticsResponse implements Serializable {
|
||||
|
||||
private static final long serialVersionUID=1L;
|
||||
|
||||
@ApiModelProperty(value = "总用户数")
|
||||
private Integer totalUsers;
|
||||
|
||||
@ApiModelProperty(value = "今日新增用户数")
|
||||
private Integer todayNewUsers;
|
||||
|
||||
@ApiModelProperty(value = "本周新增用户数")
|
||||
private Integer weekNewUsers;
|
||||
|
||||
@ApiModelProperty(value = "本月新增用户数")
|
||||
private Integer monthNewUsers;
|
||||
|
||||
@ApiModelProperty(value = "正常用户数")
|
||||
private Integer activeUsers;
|
||||
|
||||
@ApiModelProperty(value = "禁用用户数")
|
||||
private Integer disabledUsers;
|
||||
|
||||
@ApiModelProperty(value = "推广员数量")
|
||||
private Integer promoterCount;
|
||||
|
||||
@ApiModelProperty(value = "总余额")
|
||||
private BigDecimal totalBalance;
|
||||
|
||||
@ApiModelProperty(value = "总佣金")
|
||||
private BigDecimal totalBrokerage;
|
||||
|
||||
@ApiModelProperty(value = "总积分")
|
||||
private Long totalIntegral;
|
||||
|
||||
@ApiModelProperty(value = "付费用户数")
|
||||
private Integer paidUsers;
|
||||
|
||||
@ApiModelProperty(value = "付费率")
|
||||
private String paidRate;
|
||||
}
|
||||
|
|
@ -41,9 +41,9 @@ public class FrontTokenComponent {
|
|||
|
||||
private static final Long MILLIS_MINUTE = 60 * 1000L;
|
||||
|
||||
// 令牌有效期(默认30分钟) todo 调试期改为5小时
|
||||
// private static final int expireTime = 30;
|
||||
private static final int expireTime = 5 * 60;
|
||||
// 令牌有效期(设置为30天,每次访问自动刷新)
|
||||
// 只有用户主动退出登录时才会删除token
|
||||
private static final long expireTime = 30 * 24 * 60; // 30天
|
||||
|
||||
/**
|
||||
* 获取用户身份信息
|
||||
|
|
@ -81,13 +81,16 @@ public class FrontTokenComponent {
|
|||
|
||||
/**
|
||||
* 创建令牌
|
||||
* Token有效期30天,每次访问自动刷新
|
||||
* 只有用户主动退出登录时才会删除
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 令牌
|
||||
*/
|
||||
public String createToken(User user) {
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
redisUtil.set(getTokenKey(token), user.getUid(), Constants.TOKEN_EXPRESS_MINUTES, TimeUnit.MINUTES);
|
||||
// 设置30天过期时间,每次访问会自动刷新
|
||||
redisUtil.set(getTokenKey(token), user.getUid(), expireTime, TimeUnit.MINUTES);
|
||||
return token;
|
||||
}
|
||||
|
||||
|
|
@ -137,12 +140,16 @@ public class FrontTokenComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* 推出登录
|
||||
* 退出登录
|
||||
* 删除Redis中的Token,使Token立即失效
|
||||
*
|
||||
* @param request HttpServletRequest
|
||||
*/
|
||||
public void logout(HttpServletRequest request) {
|
||||
String token = getToken(request);
|
||||
delLoginUser(token);
|
||||
if (StrUtil.isNotEmpty(token)) {
|
||||
delLoginUser(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,13 +215,25 @@ public class FrontTokenComponent {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否有效
|
||||
* 如果Token存在,自动刷新过期时间(滑动过期策略)
|
||||
* 这样只要用户持续使用,Token就不会过期
|
||||
* 只有用户主动退出登录时才会删除Token
|
||||
*
|
||||
* @param token Token值
|
||||
* @param request 请求对象
|
||||
* @return true-有效 false-无效
|
||||
*/
|
||||
public Boolean check(String token, HttpServletRequest request){
|
||||
|
||||
try {
|
||||
boolean exists = redisUtil.exists(getTokenKey(token));
|
||||
if(exists){
|
||||
// Token存在,获取用户ID
|
||||
Integer uid = redisUtil.get(getTokenKey(token));
|
||||
redisUtil.set(getTokenKey(token), uid, Constants.TOKEN_EXPRESS_MINUTES, TimeUnit.MINUTES);
|
||||
// 自动刷新Token过期时间(滑动过期)
|
||||
// 每次访问都重置为30天后过期
|
||||
redisUtil.set(getTokenKey(token), uid, expireTime, TimeUnit.MINUTES);
|
||||
}else{
|
||||
//判断路由,部分路由不管用户是否登录/token过期都可以访问
|
||||
exists = checkRouter(RequestUtil.getUri(request));
|
||||
|
|
|
|||
|
|
@ -274,13 +274,31 @@ public class LiveRoomController {
|
|||
@Autowired
|
||||
private com.zbkj.service.service.GiftRecordService giftRecordService;
|
||||
|
||||
@Autowired
|
||||
private com.zbkj.front.service.GiftWebSocketService giftWebSocketService;
|
||||
|
||||
@Autowired
|
||||
private com.zbkj.service.service.GiftService giftService;
|
||||
|
||||
@ApiOperation(value = "赠送礼物(需要登录)")
|
||||
@PostMapping("/rooms/{roomId}/gift")
|
||||
public CommonResult<com.zbkj.common.response.SendGiftResponse> sendGift(
|
||||
@PathVariable Integer roomId,
|
||||
@RequestBody @Validated com.zbkj.common.request.SendGiftRequest request) {
|
||||
|
||||
// 🔍 添加调试日志
|
||||
log.info("========================================");
|
||||
log.info("🎁 收到礼物赠送请求");
|
||||
log.info("roomId: {}", roomId);
|
||||
log.info("request.giftId: {}", request.getGiftId());
|
||||
log.info("request.receiverId: {}", request.getReceiverId());
|
||||
log.info("request.giftCount: {}", request.getGiftCount());
|
||||
log.info("========================================");
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Integer currentUserId = frontTokenComponent.getUserId();
|
||||
log.info("currentUserId: {}", currentUserId);
|
||||
|
||||
if (currentUserId == null) {
|
||||
return CommonResult.failed("请先登录");
|
||||
}
|
||||
|
|
@ -295,19 +313,53 @@ public class LiveRoomController {
|
|||
return CommonResult.failed("直播间不存在");
|
||||
}
|
||||
|
||||
// TODO: 后续可能需要恢复此验证
|
||||
// 不能给自己送礼物
|
||||
if (currentUserId.equals(request.getReceiverId())) {
|
||||
return CommonResult.failed("不能给自己赠送礼物");
|
||||
}
|
||||
// if (currentUserId.equals(request.getReceiverId())) {
|
||||
// return CommonResult.failed("不能给自己赠送礼物");
|
||||
// }
|
||||
|
||||
try {
|
||||
// 调用服务层赠送礼物
|
||||
com.zbkj.common.response.SendGiftResponse response =
|
||||
giftRecordService.sendGift(roomId, currentUserId, request);
|
||||
log.info("✅ 礼物赠送成功");
|
||||
|
||||
// 🎁 通过WebSocket推送礼物消息到直播间
|
||||
try {
|
||||
// 获取用户信息
|
||||
com.zbkj.common.model.user.User sender = userService.getById(currentUserId);
|
||||
com.zbkj.common.model.user.User receiver = userService.getById(request.getReceiverId());
|
||||
com.zbkj.common.model.gift.Gift gift = giftService.getGiftById(request.getGiftId());
|
||||
|
||||
if (sender != null && receiver != null && gift != null) {
|
||||
giftWebSocketService.broadcastGift(
|
||||
String.valueOf(roomId),
|
||||
gift.getId(),
|
||||
gift.getName(),
|
||||
request.getGiftCount(),
|
||||
currentUserId,
|
||||
sender.getNickname(),
|
||||
request.getReceiverId(),
|
||||
receiver.getNickname(),
|
||||
response.getTotalDiamond().intValue()
|
||||
);
|
||||
log.info("✅ 礼物WebSocket推送成功: roomId={}, giftName={}, sender={}",
|
||||
roomId, gift.getName(), sender.getNickname());
|
||||
} else {
|
||||
log.warn("⚠️ 无法推送礼物消息:用户或礼物信息不完整");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 礼物WebSocket推送失败", e);
|
||||
// 不影响主流程,继续返回成功
|
||||
}
|
||||
|
||||
return CommonResult.success(response);
|
||||
} catch (com.zbkj.common.exception.CrmebException e) {
|
||||
log.error("❌ 礼物赠送失败: {}", e.getMessage());
|
||||
return CommonResult.failed(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 礼物赠送异常", e);
|
||||
return CommonResult.failed("赠送礼物失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,10 +68,27 @@ public class UserController {
|
|||
@ApiOperation(value = "修改个人资料")
|
||||
@RequestMapping(value = "/user/edit", method = RequestMethod.POST)
|
||||
public CommonResult<Object> personInfo(@RequestBody @Validated UserEditRequest request) {
|
||||
if (userService.editUser(request)) {
|
||||
return CommonResult.success();
|
||||
log.info("========== 收到修改个人资料请求 ==========");
|
||||
log.info("请求参数: {}", request);
|
||||
log.info("昵称: {}", request.getNickname());
|
||||
log.info("头像: {}", request.getAvatar());
|
||||
log.info("个人签名: {}", request.getMark());
|
||||
log.info("生日: {}", request.getBirthday());
|
||||
log.info("性别: {}", request.getSex());
|
||||
log.info("所在地: {}", request.getAddres());
|
||||
|
||||
try {
|
||||
boolean result = userService.editUser(request);
|
||||
log.info("修改结果: {}", result);
|
||||
|
||||
if (result) {
|
||||
return CommonResult.success();
|
||||
}
|
||||
return CommonResult.failed();
|
||||
} catch (Exception e) {
|
||||
log.error("修改个人资料失败", e);
|
||||
return CommonResult.failed("修改失败:" + e.getMessage());
|
||||
}
|
||||
return CommonResult.failed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -80,7 +97,15 @@ public class UserController {
|
|||
@ApiOperation(value = "个人中心-用户信息")
|
||||
@RequestMapping(value = "/user", method = RequestMethod.GET)
|
||||
public CommonResult<UserCenterResponse> getUserCenter() {
|
||||
return CommonResult.success(userService.getUserCenter());
|
||||
log.info("========== 收到获取用户信息请求 ==========");
|
||||
UserCenterResponse response = userService.getUserCenter();
|
||||
log.info("========== 返回用户信息 ==========");
|
||||
log.info("昵称: {}", response.getNickname());
|
||||
log.info("生日: {}", response.getBirthday());
|
||||
log.info("性别: {}", response.getSex());
|
||||
log.info("所在地: {}", response.getAddres());
|
||||
log.info("真实姓名: {}", response.getRealName());
|
||||
return CommonResult.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.zbkj.front.response.live;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 直播类型响应对象
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "LiveTypeResponse", description = "直播类型响应")
|
||||
public class LiveTypeResponse implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty(value = "类型ID")
|
||||
private Integer id;
|
||||
|
||||
@ApiModelProperty(value = "类型名称")
|
||||
private String name;
|
||||
|
||||
@ApiModelProperty(value = "类型编码")
|
||||
private String code;
|
||||
|
||||
@ApiModelProperty(value = "类型图标")
|
||||
private String icon;
|
||||
|
||||
@ApiModelProperty(value = "类型描述")
|
||||
private String description;
|
||||
|
||||
@ApiModelProperty(value = "排序")
|
||||
private Integer sort;
|
||||
}
|
||||
|
|
@ -28,4 +28,12 @@ public interface FriendRequestDao extends BaseMapper<FriendRequest> {
|
|||
* 检查是否已发送过请求
|
||||
*/
|
||||
Long checkExistingRequest(@Param("fromUserId") Integer fromUserId, @Param("toUserId") Integer toUserId);
|
||||
|
||||
/**
|
||||
* 插入好友请求(自定义插入,避免字段冲突)
|
||||
*/
|
||||
int insertFriendRequest(@Param("fromUserId") Integer fromUserId,
|
||||
@Param("toUserId") Integer toUserId,
|
||||
@Param("message") String message,
|
||||
@Param("status") Integer status);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.zbkj.service.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.zbkj.common.model.live.LiveType;
|
||||
|
||||
/**
|
||||
* 直播类型 Mapper 接口
|
||||
*/
|
||||
public interface LiveTypeDao extends BaseMapper<LiveType> {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.zbkj.service.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.zbkj.common.model.live.LiveType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 直播类型服务接口
|
||||
*/
|
||||
public interface LiveTypeService extends IService<LiveType> {
|
||||
|
||||
/**
|
||||
* 获取所有启用的直播类型列表
|
||||
* @return 直播类型列表,按sort字段降序排列
|
||||
*/
|
||||
List<LiveType> getEnabledList();
|
||||
|
||||
/**
|
||||
* 根据编码获取直播类型
|
||||
* @param code 类型编码
|
||||
* @return 直播类型
|
||||
*/
|
||||
LiveType getByCode(String code);
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ public class GiftRecordServiceImpl extends ServiceImpl<GiftRecordDao, GiftRecord
|
|||
|
||||
// 5. 检查余额(假设使用now_money字段作为钻石余额)
|
||||
if (sender.getNowMoney().compareTo(totalDiamond) < 0) {
|
||||
throw new CrmebException("钻石余额不足");
|
||||
throw new CrmebException("金币余额不足");
|
||||
}
|
||||
|
||||
// 6. 扣除赠送者钻石
|
||||
|
|
@ -106,7 +106,12 @@ public class GiftRecordServiceImpl extends ServiceImpl<GiftRecordDao, GiftRecord
|
|||
response.setGiftName(gift.getName());
|
||||
response.setGiftCount(request.getGiftCount());
|
||||
response.setTotalDiamond(totalDiamond);
|
||||
response.setRemainingDiamond(sender.getNowMoney().subtract(totalDiamond));
|
||||
|
||||
// 计算剩余余额
|
||||
BigDecimal remainingBalance = sender.getNowMoney().subtract(totalDiamond);
|
||||
response.setRemainingDiamond(remainingBalance);
|
||||
response.setNewBalance(remainingBalance); // 兼容 Android 客户端
|
||||
|
||||
response.setIntimacy(record.getIntimacy());
|
||||
response.setSendTime(record.getCreateTime().getTime());
|
||||
|
||||
|
|
|
|||
|
|
@ -43,12 +43,22 @@ public class LiveRoomServiceImpl extends ServiceImpl<LiveRoomDao, LiveRoom> impl
|
|||
Integer categoryId, String description, String coverImage,
|
||||
String tags, String notice) {
|
||||
String streamKey = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
// 验证和处理type参数
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
log.warn("createRoom - type参数为空,使用默认值: game");
|
||||
type = "game";
|
||||
}
|
||||
|
||||
log.info("createRoom - 开始创建直播间: uid={}, title={}, type={}, categoryId={}",
|
||||
uid, title, type, categoryId);
|
||||
|
||||
LiveRoom room = new LiveRoom();
|
||||
room.setUid(uid);
|
||||
room.setTitle(title);
|
||||
room.setStreamerName(streamerName);
|
||||
room.setStreamKey(streamKey);
|
||||
room.setType(type != null ? type : "live");
|
||||
room.setType(type);
|
||||
room.setCategoryId(categoryId);
|
||||
room.setDescription(description);
|
||||
room.setCoverImage(coverImage);
|
||||
|
|
@ -62,7 +72,12 @@ public class LiveRoomServiceImpl extends ServiceImpl<LiveRoomDao, LiveRoom> impl
|
|||
room.setOnlineCount(0);
|
||||
room.setCreateTime(new Date());
|
||||
room.setStartedAt(null);
|
||||
|
||||
dao.insert(room);
|
||||
|
||||
log.info("createRoom - 直播间创建成功: roomId={}, type={}, streamKey={}",
|
||||
room.getId(), room.getType(), streamKey);
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package com.zbkj.service.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.zbkj.common.model.live.LiveType;
|
||||
import com.zbkj.service.dao.LiveTypeDao;
|
||||
import com.zbkj.service.service.LiveTypeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 直播类型服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class LiveTypeServiceImpl extends ServiceImpl<LiveTypeDao, LiveType> implements LiveTypeService {
|
||||
|
||||
/**
|
||||
* 获取所有启用的直播类型列表
|
||||
* @return 直播类型列表,按sort字段降序排列
|
||||
*/
|
||||
@Override
|
||||
public List<LiveType> getEnabledList() {
|
||||
LambdaQueryWrapper<LiveType> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LiveType::getStatus, 1); // 只查询启用状态的
|
||||
wrapper.orderByDesc(LiveType::getSort); // 按排序字段降序
|
||||
wrapper.orderByAsc(LiveType::getId); // 排序相同时按ID升序
|
||||
|
||||
List<LiveType> list = list(wrapper);
|
||||
log.info("获取启用的直播类型列表,共 {} 条", list.size());
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编码获取直播类型
|
||||
* @param code 类型编码
|
||||
* @return 直播类型
|
||||
*/
|
||||
@Override
|
||||
public LiveType getByCode(String code) {
|
||||
LambdaQueryWrapper<LiveType> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LiveType::getCode, code);
|
||||
wrapper.eq(LiveType::getStatus, 1);
|
||||
return getOne(wrapper);
|
||||
}
|
||||
}
|
||||
|
|
@ -86,12 +86,20 @@ public class SystemAttachmentServiceImpl extends ServiceImpl<SystemAttachmentDao
|
|||
*/
|
||||
@Override
|
||||
public String prefixImage(String path) {
|
||||
// 如果路径为空或已经包含http://或https://,直接返回
|
||||
if (StringUtils.isBlank(path) || path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
// 如果那些域名不需要加,则跳过
|
||||
return path.replace(UploadConstants.UPLOAD_FILE_KEYWORD+"/", getCdnUrl() + "/"+ UploadConstants.UPLOAD_FILE_KEYWORD+"/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prefixUploadf(String path) {
|
||||
// 如果路径为空或已经包含http://或https://,直接返回
|
||||
if (StringUtils.isBlank(path) || path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
// 如果那些域名不需要加,则跳过
|
||||
return path.replace("crmebimage/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD+"/", getCdnUrl() + "/" +"crmebimage/" + UploadConstants.UPLOAD_AFTER_FILE_KEYWORD+"/");
|
||||
}
|
||||
|
|
@ -103,6 +111,10 @@ public class SystemAttachmentServiceImpl extends ServiceImpl<SystemAttachmentDao
|
|||
*/
|
||||
@Override
|
||||
public String prefixFile(String path) {
|
||||
// 如果路径为空或已经包含http://或https://,直接返回
|
||||
if (StringUtils.isBlank(path) || path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
if (path.contains(Constants.WECHAT_SOURCE_CODE_FILE_NAME)) {
|
||||
String cdnUrl = systemConfigService.getValueByKey("local" + "UploadUrl");
|
||||
return path.replace("crmebimage/", cdnUrl + "/crmebimage/");
|
||||
|
|
|
|||
|
|
@ -180,8 +180,8 @@ public class UploadServiceImpl implements UploadService {
|
|||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
|
||||
// 服务器存储地址
|
||||
String rootPath = crmebConfig.getImagePath().trim();
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
// 模块
|
||||
String modelPath = "public/" + model + "/";
|
||||
// 类型
|
||||
|
|
@ -356,8 +356,8 @@ public class UploadServiceImpl implements UploadService {
|
|||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
|
||||
// 服务器存储地址
|
||||
String rootPath = crmebConfig.getImagePath().trim();
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
String modelPath = "public/" + model + "/";
|
||||
String type = "video/";
|
||||
|
||||
|
|
@ -431,8 +431,8 @@ public class UploadServiceImpl implements UploadService {
|
|||
fileName = StrUtil.subPre(fileName, 90).concat(".").concat(extName);
|
||||
}
|
||||
|
||||
// 服务器存储地址
|
||||
String rootPath = crmebConfig.getImagePath().trim();
|
||||
// 服务器存储地址 - 使用绝对路径
|
||||
String rootPath = crmebConfig.getAbsoluteImagePath();
|
||||
String modelPath = "public/" + model + "/";
|
||||
String type = "voice/";
|
||||
|
||||
|
|
|
|||
|
|
@ -343,7 +343,8 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
|||
}
|
||||
|
||||
/**
|
||||
* 更新用户金额
|
||||
* 更新用户金额(并发安全版本)
|
||||
* 使用数据库原子操作,避免并发竞态条件
|
||||
*
|
||||
* @param user 用户
|
||||
* @param price 金额
|
||||
|
|
@ -352,17 +353,31 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
|||
*/
|
||||
@Override
|
||||
public Boolean updateNowMoney(User user, BigDecimal price, String type) {
|
||||
LambdaUpdateWrapper<User> lambdaUpdateWrapper = Wrappers.lambdaUpdate();
|
||||
// 🔒 使用 UpdateWrapper 直接在 SQL 层面进行原子操作
|
||||
// 这样可以避免并发时的竞态条件
|
||||
// 原理:UPDATE eb_user SET now_money = now_money +/- price WHERE uid = ?
|
||||
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
|
||||
|
||||
if (type.equals("add")) {
|
||||
lambdaUpdateWrapper.set(User::getNowMoney, user.getNowMoney().add(price));
|
||||
// SQL: UPDATE eb_user SET now_money = now_money + price WHERE uid = ?
|
||||
updateWrapper.setSql(StrUtil.format("now_money = now_money + {}", price));
|
||||
} else {
|
||||
lambdaUpdateWrapper.set(User::getNowMoney, user.getNowMoney().subtract(price));
|
||||
// SQL: UPDATE eb_user SET now_money = now_money - price WHERE uid = ? AND now_money - price >= 0
|
||||
updateWrapper.setSql(StrUtil.format("now_money = now_money - {}", price));
|
||||
// 确保余额不会变成负数(在 SQL 层面检查)
|
||||
updateWrapper.last(StrUtil.format(" AND (now_money - {} >= 0)", price));
|
||||
}
|
||||
lambdaUpdateWrapper.eq(User::getUid, user.getUid());
|
||||
if (type.equals("sub")) {
|
||||
lambdaUpdateWrapper.apply(StrUtil.format(" now_money - {} >= 0", price));
|
||||
|
||||
updateWrapper.eq("uid", user.getUid());
|
||||
|
||||
boolean result = update(updateWrapper);
|
||||
|
||||
// 如果是扣减操作且更新失败,说明余额不足
|
||||
if (!result && type.equals("sub")) {
|
||||
logger.warn("⚠️ 用户 {} 余额不足,无法扣除 {} 元", user.getUid(), price);
|
||||
}
|
||||
return update(lambdaUpdateWrapper);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -576,6 +591,12 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
|
|||
BeanUtils.copyProperties(currentUser, userCenterResponse);
|
||||
// 设置用户ID
|
||||
userCenterResponse.setUid(currentUser.getUid());
|
||||
|
||||
// 手动映射 mark -> bio(个人签名)
|
||||
if (currentUser.getMark() != null) {
|
||||
userCenterResponse.setBio(currentUser.getMark());
|
||||
}
|
||||
|
||||
// 优惠券数量
|
||||
userCenterResponse.setCouponCount(storeCouponUserService.getUseCount(currentUser.getUid()));
|
||||
// 收藏数量
|
||||
|
|
|
|||
|
|
@ -35,4 +35,10 @@
|
|||
AND status = 0
|
||||
</select>
|
||||
|
||||
<!-- 插入好友请求(明确指定字段,避免字段冲突) -->
|
||||
<insert id="insertFriendRequest">
|
||||
INSERT INTO eb_friend_request (from_user_id, to_user_id, target_uid, message, status, create_time)
|
||||
VALUES (#{fromUserId}, #{toUserId}, #{toUserId}, #{message}, #{status}, NOW())
|
||||
</insert>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,13 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS `eb_live_room` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`uid` int(11) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`streamer_name` varchar(255) NOT NULL,
|
||||
`stream_key` varchar(64) NOT NULL,
|
||||
`is_live` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`create_time` datetime DEFAULT NULL,
|
||||
`started_at` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_stream_key` (`stream_key`),
|
||||
KEY `idx_uid` (`uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
-- 好友关系表
|
||||
CREATE TABLE IF NOT EXISTS `eb_friend` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL COMMENT '用户ID',
|
||||
`friend_id` int(11) NOT NULL COMMENT '好友ID',
|
||||
`status` tinyint(1) 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='好友关系表';
|
||||
|
||||
-- 好友请求表
|
||||
CREATE TABLE IF NOT EXISTS `eb_friend_request` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`from_user_id` int(11) NOT NULL COMMENT '发送者用户ID',
|
||||
`to_user_id` int(11) NOT NULL COMMENT '接收者用户ID',
|
||||
`message` varchar(255) DEFAULT '' COMMENT '请求消息',
|
||||
`status` tinyint(1) 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='好友请求表';
|
||||
|
||||
-- 私聊会话表
|
||||
CREATE TABLE IF NOT EXISTS `eb_conversation` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user1_id` int(11) NOT NULL COMMENT '用户1 ID (较小的ID)',
|
||||
`user2_id` int(11) NOT NULL COMMENT '用户2 ID (较大的ID)',
|
||||
`last_message_id` int(11) DEFAULT NULL COMMENT '最后一条消息ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP 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='私聊会话表';
|
||||
|
||||
-- 私聊消息表
|
||||
CREATE TABLE IF NOT EXISTS `eb_conversation_message` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`conversation_id` int(11) NOT NULL COMMENT '会话ID',
|
||||
`sender_id` int(11) NOT NULL COMMENT '发送者ID',
|
||||
`content` text NOT NULL COMMENT '消息内容',
|
||||
`msg_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '消息类型: 1=文本, 2=图片, 3=语音',
|
||||
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态: 0=未读, 1=已读',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_conversation` (`conversation_id`),
|
||||
KEY `idx_sender` (`sender_id`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私聊消息表';
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
-- 创建 zhibo 数据库
|
||||
CREATE DATABASE IF NOT EXISTS `zhibo` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- 使用 zhibo 数据库
|
||||
USE `zhibo`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -102,6 +102,9 @@ dependencies {
|
|||
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||
|
||||
// SVG支持
|
||||
implementation("com.caverock:androidsvg-aar:1.4")
|
||||
|
||||
implementation("de.hdodenhof:circleimageview:3.1.0")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
/**
|
||||
* 余额页面的ViewPager适配器
|
||||
* 包含充值记录和消费记录两个Tab
|
||||
*/
|
||||
public class BalancePagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
public BalancePagerAdapter(@NonNull FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new RechargeRecordFragment();
|
||||
case 1:
|
||||
return new ConsumeRecordFragment();
|
||||
default:
|
||||
return new RechargeRecordFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 2; // 充值记录 + 消费记录
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* 消费记录Fragment
|
||||
*/
|
||||
public class ConsumeRecordFragment extends Fragment {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TextView tvEmpty;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_record_list, container, false);
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
tvEmpty = view.findViewById(R.id.tv_empty);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
// TODO: 加载消费记录数据
|
||||
showEmpty();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void showEmpty() {
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
tvEmpty.setVisibility(View.VISIBLE);
|
||||
tvEmpty.setText("暂无消费记录");
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,16 @@ public class Gift {
|
|||
this.level = level;
|
||||
}
|
||||
|
||||
// 新增构造函数 - 支持URL
|
||||
public Gift(String id, String name, int price, String iconUrl, int level) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
this.iconUrl = iconUrl;
|
||||
this.iconResId = R.drawable.ic_gift_24; // 默认占位图
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
@ -90,4 +100,11 @@ public class Gift {
|
|||
public String getFormattedPrice() {
|
||||
return price + " 金币";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有图标URL
|
||||
*/
|
||||
public boolean hasIconUrl() {
|
||||
return iconUrl != null && !iconUrl.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -9,11 +10,17 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.example.livestreaming.glide.SvgSoftwareLayerSetter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 礼物列表适配器
|
||||
* 支持PNG、JPG和SVG格式图标
|
||||
*/
|
||||
public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder> {
|
||||
|
||||
|
|
@ -77,7 +84,23 @@ public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder
|
|||
public void bind(Gift gift) {
|
||||
giftName.setText(gift.getName());
|
||||
giftPrice.setText(gift.getFormattedPrice());
|
||||
giftIcon.setImageResource(gift.getIconResId());
|
||||
|
||||
if (gift.hasIconUrl()) {
|
||||
String iconUrl = gift.getIconUrl();
|
||||
android.util.Log.d("GiftAdapter", "加载图标: " + gift.getName() + ", URL: " + iconUrl);
|
||||
|
||||
// 检查是否是SVG格式
|
||||
if (iconUrl.toLowerCase().endsWith(".svg") || iconUrl.toLowerCase().contains(".svg?")) {
|
||||
// 加载SVG格式
|
||||
loadSvgImage(iconUrl, gift.getName());
|
||||
} else {
|
||||
// 加载PNG/JPG等格式
|
||||
loadRegularImage(iconUrl, gift.getName());
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("GiftAdapter", "没有iconUrl: " + gift.getName());
|
||||
giftIcon.setImageResource(gift.getIconResId());
|
||||
}
|
||||
|
||||
// 选中状态
|
||||
boolean isSelected = selectedGift != null && selectedGift.getId().equals(gift.getId());
|
||||
|
|
@ -93,5 +116,69 @@ public class GiftAdapter extends RecyclerView.Adapter<GiftAdapter.GiftViewHolder
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载SVG格式图片
|
||||
*/
|
||||
private void loadSvgImage(String iconUrl, String giftName) {
|
||||
RequestBuilder<PictureDrawable> requestBuilder = Glide.with(itemView.getContext())
|
||||
.as(PictureDrawable.class)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<PictureDrawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<PictureDrawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("GiftAdapter", "SVG加载失败: " + giftName + ", URL: " + iconUrl, e);
|
||||
// 加载失败时显示默认图标
|
||||
giftIcon.setImageResource(R.drawable.ic_gift_24);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(PictureDrawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<PictureDrawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("GiftAdapter", "SVG加载成功: " + giftName);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
requestBuilder.load(iconUrl).into(new SvgSoftwareLayerSetter(giftIcon));
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载常规格式图片(PNG/JPG等)
|
||||
*/
|
||||
private void loadRegularImage(String iconUrl, String giftName) {
|
||||
Glide.with(itemView.getContext())
|
||||
.load(iconUrl)
|
||||
.placeholder(R.drawable.ic_gift_24)
|
||||
.error(R.drawable.ic_gift_24)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("GiftAdapter", "图片加载失败: " + giftName + ", URL: " + iconUrl, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("GiftAdapter", "图片加载成功: " + giftName);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(giftIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,625 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 礼物动画管理器
|
||||
* 负责显示不同等级的礼物特效
|
||||
*/
|
||||
public class GiftAnimationManager {
|
||||
|
||||
private final Context context;
|
||||
private final View rootView;
|
||||
private final Handler handler;
|
||||
private final Queue<GiftAnimationData> animationQueue;
|
||||
private boolean isAnimating = false;
|
||||
private final Random random = new Random();
|
||||
|
||||
// 礼物等级定义
|
||||
private static final int LEVEL_BASIC = 1; // 0-20金币
|
||||
private static final int LEVEL_NORMAL = 2; // 21-100金币
|
||||
private static final int LEVEL_ADVANCED = 3; // 101-1000金币
|
||||
private static final int LEVEL_PREMIUM = 4; // 1001-3000金币
|
||||
private static final int LEVEL_LEGENDARY = 5; // 3000+金币
|
||||
|
||||
public GiftAnimationManager(Context context, View rootView) {
|
||||
this.context = context;
|
||||
this.rootView = rootView;
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
this.animationQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示礼物动画
|
||||
*/
|
||||
public void showGiftAnimation(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl) {
|
||||
showGiftAnimation(giftName, giftPrice, count, senderName, giftIconUrl, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示礼物动画(带头像)
|
||||
*/
|
||||
public void showGiftAnimation(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl, String senderAvatarUrl) {
|
||||
GiftAnimationData data = new GiftAnimationData(
|
||||
giftName, giftPrice, count, senderName, giftIconUrl, senderAvatarUrl
|
||||
);
|
||||
|
||||
animationQueue.offer(data);
|
||||
|
||||
if (!isAnimating) {
|
||||
processNextAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下一个动画
|
||||
*/
|
||||
private void processNextAnimation() {
|
||||
if (animationQueue.isEmpty()) {
|
||||
isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isAnimating = true;
|
||||
GiftAnimationData data = animationQueue.poll();
|
||||
|
||||
int level = getGiftLevel(data.giftPrice);
|
||||
|
||||
switch (level) {
|
||||
case LEVEL_BASIC:
|
||||
playBasicAnimation(data);
|
||||
break;
|
||||
case LEVEL_NORMAL:
|
||||
playNormalAnimation(data);
|
||||
break;
|
||||
case LEVEL_ADVANCED:
|
||||
playAdvancedAnimation(data);
|
||||
break;
|
||||
case LEVEL_PREMIUM:
|
||||
playPremiumAnimation(data);
|
||||
break;
|
||||
case LEVEL_LEGENDARY:
|
||||
playLegendaryAnimation(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取礼物等级
|
||||
*/
|
||||
private int getGiftLevel(int price) {
|
||||
if (price <= 20) return LEVEL_BASIC;
|
||||
if (price <= 100) return LEVEL_NORMAL;
|
||||
if (price <= 1000) return LEVEL_ADVANCED;
|
||||
if (price <= 3000) return LEVEL_PREMIUM;
|
||||
return LEVEL_LEGENDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础动画 (0-20金币) - 简单滑入滑出
|
||||
*/
|
||||
private void playBasicAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setTranslationX(-300f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "translationX", -300f, 0f)
|
||||
);
|
||||
animSet.setDuration(400);
|
||||
animSet.setInterpolator(new DecelerateInterpolator());
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 2000);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通动画 (21-100金币) - 滑入+缩放+粒子
|
||||
*/
|
||||
private void playNormalAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setTranslationX(-300f);
|
||||
giftIcon.setScaleX(0.5f);
|
||||
giftIcon.setScaleY(0.5f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "translationX", -300f, 0f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleX", 0.5f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleY", 0.5f, 1.2f, 1f)
|
||||
);
|
||||
animSet.setDuration(500);
|
||||
animSet.setInterpolator(new OvershootInterpolator());
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createSimpleParticles();
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 2500);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级动画 (101-1000金币) - 华丽滑入+旋转+大量粒子
|
||||
*/
|
||||
private void playAdvancedAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setTranslationX(-400f);
|
||||
giftIcon.setScaleX(0.3f);
|
||||
giftIcon.setScaleY(0.3f);
|
||||
giftIcon.setRotation(-180f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "translationX", -400f, 0f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleX", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "scaleY", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "rotation", -180f, 0f)
|
||||
);
|
||||
animSet.setDuration(600);
|
||||
animSet.setInterpolator(new OvershootInterpolator());
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createAdvancedParticles();
|
||||
pulseAnimation(giftIcon);
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 豪华动画 (1001-3000金币) - 震撼登场+闪光+爆炸粒子
|
||||
*/
|
||||
private void playPremiumAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
FrameLayout fullscreenEffect = rootView.findViewById(R.id.fullscreenEffectContainer);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
// 显示全屏闪光效果
|
||||
fullscreenEffect.setVisibility(View.VISIBLE);
|
||||
fullscreenEffect.setBackgroundColor(Color.argb(100, 255, 215, 0));
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setScaleX(0.5f);
|
||||
effectLayout.setScaleY(0.5f);
|
||||
giftIcon.setRotation(-360f);
|
||||
|
||||
// 闪光动画
|
||||
ObjectAnimator flashAnim = ObjectAnimator.ofFloat(fullscreenEffect, "alpha", 0.8f, 0f);
|
||||
flashAnim.setDuration(500);
|
||||
flashAnim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
fullscreenEffect.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
flashAnim.start();
|
||||
|
||||
// 主动画
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleX", 0.5f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleY", 0.5f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "rotation", -360f, 0f)
|
||||
);
|
||||
animSet.setDuration(700);
|
||||
animSet.setInterpolator(new OvershootInterpolator(1.5f));
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createPremiumParticles();
|
||||
continuousPulse(giftIcon);
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 3500);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 传说动画 (3000+金币) - 超级震撼全屏特效
|
||||
*/
|
||||
private void playLegendaryAnimation(GiftAnimationData data) {
|
||||
View effectLayout = rootView.findViewById(R.id.giftEffectLayout);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
View giftInfoCard = rootView.findViewById(R.id.giftInfoCard);
|
||||
FrameLayout fullscreenEffect = rootView.findViewById(R.id.fullscreenEffectContainer);
|
||||
|
||||
setupGiftInfo(data);
|
||||
|
||||
// 全屏彩虹渐变效果
|
||||
fullscreenEffect.setVisibility(View.VISIBLE);
|
||||
|
||||
ValueAnimator colorAnim = ValueAnimator.ofArgb(
|
||||
Color.argb(150, 255, 0, 0),
|
||||
Color.argb(150, 255, 165, 0),
|
||||
Color.argb(150, 255, 255, 0),
|
||||
Color.argb(150, 0, 255, 0),
|
||||
Color.argb(150, 0, 0, 255),
|
||||
Color.argb(150, 139, 0, 255)
|
||||
);
|
||||
colorAnim.setDuration(1000);
|
||||
colorAnim.addUpdateListener(animation -> {
|
||||
fullscreenEffect.setBackgroundColor((int) animation.getAnimatedValue());
|
||||
});
|
||||
colorAnim.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(fullscreenEffect, "alpha", 1f, 0f);
|
||||
fadeOut.setDuration(500);
|
||||
fadeOut.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
fullscreenEffect.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
fadeOut.start();
|
||||
}
|
||||
});
|
||||
colorAnim.start();
|
||||
|
||||
effectLayout.setVisibility(View.VISIBLE);
|
||||
effectLayout.setAlpha(0f);
|
||||
effectLayout.setScaleX(0.3f);
|
||||
effectLayout.setScaleY(0.3f);
|
||||
effectLayout.setRotation(-180f);
|
||||
giftIcon.setRotation(-720f);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(effectLayout, "alpha", 0f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleX", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "scaleY", 0.3f, 1.3f, 1f),
|
||||
ObjectAnimator.ofFloat(effectLayout, "rotation", -180f, 0f),
|
||||
ObjectAnimator.ofFloat(giftIcon, "rotation", -720f, 0f)
|
||||
);
|
||||
animSet.setDuration(1000);
|
||||
animSet.setInterpolator(new OvershootInterpolator(2f));
|
||||
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
createLegendaryParticles();
|
||||
explosivePulse(giftIcon);
|
||||
handler.postDelayed(() -> hideAnimation(effectLayout), 4000);
|
||||
}
|
||||
});
|
||||
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置礼物信息
|
||||
*/
|
||||
private void setupGiftInfo(GiftAnimationData data) {
|
||||
TextView senderName = rootView.findViewById(R.id.senderName);
|
||||
TextView giftName = rootView.findViewById(R.id.giftName);
|
||||
ImageView giftIcon = rootView.findViewById(R.id.giftIcon);
|
||||
ImageView senderAvatar = rootView.findViewById(R.id.senderAvatar);
|
||||
TextView comboCount = rootView.findViewById(R.id.comboCount);
|
||||
|
||||
senderName.setText(data.senderName);
|
||||
giftName.setText(data.giftName);
|
||||
|
||||
// 加载礼物图标(优先使用网络图片)
|
||||
if (data.giftIconUrl != null && !data.giftIconUrl.isEmpty()) {
|
||||
// 使用Glide加载网络图片
|
||||
com.bumptech.glide.Glide.with(context)
|
||||
.load(data.giftIconUrl)
|
||||
.placeholder(getGiftIconResource(data.giftName))
|
||||
.error(getGiftIconResource(data.giftName))
|
||||
.into(giftIcon);
|
||||
} else {
|
||||
// 使用本地资源
|
||||
giftIcon.setImageResource(getGiftIconResource(data.giftName));
|
||||
}
|
||||
|
||||
// 加载用户头像(如果有的话)
|
||||
if (data.senderAvatarUrl != null && !data.senderAvatarUrl.isEmpty()) {
|
||||
com.bumptech.glide.Glide.with(context)
|
||||
.load(data.senderAvatarUrl)
|
||||
.placeholder(R.drawable.ic_user_24)
|
||||
.error(R.drawable.ic_user_24)
|
||||
.circleCrop()
|
||||
.into(senderAvatar);
|
||||
} else {
|
||||
senderAvatar.setImageResource(R.drawable.ic_user_24);
|
||||
}
|
||||
|
||||
if (data.count > 1) {
|
||||
comboCount.setVisibility(View.VISIBLE);
|
||||
comboCount.setText("x" + data.count);
|
||||
} else {
|
||||
comboCount.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取礼物图标资源
|
||||
*/
|
||||
private int getGiftIconResource(String giftName) {
|
||||
// 根据礼物名称返回对应的图标资源
|
||||
// 这里简化处理,实际应该从网络加载
|
||||
switch (giftName) {
|
||||
case "小心心": return R.drawable.ic_gift_heart;
|
||||
case "玫瑰花": return R.drawable.ic_gift_rose;
|
||||
case "皇冠": return R.drawable.ic_gift_crown;
|
||||
case "火箭": return R.drawable.ic_gift_rocket;
|
||||
case "跑车": return R.drawable.ic_gift_car;
|
||||
case "游艇": return R.drawable.ic_gift_yacht;
|
||||
case "城堡": return R.drawable.ic_gift_castle;
|
||||
default: return R.drawable.ic_gift_24;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏动画
|
||||
*/
|
||||
private void hideAnimation(View effectLayout) {
|
||||
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(effectLayout, "alpha", 1f, 0f);
|
||||
fadeOut.setDuration(300);
|
||||
fadeOut.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
effectLayout.setVisibility(View.GONE);
|
||||
processNextAnimation();
|
||||
}
|
||||
});
|
||||
fadeOut.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建简单粒子效果
|
||||
*/
|
||||
private void createSimpleParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
createParticle(particleContainer, Color.argb(200, 255, 215, 0), 20);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高级粒子效果
|
||||
*/
|
||||
private void createAdvancedParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
int[] colors = {
|
||||
Color.argb(200, 255, 215, 0),
|
||||
Color.argb(200, 255, 140, 0),
|
||||
Color.argb(200, 255, 69, 0)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int color = colors[random.nextInt(colors.length)];
|
||||
createParticle(particleContainer, color, 30);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建豪华粒子效果
|
||||
*/
|
||||
private void createPremiumParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
int[] colors = {
|
||||
Color.argb(220, 255, 215, 0),
|
||||
Color.argb(220, 255, 0, 0),
|
||||
Color.argb(220, 255, 105, 180),
|
||||
Color.argb(220, 138, 43, 226)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
int color = colors[random.nextInt(colors.length)];
|
||||
createParticle(particleContainer, color, 40);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建传说粒子效果
|
||||
*/
|
||||
private void createLegendaryParticles() {
|
||||
FrameLayout particleContainer = rootView.findViewById(R.id.particleContainer);
|
||||
|
||||
int[] colors = {
|
||||
Color.argb(255, 255, 0, 0),
|
||||
Color.argb(255, 255, 165, 0),
|
||||
Color.argb(255, 255, 255, 0),
|
||||
Color.argb(255, 0, 255, 0),
|
||||
Color.argb(255, 0, 0, 255),
|
||||
Color.argb(255, 139, 0, 255)
|
||||
};
|
||||
|
||||
for (int i = 0; i < 80; i++) {
|
||||
int color = colors[random.nextInt(colors.length)];
|
||||
createParticle(particleContainer, color, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个粒子
|
||||
*/
|
||||
private void createParticle(FrameLayout container, int color, int maxSize) {
|
||||
View particle = new View(context);
|
||||
int size = random.nextInt(maxSize - 10) + 10;
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
|
||||
particle.setLayoutParams(params);
|
||||
particle.setBackgroundColor(color);
|
||||
|
||||
// 随机起始位置
|
||||
int startX = random.nextInt(container.getWidth());
|
||||
int startY = container.getHeight() / 2;
|
||||
particle.setX(startX);
|
||||
particle.setY(startY);
|
||||
|
||||
container.addView(particle);
|
||||
|
||||
// 随机目标位置
|
||||
float targetX = startX + (random.nextFloat() - 0.5f) * 400;
|
||||
float targetY = startY - random.nextInt(300) - 100;
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(particle, "x", startX, targetX),
|
||||
ObjectAnimator.ofFloat(particle, "y", startY, targetY),
|
||||
ObjectAnimator.ofFloat(particle, "alpha", 1f, 0f),
|
||||
ObjectAnimator.ofFloat(particle, "rotation", 0f, random.nextInt(720) - 360)
|
||||
);
|
||||
animSet.setDuration(random.nextInt(1000) + 1000);
|
||||
animSet.setInterpolator(new DecelerateInterpolator());
|
||||
animSet.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
container.removeView(particle);
|
||||
}
|
||||
});
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 脉冲动画
|
||||
*/
|
||||
private void pulseAnimation(View view) {
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(
|
||||
ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.2f, 1f),
|
||||
ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.2f, 1f)
|
||||
);
|
||||
animSet.setDuration(500);
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 持续脉冲动画
|
||||
*/
|
||||
private void continuousPulse(View view) {
|
||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.15f, 1f);
|
||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.15f, 1f);
|
||||
|
||||
scaleX.setDuration(400);
|
||||
scaleX.setRepeatCount(3);
|
||||
scaleY.setDuration(400);
|
||||
scaleY.setRepeatCount(3);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(scaleX, scaleY);
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 爆炸式脉冲动画
|
||||
*/
|
||||
private void explosivePulse(View view) {
|
||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.3f, 1f);
|
||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.3f, 1f);
|
||||
ObjectAnimator rotation = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
|
||||
|
||||
scaleX.setDuration(600);
|
||||
scaleX.setRepeatCount(2);
|
||||
scaleY.setDuration(600);
|
||||
scaleY.setRepeatCount(2);
|
||||
rotation.setDuration(600);
|
||||
rotation.setRepeatCount(2);
|
||||
|
||||
AnimatorSet animSet = new AnimatorSet();
|
||||
animSet.playTogether(scaleX, scaleY, rotation);
|
||||
animSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
animSet.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼物动画数据类
|
||||
*/
|
||||
private static class GiftAnimationData {
|
||||
String giftName;
|
||||
int giftPrice;
|
||||
int count;
|
||||
String senderName;
|
||||
String giftIconUrl;
|
||||
String senderAvatarUrl;
|
||||
|
||||
GiftAnimationData(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl) {
|
||||
this.giftName = giftName;
|
||||
this.giftPrice = giftPrice;
|
||||
this.count = count;
|
||||
this.senderName = senderName;
|
||||
this.giftIconUrl = giftIconUrl;
|
||||
this.senderAvatarUrl = null;
|
||||
}
|
||||
|
||||
GiftAnimationData(String giftName, int giftPrice, int count,
|
||||
String senderName, String giftIconUrl, String senderAvatarUrl) {
|
||||
this.giftName = giftName;
|
||||
this.giftPrice = giftPrice;
|
||||
this.count = count;
|
||||
this.senderName = senderName;
|
||||
this.giftIconUrl = giftIconUrl;
|
||||
this.senderAvatarUrl = senderAvatarUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1115,17 +1115,12 @@ public class MainActivity extends AppCompatActivity {
|
|||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null);
|
||||
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView);
|
||||
|
||||
// 设置直播类型选择器
|
||||
String[] liveTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, liveTypes);
|
||||
|
||||
// 使用正确的资源ID获取typeSpinner
|
||||
int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName());
|
||||
MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId);
|
||||
|
||||
if (typeSpinner != null) {
|
||||
typeSpinner.setAdapter(adapter);
|
||||
}
|
||||
// 从后端加载直播类型分类
|
||||
loadLiveTypesForDialog(typeSpinner);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("创建直播间")
|
||||
|
|
@ -1137,7 +1132,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
dialog.setOnShowListener(d -> {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
|
||||
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
|
||||
String typeDisplay = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
|
||||
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
dialogBinding.titleLayout.setError("标题不能为空");
|
||||
|
|
@ -1146,7 +1141,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
dialogBinding.titleLayout.setError(null);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(type)) {
|
||||
if (TextUtils.isEmpty(typeDisplay)) {
|
||||
int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
|
||||
TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
|
||||
if (typeLayout != null) {
|
||||
|
|
@ -1161,14 +1156,74 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
// 从后端返回的类型列表中查找对应的类型编码
|
||||
String typeCode = "game"; // 默认值:游戏
|
||||
Object tag = typeSpinner.getTag();
|
||||
|
||||
Log.d(TAG, "创建直播间 - 用户选择的类型显示名称: " + typeDisplay);
|
||||
Log.d(TAG, "创建直播间 - typeSpinner.getTag() 类型: " + (tag != null ? tag.getClass().getName() : "null"));
|
||||
|
||||
if (tag instanceof List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<com.example.livestreaming.net.LiveTypeResponse> types =
|
||||
(List<com.example.livestreaming.net.LiveTypeResponse>) tag;
|
||||
|
||||
Log.d(TAG, "创建直播间 - 从Tag中获取到 " + types.size() + " 个类型");
|
||||
|
||||
boolean found = false;
|
||||
for (com.example.livestreaming.net.LiveTypeResponse type : types) {
|
||||
Log.d(TAG, "创建直播间 - 比对类型: " + type.getName() + " vs " + typeDisplay);
|
||||
if (type.getName().equals(typeDisplay)) {
|
||||
typeCode = type.getCode();
|
||||
found = true;
|
||||
Log.d(TAG, "创建直播间 - 匹配成功!使用类型编码: " + typeCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
Log.w(TAG, "创建直播间 - 未找到匹配的类型,使用默认值: " + typeCode);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "创建直播间 - Tag不是List类型,使用默认映射");
|
||||
// 如果没有从后端获取到类型列表,使用默认映射
|
||||
switch (typeDisplay) {
|
||||
case "游戏":
|
||||
typeCode = "game";
|
||||
break;
|
||||
case "才艺":
|
||||
typeCode = "talent";
|
||||
break;
|
||||
case "户外":
|
||||
typeCode = "outdoor";
|
||||
break;
|
||||
case "音乐":
|
||||
typeCode = "music";
|
||||
break;
|
||||
case "美食":
|
||||
typeCode = "food";
|
||||
break;
|
||||
case "聊天":
|
||||
typeCode = "chat";
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "创建直播间 - 未知的类型名称: " + typeDisplay + ",使用默认值: game");
|
||||
typeCode = "game";
|
||||
break;
|
||||
}
|
||||
Log.d(TAG, "创建直播间 - 默认映射结果: " + typeDisplay + " -> " + typeCode);
|
||||
}
|
||||
|
||||
// 获取用户昵称
|
||||
String streamerName = getSharedPreferences("profile_prefs", MODE_PRIVATE)
|
||||
.getString("profile_name", "未知用户");
|
||||
|
||||
Log.d(TAG, "创建直播间 - 最终参数: title=" + title + ", streamerName=" + streamerName + ", typeCode=" + typeCode);
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
|
||||
// 调用后端接口创建直播间
|
||||
ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, "live"))
|
||||
// 调用后端接口创建直播间,使用从后端获取的类型编码
|
||||
ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, typeCode))
|
||||
.enqueue(new Callback<ApiResponse<Room>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
|
||||
|
|
@ -1727,6 +1782,96 @@ public class MainActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为创建直播间对话框加载直播类型
|
||||
* 从后端数据库获取直播类型列表(游戏、才艺、户外、音乐、美食、聊天等)
|
||||
*/
|
||||
private void loadLiveTypesForDialog(MaterialAutoCompleteTextView typeSpinner) {
|
||||
if (typeSpinner == null) {
|
||||
Log.w(TAG, "loadLiveTypesForDialog() typeSpinner is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "loadLiveTypesForDialog() 开始从后端加载直播类型");
|
||||
|
||||
// 调用后端接口获取直播类型列表
|
||||
ApiClient.getService(getApplicationContext()).getLiveTypes()
|
||||
.enqueue(new Callback<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>> call,
|
||||
Response<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>> response) {
|
||||
Log.d(TAG, "loadLiveTypesForDialog() onResponse: code=" + response.code());
|
||||
|
||||
ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>> body = response.body();
|
||||
List<com.example.livestreaming.net.LiveTypeResponse> types =
|
||||
response.isSuccessful() && body != null && body.isOk() && body.getData() != null
|
||||
? body.getData()
|
||||
: null;
|
||||
|
||||
runOnUiThread(() -> {
|
||||
if (types != null && !types.isEmpty()) {
|
||||
Log.d(TAG, "loadLiveTypesForDialog() 成功获取 " + types.size() + " 个直播类型");
|
||||
|
||||
// 打印所有类型信息,便于调试
|
||||
for (com.example.livestreaming.net.LiveTypeResponse type : types) {
|
||||
Log.d(TAG, " 类型: " + type.getName() + " (code=" + type.getCode() + ", sort=" + type.getSort() + ")");
|
||||
}
|
||||
|
||||
// 提取类型名称
|
||||
String[] typeNames = new String[types.size()];
|
||||
for (int i = 0; i < types.size(); i++) {
|
||||
typeNames[i] = types.get(i).getName();
|
||||
}
|
||||
|
||||
// 使用 simple_list_item_1 而不是 simple_dropdown_item_1line
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this,
|
||||
android.R.layout.simple_list_item_1, typeNames);
|
||||
typeSpinner.setAdapter(adapter);
|
||||
|
||||
// 设置默认选中第一项,使用 false 参数避免触发过滤
|
||||
if (typeNames.length > 0) {
|
||||
typeSpinner.setText(typeNames[0], false);
|
||||
Log.d(TAG, "loadLiveTypesForDialog() 默认选中: " + typeNames[0]);
|
||||
}
|
||||
|
||||
// 保存类型列表供后续使用
|
||||
typeSpinner.setTag(types);
|
||||
Log.d(TAG, "loadLiveTypesForDialog() 类型列表已保存到Tag中");
|
||||
} else {
|
||||
Log.w(TAG, "loadLiveTypesForDialog() 未获取到类型数据,使用默认类型");
|
||||
useDefaultLiveTypes(typeSpinner);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>> call, Throwable t) {
|
||||
Log.e(TAG, "loadLiveTypesForDialog() onFailure: " + t.getMessage(), t);
|
||||
// 网络错误,使用默认类型
|
||||
runOnUiThread(() -> {
|
||||
useDefaultLiveTypes(typeSpinner);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的直播类型(当后端接口失败时的备用方案)
|
||||
*/
|
||||
private void useDefaultLiveTypes(MaterialAutoCompleteTextView typeSpinner) {
|
||||
String[] defaultTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this,
|
||||
android.R.layout.simple_list_item_1, defaultTypes);
|
||||
typeSpinner.setAdapter(adapter);
|
||||
|
||||
// 设置默认选中第一项
|
||||
if (defaultTypes.length > 0) {
|
||||
typeSpinner.setText(defaultTypes[0], false);
|
||||
}
|
||||
|
||||
Log.d(TAG, "useDefaultLiveTypes() 使用默认类型,共 " + defaultTypes.length + " 个");
|
||||
}
|
||||
|
||||
private void loadCoverAssetsAsync() {
|
||||
// 在后台线程加载资源文件,避免阻塞UI
|
||||
new Thread(() -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 充值套餐适配器
|
||||
*/
|
||||
public class RechargePackageAdapter extends RecyclerView.Adapter<RechargePackageAdapter.ViewHolder> {
|
||||
|
||||
private List<Map<String, Object>> packages;
|
||||
private int selectedPosition = -1;
|
||||
private OnItemClickListener listener;
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(Integer packageId);
|
||||
}
|
||||
|
||||
public RechargePackageAdapter(List<Map<String, Object>> packages) {
|
||||
this.packages = packages != null ? packages : new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setOnItemClickListener(OnItemClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void updateData(List<Map<String, Object>> newPackages) {
|
||||
this.packages = newPackages != null ? newPackages : new ArrayList<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_recharge_package, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
Map<String, Object> pkg = packages.get(position);
|
||||
|
||||
// 获取套餐信息
|
||||
Integer id = (Integer) pkg.get("id");
|
||||
String name = (String) pkg.get("name");
|
||||
Object amountObj = pkg.get("amount");
|
||||
Object priceObj = pkg.get("price");
|
||||
String label = (String) pkg.get("label");
|
||||
|
||||
double amount = amountObj instanceof Number ? ((Number) amountObj).doubleValue() : 0;
|
||||
double price = priceObj instanceof Number ? ((Number) priceObj).doubleValue() : 0;
|
||||
|
||||
// 设置数据
|
||||
holder.tvAmount.setText(String.format("%.0f", amount));
|
||||
holder.tvPrice.setText(String.format("¥%.2f", price));
|
||||
|
||||
if (label != null && !label.isEmpty()) {
|
||||
holder.tvLabel.setVisibility(View.VISIBLE);
|
||||
holder.tvLabel.setText(label);
|
||||
} else {
|
||||
holder.tvLabel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 设置选中状态
|
||||
boolean isSelected = position == selectedPosition;
|
||||
holder.cardView.setCardBackgroundColor(
|
||||
holder.itemView.getContext().getResources().getColor(
|
||||
isSelected ? R.color.colorPrimary : android.R.color.white
|
||||
)
|
||||
);
|
||||
holder.tvAmount.setTextColor(
|
||||
holder.itemView.getContext().getResources().getColor(
|
||||
isSelected ? android.R.color.white : R.color.colorPrimary
|
||||
)
|
||||
);
|
||||
holder.tvPrice.setTextColor(
|
||||
holder.itemView.getContext().getResources().getColor(
|
||||
isSelected ? android.R.color.white : R.color.text_secondary
|
||||
)
|
||||
);
|
||||
|
||||
// 点击事件
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int oldPosition = selectedPosition;
|
||||
selectedPosition = holder.getAdapterPosition();
|
||||
|
||||
if (oldPosition != -1) {
|
||||
notifyItemChanged(oldPosition);
|
||||
}
|
||||
notifyItemChanged(selectedPosition);
|
||||
|
||||
if (listener != null && id != null) {
|
||||
listener.onItemClick(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return packages.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
CardView cardView;
|
||||
TextView tvAmount;
|
||||
TextView tvPrice;
|
||||
TextView tvLabel;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
cardView = (CardView) itemView;
|
||||
tvAmount = itemView.findViewById(R.id.tv_amount);
|
||||
tvPrice = itemView.findViewById(R.id.tv_price);
|
||||
tvLabel = itemView.findViewById(R.id.tv_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* 充值记录Fragment
|
||||
*/
|
||||
public class RechargeRecordFragment extends Fragment {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private TextView tvEmpty;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_record_list, container, false);
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
tvEmpty = view.findViewById(R.id.tv_empty);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
// TODO: 加载充值记录数据
|
||||
showEmpty();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void showEmpty() {
|
||||
recyclerView.setVisibility(View.GONE);
|
||||
tvEmpty.setVisibility(View.VISIBLE);
|
||||
tvEmpty.setText("暂无充值记录");
|
||||
}
|
||||
}
|
||||
|
|
@ -18,10 +18,12 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
|
@ -151,6 +153,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
private GiftAdapter giftAdapter;
|
||||
private List<Gift> availableGifts;
|
||||
private int userCoinBalance = 0; // 用户金币余额(从后端加载)
|
||||
private GiftAnimationManager giftAnimationManager; // 礼物动画管理器
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
|
@ -180,6 +183,20 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
setupChat();
|
||||
setupGifts();
|
||||
|
||||
// 初始化礼物动画管理器
|
||||
android.util.Log.d("RoomDetail", "=== 初始化礼物动画管理器 ===");
|
||||
|
||||
// 从binding的根视图中查找
|
||||
View giftAnimationOverlay = binding.getRoot().findViewById(R.id.giftAnimationOverlay);
|
||||
android.util.Log.d("RoomDetail", "giftAnimationOverlay = " + giftAnimationOverlay);
|
||||
|
||||
if (giftAnimationOverlay != null) {
|
||||
giftAnimationManager = new GiftAnimationManager(this, giftAnimationOverlay);
|
||||
android.util.Log.d("RoomDetail", "✅ 礼物动画管理器初始化成功");
|
||||
} else {
|
||||
android.util.Log.e("RoomDetail", "❌ 找不到giftAnimationOverlay布局!");
|
||||
}
|
||||
|
||||
// 加载房间信息
|
||||
loadRoomInfo();
|
||||
|
||||
|
|
@ -452,11 +469,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 停止之前的重连任务
|
||||
stopChatReconnect();
|
||||
|
||||
String wsUrl = getWsChatBaseUrl() + roomId;
|
||||
android.util.Log.d("ChatWebSocket", "准备连接WebSocket: " + wsUrl);
|
||||
|
||||
chatWsClient = new OkHttpClient.Builder()
|
||||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
||||
.build();
|
||||
Request request = new Request.Builder()
|
||||
.url(getWsChatBaseUrl() + roomId)
|
||||
.url(wsUrl)
|
||||
.build();
|
||||
|
||||
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
||||
|
|
@ -472,9 +492,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
// 收到消息,解析并显示
|
||||
android.util.Log.d("ChatWebSocket", "收到消息: " + text);
|
||||
try {
|
||||
JSONObject json = new JSONObject(text);
|
||||
String type = json.optString("type", "");
|
||||
android.util.Log.d("ChatWebSocket", "消息类型: " + type);
|
||||
|
||||
if ("chat".equals(type)) {
|
||||
String nickname = json.optString("nickname", "匿名");
|
||||
|
|
@ -490,11 +512,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
String senderNickname = json.optString("senderNickname", "匿名");
|
||||
int totalPrice = json.optInt("totalPrice", 0);
|
||||
|
||||
android.util.Log.d("WebSocket", "收到礼物消息: " + json.toString());
|
||||
|
||||
handler.post(() -> {
|
||||
String giftMsg = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||
addChatMessage(new ChatMessage(giftMsg, true)); // 系统消息样式
|
||||
|
||||
// TODO: 显示礼物动画效果
|
||||
// 显示礼物动画效果
|
||||
android.util.Log.d("WebSocket", "准备显示礼物动画: " + giftName);
|
||||
showGiftAnimation(giftName, count, senderNickname);
|
||||
});
|
||||
} else if ("gift_combo".equals(type)) {
|
||||
|
|
@ -1189,7 +1214,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 防止重复显示连接消息
|
||||
private boolean hasShownConnectedMessage = false;
|
||||
|
||||
private void startHls(String url, @Nullable String altUrl) {
|
||||
@OptIn(markerClass = UnstableApi.class) private void startHls(String url, @Nullable String altUrl) {
|
||||
releaseIjkPlayer();
|
||||
if (binding != null) {
|
||||
binding.flvTextureView.setVisibility(View.GONE);
|
||||
|
|
@ -1566,6 +1591,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
* 从后端加载礼物列表
|
||||
*/
|
||||
private void loadGiftsFromBackend() {
|
||||
android.util.Log.d("RoomDetail", "=== 开始加载礼物列表 ===");
|
||||
|
||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||
Call<ApiResponse<List<GiftResponse>>> call = apiService.getGiftList();
|
||||
|
||||
|
|
@ -1575,32 +1602,41 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
Response<ApiResponse<List<GiftResponse>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<List<GiftResponse>> apiResponse = response.body();
|
||||
android.util.Log.d("RoomDetail", "礼物列表响应码: " + apiResponse.getCode());
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
availableGifts = new ArrayList<>();
|
||||
for (GiftResponse giftResponse : apiResponse.getData()) {
|
||||
// 使用URL构造函数,从服务器加载图标
|
||||
String iconUrl = giftResponse.getIconUrl();
|
||||
android.util.Log.d("RoomDetail", "礼物: " + giftResponse.getName() +
|
||||
", 价格: " + giftResponse.getPrice() +
|
||||
", iconUrl: " + iconUrl);
|
||||
|
||||
Gift gift = new Gift(
|
||||
String.valueOf(giftResponse.getId()),
|
||||
giftResponse.getName(),
|
||||
giftResponse.getPrice().intValue(),
|
||||
R.drawable.ic_gift_rose, // 默认图标,实际应从URL加载
|
||||
iconUrl, // ✅ 使用URL而不是硬编码
|
||||
giftResponse.getLevel() != null ? giftResponse.getLevel() : 1
|
||||
);
|
||||
availableGifts.add(gift);
|
||||
}
|
||||
android.util.Log.d("RoomDetail", "成功加载 " + availableGifts.size() + " 个礼物");
|
||||
android.util.Log.d("RoomDetail", "✅ 成功加载 " + availableGifts.size() + " 个礼物");
|
||||
android.util.Log.d("RoomDetail", "礼物列表: " + getGiftNames());
|
||||
} else {
|
||||
android.util.Log.w("RoomDetail", "加载礼物列表失败: " + apiResponse.getMessage());
|
||||
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败: " + apiResponse.getMessage());
|
||||
setDefaultGifts();
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("RoomDetail", "加载礼物列表失败");
|
||||
android.util.Log.w("RoomDetail", "❌ 加载礼物列表失败,响应码: " + response.code());
|
||||
setDefaultGifts();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<GiftResponse>>> call, Throwable t) {
|
||||
android.util.Log.e("RoomDetail", "加载礼物列表失败: " + t.getMessage());
|
||||
android.util.Log.e("RoomDetail", "❌ 加载礼物列表网络错误: " + t.getMessage(), t);
|
||||
setDefaultGifts();
|
||||
}
|
||||
});
|
||||
|
|
@ -1614,6 +1650,80 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 不再使用模拟数据,只从后端接口获取真实礼物数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载礼物图标到ImageView(支持PNG和SVG格式)
|
||||
* @param imageView 目标ImageView
|
||||
* @param gift 礼物对象
|
||||
*/
|
||||
private void loadGiftIcon(android.widget.ImageView imageView, Gift gift) {
|
||||
if (gift.hasIconUrl()) {
|
||||
String iconUrl = gift.getIconUrl();
|
||||
android.util.Log.d("RoomDetail", "加载礼物图标: " + gift.getName() + ", URL: " + iconUrl);
|
||||
|
||||
// 检查是否是SVG格式
|
||||
if (iconUrl.toLowerCase().endsWith(".svg") || iconUrl.toLowerCase().contains(".svg?")) {
|
||||
// 加载SVG格式
|
||||
com.bumptech.glide.RequestBuilder<android.graphics.drawable.PictureDrawable> requestBuilder =
|
||||
com.bumptech.glide.Glide.with(this)
|
||||
.as(android.graphics.drawable.PictureDrawable.class)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.PictureDrawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.PictureDrawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("RoomDetail", "SVG加载失败: " + gift.getName(), e);
|
||||
imageView.setImageResource(R.drawable.ic_gift_24);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.PictureDrawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.PictureDrawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("RoomDetail", "SVG加载成功: " + gift.getName());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
requestBuilder.load(iconUrl).into(new com.example.livestreaming.glide.SvgSoftwareLayerSetter(imageView));
|
||||
} else {
|
||||
// 加载PNG/JPG等格式
|
||||
com.bumptech.glide.Glide.with(this)
|
||||
.load(iconUrl)
|
||||
.placeholder(R.drawable.ic_gift_24)
|
||||
.error(R.drawable.ic_gift_24)
|
||||
.diskCacheStrategy(com.bumptech.glide.load.engine.DiskCacheStrategy.ALL)
|
||||
.listener(new com.bumptech.glide.request.RequestListener<android.graphics.drawable.Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.e("RoomDetail", "图片加载失败: " + gift.getName(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(android.graphics.drawable.Drawable resource,
|
||||
Object model,
|
||||
com.bumptech.glide.request.target.Target<android.graphics.drawable.Drawable> target,
|
||||
com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
android.util.Log.d("RoomDetail", "图片加载成功: " + gift.getName());
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(imageView);
|
||||
}
|
||||
} else {
|
||||
// 如果没有URL,使用本地资源
|
||||
imageView.setImageResource(gift.getIconResId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示礼物选择弹窗
|
||||
*/
|
||||
|
|
@ -1657,7 +1767,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 礼物选择监听
|
||||
giftAdapter.setOnGiftClickListener(gift -> {
|
||||
selectedGiftLayout.setVisibility(View.VISIBLE);
|
||||
selectedGiftIcon.setImageResource(gift.getIconResId());
|
||||
|
||||
// 使用辅助方法加载礼物图标
|
||||
loadGiftIcon(selectedGiftIcon, gift);
|
||||
|
||||
selectedGiftName.setText(gift.getName());
|
||||
selectedGiftPrice.setText(gift.getFormattedPrice());
|
||||
giftCount[0] = 1;
|
||||
|
|
@ -2003,14 +2116,34 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
// 获取主播用户ID
|
||||
Integer streamerId = room.getStreamerId();
|
||||
if (streamerId == null) {
|
||||
streamerId = room.getUid();
|
||||
}
|
||||
|
||||
if (streamerId == null) {
|
||||
Toast.makeText(this, "无法获取主播信息", Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "主播ID为空: roomId=" + roomId + ", room=" + room);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔍 详细日志
|
||||
android.util.Log.d("RoomDetail", "========================================");
|
||||
android.util.Log.d("RoomDetail", "📤 准备发送礼物请求");
|
||||
android.util.Log.d("RoomDetail", "roomId: " + roomId);
|
||||
android.util.Log.d("RoomDetail", "streamerId: " + streamerId);
|
||||
android.util.Log.d("RoomDetail", "giftId: " + selectedGift.getId());
|
||||
android.util.Log.d("RoomDetail", "count: " + count);
|
||||
android.util.Log.d("RoomDetail", "API URL: " + ApiClient.getCurrentBaseUrl(getApplicationContext()) + "api/front/live/rooms/" + roomId + "/gift");
|
||||
android.util.Log.d("RoomDetail", "========================================");
|
||||
|
||||
ApiService apiService = ApiClient.getService(getApplicationContext());
|
||||
|
||||
// 获取主播ID(使用房间ID作为主播ID,或者从房间信息中获取)
|
||||
Integer streamerId = Integer.parseInt(roomId);
|
||||
SendGiftRequest request = new SendGiftRequest(
|
||||
Integer.parseInt(selectedGift.getId()),
|
||||
streamerId,
|
||||
count
|
||||
streamerId, // receiverId - 接收者用户ID(主播ID)
|
||||
count // giftCount - 礼物数量
|
||||
);
|
||||
|
||||
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
|
||||
|
|
@ -2019,20 +2152,46 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
@Override
|
||||
public void onResponse(Call<ApiResponse<SendGiftResponse>> call,
|
||||
Response<ApiResponse<SendGiftResponse>> response) {
|
||||
android.util.Log.d("RoomDetail", "📥 收到响应: HTTP " + response.code());
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<SendGiftResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
SendGiftResponse giftResponse = apiResponse.getData();
|
||||
|
||||
// 更新余额
|
||||
userCoinBalance = giftResponse.getNewBalance().intValue();
|
||||
coinBalance.setText(String.valueOf(userCoinBalance));
|
||||
// 🔒 安全更新余额(添加空值检查)
|
||||
if (giftResponse.getNewBalance() != null) {
|
||||
userCoinBalance = giftResponse.getNewBalance().intValue();
|
||||
coinBalance.setText(String.valueOf(userCoinBalance));
|
||||
android.util.Log.d("RoomDetail", "✅ 余额更新成功: " + userCoinBalance);
|
||||
} else {
|
||||
android.util.Log.w("RoomDetail", "⚠️ newBalance 为 null,尝试重新加载余额");
|
||||
// 降级处理:重新加载余额
|
||||
loadUserBalance(coinBalance);
|
||||
}
|
||||
|
||||
// 在聊天区显示赠送消息
|
||||
String giftMessage = String.format("送出了 %d 个 %s", count, selectedGift.getName());
|
||||
addChatMessage(new ChatMessage("我", giftMessage, true));
|
||||
|
||||
Toast.makeText(RoomDetailActivity.this, "赠送成功!", Toast.LENGTH_SHORT).show();
|
||||
// ✨ 立即显示礼物特效动画
|
||||
String currentUserNickname = "我"; // 可以从用户信息中获取
|
||||
String currentUserAvatar = AuthStore.getAvatar(RoomDetailActivity.this); // 获取当前用户头像
|
||||
android.util.Log.d("RoomDetail", "🎁 礼物发送成功,显示特效动画");
|
||||
android.util.Log.d("RoomDetail", "礼物图标URL: " + selectedGift.getIconUrl());
|
||||
android.util.Log.d("RoomDetail", "用户头像URL: " + currentUserAvatar);
|
||||
|
||||
// 使用带头像的方法
|
||||
if (giftAnimationManager != null) {
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
selectedGift.getName(),
|
||||
selectedGift.getPrice(),
|
||||
count,
|
||||
currentUserNickname,
|
||||
selectedGift.getIconUrl(),
|
||||
currentUserAvatar
|
||||
);
|
||||
}
|
||||
|
||||
if (giftDialog != null) {
|
||||
giftDialog.dismiss();
|
||||
|
|
@ -2045,14 +2204,17 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
selectedGiftLayout.setVisibility(View.GONE);
|
||||
giftCountText.setText("1");
|
||||
} else {
|
||||
String errorMsg = apiResponse.getMessage() != null ? apiResponse.getMessage() : "未知错误";
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"赠送失败: " + apiResponse.getMessage(),
|
||||
"赠送失败: " + errorMsg,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "赠送礼物失败: " + errorMsg);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"赠送失败",
|
||||
"赠送失败: HTTP " + response.code(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "赠送礼物HTTP错误: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2061,6 +2223,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
Toast.makeText(RoomDetailActivity.this,
|
||||
"网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
android.util.Log.e("RoomDetail", "赠送礼物网络错误", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -2336,21 +2499,93 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
* @param senderNickname 赠送者昵称
|
||||
*/
|
||||
private void showGiftAnimation(String giftName, int count, String senderNickname) {
|
||||
// TODO: 实现礼物动画效果
|
||||
// 这里可以使用Lottie动画库或自定义动画
|
||||
// 示例:显示一个Toast提示
|
||||
runOnUiThread(() -> {
|
||||
String message = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
// 详细日志:诊断问题
|
||||
android.util.Log.d("GiftAnimation", "=== 开始显示礼物动画 ===");
|
||||
android.util.Log.d("GiftAnimation", "礼物名称: " + giftName);
|
||||
android.util.Log.d("GiftAnimation", "数量: " + count);
|
||||
android.util.Log.d("GiftAnimation", "赠送者: " + senderNickname);
|
||||
android.util.Log.d("GiftAnimation", "动画管理器状态: " + (giftAnimationManager != null ? "已初始化" : "未初始化"));
|
||||
android.util.Log.d("GiftAnimation", "礼物列表状态: " + (availableGifts != null ? "已加载(" + availableGifts.size() + "个)" : "未加载"));
|
||||
|
||||
// 可以在这里添加更复杂的动画效果,例如:
|
||||
// 1. 使用Lottie播放礼物动画
|
||||
// 2. 显示礼物图标飞行动画
|
||||
// 3. 播放音效
|
||||
// 4. 显示特效粒子
|
||||
// 检查动画管理器是否初始化
|
||||
if (giftAnimationManager == null) {
|
||||
android.util.Log.e("GiftAnimation", "❌ 礼物动画管理器未初始化!");
|
||||
String message = senderNickname + " 送出了 " + count + " 个 " + giftName;
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
android.util.Log.d("GiftAnimation", "显示礼物动画: " + message);
|
||||
// 检查礼物列表是否加载
|
||||
if (availableGifts == null || availableGifts.isEmpty()) {
|
||||
android.util.Log.w("GiftAnimation", "⚠️ 礼物列表未加载或为空,使用默认价格显示特效");
|
||||
// 使用默认价格显示特效(而不是降级到Toast)
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
giftName,
|
||||
100, // 默认价格:普通特效
|
||||
count,
|
||||
senderNickname,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找礼物信息
|
||||
Gift gift = null;
|
||||
for (Gift g : availableGifts) {
|
||||
if (g.getName().equals(giftName)) {
|
||||
gift = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (gift != null) {
|
||||
// 使用礼物动画管理器显示特效
|
||||
android.util.Log.d("GiftAnimation", "✅ 找到礼物信息: " + gift.getName() + ", 价格: " + gift.getPrice());
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
giftName,
|
||||
gift.getPrice(),
|
||||
count,
|
||||
senderNickname,
|
||||
gift.getIconUrl()
|
||||
);
|
||||
|
||||
android.util.Log.d("GiftAnimation",
|
||||
String.format("✅ 显示礼物动画: %s x%d (价格:%d) - %s",
|
||||
giftName, count, gift.getPrice(), senderNickname));
|
||||
} else {
|
||||
// 如果找不到礼物信息,使用默认价格显示特效
|
||||
android.util.Log.w("GiftAnimation", "⚠️ 未找到礼物信息: " + giftName);
|
||||
android.util.Log.w("GiftAnimation", "可用礼物列表: " + getGiftNames());
|
||||
|
||||
// 使用默认价格显示特效(而不是降级到Toast)
|
||||
giftAnimationManager.showGiftAnimation(
|
||||
giftName,
|
||||
100, // 默认价格:普通特效
|
||||
count,
|
||||
senderNickname,
|
||||
null
|
||||
);
|
||||
|
||||
android.util.Log.d("GiftAnimation", "✅ 使用默认价格显示特效");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取礼物名称列表(用于调试)
|
||||
*/
|
||||
private String getGiftNames() {
|
||||
if (availableGifts == null || availableGifts.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
for (int i = 0; i < availableGifts.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append(availableGifts.get(i).getName());
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Glide SVG模块 - 注册SVG支持
|
||||
*/
|
||||
@GlideModule
|
||||
public class GlideSvgModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
|
||||
@NonNull Registry registry) {
|
||||
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
|
||||
.append(InputStream.class, SVG.class, new SvgDecoder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isManifestParsingEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* SVG解码器 - 将InputStream解码为SVG对象
|
||||
*/
|
||||
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
|
||||
@NonNull Options options) throws IOException {
|
||||
try {
|
||||
SVG svg = SVG.getFromInputStream(source);
|
||||
if (width != Integer.MIN_VALUE) {
|
||||
svg.setDocumentWidth(width);
|
||||
}
|
||||
if (height != Integer.MIN_VALUE) {
|
||||
svg.setDocumentHeight(height);
|
||||
}
|
||||
return new SimpleResource<>(svg);
|
||||
} catch (SVGParseException ex) {
|
||||
throw new IOException("Cannot load SVG from stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import android.graphics.Picture;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
/**
|
||||
* SVG转换器 - 将SVG对象转换为PictureDrawable
|
||||
*/
|
||||
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
|
||||
@NonNull Options options) {
|
||||
SVG svg = toTranscode.get();
|
||||
Picture picture = svg.renderToPicture();
|
||||
PictureDrawable drawable = new PictureDrawable(picture);
|
||||
return new SimpleResource<>(drawable);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.example.livestreaming.glide;
|
||||
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
import android.widget.ImageView;
|
||||
import com.bumptech.glide.request.target.ImageViewTarget;
|
||||
|
||||
/**
|
||||
* SVG图层设置器 - 确保SVG正确渲染
|
||||
*/
|
||||
public class SvgSoftwareLayerSetter extends ImageViewTarget<PictureDrawable> {
|
||||
|
||||
public SvgSoftwareLayerSetter(ImageView view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setResource(PictureDrawable resource) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
view.setImageDrawable(resource);
|
||||
}
|
||||
}
|
||||
|
|
@ -327,6 +327,14 @@ public interface ApiService {
|
|||
@GET("api/front/category/statistics")
|
||||
Call<ApiResponse<Map<String, Object>>> getCategoryStatistics(@Query("type") int type);
|
||||
|
||||
// ==================== 直播类型接口 ====================
|
||||
|
||||
/**
|
||||
* 获取直播类型列表(游戏、才艺、户外、音乐、美食、聊天等)
|
||||
*/
|
||||
@GET("api/front/live/public/types")
|
||||
Call<ApiResponse<List<LiveTypeResponse>>> getLiveTypes();
|
||||
|
||||
// ==================== 关注接口 ====================
|
||||
|
||||
@GET("api/front/follow/followers")
|
||||
|
|
@ -381,6 +389,31 @@ public interface ApiService {
|
|||
@DELETE("api/front/works/{id}/collect")
|
||||
Call<ApiResponse<Boolean>> uncollectWork(@Path("id") long id);
|
||||
|
||||
/**
|
||||
* 获取指定用户的作品列表
|
||||
*/
|
||||
@GET("api/front/works/user/{userId}")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getUserWorks(
|
||||
@Path("userId") int userId,
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
/**
|
||||
* 获取我点赞的作品列表
|
||||
*/
|
||||
@GET("api/front/works/my/liked")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getMyLikedWorks(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
/**
|
||||
* 获取我收藏的作品列表
|
||||
*/
|
||||
@GET("api/front/works/my/collected")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getMyCollectedWorks(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
// ==================== 搜索接口 ====================
|
||||
|
||||
@GET("api/front/live/public/rooms/search")
|
||||
|
|
@ -653,6 +686,48 @@ public interface ApiService {
|
|||
@Query("page") int page,
|
||||
@Query("limit") int limit);
|
||||
|
||||
// ==================== 虚拟货币和充值接口 ====================
|
||||
|
||||
/**
|
||||
* 获取虚拟货币余额
|
||||
*/
|
||||
@GET("api/front/virtual/balance")
|
||||
Call<ApiResponse<Map<String, Object>>> getVirtualBalance();
|
||||
|
||||
/**
|
||||
* 获取充值套餐列表
|
||||
*/
|
||||
@GET("api/front/virtual/recharge/packages")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getRechargePackages();
|
||||
|
||||
/**
|
||||
* 创建充值订单
|
||||
*/
|
||||
@POST("api/front/virtual/recharge/order")
|
||||
Call<ApiResponse<Map<String, Object>>> createRechargeOrder(@Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 模拟支付成功(测试用)
|
||||
*/
|
||||
@POST("api/front/virtual/recharge/mock-pay")
|
||||
Call<ApiResponse<String>> mockPaySuccess(@Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 获取充值记录
|
||||
*/
|
||||
@GET("api/front/virtual/recharge/records")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getRechargeRecords(
|
||||
@Query("page") int page,
|
||||
@Query("limit") int limit);
|
||||
|
||||
/**
|
||||
* 获取消费记录
|
||||
*/
|
||||
@GET("api/front/virtual/consume/records")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getConsumeRecords(
|
||||
@Query("page") int page,
|
||||
@Query("limit") int limit);
|
||||
|
||||
// ==================== 用户活动记录接口 ====================
|
||||
|
||||
/**
|
||||
|
|
@ -756,4 +831,50 @@ public interface ApiService {
|
|||
*/
|
||||
@DELETE("api/front/search/history/{historyId}")
|
||||
Call<ApiResponse<String>> deleteSearchHistoryItem(@Path("historyId") long historyId);
|
||||
|
||||
// ==================== 封禁/黑名单接口 ====================
|
||||
|
||||
/**
|
||||
* 检查当前用户封禁状态
|
||||
*/
|
||||
@GET("api/front/ban/check/me")
|
||||
Call<ApiResponse<Map<String, Object>>> checkMyBanStatus();
|
||||
|
||||
/**
|
||||
* 检查指定用户封禁状态
|
||||
*/
|
||||
@GET("api/front/ban/check/user/{userId}")
|
||||
Call<ApiResponse<Map<String, Object>>> checkUserBanStatus(@Path("userId") int userId);
|
||||
|
||||
/**
|
||||
* 检查房间封禁状态
|
||||
*/
|
||||
@GET("api/front/ban/check/room/{roomId}")
|
||||
Call<ApiResponse<Map<String, Object>>> checkRoomBanStatus(@Path("roomId") int roomId);
|
||||
|
||||
/**
|
||||
* 获取我的黑名单列表
|
||||
*/
|
||||
@GET("api/front/ban/blacklist/list")
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> getMyBlacklist(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
/**
|
||||
* 添加用户到黑名单
|
||||
*/
|
||||
@POST("api/front/ban/blacklist/add")
|
||||
Call<ApiResponse<Map<String, Object>>> addToBlacklist(@Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 从黑名单移除用户
|
||||
*/
|
||||
@POST("api/front/ban/blacklist/remove")
|
||||
Call<ApiResponse<Map<String, Object>>> removeFromBlacklist(@Body Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 检查用户是否在黑名单中
|
||||
*/
|
||||
@GET("api/front/ban/blacklist/check/{targetUserId}")
|
||||
Call<ApiResponse<Map<String, Object>>> checkBlacklistStatus(@Path("targetUserId") int targetUserId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
package com.example.livestreaming.net
|
||||
|
||||
|
|
@ -48,28 +48,71 @@ public final class AuthStore {
|
|||
|
||||
private static final String KEY_USER_ID = "user_id";
|
||||
private static final String KEY_NICKNAME = "nickname";
|
||||
private static final String KEY_AVATAR = "avatar";
|
||||
private static final String KEY_IS_STREAMER = "is_streamer";
|
||||
private static final String KEY_STREAMER_MODE = "streamer_mode";
|
||||
|
||||
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) {
|
||||
setUserInfo(context, userId, nickname, null);
|
||||
}
|
||||
|
||||
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname, @Nullable String avatar) {
|
||||
if (context == null) return;
|
||||
Log.d(TAG, "setUserInfo: userId=" + userId + ", nickname=" + nickname);
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_USER_ID, userId)
|
||||
.putString(KEY_NICKNAME, nickname)
|
||||
.apply();
|
||||
|
||||
// 清理和验证 userId
|
||||
String cleanUserId = null;
|
||||
if (userId != null) {
|
||||
cleanUserId = userId.trim();
|
||||
// 如果是 "null" 字符串或空字符串,设置为 null
|
||||
if (cleanUserId.isEmpty() || "null".equalsIgnoreCase(cleanUserId)) {
|
||||
Log.w(TAG, "setUserInfo: invalid userId value: '" + userId + "', will not save");
|
||||
cleanUserId = null;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "setUserInfo: userId=" + cleanUserId + ", nickname=" + nickname + ", avatar=" + avatar);
|
||||
|
||||
android.content.SharedPreferences.Editor editor = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit();
|
||||
|
||||
if (cleanUserId != null) {
|
||||
editor.putString(KEY_USER_ID, cleanUserId);
|
||||
} else {
|
||||
editor.remove(KEY_USER_ID);
|
||||
}
|
||||
|
||||
if (nickname != null && !nickname.trim().isEmpty()) {
|
||||
editor.putString(KEY_NICKNAME, nickname.trim());
|
||||
} else {
|
||||
editor.remove(KEY_NICKNAME);
|
||||
}
|
||||
|
||||
if (avatar != null && !avatar.trim().isEmpty()) {
|
||||
editor.putString(KEY_AVATAR, avatar.trim());
|
||||
} else {
|
||||
editor.remove(KEY_AVATAR);
|
||||
}
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getUserId(Context context) {
|
||||
if (context == null) return null;
|
||||
if (context == null) {
|
||||
Log.w(TAG, "getUserId: context is null");
|
||||
return null;
|
||||
}
|
||||
String userId = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString(KEY_USER_ID, null);
|
||||
// 确保空字符串也返回 null
|
||||
if (userId != null && userId.trim().isEmpty()) {
|
||||
userId = null;
|
||||
|
||||
// 确保空字符串、"null" 字符串也返回 null
|
||||
if (userId != null) {
|
||||
userId = userId.trim();
|
||||
if (userId.isEmpty() || "null".equalsIgnoreCase(userId)) {
|
||||
Log.w(TAG, "getUserId: invalid userId value: '" + userId + "', returning null");
|
||||
userId = null;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "getUserId: " + userId);
|
||||
return userId;
|
||||
}
|
||||
|
|
@ -81,6 +124,13 @@ public final class AuthStore {
|
|||
.getString(KEY_NICKNAME, null);
|
||||
return nickname != null ? nickname : "用户";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getAvatar(Context context) {
|
||||
if (context == null) return null;
|
||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString(KEY_AVATAR, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户是否是认证主播
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* 直播类型响应对象
|
||||
*/
|
||||
public class LiveTypeResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
private Integer id;
|
||||
|
||||
@SerializedName("name")
|
||||
private String name;
|
||||
|
||||
@SerializedName("code")
|
||||
private String code;
|
||||
|
||||
@SerializedName("icon")
|
||||
private String icon;
|
||||
|
||||
@SerializedName("description")
|
||||
private String description;
|
||||
|
||||
@SerializedName("sort")
|
||||
private Integer sort;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Integer getSort() {
|
||||
return sort;
|
||||
}
|
||||
|
||||
public void setSort(Integer sort) {
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LiveTypeResponse{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", code='" + code + '\'' +
|
||||
", icon='" + icon + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", sort=" + sort +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,20 @@ public class LoginResponse {
|
|||
}
|
||||
|
||||
public String getUid() {
|
||||
return uid != null ? String.valueOf(uid) : null;
|
||||
if (uid == null) {
|
||||
return null;
|
||||
}
|
||||
// 如果 uid 是数字类型,直接转换
|
||||
if (uid instanceof Number) {
|
||||
return String.valueOf(((Number) uid).intValue());
|
||||
}
|
||||
// 如果是字符串类型,返回字符串
|
||||
String uidStr = String.valueOf(uid);
|
||||
// 避免返回 "null" 字符串
|
||||
if ("null".equalsIgnoreCase(uidStr) || uidStr.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return uidStr;
|
||||
}
|
||||
|
||||
public String getNikeName() {
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ public class SendGiftRequest {
|
|||
@SerializedName("giftId")
|
||||
private Integer giftId;
|
||||
|
||||
@SerializedName("streamerId")
|
||||
private Integer streamerId;
|
||||
@SerializedName("giftCount") // 后端期望的字段名是giftCount,不是count
|
||||
private Integer giftCount;
|
||||
|
||||
@SerializedName("count")
|
||||
private Integer count;
|
||||
@SerializedName("receiverId") // 后端期望的字段名是receiverId,不是streamerId
|
||||
private Integer receiverId;
|
||||
|
||||
public SendGiftRequest(Integer giftId, Integer streamerId, Integer count) {
|
||||
public SendGiftRequest(Integer giftId, Integer receiverId, Integer giftCount) {
|
||||
this.giftId = giftId;
|
||||
this.streamerId = streamerId;
|
||||
this.count = count;
|
||||
this.receiverId = receiverId;
|
||||
this.giftCount = giftCount;
|
||||
}
|
||||
|
||||
public Integer getGiftId() { return giftId; }
|
||||
public Integer getStreamerId() { return streamerId; }
|
||||
public Integer getCount() { return count; }
|
||||
public Integer getReceiverId() { return receiverId; }
|
||||
public Integer getGiftCount() { return giftCount; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#CCCCCC" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 紫粉渐变,参考主流直播平台风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从深紫到粉紫 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E6663399"
|
||||
android:centerColor="#E68B4789"
|
||||
android:endColor="#E6B565A7"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:金色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFD700" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 蓝色渐变,清新风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从深蓝到浅蓝 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E61E3A8A"
|
||||
android:centerColor="#E63B5998"
|
||||
android:endColor="#E65B7BB4"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:蓝色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#8087CEEB" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 金色渐变,奢华风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从深金到亮金 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E6B8860B"
|
||||
android:centerColor="#E6DAA520"
|
||||
android:endColor="#E6FFD700"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:白色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFFFFF" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物卡片渐变背景 - 橙红渐变,热情风格 -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<!-- 渐变背景:从橙色到红色 -->
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#E6FF6B35"
|
||||
android:centerColor="#E6FF5252"
|
||||
android:endColor="#E6FF4081"
|
||||
android:type="linear" />
|
||||
|
||||
<!-- 圆角 -->
|
||||
<corners android:radius="12dp" />
|
||||
|
||||
<!-- 边框:金色光晕效果 -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#80FFD700" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:startColor="#FF6B9D"
|
||||
android:endColor="#C06C84"
|
||||
android:type="linear" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
10
android-app/app/src/main/res/drawable/ic_back_24.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_back_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#333333"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,17 +6,11 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#2196F3"
|
||||
android:pathData="M8,20l4-8h24l4,8v12H8z"/>
|
||||
<path
|
||||
android:fillColor="#1976D2"
|
||||
android:pathData="M12,12h8v8h-8z"/>
|
||||
<path
|
||||
android:fillColor="#1976D2"
|
||||
android:pathData="M28,12h8v8h-8z"/>
|
||||
android:pathData="M8,24L12,16L36,16L40,24L40,32L8,32L8,24Z" />
|
||||
<path
|
||||
android:fillColor="#424242"
|
||||
android:pathData="M14,28a4,4 0,1,1 0,8 4,4 0,1,1 0,-8z"/>
|
||||
android:pathData="M12,28C13.1,28 14,28.9 14,30C14,31.1 13.1,32 12,32C10.9,32 10,31.1 10,30C10,28.9 10.9,28 12,28Z" />
|
||||
<path
|
||||
android:fillColor="#424242"
|
||||
android:pathData="M34,28a4,4 0,1,1 0,8 4,4 0,1,1 0,-8z"/>
|
||||
android:pathData="M36,28C37.1,28 38,28.9 38,30C38,31.1 37.1,32 36,32C34.9,32 34,31.1 34,30C34,28.9 34.9,28 36,28Z" />
|
||||
</vector>
|
||||
|
|
|
|||
13
android-app/app/src/main/res/drawable/ic_gift_castle.xml
Normal file
13
android-app/app/src/main/res/drawable/ic_gift_castle.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#9C27B0"
|
||||
android:pathData="M8,16L8,36L40,36L40,16L36,16L36,12L32,12L32,16L28,16L28,12L24,12L24,16L20,16L20,12L16,12L16,16L12,16L12,12L8,12L8,16Z" />
|
||||
<path
|
||||
android:fillColor="#7B1FA2"
|
||||
android:pathData="M20,24L20,36L28,36L28,24L20,24Z" />
|
||||
</vector>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,17 +6,8 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M8,36h32v4H8z"/>
|
||||
android:pathData="M8,32L8,36L40,36L40,32L35,20L30,26L24,16L18,26L13,20L8,32Z" />
|
||||
<path
|
||||
android:fillColor="#FFC107"
|
||||
android:pathData="M6,16l6,12h24l6-12-8,4-4-8-4,8-4-8-4,8-4-8z"/>
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M24,10a2,2 0,1,1 0,4 2,2 0,1,1 0,-4z"/>
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M12,14a2,2 0,1,1 0,4 2,2 0,1,1 0,-4z"/>
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M36,14a2,2 0,1,1 0,4 2,2 0,1,1 0,-4z"/>
|
||||
android:fillColor="#FFA000"
|
||||
android:pathData="M24,12C25.1,12 26,12.9 26,14C26,15.1 25.1,16 24,16C22.9,16 22,15.1 22,14C22,12.9 22.9,12 24,12Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#F44336"
|
||||
android:pathData="M24,40l-2.55-2.32C12.4,29.8 6,24.22 6,17.5 6,12.42 10.02,8.4 15.1,8.4c2.83,0 5.55,1.31 7.4,3.38 1.85-2.07 4.57-3.38 7.4-3.38C35.98,8.4 40,12.42 40,17.5c0,6.72-6.4,12.3-15.45,20.18L24,40z"/>
|
||||
android:fillColor="#FF4444"
|
||||
android:pathData="M24,42L20,38C10,29 4,23.5 4,17C4,12 7.5,8.5 12,8.5C15,8.5 18,10 20,12.5C22,10 25,8.5 28,8.5C32.5,8.5 36,12 36,17C36,23.5 30,29 20,38L24,42Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,11 +6,11 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#FF5722"
|
||||
android:pathData="M24,4c-4,0-8,4-10,10l-4,8 4,2 2-4c0,4 2,8 4,10v8l4,4 4-4v-8c2-2 4-6 4-10l2,4 4-2-4-8c-2-6-6-10-10-10z"/>
|
||||
android:pathData="M24,4L20,12L16,20L20,20L20,28L28,28L28,20L32,20L28,12L24,4Z" />
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M24,15a3,3 0,1,1 0,6 3,3 0,1,1 0,-6z"/>
|
||||
android:fillColor="#FFC107"
|
||||
android:pathData="M18,28L18,36L22,40L22,28L18,28Z" />
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M20,38l-2,6 2-2 4,2 4-2 2,2-2-6z"/>
|
||||
android:fillColor="#FFC107"
|
||||
android:pathData="M30,28L30,36L26,40L26,28L30,28Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
|
|
@ -5,11 +6,11 @@
|
|||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#E91E63"
|
||||
android:pathData="M24,8c-3.31,0-6,2.69-6,6 0,1.66 0.68,3.16 1.77,4.24L24,22.48l4.23-4.24C29.32,17.16 30,15.66 30,14c0-3.31-2.69-6-6-6z"/>
|
||||
android:pathData="M24,8C20,8 17,11 17,15C17,19 20,22 24,22C28,22 31,19 31,15C31,11 28,8 24,8Z" />
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M24,22.48l-4.23,4.24c-0.39,0.39-0.39,1.02 0,1.41l0,0c0.39,0.39 1.02,0.39 1.41,0L24,25.31l2.82,2.82c0.39,0.39 1.02,0.39 1.41,0l0,0c0.39-0.39 0.39-1.02 0-1.41L24,22.48z"/>
|
||||
android:pathData="M22,22L22,40L26,40L26,22Z" />
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M22,25v15h4V25z"/>
|
||||
android:fillColor="#8BC34A"
|
||||
android:pathData="M22,28C18,28 15,30 15,32C15,34 18,36 22,36L22,28Z" />
|
||||
</vector>
|
||||
|
|
|
|||
16
android-app/app/src/main/res/drawable/ic_gift_yacht.xml
Normal file
16
android-app/app/src/main/res/drawable/ic_gift_yacht.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#03A9F4"
|
||||
android:pathData="M8,28L12,20L36,20L40,28L8,28Z" />
|
||||
<path
|
||||
android:fillColor="#0288D1"
|
||||
android:pathData="M6,28L6,32L42,32L42,28L6,28Z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M24,8L24,20L28,20L24,8Z" />
|
||||
</vector>
|
||||
119
android-app/app/src/main/res/layout/activity_recharge.xml
Normal file
119
android-app/app/src/main/res/layout/activity_recharge.xml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<!-- 顶部标题栏 -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@color/colorPrimary"
|
||||
android:elevation="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_back_24"
|
||||
android:contentDescription="返回" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="充值"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</RelativeLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 充值套餐标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择充值套餐"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- 充值套餐列表 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_packages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
<!-- 支付方式标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择支付方式"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_alipay"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="支付宝"
|
||||
android:textSize="16sp"
|
||||
app:cornerRadius="8dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_wechat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="微信支付"
|
||||
android:textSize="16sp"
|
||||
app:cornerRadius="8dp"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- 底部确认按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_margin="16dp"
|
||||
android:text="确认充值"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:enabled="false"
|
||||
app:cornerRadius="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -363,4 +363,14 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 礼物特效覆盖层 - 显示在发送按钮上方 -->
|
||||
<include
|
||||
android:id="@+id/giftAnimationOverlay"
|
||||
layout="@layout/gift_animation_overlay"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/typeLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
|
|
@ -35,7 +36,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="选择直播类型"
|
||||
android:inputType="none" />
|
||||
android:inputType="none"
|
||||
android:editable="false" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
146
android-app/app/src/main/res/layout/gift_animation_overlay.xml
Normal file
146
android-app/app/src/main/res/layout/gift_animation_overlay.xml
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 礼物特效覆盖层 - 显示在发送按钮上方 -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/giftAnimationContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<!-- 礼物特效主容器 -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/giftEffectLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:visibility="gone"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<!-- 左侧礼物图标和信息 - 无背景版本 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/giftInfoCard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<ImageView
|
||||
android:id="@+id/senderAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_user_24"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<!-- 礼物图标 -->
|
||||
<ImageView
|
||||
android:id="@+id/giftIcon"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<!-- 礼物信息 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/senderName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="用户名"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="8" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftAction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="送出了"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="8" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="礼物名称"
|
||||
android:textColor="#FFD700"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="8" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 连击数 -->
|
||||
<TextView
|
||||
android:id="@+id/comboCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="x1"
|
||||
android:textColor="#FF4444"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="10" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 粒子特效容器 - 用于显示飘散的粒子 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/particleContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- 全屏特效容器 - 用于高级礼物的全屏动画 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/fullscreenEffectContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 标签 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/bg_hot_badge"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:text="热门"
|
||||
android:textSize="10sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 金额 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="100"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/colorPrimary" />
|
||||
|
||||
<!-- 虚拟币单位 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_amount"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="虚拟币"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#999999"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<!-- 价格 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_price"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="¥10.00"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
<resources>
|
||||
<color name="colorPrimary">#FF6B9D</color>
|
||||
<color name="colorPrimaryDark">#C06C84</color>
|
||||
<color name="colorAccent">#FF6B9D</color>
|
||||
|
||||
<color name="purple_500">#6200EE</color>
|
||||
<color name="purple_700">#3700B3</color>
|
||||
<color name="teal_200">#03DAC5</color>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
plugins {
|
||||
id("com.android.application") version "8.3.0" apply false
|
||||
id("com.android.application") version "8.1.2" apply false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.7-bin.zip
|
||||
distributionUrl=file:///D:/soft/gradle-8.1-bin.zip
|
||||
networkTimeout=600000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
25
测试关注插入.sql
25
测试关注插入.sql
|
|
@ -1,25 +0,0 @@
|
|||
USE zhibo;
|
||||
|
||||
-- 测试手动插入一条关注记录
|
||||
INSERT INTO eb_follow_record (
|
||||
follower_id,
|
||||
follower_nickname,
|
||||
follower_phone,
|
||||
followed_id,
|
||||
followed_nickname,
|
||||
followed_phone,
|
||||
follow_status,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
120, -- 关注者ID(你的测试用户)
|
||||
'测试用户',
|
||||
'18888888888',
|
||||
44, -- 被关注者ID(主播)
|
||||
'主播',
|
||||
'15637617378',
|
||||
'关注', -- 明确设置为'关注'
|
||||
0
|
||||
);
|
||||
|
||||
-- 查看刚插入的记录
|
||||
SELECT * FROM eb_follow_record WHERE follower_id = 120 AND followed_id = 44 ORDER BY create_time DESC LIMIT 1;
|
||||
Loading…
Reference in New Issue
Block a user