Compare commits

...

5 Commits

Author SHA1 Message Date
xiao12feng8
b97c82899f Merge branch 'master' of http://115.190.64.57:8000/xiaozhang/zhibo 2025-12-30 11:13:09 +08:00
xiao12feng8
e83568f5fb bug:修复消息不能撤回 2025-12-30 11:11:07 +08:00
xiao12feng8
7d1896d9d2 bug:修复消息不能正确判断位置 2025-12-30 10:10:35 +08:00
xiao12feng8
72e90f4e0e Merge remote-tracking branch 'origin/master' - resolve ApiService conflict 2025-12-30 09:42:28 +08:00
xiao12feng8
5b0c786f1f 修复app的bug 2025-12-30 09:31:15 +08:00
23 changed files with 1248 additions and 237 deletions

167
0-待完成接入接口.md Normal file
View File

@ -0,0 +1,167 @@
# 待完成接入接口清单
> 更新时间: 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个接口
- 核心功能已基本完成,待接入的主要是群组和辅助功能

View File

@ -10,7 +10,27 @@ export function sessionDetailApi(id) {
return request({ url: '/admin/session/detail/' + id, method: 'get' }) return request({ url: '/admin/session/detail/' + id, method: 'get' })
} }
// 会话消息列表
export function sessionMessagesApi(conversationId, params) {
return request({ url: '/admin/session/' + conversationId + '/messages', method: 'get', params })
}
// 删除会话 // 删除会话
export function sessionDeleteApi(id) { export function sessionDeleteApi(id) {
return request({ url: '/admin/session/delete/' + id, method: 'post' }) return request({ url: '/admin/session/delete/' + id, method: 'post' })
} }
// 删除消息
export function sessionMessageDeleteApi(messageId) {
return request({ url: '/admin/session/message/delete/' + messageId, method: 'post' })
}
// 撤回消息
export function sessionMessageRecallApi(messageId) {
return request({ url: '/admin/session/message/recall/' + messageId, method: 'post' })
}
// 会话统计
export function sessionStatisticsApi() {
return request({ url: '/admin/session/statistics', method: 'get' })
}

View File

@ -1,164 +1,602 @@
<template> <template>
<div class="divBox"> <div class="divBox relative">
<el-card shadow="never" class="ivu-mt"> <el-card :bordered="false" shadow="never" class="ivu-mt" :body-style="{ padding: 0 }">
<div class="padding-add"> <div class="padding-add">
<!-- 搜索表单 --> <el-form inline size="small" :model="queryForm" ref="queryForm" label-width="90px">
<el-form :inline="true" :model="searchForm" size="small" class="mb20"> <div class="acea-row search-form row-between">
<el-form-item label="发送方昵称"> <div class="search-form-box">
<el-input v-model="searchForm.senderNickname" placeholder="请输入发送方昵称" clearable class="selWidth" /> <el-form-item label="发送方昵称:">
<el-input v-model="queryForm.senderNickname" placeholder="请输入发送方昵称" clearable class="selWidth" />
</el-form-item> </el-form-item>
<el-form-item label="发送方电话"> <el-form-item label="发送方电话">
<el-input v-model="searchForm.senderPhone" placeholder="请输入发送方电话" clearable class="selWidth" /> <el-input v-model="queryForm.senderPhone" placeholder="请输入发送方电话" clearable class="selWidth" />
</el-form-item> </el-form-item>
<el-form-item label="分类时间"> <el-form-item label="时间选择:">
<el-date-picker <el-date-picker
v-model="searchForm.startTime" v-model="dateRange"
type="datetime" align="right"
placeholder="开始时间" unlink-panels
value-format="yyyy-MM-dd HH:mm:ss" value-format="yyyy-MM-dd HH:mm:ss"
format="yyyy-MM-dd"
size="small"
type="daterange"
placeholder="选择时间范围"
class="selWidth" class="selWidth"
@change="onDateChange"
start-placeholder="开始时间"
end-placeholder="结束时间"
/> />
</el-form-item> </el-form-item>
<el-form-item label="起止时间"> </div>
<el-date-picker <el-form-item class="search-form-sub">
v-model="searchForm.endTime" <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
type="datetime" <el-button @click="handleReset" size="small">重置</el-button>
placeholder="结束时间"
value-format="yyyy-MM-dd HH:mm:ss"
class="selWidth"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item> </el-form-item>
</div>
</el-form> </el-form>
</div>
</el-card>
<!-- 数据表格 --> <el-card class="box-card mt14">
<el-table v-loading="loading" :data="tableData" border size="small"> <div slot="header" class="clearfix">
<el-table-column prop="id" label="id" width="80" align="center" /> <span class="card-title">会话管理</span>
<el-table-column label="发送者头像" width="100" align="center"> <div class="header-right">
<template slot-scope="scope"> <el-tag type="info">总会话数{{ statistics.totalConversations || 0 }}</el-tag>
<el-image <el-tag type="success" class="ml10">总消息数{{ statistics.totalMessages || 0 }}</el-tag>
v-if="scope.row.sender_avatar" <el-tag type="warning" class="ml10">今日消息{{ statistics.todayMessages || 0 }}</el-tag>
:src="scope.row.sender_avatar" </div>
:preview-src-list="[scope.row.sender_avatar]" </div>
style="width: 50px; height: 50px; cursor: pointer"
fit="cover" <el-table
/> ref="table"
v-loading="loading"
:data="tableData"
style="width: 100%"
size="mini"
highlight-current-row
>
<el-table-column type="expand">
<template slot-scope="props">
<div class="message-preview" v-loading="props.row.messagesLoading">
<div class="message-header">
<span>最近消息记录</span>
<el-button type="text" size="mini" @click="loadMessages(props.row)">刷新</el-button>
</div>
<div class="message-list" v-if="props.row.messages && props.row.messages.length">
<div
class="message-item"
v-for="msg in props.row.messages"
:key="msg.id"
:class="{ 'message-right': msg.senderId === props.row.user1_id }"
>
<div class="message-sender">{{ msg.senderName || '用户' + msg.senderId }}</div>
<div class="message-content">{{ msg.content }}</div>
<div class="message-time">{{ msg.createTime }}</div>
</div>
</div>
<el-empty v-else description="暂无消息记录" :image-size="60"></el-empty>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="sender_nickname" label="发送者昵称" width="120" align="center" /> <el-table-column prop="id" label="会话ID" width="80" />
<el-table-column prop="sender_phone" label="发送者电话" width="130" align="center" /> <el-table-column label="发送者" min-width="150">
<el-table-column label="对方头像" width="100" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-image <div class="user-info">
v-if="scope.row.receiver_avatar" <el-avatar :size="32" :src="scope.row.sender_avatar" icon="el-icon-user"></el-avatar>
:src="scope.row.receiver_avatar" <div class="user-detail">
:preview-src-list="[scope.row.receiver_avatar]" <span class="user-name">{{ scope.row.sender_nickname || '用户' + scope.row.user1_id }}</span>
style="width: 50px; height: 50px; cursor: pointer" <span class="user-id">{{ scope.row.sender_phone || 'ID: ' + scope.row.user1_id }}</span>
fit="cover" </div>
/> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="receiver_nickname" label="对方昵称" width="120" align="center" /> <el-table-column label="对方" min-width="150">
<el-table-column prop="receiver_phone" label="对方昵称电话" width="130" align="center" />
<el-table-column prop="create_time" label="创建时间" width="160" align="center" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="small" @click="handleDelete(scope.row.id)">删除</el-button> <div class="user-info">
<el-avatar :size="32" :src="scope.row.receiver_avatar" icon="el-icon-user"></el-avatar>
<div class="user-detail">
<span class="user-name">{{ scope.row.receiver_nickname || '用户' + scope.row.user2_id }}</span>
<span class="user-id">{{ scope.row.receiver_phone || 'ID: ' + scope.row.user2_id }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="last_message" label="最后消息" min-width="200" show-overflow-tooltip />
<el-table-column prop="last_message_time" label="最后消息时间" width="160" />
<el-table-column label="消息数" width="80">
<template slot-scope="scope">
<el-tag size="mini">{{ scope.row.message_count || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="viewMessages(scope.row)">查看</el-button>
<el-divider direction="vertical"></el-divider>
<el-popconfirm title="确定删除该会话及所有消息吗?" @confirm="deleteConversation(scope.row)">
<el-button slot="reference" type="text" size="mini" class="text-danger">删除</el-button>
</el-popconfirm>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <div class="block">
<div class="acea-row row-center page mt20">
<span class="mr10"> {{ total }} </span>
<el-pagination <el-pagination
:current-page="page"
:page-sizes="[10, 20, 50, 100]" :page-sizes="[10, 20, 50, 100]"
:page-size="limit" :page-size="queryForm.limit"
:current-page="queryForm.page"
layout="total, sizes, prev, pager, next, jumper"
:total="total" :total="total"
layout="prev, pager, next, sizes, jumper"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handlePageChange"
background
/> />
</div> </div>
</div>
</el-card> </el-card>
<!-- 消息详情对话框 -->
<el-dialog
:title="'会话详情 - ' + (currentConversation ? currentConversation.id : '')"
:visible.sync="dialogVisible"
width="700px"
top="5vh"
>
<div class="dialog-users" v-if="currentConversation">
<div class="dialog-user">
<el-avatar :size="48" :src="currentConversation.sender_avatar" icon="el-icon-user"></el-avatar>
<div>
<div class="user-name">{{ currentConversation.sender_nickname || '用户' + currentConversation.user1_id }}</div>
<div class="user-id">{{ currentConversation.sender_phone || 'ID: ' + currentConversation.user1_id }}</div>
</div>
</div>
<div class="dialog-arrow">
<i class="el-icon-sort"></i>
</div>
<div class="dialog-user">
<el-avatar :size="48" :src="currentConversation.receiver_avatar" icon="el-icon-user"></el-avatar>
<div>
<div class="user-name">{{ currentConversation.receiver_nickname || '用户' + currentConversation.user2_id }}</div>
<div class="user-id">{{ currentConversation.receiver_phone || 'ID: ' + currentConversation.user2_id }}</div>
</div>
</div>
</div>
<div class="dialog-messages" v-loading="messagesLoading">
<div
class="dialog-message"
v-for="msg in currentMessages"
:key="msg.id"
:class="{ 'message-from-user1': currentConversation && msg.senderId === currentConversation.user1_id }"
>
<div class="msg-avatar">
<el-avatar
:size="36"
:src="getMessageAvatar(msg)"
icon="el-icon-user"
></el-avatar>
</div>
<div class="msg-body">
<div class="msg-header">
<span class="msg-sender">{{ msg.senderName || '用户' + msg.senderId }}</span>
<el-tag v-if="msg.isRecalled" type="warning" size="mini" class="recalled-tag">已撤回</el-tag>
<span class="msg-time">{{ msg.createTime }}</span>
<el-popconfirm
v-if="!msg.isRecalled"
title="确定要撤回这条消息吗?"
@confirm="recallMessage(msg)"
class="msg-recall"
>
<el-button slot="reference" type="text" size="mini" class="recall-btn">撤回</el-button>
</el-popconfirm>
</div>
<!-- 如果消息已撤回显示原始内容 -->
<div v-if="msg.isRecalled && msg.originalContent" class="msg-content recalled-content">
<div class="original-label">原始内容</div>
<div class="original-text">{{ msg.originalContent }}</div>
</div>
<div v-else class="msg-content">{{ msg.content }}</div>
</div>
</div>
<el-empty v-if="!messagesLoading && currentMessages.length === 0" description="暂无消息记录"></el-empty>
</div>
<div class="dialog-pagination" v-if="messageTotal > messagePageSize">
<el-pagination
small
layout="prev, pager, next"
:total="messageTotal"
:page-size="messagePageSize"
:current-page="messagePage"
@current-change="handleMessagePageChange"
/>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { sessionListApi, sessionDeleteApi } from '@/api/session'; import { sessionListApi, sessionMessagesApi, sessionDeleteApi, sessionStatisticsApi, sessionMessageRecallApi } from '@/api/session';
export default { export default {
name: 'SessionList', name: 'SessionList',
data() { data() {
return { return {
loading: false, loading: false,
searchForm: { tableData: [],
total: 0,
queryForm: {
senderNickname: '', senderNickname: '',
senderPhone: '', senderPhone: '',
startTime: '', startTime: '',
endTime: '', endTime: '',
},
tableData: [],
page: 1, page: 1,
limit: 10, limit: 10,
total: 0, },
dateRange: [],
statistics: {
totalConversations: 0,
totalMessages: 0,
todayMessages: 0,
},
dialogVisible: false,
currentConversation: null,
currentMessages: [],
messagesLoading: false,
messagePage: 1,
messagePageSize: 20,
messageTotal: 0,
}; };
}, },
mounted() { mounted() {
this.getList(); this.getList();
this.getStatistics();
}, },
methods: { methods: {
getList() { async getList() {
this.loading = true; this.loading = true;
sessionListApi({ try {
...this.searchForm, const res = await sessionListApi(this.queryForm);
page: this.page, this.tableData = (res.list || []).map(item => ({
limit: this.limit ...item,
}) messages: [],
.then((res) => { messagesLoading: false,
this.tableData = res.data.list || []; }));
this.total = res.data.total || 0; this.total = res.total || 0;
this.loading = false; } catch (error) {
}) console.error('获取会话列表失败:', error);
.catch(() => { this.$message.error('获取会话列表失败');
} finally {
this.loading = false; this.loading = false;
}
},
async getStatistics() {
try {
const res = await sessionStatisticsApi();
this.statistics = res || {};
} catch (error) {
console.error('获取统计数据失败:', error);
}
},
async loadMessages(row) {
row.messagesLoading = true;
try {
const res = await sessionMessagesApi(row.id, { page: 1, pageSize: 5 });
row.messages = res.list || [];
} catch (error) {
console.error('加载消息失败:', error);
} finally {
row.messagesLoading = false;
}
},
async viewMessages(row) {
this.currentConversation = row;
this.dialogVisible = true;
this.messagePage = 1;
await this.loadDialogMessages();
},
async loadDialogMessages() {
if (!this.currentConversation) return;
this.messagesLoading = true;
try {
const res = await sessionMessagesApi(this.currentConversation.id, {
page: this.messagePage,
pageSize: this.messagePageSize,
}); });
this.currentMessages = res.list || [];
this.messageTotal = res.total || 0;
} catch (error) {
console.error('加载消息失败:', error);
this.$message.error('加载消息失败');
} finally {
this.messagesLoading = false;
}
}, },
handleSearch() { async deleteConversation(row) {
this.page = 1; try {
this.getList(); await sessionDeleteApi(row.id);
},
handleSizeChange(val) {
this.limit = val;
this.page = 1;
this.getList();
},
handleCurrentChange(val) {
this.page = val;
this.getList();
},
handleDelete(id) {
this.$confirm('确定要删除该会话吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
sessionDeleteApi(id).then(() => {
this.$message.success('删除成功'); this.$message.success('删除成功');
this.getList(); this.getList();
}); this.getStatistics();
}); } catch (error) {
console.error('删除会话失败:', error);
this.$message.error('删除失败');
}
},
async recallMessage(msg) {
try {
await sessionMessageRecallApi(msg.id);
this.$message.success('撤回成功');
//
await this.loadDialogMessages();
} catch (error) {
console.error('撤回消息失败:', error);
this.$message.error('撤回失败');
}
},
handleSearch() {
this.queryForm.page = 1;
this.getList();
},
handleReset() {
this.queryForm = {
senderNickname: '',
senderPhone: '',
startTime: '',
endTime: '',
page: 1,
limit: 10,
};
this.dateRange = [];
this.getList();
},
onDateChange(val) {
if (val && val.length === 2) {
this.queryForm.startTime = val[0];
this.queryForm.endTime = val[1];
} else {
this.queryForm.startTime = '';
this.queryForm.endTime = '';
}
},
handleSizeChange(val) {
this.queryForm.limit = val;
this.getList();
},
handlePageChange(val) {
this.queryForm.page = val;
this.getList();
},
handleMessagePageChange(val) {
this.messagePage = val;
this.loadDialogMessages();
},
getMessageAvatar(msg) {
if (!this.currentConversation) return '';
if (msg.senderId === this.currentConversation.user1_id) {
return this.currentConversation.sender_avatar || '';
}
return this.currentConversation.receiver_avatar || '';
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
.padding-add {
padding: 20px;
}
.selWidth { .selWidth {
width: 180px; width: 200px;
}
.mt14 {
margin-top: 14px;
}
.ml10 {
margin-left: 10px;
}
.card-title {
font-size: 16px;
font-weight: 500;
}
.header-right {
float: right;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.user-detail {
display: flex;
flex-direction: column;
}
.user-name {
font-size: 13px;
color: #303133;
}
.user-id {
font-size: 12px;
color: #909399;
}
.text-danger {
color: #f56c6c !important;
}
.message-preview {
padding: 10px 20px;
background: #fafafa;
border-radius: 4px;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 13px;
color: #606266;
}
.message-list {
max-height: 200px;
overflow-y: auto;
}
.message-item {
padding: 8px 12px;
margin-bottom: 8px;
background: #fff;
border-radius: 4px;
border-left: 3px solid #409eff;
}
.message-item.message-right {
border-left-color: #67c23a;
}
.message-sender {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.message-content {
font-size: 13px;
color: #303133;
word-break: break-all;
}
.message-time {
font-size: 11px;
color: #c0c4cc;
margin-top: 4px;
}
.dialog-users {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
margin-bottom: 20px;
}
.dialog-user {
display: flex;
align-items: center;
gap: 12px;
}
.dialog-arrow {
font-size: 24px;
color: #909399;
transform: rotate(90deg);
}
.dialog-messages {
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.dialog-message {
display: flex;
gap: 10px;
margin-bottom: 16px;
padding: 10px;
background: #fff;
border-radius: 8px;
border: 1px solid #ebeef5;
}
.dialog-message.message-from-user1 {
background: #ecf5ff;
border-color: #b3d8ff;
}
.msg-avatar {
flex-shrink: 0;
}
.msg-body {
flex: 1;
}
.msg-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.msg-sender {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.msg-time {
font-size: 12px;
color: #909399;
margin-left: auto;
margin-right: 10px;
}
.recall-btn {
color: #f56c6c !important;
font-size: 12px;
padding: 0;
}
.msg-content {
font-size: 14px;
color: #606266;
line-height: 1.5;
word-break: break-all;
}
.recalled-tag {
margin-left: 8px;
}
.recalled-content {
background: #fff3e0;
padding: 8px;
border-radius: 4px;
border-left: 3px solid #ff9800;
}
.original-label {
font-size: 12px;
color: #ff9800;
margin-bottom: 4px;
font-weight: 500;
}
.original-text {
color: #666;
}
.dialog-pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
.block {
margin-top: 20px;
text-align: right;
} }
</style> </style>

View File

@ -10,80 +10,302 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 会话管理控制器社交互动模块
* 与私聊管理使用相同的数据表eb_conversation eb_private_message
*/
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("api/admin/session") @RequestMapping("api/admin/session")
@Api(tags = "会话管理") @Api(tags = "社交互动 - 会话管理")
@Validated @Validated
public class SessionController { public class SessionController {
@Autowired @Autowired
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
/**
* 获取会话列表
*/
@ApiOperation(value = "会话列表") @ApiOperation(value = "会话列表")
@RequestMapping(value = "/list", method = RequestMethod.GET) @GetMapping("/list")
public CommonResult<CommonPage<Map<String, Object>>> getList( public CommonResult<CommonPage<Map<String, Object>>> getList(
@RequestParam(value = "senderNickname", required = false) String senderNickname, @RequestParam(value = "senderNickname", required = false) String senderNickname,
@RequestParam(value = "senderPhone", required = false) String senderPhone, @RequestParam(value = "senderPhone", required = false) String senderPhone,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "startTime", required = false) String startTime, @RequestParam(value = "startTime", required = false) String startTime,
@RequestParam(value = "endTime", required = false) String endTime, @RequestParam(value = "endTime", required = false) String endTime,
@RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit) { @RequestParam(value = "limit", defaultValue = "10") Integer limit) {
StringBuilder sql = new StringBuilder("SELECT * FROM eb_session WHERE 1=1"); StringBuilder sql = new StringBuilder();
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM eb_session WHERE 1=1"); sql.append("SELECT c.id, c.user1_id, c.user2_id, c.last_message, c.last_message_time, c.create_time, ");
sql.append("u1.nickname as sender_nickname, u1.avatar as sender_avatar, u1.phone as sender_phone, ");
sql.append("u2.nickname as receiver_nickname, u2.avatar as receiver_avatar, u2.phone as receiver_phone, ");
sql.append("(SELECT COUNT(*) FROM eb_private_message WHERE conversation_id = c.id) as message_count ");
sql.append("FROM eb_conversation c ");
sql.append("LEFT JOIN eb_user u1 ON c.user1_id = u1.uid ");
sql.append("LEFT JOIN eb_user u2 ON c.user2_id = u2.uid ");
sql.append("WHERE 1=1 ");
if (senderNickname != null && !senderNickname.isEmpty()) { StringBuilder countSql = new StringBuilder();
String safeName = senderNickname.replace("'", "''"); countSql.append("SELECT COUNT(*) FROM eb_conversation c ");
sql.append(" AND sender_nickname LIKE '%").append(safeName).append("%'"); countSql.append("LEFT JOIN eb_user u1 ON c.user1_id = u1.uid ");
countSql.append(" AND sender_nickname LIKE '%").append(safeName).append("%'"); countSql.append("LEFT JOIN eb_user u2 ON c.user2_id = u2.uid ");
} countSql.append("WHERE 1=1 ");
if (senderPhone != null && !senderPhone.isEmpty()) {
String safePhone = senderPhone.replace("'", "''"); // 发送方昵称搜索
sql.append(" AND sender_phone LIKE '%").append(safePhone).append("%'"); if (senderNickname != null && !senderNickname.trim().isEmpty()) {
countSql.append(" AND sender_phone LIKE '%").append(safePhone).append("%'"); String condition = " AND (u1.nickname LIKE '%" + senderNickname.replace("'", "''") + "%' OR u2.nickname LIKE '%" + senderNickname.replace("'", "''") + "%') ";
} sql.append(condition);
if (startTime != null && !startTime.isEmpty()) { countSql.append(condition);
sql.append(" AND create_time >= '").append(startTime).append("'");
countSql.append(" AND create_time >= '").append(startTime).append("'");
}
if (endTime != null && !endTime.isEmpty()) {
sql.append(" AND create_time <= '").append(endTime).append("'");
countSql.append(" AND create_time <= '").append(endTime).append("'");
} }
sql.append(" ORDER BY id DESC"); // 发送方电话搜索
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class); if (senderPhone != null && !senderPhone.trim().isEmpty()) {
String condition = " AND (u1.phone LIKE '%" + senderPhone.replace("'", "''") + "%' OR u2.phone LIKE '%" + senderPhone.replace("'", "''") + "%') ";
sql.append(condition);
countSql.append(condition);
}
// 关键词搜索兼容私聊管理的参数
if (keyword != null && !keyword.trim().isEmpty()) {
String condition = " AND (u1.nickname LIKE '%" + keyword + "%' OR u2.nickname LIKE '%" + keyword + "%' "
+ "OR c.user1_id = '" + keyword + "' OR c.user2_id = '" + keyword + "') ";
sql.append(condition);
countSql.append(condition);
}
// 时间范围
if (startTime != null && !startTime.trim().isEmpty()) {
String condition = " AND c.create_time >= '" + startTime + "' ";
sql.append(condition);
countSql.append(condition);
}
if (endTime != null && !endTime.trim().isEmpty()) {
String condition = " AND c.create_time <= '" + endTime + "' ";
sql.append(condition);
countSql.append(condition);
}
// 排序
sql.append("ORDER BY c.last_message_time DESC ");
// 统计总数
Long total = 0L;
try {
total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
} catch (Exception e) {
log.error("查询会话总数失败: {}", e.getMessage());
}
// 分页
int offset = (page - 1) * limit; int offset = (page - 1) * limit;
sql.append(" LIMIT ").append(offset).append(", ").append(limit); sql.append("LIMIT ").append(offset).append(", ").append(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
List<Map<String, Object>> list;
try {
list = jdbcTemplate.queryForList(sql.toString());
} catch (Exception e) {
log.error("查询会话列表失败: {}", e.getMessage());
list = java.util.Collections.emptyList();
}
CommonPage<Map<String, Object>> result = new CommonPage<>(); CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list); result.setList(list);
result.setTotal(total); result.setTotal(total != null ? total : 0L);
result.setPage(page); result.setPage(page);
result.setLimit(limit); result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) total / limit)); result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result); return CommonResult.success(result);
} }
/**
* 获取会话详情
*/
@ApiOperation(value = "会话详情") @ApiOperation(value = "会话详情")
@RequestMapping(value = "/detail/{id}", method = RequestMethod.GET) @GetMapping("/detail/{id}")
public CommonResult<Map<String, Object>> getDetail(@PathVariable Integer id) { public CommonResult<Map<String, Object>> getDetail(@PathVariable Long id) {
String sql = "SELECT * FROM eb_session WHERE id = ?"; try {
Map<String, Object> detail = jdbcTemplate.queryForMap(sql, id); StringBuilder sql = new StringBuilder();
sql.append("SELECT c.*, ");
sql.append("u1.nickname as sender_nickname, u1.avatar as sender_avatar, u1.phone as sender_phone, ");
sql.append("u2.nickname as receiver_nickname, u2.avatar as receiver_avatar, u2.phone as receiver_phone ");
sql.append("FROM eb_conversation c ");
sql.append("LEFT JOIN eb_user u1 ON c.user1_id = u1.uid ");
sql.append("LEFT JOIN eb_user u2 ON c.user2_id = u2.uid ");
sql.append("WHERE c.id = ?");
Map<String, Object> detail = jdbcTemplate.queryForMap(sql.toString(), id);
return CommonResult.success(detail); return CommonResult.success(detail);
} catch (Exception e) {
log.error("获取会话详情失败: {}", e.getMessage());
return CommonResult.failed("获取会话详情失败");
}
} }
/**
* 获取会话消息列表
*/
@ApiOperation(value = "会话消息列表")
@GetMapping("/{conversationId}/messages")
public CommonResult<CommonPage<Map<String, Object>>> getMessages(
@PathVariable Long conversationId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT m.*, u.nickname as sender_name, u.avatar as sender_avatar ");
sql.append("FROM eb_private_message m ");
sql.append("LEFT JOIN eb_user u ON m.sender_id = u.uid ");
sql.append("WHERE m.conversation_id = ? ");
sql.append("ORDER BY m.create_time DESC ");
Long total = 0L;
try {
String countSql = "SELECT COUNT(*) FROM eb_private_message WHERE conversation_id = ?";
total = jdbcTemplate.queryForObject(countSql, Long.class, conversationId);
} catch (Exception e) {
log.error("查询消息总数失败: {}", e.getMessage());
}
int offset = (page - 1) * pageSize;
sql.append("LIMIT ").append(offset).append(", ").append(pageSize);
List<Map<String, Object>> list;
try {
list = jdbcTemplate.queryForList(sql.toString(), conversationId);
// 转换字段名
list.forEach(item -> {
item.put("senderId", item.get("sender_id"));
item.put("receiverId", item.get("receiver_id"));
item.put("senderName", item.get("sender_name"));
item.put("senderAvatar", item.get("sender_avatar"));
item.put("messageType", item.get("message_type"));
item.put("createTime", item.get("create_time"));
item.put("isRecalled", item.get("is_recalled"));
item.put("originalContent", item.get("original_content"));
item.put("recallTime", item.get("recall_time"));
});
} catch (Exception e) {
log.error("查询消息列表失败: {}", e.getMessage());
list = java.util.Collections.emptyList();
}
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(pageSize);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / pageSize));
return CommonResult.success(result);
}
/**
* 删除会话包括所有消息
*/
@ApiOperation(value = "删除会话") @ApiOperation(value = "删除会话")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST) @PostMapping("/delete/{id}")
public CommonResult<String> delete(@PathVariable Integer id) { public CommonResult<String> delete(@PathVariable Long id) {
jdbcTemplate.update("DELETE FROM eb_session WHERE id = ?", id); try {
// 先删除消息
jdbcTemplate.update("DELETE FROM eb_private_message WHERE conversation_id = ?", id);
// 删除会话
jdbcTemplate.update("DELETE FROM eb_conversation WHERE id = ?", id);
return CommonResult.success("删除成功"); return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除会话失败: {}", e.getMessage());
return CommonResult.failed("删除失败: " + e.getMessage());
}
}
/**
* 删除单条消息
*/
@ApiOperation(value = "删除消息")
@PostMapping("/message/delete/{messageId}")
public CommonResult<String> deleteMessage(@PathVariable Long messageId) {
try {
jdbcTemplate.update("DELETE FROM eb_private_message WHERE id = ?", messageId);
return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除消息失败: {}", e.getMessage());
return CommonResult.failed("删除失败: " + e.getMessage());
}
}
/**
* 撤回消息管理员可以撤回任意消息
*/
@ApiOperation(value = "撤回消息")
@PostMapping("/message/recall/{messageId}")
public CommonResult<String> recallMessage(@PathVariable Long messageId) {
try {
// 先获取原始消息内容
Map<String, Object> message = jdbcTemplate.queryForMap(
"SELECT content FROM eb_private_message WHERE id = ?", messageId);
String originalContent = (String) message.get("content");
// 保存原始内容并标记为撤回
int updated = jdbcTemplate.update(
"UPDATE eb_private_message SET is_recalled = 1, original_content = ?, recall_time = NOW(), content = '[消息已被管理员撤回]' WHERE id = ?",
originalContent, messageId
);
if (updated > 0) {
return CommonResult.success("撤回成功");
} else {
return CommonResult.failed("消息不存在");
}
} catch (Exception e) {
log.error("撤回消息失败: {}", e.getMessage());
return CommonResult.failed("撤回失败: " + e.getMessage());
}
}
/**
* 获取会话统计数据
*/
@ApiOperation(value = "会话统计")
@GetMapping("/statistics")
public CommonResult<Map<String, Object>> getStatistics() {
Map<String, Object> stats = new HashMap<>();
try {
// 总会话数
Long totalConversations = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_conversation", Long.class);
stats.put("totalConversations", totalConversations != null ? totalConversations : 0);
// 总消息数
Long totalMessages = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_private_message", Long.class);
stats.put("totalMessages", totalMessages != null ? totalMessages : 0);
// 今日消息数
Long todayMessages = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_private_message WHERE DATE(create_time) = CURDATE()", Long.class);
stats.put("todayMessages", todayMessages != null ? todayMessages : 0);
// 活跃会话数最近7天有消息的会话
Long activeConversations = jdbcTemplate.queryForObject(
"SELECT COUNT(DISTINCT conversation_id) FROM eb_private_message WHERE create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)",
Long.class);
stats.put("activeConversations", activeConversations != null ? activeConversations : 0);
} catch (Exception e) {
log.error("获取统计数据失败: {}", e.getMessage());
stats.put("totalConversations", 0);
stats.put("totalMessages", 0);
stats.put("todayMessages", 0);
stats.put("activeConversations", 0);
}
return CommonResult.success(stats);
} }
} }

View File

@ -82,4 +82,12 @@ public class PrivateMessage implements Serializable {
@ApiModelProperty(value = "是否已撤回") @ApiModelProperty(value = "是否已撤回")
@Column(name = "is_recalled", columnDefinition = "TINYINT(1) DEFAULT 0") @Column(name = "is_recalled", columnDefinition = "TINYINT(1) DEFAULT 0")
private Boolean isRecalled; private Boolean isRecalled;
@ApiModelProperty(value = "原始消息内容(撤回前)")
@Column(name = "original_content", columnDefinition = "TEXT")
private String originalContent;
@ApiModelProperty(value = "撤回时间")
@Column(name = "recall_time")
private Date recallTime;
} }

View File

@ -229,3 +229,4 @@ public class CallController {
return response; return response;
} }
} }

View File

@ -145,4 +145,15 @@ public class ConversationController {
Integer userId = userService.getUserIdException(); Integer userId = userService.getUserIdException();
return CommonResult.success(conversationService.deleteMessage(id, userId)); return CommonResult.success(conversationService.deleteMessage(id, userId));
} }
/**
* 撤回消息
*/
@ApiOperation(value = "撤回消息")
@ApiImplicitParam(name = "id", value = "消息ID", required = true)
@PostMapping("/messages/{id}/recall")
public CommonResult<Boolean> recallMessage(@PathVariable Long id) {
Integer userId = userService.getUserIdException();
return CommonResult.success(conversationService.recallMessage(id, userId));
}
} }

View File

@ -412,10 +412,12 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
if (diffMinutes > 2) { if (diffMinutes > 2) {
throw new CrmebException("消息发送超过2分钟无法撤回"); throw new CrmebException("消息发送超过2分钟无法撤回");
} }
// 标记消息为已撤回 // 保存原始内容然后标记消息为已撤回
LambdaUpdateWrapper<PrivateMessage> uw = new LambdaUpdateWrapper<>(); LambdaUpdateWrapper<PrivateMessage> uw = new LambdaUpdateWrapper<>();
uw.eq(PrivateMessage::getId, messageId) uw.eq(PrivateMessage::getId, messageId)
.set(PrivateMessage::getIsRecalled, true) .set(PrivateMessage::getIsRecalled, true)
.set(PrivateMessage::getOriginalContent, message.getContent()) // 保存原始内容
.set(PrivateMessage::getRecallTime, new Date()) // 记录撤回时间
.set(PrivateMessage::getContent, "[消息已撤回]"); .set(PrivateMessage::getContent, "[消息已撤回]");
return privateMessageDao.update(null, uw) > 0; return privateMessageDao.update(null, uw) > 0;
} }

View File

@ -0,0 +1,8 @@
-- 为私信消息表添加撤回相关字段
-- 执行此脚本前请备份数据库
-- 添加原始内容字段(保存撤回前的消息内容)
ALTER TABLE eb_private_message ADD COLUMN original_content TEXT COMMENT '原始消息内容(撤回前)';
-- 添加撤回时间字段
ALTER TABLE eb_private_message ADD COLUMN recall_time DATETIME COMMENT '撤回时间';

Binary file not shown.

View File

@ -25,6 +25,7 @@ public class ChatMessage {
private long timestamp; private long timestamp;
private boolean isSystemMessage; private boolean isSystemMessage;
private boolean isGiftMessage; // 是否是礼物消息 private boolean isGiftMessage; // 是否是礼物消息
private boolean isOutgoing; // 是否是自己发送的消息
private MessageStatus status; private MessageStatus status;
private String avatarUrl; // 发送者头像 URL后续从后端获取 private String avatarUrl; // 发送者头像 URL后续从后端获取
@ -49,7 +50,8 @@ public class ChatMessage {
this.isGiftMessage = false; this.isGiftMessage = false;
this.messageType = MessageType.TEXT; this.messageType = MessageType.TEXT;
// 如果是自己发送的消息初始状态为发送中 // 如果是自己发送的消息初始状态为发送中
this.status = "".equals(username) ? MessageStatus.SENDING : MessageStatus.SENT; this.isOutgoing = "".equals(username);
this.status = this.isOutgoing ? MessageStatus.SENDING : MessageStatus.SENT;
} }
// 系统消息构造函数 // 系统消息构造函数
@ -86,6 +88,20 @@ public class ChatMessage {
this.isSystemMessage = isSystemMessage; this.isSystemMessage = isSystemMessage;
this.status = status; this.status = status;
this.messageType = MessageType.TEXT; this.messageType = MessageType.TEXT;
this.isOutgoing = "".equals(username);
}
// 完整构造函数从后端数据构造 isOutgoing 参数
public ChatMessage(String messageId, String username, String message, long timestamp,
boolean isSystemMessage, MessageStatus status, boolean isOutgoing) {
this.messageId = messageId;
this.username = username;
this.message = message;
this.timestamp = timestamp;
this.isSystemMessage = isSystemMessage;
this.status = status;
this.messageType = MessageType.TEXT;
this.isOutgoing = isOutgoing;
} }
// 图片消息构造函数 // 图片消息构造函数
@ -140,6 +156,14 @@ public class ChatMessage {
isGiftMessage = giftMessage; isGiftMessage = giftMessage;
} }
public boolean isOutgoing() {
return isOutgoing;
}
public void setOutgoing(boolean outgoing) {
isOutgoing = outgoing;
}
public MessageStatus getStatus() { public MessageStatus getStatus() {
return status; return status;
} }

View File

@ -109,6 +109,9 @@ public class ConversationActivity extends AppCompatActivity {
setupMessages(); setupMessages();
setupInput(); setupInput();
// 确保输入框显示正确的提示文本
binding.messageInput.setHint("输入消息...");
// 标记会话为已读 // 标记会话为已读
if (initialUnreadCount > 0 && conversationId != null) { if (initialUnreadCount > 0 && conversationId != null) {
markConversationAsRead(); markConversationAsRead();
@ -181,7 +184,8 @@ public class ConversationActivity extends AppCompatActivity {
return; return;
} }
String url = ApiConfig.getBaseUrl() + "/api/front/user/info"; // 使用正确的用户信息接口
String url = ApiConfig.getBaseUrl() + "/api/front/user";
Log.d(TAG, "获取用户信息: " + url); Log.d(TAG, "获取用户信息: " + url);
Request request = new Request.Builder() Request request = new Request.Builder()
@ -205,14 +209,31 @@ public class ConversationActivity extends AppCompatActivity {
if (json.optInt("code", -1) == 200) { if (json.optInt("code", -1) == 200) {
JSONObject data = json.optJSONObject("data"); JSONObject data = json.optJSONObject("data");
if (data != null) { if (data != null) {
// 尝试多种方式获取用户ID
int uid = data.optInt("uid", 0); int uid = data.optInt("uid", 0);
if (uid == 0) {
uid = data.optInt("id", 0);
}
if (uid == 0) {
// 尝试从字符串解析
String uidStr = data.optString("uid", "");
if (!uidStr.isEmpty()) {
try {
uid = (int) Double.parseDouble(uidStr);
} catch (NumberFormatException e) {
Log.e(TAG, "解析uid失败: " + uidStr, e);
}
}
}
if (uid > 0) { if (uid > 0) {
currentUserId = String.valueOf(uid); currentUserId = String.valueOf(uid);
// 保存到 AuthStore // 保存到 AuthStore
AuthStore.setUserInfo(ConversationActivity.this, currentUserId, data.optString("nickname", "")); AuthStore.setUserInfo(ConversationActivity.this, currentUserId, data.optString("nickname", data.optString("nikeName", "")));
Log.d(TAG, "从服务器获取到用户ID: " + currentUserId); Log.d(TAG, "从服务器获取到用户ID: " + currentUserId);
// 重新加载消息以正确显示 // 重新加载消息以正确显示
runOnUiThread(() -> loadMessagesFromServer()); runOnUiThread(() -> loadMessagesFromServer());
} else {
Log.w(TAG, "无法从响应中获取用户ID: " + body);
} }
} }
} }
@ -308,8 +329,28 @@ public class ConversationActivity extends AppCompatActivity {
boolean isMine = false; boolean isMine = false;
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) { if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
// 处理可能的浮点数格式 "1.0" vs "1"
try {
int myUid = (int) Double.parseDouble(myUserId);
isMine = (myUid == senderId);
} catch (NumberFormatException e) {
isMine = myUserId.equals(String.valueOf(senderId)); isMine = myUserId.equals(String.valueOf(senderId));
} }
}
// 如果 senderId 0尝试从 senderId 字段获取
if (senderId == 0) {
senderId = item.optInt("senderId", 0);
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
try {
int myUid = (int) Double.parseDouble(myUserId);
isMine = (myUid == senderId);
} catch (NumberFormatException e) {
isMine = myUserId.equals(String.valueOf(senderId));
}
}
}
Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine); Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine);
String displayName = isMine ? "" : username; String displayName = isMine ? "" : username;
@ -327,6 +368,7 @@ public class ConversationActivity extends AppCompatActivity {
} }
ChatMessage chatMessage = new ChatMessage(messageId, displayName, message, timestamp, isSystem, msgStatus); ChatMessage chatMessage = new ChatMessage(messageId, displayName, message, timestamp, isSystem, msgStatus);
chatMessage.setOutgoing(isMine); // 关键设置消息方向确保自己发送的消息显示在右侧
chatMessage.setAvatarUrl(avatarUrl); chatMessage.setAvatarUrl(avatarUrl);
return chatMessage; return chatMessage;
} catch (Exception e) { } catch (Exception e) {
@ -375,9 +417,16 @@ public class ConversationActivity extends AppCompatActivity {
PopupMenu popupMenu = new PopupMenu(this, anchorView); PopupMenu popupMenu = new PopupMenu(this, anchorView);
popupMenu.getMenu().add(0, 0, 0, "复制"); popupMenu.getMenu().add(0, 0, 0, "复制");
popupMenu.getMenu().add(0, 2, 0, "表情回应"); popupMenu.getMenu().add(0, 2, 0, "表情回应");
// 只有自己发送的消息才能删除 // 只有自己发送的消息才能删除和撤回
if ("".equals(message.getUsername())) { if ("".equals(message.getUsername()) || message.isOutgoing()) {
popupMenu.getMenu().add(0, 1, 0, "删除"); popupMenu.getMenu().add(0, 1, 0, "删除");
// 检查是否在2分钟内可以撤回
long messageTime = message.getTimestamp();
long now = System.currentTimeMillis();
long diffMinutes = (now - messageTime) / (1000 * 60);
if (diffMinutes <= 2) {
popupMenu.getMenu().add(0, 3, 0, "撤回");
}
} }
popupMenu.setOnMenuItemClickListener(item -> { popupMenu.setOnMenuItemClickListener(item -> {
@ -390,6 +439,9 @@ public class ConversationActivity extends AppCompatActivity {
} else if (item.getItemId() == 2) { } else if (item.getItemId() == 2) {
showEmojiPicker(message); showEmojiPicker(message);
return true; return true;
} else if (item.getItemId() == 3) {
recallMessage(message, position);
return true;
} }
return false; return false;
}); });
@ -458,6 +510,68 @@ public class ConversationActivity extends AppCompatActivity {
}); });
} }
/**
* 撤回消息
*/
private void recallMessage(ChatMessage message, int position) {
if (position < 0 || position >= messages.size()) return;
new AlertDialog.Builder(this)
.setTitle("撤回消息")
.setMessage("确定要撤回这条消息吗?")
.setPositiveButton("撤回", (dialog, which) -> recallMessageFromServer(message, position))
.setNegativeButton("取消", null)
.show();
}
/**
* 调用服务器撤回消息接口
*/
private void recallMessageFromServer(ChatMessage message, int position) {
String token = AuthStore.getToken(this);
if (token == null) return;
String messageId = message.getMessageId();
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId + "/recall";
Log.d(TAG, "撤回消息: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.post(RequestBody.create("", MediaType.parse("application/json")))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "撤回消息失败", e);
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "撤回失败", Snackbar.LENGTH_SHORT).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "撤回消息响应: " + body);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
// 更新本地消息显示为已撤回
message.setMessage("[消息已撤回]");
adapter.notifyItemChanged(position);
Snackbar.make(binding.getRoot(), "消息已撤回", Snackbar.LENGTH_SHORT).show();
} else {
String errorMsg = json.optString("message", "撤回失败");
Snackbar.make(binding.getRoot(), errorMsg, Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析撤回响应失败", e);
}
});
}
});
}
private void setupInput() { private void setupInput() {
binding.sendButton.setOnClickListener(new DebounceClickListener(300) { binding.sendButton.setOnClickListener(new DebounceClickListener(300) {

View File

@ -70,7 +70,8 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
ChatMessage msg = getItem(position); ChatMessage msg = getItem(position);
if (msg == null) return TYPE_TEXT_INCOMING; if (msg == null) return TYPE_TEXT_INCOMING;
boolean isOutgoing = "".equals(msg.getUsername()); // 优先使用 isOutgoing 字段如果没有设置则回退到检查用户名
boolean isOutgoing = msg.isOutgoing() || "".equals(msg.getUsername());
ChatMessage.MessageType type = msg.getMessageType(); ChatMessage.MessageType type = msg.getMessageType();
if (type == null) type = ChatMessage.MessageType.TEXT; if (type == null) type = ChatMessage.MessageType.TEXT;

View File

@ -13,7 +13,7 @@ import com.example.livestreaming.databinding.ActivityFansListBinding;
import com.example.livestreaming.net.ApiResponse; import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService; import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.PageResponse;
import com.example.livestreaming.net.RetrofitClient; import com.example.livestreaming.net.ApiClient;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -58,7 +58,7 @@ public class FansListActivity extends AppCompatActivity {
if (isLoading) return; if (isLoading) return;
isLoading = true; isLoading = true;
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<PageResponse<Map<String, Object>>>> call = apiService.getFollowersList(currentPage, 20); Call<ApiResponse<PageResponse<Map<String, Object>>>> call = apiService.getFollowersList(currentPage, 20);
call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() { call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
@ -96,7 +96,7 @@ public class FansListActivity extends AppCompatActivity {
} }
} else { } else {
Toast.makeText(FansListActivity.this, Toast.makeText(FansListActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "获取粉丝列表失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "获取粉丝列表失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {

View File

@ -13,7 +13,7 @@ import com.example.livestreaming.databinding.ActivityFollowingListBinding;
import com.example.livestreaming.net.ApiResponse; import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService; import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.PageResponse;
import com.example.livestreaming.net.RetrofitClient; import com.example.livestreaming.net.ApiClient;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -58,7 +58,7 @@ public class FollowingListActivity extends AppCompatActivity {
if (isLoading) return; if (isLoading) return;
isLoading = true; isLoading = true;
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<PageResponse<Map<String, Object>>>> call = apiService.getFollowingList(currentPage, 20); Call<ApiResponse<PageResponse<Map<String, Object>>>> call = apiService.getFollowingList(currentPage, 20);
call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() { call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
@ -94,7 +94,7 @@ public class FollowingListActivity extends AppCompatActivity {
} }
} else { } else {
Toast.makeText(FollowingListActivity.this, Toast.makeText(FollowingListActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "获取关注列表失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "获取关注列表失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {

View File

@ -679,7 +679,7 @@ public class ProfileActivity extends AppCompatActivity {
*/ */
private void loadFollowStats() { private void loadFollowStats() {
com.example.livestreaming.net.ApiService apiService = com.example.livestreaming.net.ApiService apiService =
com.example.livestreaming.net.RetrofitClient.getInstance(this).getApiService(); com.example.livestreaming.net.ApiClient.getService(this);
retrofit2.Call<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>> call = retrofit2.Call<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>> call =
apiService.getFollowStats(null); // null表示查询当前用户 apiService.getFollowStats(null); // null表示查询当前用户

View File

@ -638,7 +638,7 @@ public class PublishWorkActivity extends AppCompatActivity {
RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works"); RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works");
RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0"); RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0");
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<FileUploadResponse>> call = apiService.uploadImage(body, model, pid); Call<ApiResponse<FileUploadResponse>> call = apiService.uploadImage(body, model, pid);
call.enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() { call.enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() {
@ -649,7 +649,7 @@ public class PublishWorkActivity extends AppCompatActivity {
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
callback.onSuccess(apiResponse.getData().getUrl()); callback.onSuccess(apiResponse.getData().getUrl());
} else { } else {
callback.onFailure(apiResponse.getMsg() != null ? apiResponse.getMsg() : "上传失败"); callback.onFailure(apiResponse.getMessage() != null ? apiResponse.getMessage() : "上传失败");
} }
} else { } else {
callback.onFailure("上传失败"); callback.onFailure("上传失败");
@ -687,7 +687,7 @@ public class PublishWorkActivity extends AppCompatActivity {
RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works"); RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works");
RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0"); RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0");
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<FileUploadResponse>> call = apiService.uploadVideo(body, model, pid); Call<ApiResponse<FileUploadResponse>> call = apiService.uploadVideo(body, model, pid);
call.enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() { call.enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() {
@ -698,7 +698,7 @@ public class PublishWorkActivity extends AppCompatActivity {
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
callback.onSuccess(apiResponse.getData().getUrl()); callback.onSuccess(apiResponse.getData().getUrl());
} else { } else {
callback.onFailure(apiResponse.getMsg() != null ? apiResponse.getMsg() : "上传失败"); callback.onFailure(apiResponse.getMessage() != null ? apiResponse.getMessage() : "上传失败");
} }
} else { } else {
callback.onFailure("上传失败"); callback.onFailure("上传失败");
@ -762,7 +762,7 @@ public class PublishWorkActivity extends AppCompatActivity {
request.setVideoUrl(videoUrl); request.setVideoUrl(videoUrl);
request.setImageUrls(imageUrls); request.setImageUrls(imageUrls);
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Long>> call = apiService.publishWork(request); Call<ApiResponse<Long>> call = apiService.publishWork(request);
call.enqueue(new retrofit2.Callback<ApiResponse<Long>>() { call.enqueue(new retrofit2.Callback<ApiResponse<Long>>() {
@ -777,7 +777,7 @@ public class PublishWorkActivity extends AppCompatActivity {
finish(); finish();
} else { } else {
Toast.makeText(PublishWorkActivity.this, Toast.makeText(PublishWorkActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "发布失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "发布失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {

View File

@ -31,12 +31,15 @@ import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.AuthStore; import com.example.livestreaming.net.AuthStore;
import com.example.livestreaming.net.CreateRechargeRequest; import com.example.livestreaming.net.CreateRechargeRequest;
import com.example.livestreaming.net.CreateRechargeResponse; import com.example.livestreaming.net.CreateRechargeResponse;
import com.example.livestreaming.net.GiftResponse;
import com.example.livestreaming.net.OrderPayRequest; import com.example.livestreaming.net.OrderPayRequest;
import com.example.livestreaming.net.OrderPayResultResponse; import com.example.livestreaming.net.OrderPayResultResponse;
import com.example.livestreaming.net.RechargeOptionResponse; import com.example.livestreaming.net.RechargeOptionResponse;
import com.example.livestreaming.net.RetrofitClient;
import com.example.livestreaming.net.Room; import com.example.livestreaming.net.Room;
import com.example.livestreaming.net.SendGiftRequest;
import com.example.livestreaming.net.SendGiftResponse;
import com.example.livestreaming.net.StreamConfig; import com.example.livestreaming.net.StreamConfig;
import com.example.livestreaming.net.UserBalanceResponse;
import com.example.livestreaming.ShareUtils; import com.example.livestreaming.ShareUtils;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -45,7 +48,9 @@ import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer; import tv.danmaku.ijk.media.player.IjkMediaPlayer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -332,8 +337,9 @@ public class RoomDetailActivity extends AppCompatActivity {
stopOnlineReconnect(); stopOnlineReconnect();
// 构建WebSocket URL添加clientId参数 // 构建WebSocket URL添加clientId参数
String clientId = AuthStore.getUserId(this) > 0 ? String userIdStr = AuthStore.getUserId(this);
String.valueOf(AuthStore.getUserId(this)) : String clientId = (userIdStr != null && !userIdStr.isEmpty()) ?
userIdStr :
"guest_" + System.currentTimeMillis(); "guest_" + System.currentTimeMillis();
String wsUrl = WS_ONLINE_BASE_URL + roomId + "?clientId=" + clientId; String wsUrl = WS_ONLINE_BASE_URL + roomId + "?clientId=" + clientId;
@ -1173,7 +1179,7 @@ public class RoomDetailActivity extends AppCompatActivity {
} }
android.util.Log.d("RoomDetail", "成功加载 " + availableGifts.size() + " 个礼物"); android.util.Log.d("RoomDetail", "成功加载 " + availableGifts.size() + " 个礼物");
} else { } else {
android.util.Log.w("RoomDetail", "加载礼物列表失败: " + apiResponse.getMsg()); android.util.Log.w("RoomDetail", "加载礼物列表失败: " + apiResponse.getMessage());
setDefaultGifts(); setDefaultGifts();
} }
} else { } else {
@ -1354,7 +1360,7 @@ public class RoomDetailActivity extends AppCompatActivity {
* 加载充值选项列表 * 加载充值选项列表
*/ */
private void loadRechargeOptions(RechargeAdapter adapter, View dialogView) { private void loadRechargeOptions(RechargeAdapter adapter, View dialogView) {
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<List<RechargeOptionResponse>>> call = apiService.getRechargeOptions(); Call<ApiResponse<List<RechargeOptionResponse>>> call = apiService.getRechargeOptions();
call.enqueue(new Callback<ApiResponse<List<RechargeOptionResponse>>>() { call.enqueue(new Callback<ApiResponse<List<RechargeOptionResponse>>>() {
@ -1377,7 +1383,7 @@ public class RoomDetailActivity extends AppCompatActivity {
adapter.setOptions(options); adapter.setOptions(options);
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"加载充值选项失败: " + apiResponse.getMsg(), "加载充值选项失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
// 使用默认选项 // 使用默认选项
setDefaultRechargeOptions(adapter); setDefaultRechargeOptions(adapter);
@ -1420,7 +1426,7 @@ public class RoomDetailActivity extends AppCompatActivity {
* 创建充值订单 * 创建充值订单
*/ */
private void createRechargeOrder(RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) { private void createRechargeOrder(RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
CreateRechargeRequest request = new CreateRechargeRequest( CreateRechargeRequest request = new CreateRechargeRequest(
Integer.parseInt(selectedOption.getId()), Integer.parseInt(selectedOption.getId()),
@ -1445,7 +1451,7 @@ public class RoomDetailActivity extends AppCompatActivity {
showPaymentMethodDialog(orderId, selectedOption, rechargeDialog); showPaymentMethodDialog(orderId, selectedOption, rechargeDialog);
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"创建充值订单失败: " + apiResponse.getMsg(), "创建充值订单失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -1506,7 +1512,7 @@ public class RoomDetailActivity extends AppCompatActivity {
*/ */
private void processPayment(String orderId, String payType, String payChannel, private void processPayment(String orderId, String payType, String payChannel,
RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) { RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
OrderPayRequest payRequest = new OrderPayRequest(orderId, payType, payChannel); OrderPayRequest payRequest = new OrderPayRequest(orderId, payType, payChannel);
Call<ApiResponse<OrderPayResultResponse>> call = apiService.payment(payRequest); Call<ApiResponse<OrderPayResultResponse>> call = apiService.payment(payRequest);
@ -1535,7 +1541,7 @@ public class RoomDetailActivity extends AppCompatActivity {
simulateRechargeSuccess(selectedOption, rechargeDialog); simulateRechargeSuccess(selectedOption, rechargeDialog);
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"支付失败: " + apiResponse.getMsg(), "支付失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -1621,10 +1627,13 @@ public class RoomDetailActivity extends AppCompatActivity {
ApiService apiService = ApiClient.getService(getApplicationContext()); ApiService apiService = ApiClient.getService(getApplicationContext());
SendGiftRequest request = new SendGiftRequest(); // 获取主播ID使用房间ID作为主播ID或者从房间信息中获取
request.setRoomId(Integer.parseInt(roomId)); Integer streamerId = Integer.parseInt(roomId);
request.setGiftId(Integer.parseInt(selectedGift.getId())); SendGiftRequest request = new SendGiftRequest(
request.setCount(count); Integer.parseInt(selectedGift.getId()),
streamerId,
count
);
Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request); Call<ApiResponse<SendGiftResponse>> call = apiService.sendRoomGift(roomId, request);
@ -1659,7 +1668,7 @@ public class RoomDetailActivity extends AppCompatActivity {
giftCountText.setText("1"); giftCountText.setText("1");
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"赠送失败: " + apiResponse.getMsg(), "赠送失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -1689,7 +1698,7 @@ public class RoomDetailActivity extends AppCompatActivity {
ApiService apiService = ApiClient.getService(getApplicationContext()); ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>(); java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("streamerId", room.getStreamerId()); body.put("streamerId", roomId); // 使用房间ID作为主播ID
body.put("action", "follow"); body.put("action", "follow");
Call<ApiResponse<Map<String, Object>>> call = apiService.followStreamer(body); Call<ApiResponse<Map<String, Object>>> call = apiService.followStreamer(body);
@ -1706,7 +1715,7 @@ public class RoomDetailActivity extends AppCompatActivity {
binding.followButton.setEnabled(false); binding.followButton.setEnabled(false);
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"关注失败: " + apiResponse.getMsg(), "关注失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -1755,7 +1764,7 @@ public class RoomDetailActivity extends AppCompatActivity {
fetchRoom(); fetchRoom();
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"开始直播失败: " + apiResponse.getMsg(), "开始直播失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -1809,7 +1818,7 @@ public class RoomDetailActivity extends AppCompatActivity {
fetchRoom(); fetchRoom();
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"结束直播失败: " + apiResponse.getMsg(), "结束直播失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {

View File

@ -18,7 +18,7 @@ import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService; import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.HotSearchResponse; import com.example.livestreaming.net.HotSearchResponse;
import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.PageResponse;
import com.example.livestreaming.net.RetrofitClient; import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.Room; import com.example.livestreaming.net.Room;
import com.example.livestreaming.net.SearchHistoryResponse; import com.example.livestreaming.net.SearchHistoryResponse;
@ -151,7 +151,7 @@ public class SearchActivity extends AppCompatActivity {
Log.d(TAG, "执行搜索: " + keyword); Log.d(TAG, "执行搜索: " + keyword);
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<PageResponse<Map<String, Object>>>> call = Call<ApiResponse<PageResponse<Map<String, Object>>>> call =
apiService.searchLiveRooms(keyword, null, null, 1, 20); apiService.searchLiveRooms(keyword, null, null, 1, 20);
@ -191,9 +191,9 @@ public class SearchActivity extends AppCompatActivity {
} }
} else { } else {
Toast.makeText(SearchActivity.this, Toast.makeText(SearchActivity.this,
"搜索失败: " + apiResponse.getMsg(), "搜索失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
Log.e(TAG, "搜索失败: " + apiResponse.getMsg()); Log.e(TAG, "搜索失败: " + apiResponse.getMessage());
} }
} else { } else {
Toast.makeText(SearchActivity.this, Toast.makeText(SearchActivity.this,
@ -253,7 +253,7 @@ public class SearchActivity extends AppCompatActivity {
private void loadHotSearch() { private void loadHotSearch() {
Log.d(TAG, "加载热门搜索"); Log.d(TAG, "加载热门搜索");
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<List<HotSearchResponse>>> call = apiService.getHotSearch(2, 10); // 2-直播间 Call<ApiResponse<List<HotSearchResponse>>> call = apiService.getHotSearch(2, 10); // 2-直播间
call.enqueue(new Callback<ApiResponse<List<HotSearchResponse>>>() { call.enqueue(new Callback<ApiResponse<List<HotSearchResponse>>>() {
@ -282,7 +282,7 @@ public class SearchActivity extends AppCompatActivity {
* 加载搜索建议可选功能 * 加载搜索建议可选功能
*/ */
private void loadSearchSuggestions(String keyword) { private void loadSearchSuggestions(String keyword) {
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<List<String>>> call = apiService.getSearchSuggestions(keyword, 2, 10); // 2-直播间 Call<ApiResponse<List<String>>> call = apiService.getSearchSuggestions(keyword, 2, 10); // 2-直播间
call.enqueue(new Callback<ApiResponse<List<String>>>() { call.enqueue(new Callback<ApiResponse<List<String>>>() {

View File

@ -12,7 +12,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import com.example.livestreaming.databinding.ActivityUserProfileReadOnlyBinding; import com.example.livestreaming.databinding.ActivityUserProfileReadOnlyBinding;
import com.example.livestreaming.net.ApiResponse; import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService; import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.RetrofitClient; import com.example.livestreaming.net.ApiClient;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList; import java.util.ArrayList;
@ -101,7 +101,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
try { try {
int userId = Integer.parseInt(currentUserId); int userId = Integer.parseInt(currentUserId);
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Map<String, Object>>> call = apiService.checkFollowStatus(userId); Call<ApiResponse<Map<String, Object>>> call = apiService.checkFollowStatus(userId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() { call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@ -137,7 +137,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
Map<String, Object> requestBody = new HashMap<>(); Map<String, Object> requestBody = new HashMap<>();
requestBody.put("userId", userId); requestBody.put("userId", userId);
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(requestBody); Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(requestBody);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() { call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@ -152,7 +152,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
Toast.makeText(UserProfileReadOnlyActivity.this, "关注成功", Toast.LENGTH_SHORT).show(); Toast.makeText(UserProfileReadOnlyActivity.this, "关注成功", Toast.LENGTH_SHORT).show();
} else { } else {
Toast.makeText(UserProfileReadOnlyActivity.this, Toast.makeText(UserProfileReadOnlyActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "关注失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "关注失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -179,7 +179,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
Map<String, Object> requestBody = new HashMap<>(); Map<String, Object> requestBody = new HashMap<>();
requestBody.put("userId", userId); requestBody.put("userId", userId);
ApiService apiService = RetrofitClient.getInstance(this).getApiService(); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Map<String, Object>>> call = apiService.unfollowUser(requestBody); Call<ApiResponse<Map<String, Object>>> call = apiService.unfollowUser(requestBody);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() { call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@ -194,7 +194,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
Toast.makeText(UserProfileReadOnlyActivity.this, "取消关注成功", Toast.LENGTH_SHORT).show(); Toast.makeText(UserProfileReadOnlyActivity.this, "取消关注成功", Toast.LENGTH_SHORT).show();
} else { } else {
Toast.makeText(UserProfileReadOnlyActivity.this, Toast.makeText(UserProfileReadOnlyActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "取消关注失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "取消关注失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -224,17 +224,6 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
} }
} }
private void updateFollowButton() {
if (binding == null) return;
if (isFollowing) {
binding.addFriendButton.setText("已关注");
binding.addFriendButton.setAlpha(0.7f);
} else {
binding.addFriendButton.setText("关注");
binding.addFriendButton.setAlpha(1f);
}
}
private void setupTabsAndWorks() { private void setupTabsAndWorks() {
if (binding == null) return; if (binding == null) return;

View File

@ -443,7 +443,7 @@ public class WorkDetailActivity extends AppCompatActivity {
try { try {
long worksId = Long.parseLong(workItem.getId()); long worksId = Long.parseLong(workItem.getId());
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Boolean>> call; Call<ApiResponse<Boolean>> call;
if (isLiked) { if (isLiked) {
@ -480,7 +480,7 @@ public class WorkDetailActivity extends AppCompatActivity {
workItem.setLikeCount(oldCount); workItem.setLikeCount(oldCount);
updateLikeButton(); updateLikeButton();
Toast.makeText(WorkDetailActivity.this, Toast.makeText(WorkDetailActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "操作失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "操作失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -514,7 +514,7 @@ public class WorkDetailActivity extends AppCompatActivity {
try { try {
long worksId = Long.parseLong(workItem.getId()); long worksId = Long.parseLong(workItem.getId());
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Boolean>> call; Call<ApiResponse<Boolean>> call;
if (isFavorited) { if (isFavorited) {
@ -544,7 +544,7 @@ public class WorkDetailActivity extends AppCompatActivity {
isFavorited = oldFavorited; isFavorited = oldFavorited;
updateFavoriteButton(); updateFavoriteButton();
Toast.makeText(WorkDetailActivity.this, Toast.makeText(WorkDetailActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "操作失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "操作失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -699,7 +699,7 @@ public class WorkDetailActivity extends AppCompatActivity {
try { try {
long worksId = Long.parseLong(workItem.getId()); long worksId = Long.parseLong(workItem.getId());
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Boolean>> call = apiService.deleteWork(worksId); Call<ApiResponse<Boolean>> call = apiService.deleteWork(worksId);
call.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() { call.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() {
@ -712,7 +712,7 @@ public class WorkDetailActivity extends AppCompatActivity {
finish(); finish();
} else { } else {
Toast.makeText(WorkDetailActivity.this, Toast.makeText(WorkDetailActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "删除失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "删除失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} }
} else { } else {
@ -787,7 +787,7 @@ public class WorkDetailActivity extends AppCompatActivity {
try { try {
long worksId = Long.parseLong(workId); long worksId = Long.parseLong(workId);
ApiService apiService = ApiClient.getApiService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<WorksResponse>> call = apiService.getWorkDetail(worksId); Call<ApiResponse<WorksResponse>> call = apiService.getWorkDetail(worksId);
call.enqueue(new retrofit2.Callback<ApiResponse<WorksResponse>>() { call.enqueue(new retrofit2.Callback<ApiResponse<WorksResponse>>() {
@ -813,7 +813,7 @@ public class WorkDetailActivity extends AppCompatActivity {
setupActionButton(); setupActionButton();
} else { } else {
Toast.makeText(WorkDetailActivity.this, Toast.makeText(WorkDetailActivity.this,
apiResponse.getMsg() != null ? apiResponse.getMsg() : "获取作品详情失败", apiResponse.getMessage() != null ? apiResponse.getMessage() : "获取作品详情失败",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
finish(); finish();
} }

View File

@ -379,12 +379,6 @@ public interface ApiService {
// ==================== 支付集成 ==================== // ==================== 支付集成 ====================
@GET("api/front/gift/recharge/options")
Call<ApiResponse<List<RechargeOptionResponse>>> getRechargeOptions();
@POST("api/front/gift/recharge/create")
Call<ApiResponse<CreateRechargeResponse>> createRecharge(@Body CreateRechargeRequest body);
@GET("api/front/pay/alipay/queryPayResult") @GET("api/front/pay/alipay/queryPayResult")
Call<ApiResponse<Boolean>> queryAliPayResult(@Query("orderNo") String orderNo); Call<ApiResponse<Boolean>> queryAliPayResult(@Query("orderNo") String orderNo);

View File

@ -123,7 +123,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText <EditText
android:id="@+id/messageInput" android:id="@+id/messageInput"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="40dp" android:layout_height="40dp"
@ -136,7 +136,10 @@
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:textColor="#111111" android:textColor="#111111"
android:textSize="14sp" /> android:textColorHint="#999999"
android:textSize="14sp"
android:importantForAutofill="no"
android:autofillHints="" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/sendButton" android:id="@+id/sendButton"