Compare commits

...

4 Commits

Author SHA1 Message Date
xiao12feng8
6e8f5c4011 整合代码+群组聊天+直播和语音通话修复完成 2025-12-31 20:06:17 +08:00
xiao12feng8
f6aeb8e01f Merge branch 'master' of http://115.190.64.57:8000/xiaozhang/zhibo 2025-12-31 19:57:54 +08:00
xiao12feng8
0655b6c8fe Merge remote changes 2025-12-31 19:55:18 +08:00
xiao12feng8
3209a6b1bc feat: 添加主播管理、群组管理功能及Android端相关页面 2025-12-31 19:41:22 +08:00
55 changed files with 4462 additions and 236 deletions

View File

@ -0,0 +1,43 @@
import request from '@/utils/request'
/**
* 群组列表
*/
export function groupListApi(params) {
return request({
url: '/admin/group/list',
method: 'get',
params
})
}
/**
* 群组统计
*/
export function groupStatisticsApi() {
return request({
url: '/admin/group/statistics',
method: 'get'
})
}
/**
* 群组成员列表
*/
export function groupMembersApi(groupId, params) {
return request({
url: `/admin/group/${groupId}/members`,
method: 'get',
params
})
}
/**
* 解散群组
*/
export function groupDeleteApi(id) {
return request({
url: `/admin/group/${id}`,
method: 'delete'
})
}

View File

@ -0,0 +1,77 @@
import request from '@/utils/request'
// 获取用户列表(用于选择主播)
export function getUserList(keyword) {
return request({
url: '/admin/streamer/users',
method: 'get',
params: { keyword }
})
}
// 获取主播下拉列表(用于创建直播间)
export function getStreamerOptions() {
return request({
url: '/admin/streamer/streamers',
method: 'get'
})
}
// 获取主播列表
export function getStreamerList(params) {
return request({
url: '/admin/streamer/list',
method: 'get',
params
})
}
// 获取主播详情(包含直播统计)
export function getStreamerDetail(userId) {
return request({
url: `/admin/streamer/detail/${userId}`,
method: 'get'
})
}
// 获取主播统计
export function getStreamerStatistics() {
return request({
url: '/admin/streamer/statistics',
method: 'get'
})
}
// 设置用户为主播
export function setStreamer(userId, data) {
return request({
url: `/admin/streamer/set/${userId}`,
method: 'post',
data
})
}
// 取消主播资格
export function cancelStreamer(userId) {
return request({
url: `/admin/streamer/cancel/${userId}`,
method: 'post'
})
}
// 封禁主播
export function banStreamer(userId, data) {
return request({
url: `/admin/streamer/ban/${userId}`,
method: 'post',
data
})
}
// 解封主播
export function unbanStreamer(userId) {
return request({
url: `/admin/streamer/unban/${userId}`,
method: 'post'
})
}

View File

@ -60,6 +60,13 @@ const liveManageRouter = {
name: 'FanGroupList',
meta: { title: '粉丝团管理', icon: '' },
},
// 主播管理
{
path: 'streamer/list',
component: () => import('@/views/streamer/list/index'),
name: 'StreamerList',
meta: { title: '主播管理', icon: '' },
},
],
};

View File

@ -61,39 +61,19 @@ const socialManageRouter = {
name: 'SessionList',
meta: { title: '会话管理', icon: '' },
},
// 聊天常用语
// 敏感词管理
{
path: 'chatPhrase/list',
component: () => import('@/views/chatphrase/list/index'),
name: 'ChatPhraseList',
meta: { title: '聊天常用语', icon: '' },
path: 'sensitiveWord/list',
component: () => import('@/views/sensitiveWord/list/index'),
name: 'SensitiveWordList',
meta: { title: '敏感词管理', icon: '' },
},
// 评论管理
// 群组管理
{
path: 'comment/dynamic',
component: () => import('@/views/comment/dynamic/index'),
name: 'CommentDynamic',
meta: { title: '动态评论', icon: '' },
},
{
path: 'comment/reply',
component: () => import('@/views/comment/reply/index'),
name: 'CommentReply',
meta: { title: '评论回复', icon: '' },
},
// 动态管理
{
path: 'dynamic/list',
component: () => import('@/views/dynamic/list/index'),
name: 'DynamicList',
meta: { title: '动态列表', icon: '' },
},
// 互动管理
{
path: 'interact/index',
component: () => import('@/views/interact/index'),
name: 'InteractList',
meta: { title: '互动列表', icon: '' },
path: 'group/list',
component: () => import('@/views/group/list/index'),
name: 'GroupList',
meta: { title: '群组管理', icon: '' },
},
],
};

View File

@ -0,0 +1,261 @@
<template>
<div class="divBox">
<el-card shadow="never" class="ivu-mt">
<div class="padding-add">
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryForm" size="small" class="mb20">
<el-form-item label="关键词">
<el-input v-model="queryForm.keyword" placeholder="群名称" clearable class="selWidth" />
</el-form-item>
<el-form-item label="群主ID">
<el-input v-model="queryForm.ownerId" placeholder="群主用户ID" clearable class="selWidth" type="number" />
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="yyyy-MM-dd"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="selWidth"
@change="onDateChange"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb20">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-title">群组总数</div>
<div class="stat-value">{{ statistics.totalGroups || 0 }}</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-title">今日新增</div>
<div class="stat-value text-success">{{ statistics.todayGroups || 0 }}</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-title">总成员数</div>
<div class="stat-value text-primary">{{ statistics.totalMembers || 0 }}</div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="tableData" border size="small">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column label="群组信息" min-width="200">
<template slot-scope="scope">
<div class="group-info">
<el-avatar :size="40" :src="scope.row.avatar" icon="el-icon-s-custom" />
<div class="group-detail">
<span class="group-name">{{ scope.row.groupName || '未命名群组' }}</span>
<span class="group-desc">{{ scope.row.description || '暂无简介' }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="群主" min-width="150">
<template slot-scope="scope">
<div class="user-info">
<el-avatar :size="32" :src="scope.row.ownerAvatar" icon="el-icon-user" />
<span class="user-name">{{ scope.row.ownerNickname || '用户' + scope.row.ownerId }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="memberCount" label="成员数" width="100" align="center">
<template slot-scope="scope">
<el-tag size="small">{{ scope.row.memberCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" align="center" />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="handleViewMembers(scope.row)">成员</el-button>
<el-popconfirm title="确定解散该群组吗?" @confirm="handleDelete(scope.row.id)">
<el-button slot="reference" type="text" size="mini" class="text-danger">解散</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="block mt20">
<el-pagination
:current-page="queryForm.page"
:page-sizes="[20, 50, 100]"
:page-size="queryForm.limit"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
background
/>
</div>
</div>
</el-card>
<!-- 成员列表弹窗 -->
<el-dialog :title="'群组成员 - ' + currentGroup.groupName" :visible.sync="memberDialogVisible" width="600px">
<el-table v-loading="memberLoading" :data="memberList" border size="small" max-height="400">
<el-table-column prop="userId" label="用户ID" width="80" align="center" />
<el-table-column label="用户信息" min-width="150">
<template slot-scope="scope">
<div class="user-info">
<el-avatar :size="32" :src="scope.row.avatar" icon="el-icon-user" />
<span class="user-name">{{ scope.row.nickname || '用户' + scope.row.userId }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="角色" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.role === 2" type="danger" size="small">群主</el-tag>
<el-tag v-else-if="scope.row.role === 1" type="warning" size="small">管理员</el-tag>
<el-tag v-else size="small">成员</el-tag>
</template>
</el-table-column>
<el-table-column prop="joinTime" label="加入时间" width="160" align="center" />
</el-table>
</el-dialog>
</div>
</template>
<script>
import { groupListApi, groupDeleteApi, groupStatisticsApi, groupMembersApi } from '@/api/group';
export default {
name: 'GroupList',
data() {
return {
loading: false,
tableData: [],
total: 0,
queryForm: {
keyword: '',
ownerId: '',
startDate: '',
endDate: '',
page: 1,
limit: 20
},
dateRange: [],
statistics: {},
memberDialogVisible: false,
memberLoading: false,
memberList: [],
currentGroup: {}
};
},
mounted() {
this.getList();
this.getStatistics();
},
methods: {
async getList() {
this.loading = true;
try {
const res = await groupListApi(this.queryForm);
this.tableData = res.list || [];
this.total = res.total || 0;
} catch (error) {
console.error('获取群组列表失败:', error);
} finally {
this.loading = false;
}
},
async getStatistics() {
try {
const res = await groupStatisticsApi();
this.statistics = res || {};
} catch (error) {
console.error('获取统计数据失败:', error);
}
},
handleSearch() {
this.queryForm.page = 1;
this.getList();
},
handleReset() {
this.queryForm = {
keyword: '',
ownerId: '',
startDate: '',
endDate: '',
page: 1,
limit: 20
};
this.dateRange = [];
this.getList();
},
onDateChange(val) {
if (val && val.length === 2) {
this.queryForm.startDate = val[0];
this.queryForm.endDate = val[1];
} else {
this.queryForm.startDate = '';
this.queryForm.endDate = '';
}
},
handleSizeChange(val) {
this.queryForm.limit = val;
this.getList();
},
handlePageChange(val) {
this.queryForm.page = val;
this.getList();
},
async handleViewMembers(row) {
this.currentGroup = row;
this.memberDialogVisible = true;
this.memberLoading = true;
try {
const res = await groupMembersApi(row.id, { page: 1, limit: 100 });
this.memberList = res.list || [];
} catch (error) {
console.error('获取成员列表失败:', error);
} finally {
this.memberLoading = false;
}
},
async handleDelete(id) {
try {
await groupDeleteApi(id);
this.$message.success('解散成功');
this.getList();
this.getStatistics();
} catch (error) {
this.$message.error('操作失败');
}
}
}
};
</script>
<style lang="scss" scoped>
.padding-add { padding: 20px; }
.selWidth { width: 180px; }
.mb20 { margin-bottom: 20px; }
.mt20 { margin-top: 20px; }
.stat-card {
text-align: center;
.stat-title { font-size: 14px; color: #909399; margin-bottom: 10px; }
.stat-value { font-size: 24px; font-weight: bold; color: #303133; }
.text-primary { color: #409eff; }
.text-success { color: #67c23a; }
}
.group-info { display: flex; align-items: center; gap: 10px; }
.group-detail { display: flex; flex-direction: column; }
.group-name { font-size: 14px; color: #303133; font-weight: 500; }
.group-desc { font-size: 12px; color: #909399; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-info { display: flex; align-items: center; gap: 8px; }
.user-name { font-size: 13px; color: #303133; }
.text-danger { color: #f56c6c !important; }
</style>

View File

@ -144,8 +144,16 @@
<el-form-item label="直播标题">
<el-input v-model="createForm.title" placeholder="请输入直播标题" />
</el-form-item>
<el-form-item label="主播名称">
<el-input v-model="createForm.streamerName" placeholder="请输入主播名称" />
<el-form-item label="选择主播">
<el-select v-model="createForm.uid" filterable placeholder="请选择主播" style="width: 100%" @focus="loadStreamerOptions">
<el-option v-for="item in streamerOptions" :key="item.userId" :label="item.nickname + ' (ID:' + item.userId + ')'" :value="item.userId">
<div style="display: flex; align-items: center;">
<img :src="item.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px;">
<span>{{ item.nickname }}</span>
<span style="color: #999; margin-left: 8px;">ID: {{ item.userId }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@ -158,6 +166,7 @@
<script>
import { roomListApi, liveRoomCreateApi, liveRoomToggleStatusApi, liveRoomChatHistoryApi } from '@/api/room';
import { getStreamerOptions } from '@/api/streamer';
export default {
name: 'RoomList',
@ -189,8 +198,9 @@ export default {
},
createForm: {
title: '',
streamerName: '',
uid: null,
},
streamerOptions: [],
};
},
mounted() {
@ -200,20 +210,34 @@ export default {
openCreate() {
this.createForm = {
title: '',
streamerName: '',
uid: null,
};
this.createVisible = true;
this.loadStreamerOptions();
},
async loadStreamerOptions() {
try {
const res = await getStreamerOptions();
this.streamerOptions = res || [];
} catch (error) {
console.error('加载主播列表失败', error);
}
},
async handleCreate() {
if (!this.createForm.title || !this.createForm.streamerName) {
this.$message.error('请填写直播标题和主播名称');
if (!this.createForm.title || !this.createForm.uid) {
this.$message.error('请填写直播标题并选择主播');
return;
}
//
const selectedStreamer = this.streamerOptions.find(s => s.userId === this.createForm.uid);
const streamerName = selectedStreamer ? selectedStreamer.nickname : '';
this.createLoading = true;
try {
const res = await liveRoomCreateApi({
title: this.createForm.title,
streamerName: this.createForm.streamerName,
uid: this.createForm.uid,
streamerName: streamerName,
});
this.createVisible = false;
await this.getList();

View File

@ -0,0 +1,188 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-input v-model="listQuery.keyword" placeholder="搜索主播昵称/手机号" style="width: 200px;" class="filter-item" clearable @keyup.enter.native="handleFilter" />
<el-select v-model="listQuery.level" placeholder="主播等级" clearable style="width: 120px" class="filter-item">
<el-option label="初级" :value="1" />
<el-option label="中级" :value="2" />
<el-option label="高级" :value="3" />
<el-option label="金牌" :value="4" />
</el-select>
<el-button class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">搜索</el-button>
<el-button class="filter-item" type="success" icon="el-icon-plus" @click="showSetStreamerDialog">设置主播</el-button>
</div>
<el-row :gutter="20" class="stat-row">
<el-col :span="6"><div class="stat-card"><div class="stat-number">{{ statistics.totalStreamers || 0 }}</div><div class="stat-label">主播总数</div></div></el-col>
<el-col :span="6"><div class="stat-card"><div class="stat-number">{{ statistics.todayStreamers || 0 }}</div><div class="stat-label">今日新增</div></div></el-col>
<el-col :span="6"><div class="stat-card"><div class="stat-number">{{ statistics.totalRooms || 0 }}</div><div class="stat-label">直播间总数</div></div></el-col>
<el-col :span="6"><div class="stat-card"><div class="stat-number">{{ statistics.liveNow || 0 }}</div><div class="stat-label">正在直播</div></div></el-col>
</el-row>
<el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%;">
<el-table-column label="主播信息" min-width="200">
<template slot-scope="{row}">
<div class="user-info">
<img :src="row.avatar || defaultAvatar" class="avatar">
<div class="info"><div class="nickname">{{ row.nickname }}</div><div class="phone">ID: {{ row.userId }}</div></div>
</div>
</template>
</el-table-column>
<el-table-column label="主播等级" width="100" align="center">
<template slot-scope="{row}"><el-tag :type="getLevelType(row.streamerLevel)">{{ getLevelText(row.streamerLevel) }}</el-tag></template>
</el-table-column>
<el-table-column label="直播间数" width="100" align="center">
<template slot-scope="{row}"><span class="room-count">{{ row.roomCount || 0 }}</span></template>
</el-table-column>
<el-table-column label="本月直播" width="100" align="center">
<template slot-scope="{row}"><span>{{ row.monthRooms || 0 }}</span></template>
</el-table-column>
<el-table-column label="认证时间" width="160" align="center">
<template slot-scope="{row}"><span>{{ row.certifiedTime }}</span></template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template slot-scope="{row}">
<el-tag v-if="row.isBanned" type="danger">封禁</el-tag>
<el-tag v-else type="success">正常</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="handleDetail(row)">详情</el-button>
<el-button v-if="!row.isBanned" type="warning" size="mini" @click="handleBan(row)">封禁</el-button>
<el-button v-else type="success" size="mini" @click="handleUnban(row)">解封</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-show="total > 0" :current-page="listQuery.page" :page-sizes="[10, 20, 50]" :page-size="listQuery.limit" :total="total" layout="total, sizes, prev, pager, next, jumper" style="margin-top: 20px;" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
<!-- 设置主播对话框 -->
<el-dialog title="设置主播" :visible.sync="setStreamerVisible" width="500px">
<el-form label-width="80px">
<el-form-item label="选择用户">
<el-select v-model="selectedUserId" filterable placeholder="请选择用户" style="width: 100%">
<el-option v-for="user in userOptions" :key="user.userId" :label="user.nickname + ' (ID:' + user.userId + ')'" :value="user.userId">
<div style="display: flex; align-items: center;">
<img :src="user.avatar || defaultAvatar" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px;">
<span>{{ user.nickname }}</span>
<span style="color: #999; margin-left: 8px;">ID: {{ user.userId }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="主播等级">
<el-select v-model="streamerLevel" style="width: 100%">
<el-option label="初级" :value="1" />
<el-option label="中级" :value="2" />
<el-option label="高级" :value="3" />
<el-option label="金牌" :value="4" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="setStreamerVisible = false">取消</el-button>
<el-button type="primary" @click="confirmSetStreamer">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getStreamerList, getStreamerStatistics, banStreamer, unbanStreamer, setStreamer, getUserList } from '@/api/streamer'
export default {
name: 'StreamerList',
data() {
return {
defaultAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
list: [],
total: 0,
listLoading: false,
listQuery: { page: 1, limit: 20, keyword: '', level: '' },
statistics: {},
setStreamerVisible: false,
selectedUserId: null,
streamerLevel: 1,
userOptions: [],
userLoading: false
}
},
created() {
this.getList()
this.getStatistics()
},
methods: {
getList() {
this.listLoading = true
getStreamerList(this.listQuery).then(response => {
this.list = response.list || []
this.total = response.total || 0
this.listLoading = false
}).catch(() => { this.listLoading = false })
},
getStatistics() {
getStreamerStatistics().then(response => { this.statistics = response || {} })
},
handleFilter() { this.listQuery.page = 1; this.getList() },
handleSizeChange(val) { this.listQuery.limit = val; this.getList() },
handleCurrentChange(val) { this.listQuery.page = val; this.getList() },
handleDetail(row) { this.$message.info('查看主播详情: ' + row.nickname) },
showSetStreamerDialog() {
this.setStreamerVisible = true
this.selectedUserId = null
this.streamerLevel = 1
this.loadUserList()
},
loadUserList() {
this.userLoading = true
getUserList('').then(response => {
this.userOptions = response || []
this.userLoading = false
}).catch(() => { this.userLoading = false })
},
confirmSetStreamer() {
if (!this.selectedUserId) {
this.$message.warning('请选择用户')
return
}
setStreamer(this.selectedUserId, { level: this.streamerLevel, intro: '' }).then(() => {
this.$message.success('设置成功')
this.setStreamerVisible = false
this.getList()
this.getStatistics()
}).catch(() => { this.$message.error('设置失败') })
},
handleBan(row) {
this.$prompt('请输入封禁原因', '封禁主播', { confirmButtonText: '确定', cancelButtonText: '取消' }).then(({ value }) => {
banStreamer(row.userId, { banType: 1, banDays: 7, banReason: value }).then(() => {
this.$message.success('封禁成功')
this.getList()
})
})
},
handleUnban(row) {
this.$confirm('确定要解封该主播吗?', '提示', { type: 'warning' }).then(() => {
unbanStreamer(row.userId).then(() => { this.$message.success('解封成功'); this.getList() })
})
},
getLevelType(level) { return { 1: 'info', 2: 'success', 3: 'warning', 4: 'danger' }[level] || 'info' },
getLevelText(level) { return { 1: '初级', 2: '中级', 3: '高级', 4: '金牌' }[level] || '未知' }
}
}
</script>
<style lang="scss" scoped>
.filter-container { padding-bottom: 20px; .filter-item { margin-right: 10px; } }
.stat-row { margin-bottom: 20px; }
.stat-card { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); text-align: center;
.stat-number { font-size: 28px; font-weight: bold; color: #409EFF; }
.stat-label { font-size: 14px; color: #666; margin-top: 8px; }
}
.user-info { display: flex; align-items: center;
.avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; }
.nickname { font-weight: bold; }
.phone { font-size: 12px; color: #999; }
}
.room-count { color: #409EFF; font-weight: bold; }
</style>

View File

@ -47,13 +47,15 @@ public class FriendAdminController {
sql.append("FROM eb_friend f ");
sql.append("LEFT JOIN eb_user u1 ON f.user_id = u1.uid ");
sql.append("LEFT JOIN eb_user u2 ON f.friend_id = u2.uid ");
sql.append("WHERE f.status = 1 ");
// 只查询 user_id < friend_id 的记录避免双向关系重复显示
sql.append("WHERE f.status = 1 AND f.user_id < f.friend_id ");
StringBuilder countSql = new StringBuilder();
countSql.append("SELECT COUNT(*) FROM eb_friend f ");
countSql.append("LEFT JOIN eb_user u1 ON f.user_id = u1.uid ");
countSql.append("LEFT JOIN eb_user u2 ON f.friend_id = u2.uid ");
countSql.append("WHERE f.status = 1 ");
// 只统计 user_id < friend_id 的记录
countSql.append("WHERE f.status = 1 AND f.user_id < f.friend_id ");
// 筛选条件
if (userId != null) {
@ -245,14 +247,14 @@ public class FriendAdminController {
Map<String, Object> stats = new HashMap<>();
try {
// 总好友对数除以2因为是双向关系
// 总好友对数只统计 user_id < friend_id 的记录
Long totalFriends = jdbcTemplate.queryForObject(
"SELECT COUNT(*) / 2 FROM eb_friend WHERE status = 1", Long.class);
"SELECT COUNT(*) FROM eb_friend WHERE status = 1 AND user_id < friend_id", Long.class);
stats.put("totalFriends", totalFriends != null ? totalFriends : 0);
// 今日新增好友
// 今日新增好友只统计 user_id < friend_id 的记录
Long todayFriends = jdbcTemplate.queryForObject(
"SELECT COUNT(*) / 2 FROM eb_friend WHERE status = 1 AND DATE(create_time) = CURDATE()", Long.class);
"SELECT COUNT(*) FROM eb_friend WHERE status = 1 AND user_id < friend_id AND DATE(create_time) = CURDATE()", Long.class);
stats.put("todayFriends", todayFriends != null ? todayFriends : 0);
// 待处理请求数

View File

@ -0,0 +1,195 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 群组管理控制器管理端
*/
@Slf4j
@RestController
@RequestMapping("api/admin/group")
@Api(tags = "群组管理")
@Validated
public class GroupAdminController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 群组列表
*/
@ApiOperation(value = "群组列表")
@GetMapping("/list")
public CommonResult<CommonPage<Map<String, Object>>> getGroupList(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "ownerId", required = false) Integer ownerId,
@RequestParam(value = "startDate", required = false) String startDate,
@RequestParam(value = "endDate", required = false) String endDate,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT g.*, ");
sql.append("u.nickname as ownerNickname, u.avatar as ownerAvatar, u.phone as ownerPhone ");
sql.append("FROM eb_group g ");
sql.append("LEFT JOIN eb_user u ON g.owner_id = u.uid ");
sql.append("WHERE g.is_deleted = 0 ");
StringBuilder countSql = new StringBuilder();
countSql.append("SELECT COUNT(*) FROM eb_group g ");
countSql.append("LEFT JOIN eb_user u ON g.owner_id = u.uid ");
countSql.append("WHERE g.is_deleted = 0 ");
// 筛选条件
if (keyword != null && !keyword.isEmpty()) {
String condition = " AND g.group_name LIKE '%" + keyword + "%' ";
sql.append(condition);
countSql.append(condition);
}
if (ownerId != null) {
String condition = " AND g.owner_id = " + ownerId;
sql.append(condition);
countSql.append(condition);
}
if (startDate != null && !startDate.isEmpty()) {
String condition = " AND g.create_time >= '" + startDate + " 00:00:00' ";
sql.append(condition);
countSql.append(condition);
}
if (endDate != null && !endDate.isEmpty()) {
String condition = " AND g.create_time <= '" + endDate + " 23:59:59' ";
sql.append(condition);
countSql.append(condition);
}
sql.append(" ORDER BY g.create_time DESC ");
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
int offset = (page - 1) * limit;
sql.append(" LIMIT ").append(offset).append(", ").append(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
// 转换字段名
list.forEach(item -> {
item.put("groupName", item.get("group_name"));
item.put("ownerId", item.get("owner_id"));
item.put("memberCount", item.get("member_count"));
item.put("maxMembers", item.get("max_members"));
item.put("createTime", item.get("create_time"));
item.put("updateTime", item.get("update_time"));
});
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
}
/**
* 群组成员列表
*/
@ApiOperation(value = "群组成员列表")
@GetMapping("/{groupId}/members")
public CommonResult<CommonPage<Map<String, Object>>> getGroupMembers(
@PathVariable Long groupId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
int offset = (page - 1) * limit;
String sql = "SELECT gm.*, u.nickname, u.avatar, u.phone " +
"FROM eb_group_member gm " +
"LEFT JOIN eb_user u ON gm.user_id = u.uid " +
"WHERE gm.group_id = ? AND gm.is_deleted = 0 " +
"ORDER BY gm.role DESC, gm.join_time ASC " +
"LIMIT ?, ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, groupId, offset, limit);
// 转换字段名
list.forEach(item -> {
item.put("userId", item.get("user_id"));
item.put("groupId", item.get("group_id"));
item.put("joinTime", item.get("join_time"));
});
String countSql = "SELECT COUNT(*) FROM eb_group_member WHERE group_id = ? AND is_deleted = 0";
Long total = jdbcTemplate.queryForObject(countSql, Long.class, groupId);
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(limit);
return CommonResult.success(result);
}
/**
* 解散群组
*/
@ApiOperation(value = "解散群组")
@DeleteMapping("/{id}")
public CommonResult<Boolean> deleteGroup(@PathVariable Long id) {
try {
// 软删除群组
jdbcTemplate.update("UPDATE eb_group SET is_deleted = 1, update_time = NOW() WHERE id = ?", id);
// 软删除所有成员
jdbcTemplate.update("UPDATE eb_group_member SET is_deleted = 1, update_time = NOW() WHERE group_id = ?", id);
return CommonResult.success(true);
} catch (Exception e) {
log.error("解散群组失败: {}", e.getMessage());
return CommonResult.failed("操作失败");
}
}
/**
* 群组统计
*/
@ApiOperation(value = "群组统计")
@GetMapping("/statistics")
public CommonResult<Map<String, Object>> getStatistics() {
Map<String, Object> stats = new HashMap<>();
try {
// 群组总数
Long totalGroups = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_group WHERE is_deleted = 0", Long.class);
stats.put("totalGroups", totalGroups != null ? totalGroups : 0);
// 今日新增群组
Long todayGroups = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_group WHERE is_deleted = 0 AND DATE(create_time) = CURDATE()", Long.class);
stats.put("todayGroups", todayGroups != null ? todayGroups : 0);
// 总成员数
Long totalMembers = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_group_member WHERE is_deleted = 0", Long.class);
stats.put("totalMembers", totalMembers != null ? totalMembers : 0);
} catch (Exception e) {
log.error("获取群组统计失败: {}", e.getMessage());
}
return CommonResult.success(stats);
}
}

View File

@ -0,0 +1,518 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 主播管理控制器管理端
*/
@Slf4j
@RestController
@RequestMapping("api/admin/streamer")
@Api(tags = "主播管理")
@Validated
public class StreamerAdminController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取所有用户列表用于选择主播
*/
@ApiOperation(value = "用户列表")
@GetMapping("/users")
public CommonResult<List<Map<String, Object>>> getUserList(
@RequestParam(value = "keyword", required = false) String keyword) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT uid as userId, nickname, avatar, phone FROM eb_user WHERE is_streamer = 0 ");
if (keyword != null && !keyword.isEmpty()) {
sql.append(" AND (nickname LIKE '%").append(keyword).append("%' OR phone LIKE '%").append(keyword).append("%' OR uid = '").append(keyword).append("') ");
}
sql.append(" ORDER BY uid DESC LIMIT 100");
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
return CommonResult.success(list);
}
/**
* 获取所有主播列表用于创建直播间选择
*/
@ApiOperation(value = "主播下拉列表")
@GetMapping("/streamers")
public CommonResult<List<Map<String, Object>>> getStreamerOptions() {
String sql = "SELECT uid as userId, nickname, avatar FROM eb_user WHERE is_streamer = 1 ORDER BY streamer_certified_time DESC";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
return CommonResult.success(list);
}
/**
* 获取主播列表
*/
@ApiOperation(value = "主播列表")
@GetMapping("/list")
public CommonResult<CommonPage<Map<String, Object>>> getStreamerList(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "level", required = false) Integer level,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT u.uid as userId, u.nickname, u.avatar, u.phone, ");
sql.append("u.is_streamer as isStreamer, u.streamer_level as streamerLevel, ");
sql.append("u.streamer_intro as streamerIntro, u.streamer_certified_time as certifiedTime, ");
sql.append("u.create_time as createTime, ");
sql.append("(SELECT COUNT(*) FROM eb_live_room r WHERE r.uid = u.uid) as roomCount, ");
sql.append("(SELECT COUNT(*) FROM eb_live_room r WHERE r.uid = u.uid AND DATE_FORMAT(r.create_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')) as monthRooms, ");
sql.append("EXISTS(SELECT 1 FROM eb_streamer_ban b WHERE b.user_id = u.uid AND b.is_active = 1 AND (b.ban_end_time IS NULL OR b.ban_end_time > NOW())) as isBanned ");
sql.append("FROM eb_user u WHERE u.is_streamer = 1 ");
StringBuilder countSql = new StringBuilder();
countSql.append("SELECT COUNT(*) FROM eb_user u WHERE u.is_streamer = 1 ");
if (keyword != null && !keyword.isEmpty()) {
String condition = " AND (u.nickname LIKE '%" + keyword + "%' OR u.phone LIKE '%" + keyword + "%') ";
sql.append(condition);
countSql.append(condition);
}
if (level != null) {
String condition = " AND u.streamer_level = " + level;
sql.append(condition);
countSql.append(condition);
}
sql.append(" ORDER BY u.streamer_certified_time DESC ");
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
int offset = (page - 1) * limit;
sql.append(" LIMIT ").append(offset).append(", ").append(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
}
/**
* 获取主播认证申请列表
*/
@ApiOperation(value = "认证申请列表")
@GetMapping("/applications")
public CommonResult<CommonPage<Map<String, Object>>> getApplicationList(
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT a.*, u.nickname, u.avatar, u.phone ");
sql.append("FROM eb_streamer_application a ");
sql.append("LEFT JOIN eb_user u ON a.user_id = u.uid ");
sql.append("WHERE 1=1 ");
StringBuilder countSql = new StringBuilder();
countSql.append("SELECT COUNT(*) FROM eb_streamer_application a WHERE 1=1 ");
if (status != null) {
String condition = " AND a.status = " + status;
sql.append(condition);
countSql.append(condition);
}
sql.append(" ORDER BY a.create_time DESC ");
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
int offset = (page - 1) * limit;
sql.append(" LIMIT ").append(offset).append(", ").append(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
// 转换字段名
list.forEach(item -> {
item.put("userId", item.get("user_id"));
item.put("realName", item.get("real_name"));
item.put("idCard", item.get("id_card"));
item.put("idCardFront", item.get("id_card_front"));
item.put("idCardBack", item.get("id_card_back"));
item.put("categoryIds", item.get("category_ids"));
item.put("rejectReason", item.get("reject_reason"));
item.put("reviewerId", item.get("reviewer_id"));
item.put("reviewTime", item.get("review_time"));
item.put("createTime", item.get("create_time"));
});
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(limit);
return CommonResult.success(result);
}
/**
* 审核主播认证申请
*/
@ApiOperation(value = "审核认证申请")
@PostMapping("/applications/{id}/review")
public CommonResult<Boolean> reviewApplication(
@PathVariable Long id,
@RequestBody Map<String, Object> body) {
Integer status = (Integer) body.get("status"); // 1-通过 2-拒绝
String rejectReason = (String) body.get("rejectReason");
Integer reviewerId = (Integer) body.get("reviewerId");
if (status == null || (status != 1 && status != 2)) {
return CommonResult.failed("审核状态错误");
}
if (status == 2 && (rejectReason == null || rejectReason.trim().isEmpty())) {
return CommonResult.failed("请填写拒绝原因");
}
try {
// 获取申请信息
String querySql = "SELECT user_id, status FROM eb_streamer_application WHERE id = ?";
List<Map<String, Object>> apps = jdbcTemplate.queryForList(querySql, id);
if (apps.isEmpty()) {
return CommonResult.failed("申请不存在");
}
Map<String, Object> app = apps.get(0);
Integer currentStatus = ((Number) app.get("status")).intValue();
if (currentStatus != 0) {
return CommonResult.failed("该申请已审核");
}
Integer userId = ((Number) app.get("user_id")).intValue();
// 更新申请状态
String updateSql = "UPDATE eb_streamer_application SET status = ?, reject_reason = ?, reviewer_id = ?, review_time = NOW() WHERE id = ?";
jdbcTemplate.update(updateSql, status, rejectReason, reviewerId, id);
// 如果通过更新用户主播状态
if (status == 1) {
String userSql = "UPDATE eb_user SET is_streamer = 1, streamer_level = 1, streamer_certified_time = NOW() WHERE uid = ?";
jdbcTemplate.update(userSql, userId);
}
return CommonResult.success(true);
} catch (Exception e) {
log.error("审核申请失败", e);
return CommonResult.failed("审核失败:" + e.getMessage());
}
}
/**
* 设置用户为主播直接认证
*/
@ApiOperation(value = "设置为主播")
@PostMapping("/set/{userId}")
public CommonResult<Boolean> setStreamer(
@PathVariable Integer userId,
@RequestBody Map<String, Object> body) {
Integer level = body.get("level") != null ? (Integer) body.get("level") : 1;
String intro = (String) body.get("intro");
try {
String sql = "UPDATE eb_user SET is_streamer = 1, streamer_level = ?, streamer_intro = ?, streamer_certified_time = NOW() WHERE uid = ?";
int rows = jdbcTemplate.update(sql, level, intro, userId);
if (rows > 0) {
return CommonResult.success(true);
} else {
return CommonResult.failed("用户不存在");
}
} catch (Exception e) {
log.error("设置主播失败", e);
return CommonResult.failed("设置失败:" + e.getMessage());
}
}
/**
* 取消主播资格
*/
@ApiOperation(value = "取消主播资格")
@PostMapping("/cancel/{userId}")
public CommonResult<Boolean> cancelStreamer(@PathVariable Integer userId) {
try {
String sql = "UPDATE eb_user SET is_streamer = 0, streamer_level = 0 WHERE uid = ?";
int rows = jdbcTemplate.update(sql, userId);
if (rows > 0) {
return CommonResult.success(true);
} else {
return CommonResult.failed("用户不存在");
}
} catch (Exception e) {
log.error("取消主播资格失败", e);
return CommonResult.failed("操作失败:" + e.getMessage());
}
}
/**
* 封禁主播
*/
@ApiOperation(value = "封禁主播")
@PostMapping("/ban/{userId}")
public CommonResult<Boolean> banStreamer(
@PathVariable Integer userId,
@RequestBody Map<String, Object> body) {
Integer banType = body.get("banType") != null ? (Integer) body.get("banType") : 1; // 1-临时 2-永久
String banReason = (String) body.get("banReason");
Integer banDays = body.get("banDays") != null ? (Integer) body.get("banDays") : 7;
Integer operatorId = (Integer) body.get("operatorId");
if (banReason == null || banReason.trim().isEmpty()) {
return CommonResult.failed("请填写封禁原因");
}
try {
String sql;
if (banType == 2) {
// 永久封禁
sql = "INSERT INTO eb_streamer_ban (user_id, ban_type, ban_reason, ban_start_time, operator_id, is_active, create_time) " +
"VALUES (?, 2, ?, NOW(), ?, 1, NOW())";
jdbcTemplate.update(sql, userId, banReason.trim(), operatorId);
} else {
// 临时封禁
sql = "INSERT INTO eb_streamer_ban (user_id, ban_type, ban_reason, ban_start_time, ban_end_time, operator_id, is_active, create_time) " +
"VALUES (?, 1, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY), ?, 1, NOW())";
jdbcTemplate.update(sql, userId, banReason.trim(), banDays, operatorId);
}
return CommonResult.success(true);
} catch (Exception e) {
log.error("封禁主播失败", e);
return CommonResult.failed("封禁失败:" + e.getMessage());
}
}
/**
* 解封主播
*/
@ApiOperation(value = "解封主播")
@PostMapping("/unban/{userId}")
public CommonResult<Boolean> unbanStreamer(
@PathVariable Integer userId,
@RequestBody Map<String, Object> body) {
Integer operatorId = (Integer) body.get("operatorId");
try {
String sql = "UPDATE eb_streamer_ban SET is_active = 0, unban_time = NOW(), unban_operator_id = ? " +
"WHERE user_id = ? AND is_active = 1";
jdbcTemplate.update(sql, operatorId, userId);
return CommonResult.success(true);
} catch (Exception e) {
log.error("解封主播失败", e);
return CommonResult.failed("解封失败:" + e.getMessage());
}
}
/**
* 获取封禁记录
*/
@ApiOperation(value = "封禁记录列表")
@GetMapping("/bans")
public CommonResult<CommonPage<Map<String, Object>>> getBanList(
@RequestParam(value = "userId", required = false) Integer userId,
@RequestParam(value = "isActive", required = false) Integer isActive,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT b.*, u.nickname, u.avatar ");
sql.append("FROM eb_streamer_ban b ");
sql.append("LEFT JOIN eb_user u ON b.user_id = u.uid ");
sql.append("WHERE 1=1 ");
StringBuilder countSql = new StringBuilder();
countSql.append("SELECT COUNT(*) FROM eb_streamer_ban b WHERE 1=1 ");
if (userId != null) {
String condition = " AND b.user_id = " + userId;
sql.append(condition);
countSql.append(condition);
}
if (isActive != null) {
String condition = " AND b.is_active = " + isActive;
sql.append(condition);
countSql.append(condition);
}
sql.append(" ORDER BY b.create_time DESC ");
Long total = jdbcTemplate.queryForObject(countSql.toString(), Long.class);
int offset = (page - 1) * limit;
sql.append(" LIMIT ").append(offset).append(", ").append(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString());
// 转换字段名
list.forEach(item -> {
item.put("userId", item.get("user_id"));
item.put("banType", item.get("ban_type"));
item.put("banReason", item.get("ban_reason"));
item.put("banStartTime", item.get("ban_start_time"));
item.put("banEndTime", item.get("ban_end_time"));
item.put("operatorId", item.get("operator_id"));
item.put("isActive", item.get("is_active"));
item.put("unbanTime", item.get("unban_time"));
item.put("createTime", item.get("create_time"));
});
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(limit);
return CommonResult.success(result);
}
/**
* 获取主播详情包含直播统计
*/
@ApiOperation(value = "主播详情")
@GetMapping("/detail/{userId}")
public CommonResult<Map<String, Object>> getStreamerDetail(@PathVariable Integer userId) {
try {
// 获取主播基本信息
String userSql = "SELECT u.uid as userId, u.nickname, u.avatar, u.phone, u.create_time as createTime, " +
"u.is_streamer as isStreamer, u.streamer_level as streamerLevel, " +
"u.streamer_intro as streamerIntro, u.streamer_certified_time as certifiedTime " +
"FROM eb_user u WHERE u.uid = ? AND u.is_streamer = 1";
List<Map<String, Object>> users = jdbcTemplate.queryForList(userSql, userId);
if (users.isEmpty()) {
return CommonResult.failed("主播不存在");
}
Map<String, Object> result = new HashMap<>(users.get(0));
// 获取直播统计数据
try {
// 直播间总数
String roomCountSql = "SELECT COUNT(*) FROM eb_live_room WHERE uid = ?";
Long totalRooms = jdbcTemplate.queryForObject(roomCountSql, Long.class, userId);
result.put("totalRooms", totalRooms != null ? totalRooms : 0);
// 本月直播次数
String monthRoomsSql = "SELECT COUNT(*) FROM eb_live_room WHERE uid = ? AND DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')";
Long monthRooms = jdbcTemplate.queryForObject(monthRoomsSql, Long.class, userId);
result.put("monthRooms", monthRooms != null ? monthRooms : 0);
// 今日直播次数
String todayRoomsSql = "SELECT COUNT(*) FROM eb_live_room WHERE uid = ? AND DATE(create_time) = CURDATE()";
Long todayRooms = jdbcTemplate.queryForObject(todayRoomsSql, Long.class, userId);
result.put("todayRooms", todayRooms != null ? todayRooms : 0);
// 最近直播记录
String recentRoomsSql = "SELECT id, title, create_time as createTime, is_live as status " +
"FROM eb_live_room WHERE uid = ? ORDER BY create_time DESC LIMIT 10";
List<Map<String, Object>> recentRooms = jdbcTemplate.queryForList(recentRoomsSql, userId);
result.put("recentRooms", recentRooms);
} catch (Exception e) {
log.warn("获取直播统计数据失败", e);
result.put("totalRooms", 0);
result.put("monthRooms", 0);
result.put("todayRooms", 0);
result.put("recentRooms", new java.util.ArrayList<>());
}
// 获取封禁状态
try {
String banSql = "SELECT ban_reason, ban_end_time FROM eb_streamer_ban " +
"WHERE user_id = ? AND is_active = 1 AND (ban_end_time IS NULL OR ban_end_time > NOW()) LIMIT 1";
List<Map<String, Object>> bans = jdbcTemplate.queryForList(banSql, userId);
result.put("isBanned", !bans.isEmpty());
if (!bans.isEmpty()) {
result.put("banInfo", bans.get(0));
}
} catch (Exception e) {
result.put("isBanned", false);
}
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取主播详情失败", e);
return CommonResult.failed("获取失败:" + e.getMessage());
}
}
/**
* 主播统计
*/
@ApiOperation(value = "主播统计")
@GetMapping("/statistics")
public CommonResult<Map<String, Object>> getStatistics() {
Map<String, Object> stats = new HashMap<>();
try {
// 主播总数
Long totalStreamers = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_user WHERE is_streamer = 1", Long.class);
stats.put("totalStreamers", totalStreamers != null ? totalStreamers : 0);
// 今日新增主播
Long todayStreamers = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_user WHERE is_streamer = 1 AND DATE(streamer_certified_time) = CURDATE()", Long.class);
stats.put("todayStreamers", todayStreamers != null ? todayStreamers : 0);
// 直播间总数
try {
Long totalRooms = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_live_room", Long.class);
stats.put("totalRooms", totalRooms != null ? totalRooms : 0);
} catch (Exception e) {
stats.put("totalRooms", 0);
}
// 正在直播数
try {
Long liveNow = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_live_room WHERE is_live = 1", Long.class);
stats.put("liveNow", liveNow != null ? liveNow : 0);
} catch (Exception e) {
stats.put("liveNow", 0);
}
// 待审核申请数
Long pendingApplications = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM eb_streamer_application WHERE status = 0", Long.class);
stats.put("pendingApplications", pendingApplications != null ? pendingApplications : 0);
// 被封禁主播数
Long bannedStreamers = jdbcTemplate.queryForObject(
"SELECT COUNT(DISTINCT user_id) FROM eb_streamer_ban WHERE is_active = 1 AND (ban_end_time IS NULL OR ban_end_time > NOW())", Long.class);
stats.put("bannedStreamers", bannedStreamers != null ? bannedStreamers : 0);
} catch (Exception e) {
log.error("获取主播统计失败", e);
}
return CommonResult.success(stats);
}
}

View File

@ -0,0 +1,671 @@
package com.zbkj.front.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import com.zbkj.service.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 群组管理控制器
*/
@Slf4j
@RestController
@RequestMapping("api/front/groups")
@Api(tags = "群组管理")
@Validated
public class GroupController {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserService userService;
/**
* 获取群组列表我加入的群组
*/
@ApiOperation(value = "获取群组列表")
@GetMapping("/list")
public CommonResult<CommonPage<Map<String, Object>>> getGroupList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
int offset = (page - 1) * pageSize;
// 查询用户加入的群组
String sql = "SELECT g.id, g.group_name as name, g.avatar as avatarUrl, g.description, " +
"g.member_count as memberCount, g.owner_id as ownerId, " +
"gm.role, g.create_time as createTime " +
"FROM eb_group g " +
"INNER JOIN eb_group_member gm ON g.id = gm.group_id " +
"WHERE gm.user_id = ? AND gm.is_deleted = 0 AND g.is_deleted = 0 " +
"ORDER BY g.update_time DESC " +
"LIMIT ?, ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, offset, pageSize);
// 处理返回数据
for (Map<String, Object> item : list) {
Integer ownerId = (Integer) item.get("ownerId");
Integer role = item.get("role") != null ? ((Number) item.get("role")).intValue() : 0;
item.put("isOwner", userId.equals(ownerId));
item.put("isAdmin", role >= 1); // role: 0-普通成员, 1-管理员, 2-群主
}
// 统计总数
String countSql = "SELECT COUNT(*) FROM eb_group g " +
"INNER JOIN eb_group_member gm ON g.id = gm.group_id " +
"WHERE gm.user_id = ? AND gm.is_deleted = 0 AND g.is_deleted = 0";
Long total = jdbcTemplate.queryForObject(countSql, Long.class, userId);
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);
} catch (Exception e) {
log.error("获取群组列表失败", e);
return CommonResult.failed("获取群组列表失败");
}
}
/**
* 创建群组
*/
@ApiOperation(value = "创建群组")
@PostMapping("/create")
public CommonResult<Map<String, Object>> createGroup(@RequestBody Map<String, Object> body) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
String name = (String) body.get("name");
String description = (String) body.get("description");
String avatar = (String) body.get("avatar");
if (name == null || name.trim().isEmpty()) {
return CommonResult.failed("群组名称不能为空");
}
// 创建群组
String insertSql = "INSERT INTO eb_group (group_name, description, avatar, owner_id, member_count, create_time, update_time) " +
"VALUES (?, ?, ?, ?, 1, NOW(), NOW())";
jdbcTemplate.update(insertSql, name.trim(), description, avatar, userId);
// 获取新创建的群组ID
Long groupId = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
// 添加创建者为群主
String memberSql = "INSERT INTO eb_group_member (group_id, user_id, role, join_time, update_time) " +
"VALUES (?, ?, 2, NOW(), NOW())";
jdbcTemplate.update(memberSql, groupId, userId);
Map<String, Object> result = new HashMap<>();
result.put("id", groupId);
result.put("name", name.trim());
result.put("description", description);
result.put("avatar", avatar);
result.put("memberCount", 1);
result.put("isOwner", true);
result.put("isAdmin", true);
return CommonResult.success(result);
} catch (Exception e) {
log.error("创建群组失败", e);
return CommonResult.failed("创建群组失败");
}
}
/**
* 获取群组详情
*/
@ApiOperation(value = "获取群组详情")
@GetMapping("/{groupId}")
public CommonResult<Map<String, Object>> getGroupDetail(@PathVariable Long groupId) {
Integer userId = userService.getUserId();
try {
String sql = "SELECT g.*, gm.role FROM eb_group g " +
"LEFT JOIN eb_group_member gm ON g.id = gm.group_id AND gm.user_id = ? AND gm.is_deleted = 0 " +
"WHERE g.id = ? AND g.is_deleted = 0";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, groupId);
if (list.isEmpty()) {
return CommonResult.failed("群组不存在");
}
Map<String, Object> group = list.get(0);
Integer ownerId = (Integer) group.get("owner_id");
Integer role = group.get("role") != null ? ((Number) group.get("role")).intValue() : -1;
Map<String, Object> result = new HashMap<>();
result.put("id", group.get("id"));
result.put("name", group.get("group_name"));
result.put("avatarUrl", group.get("avatar"));
result.put("description", group.get("description"));
result.put("announcement", group.get("announcement"));
result.put("memberCount", group.get("member_count"));
result.put("maxMembers", group.get("max_members"));
result.put("ownerId", ownerId);
result.put("isOwner", userId != null && userId.equals(ownerId));
result.put("isAdmin", role >= 1);
result.put("isMember", role >= 0);
result.put("muteAll", group.get("mute_all"));
result.put("allowMemberInvite", group.get("allow_member_invite"));
result.put("createTime", group.get("create_time"));
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取群组详情失败", e);
return CommonResult.failed("获取群组详情失败");
}
}
/**
* 更新群组信息
*/
@ApiOperation(value = "更新群组信息")
@PutMapping("/{groupId}")
public CommonResult<Map<String, Object>> updateGroup(
@PathVariable Long groupId,
@RequestBody Map<String, Object> body) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
// 检查权限
String checkSql = "SELECT role FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> members = jdbcTemplate.queryForList(checkSql, groupId, userId);
if (members.isEmpty()) {
return CommonResult.failed("您不是群成员");
}
Integer role = ((Number) members.get(0).get("role")).intValue();
if (role < 1) {
return CommonResult.failed("您没有权限修改群组信息");
}
// 更新群组信息
StringBuilder updateSql = new StringBuilder("UPDATE eb_group SET update_time = NOW()");
List<Object> params = new ArrayList<>();
if (body.containsKey("name")) {
updateSql.append(", group_name = ?");
params.add(body.get("name"));
}
if (body.containsKey("description")) {
updateSql.append(", description = ?");
params.add(body.get("description"));
}
if (body.containsKey("avatar")) {
updateSql.append(", avatar = ?");
params.add(body.get("avatar"));
}
if (body.containsKey("announcement")) {
updateSql.append(", announcement = ?");
params.add(body.get("announcement"));
}
updateSql.append(" WHERE id = ? AND is_deleted = 0");
params.add(groupId);
jdbcTemplate.update(updateSql.toString(), params.toArray());
return getGroupDetail(groupId);
} catch (Exception e) {
log.error("更新群组信息失败", e);
return CommonResult.failed("更新群组信息失败");
}
}
/**
* 解散群组
*/
@ApiOperation(value = "解散群组")
@DeleteMapping("/{groupId}")
public CommonResult<Boolean> deleteGroup(@PathVariable Long groupId) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
// 检查是否是群主
String checkSql = "SELECT owner_id FROM eb_group WHERE id = ? AND is_deleted = 0";
List<Map<String, Object>> groups = jdbcTemplate.queryForList(checkSql, groupId);
if (groups.isEmpty()) {
return CommonResult.failed("群组不存在");
}
Integer ownerId = (Integer) groups.get(0).get("owner_id");
if (!userId.equals(ownerId)) {
return CommonResult.failed("只有群主可以解散群组");
}
// 软删除群组
jdbcTemplate.update("UPDATE eb_group SET is_deleted = 1, update_time = NOW() WHERE id = ?", groupId);
// 软删除所有成员
jdbcTemplate.update("UPDATE eb_group_member SET is_deleted = 1, update_time = NOW() WHERE group_id = ?", groupId);
return CommonResult.success(true);
} catch (Exception e) {
log.error("解散群组失败", e);
return CommonResult.failed("解散群组失败");
}
}
/**
* 获取群组成员列表
*/
@ApiOperation(value = "获取群组成员列表")
@GetMapping("/{groupId}/members")
public CommonResult<CommonPage<Map<String, Object>>> getGroupMembers(
@PathVariable Long groupId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
try {
int offset = (page - 1) * pageSize;
String sql = "SELECT gm.*, u.nickname, u.avatar, u.phone " +
"FROM eb_group_member gm " +
"LEFT JOIN eb_user u ON gm.user_id = u.uid " +
"WHERE gm.group_id = ? AND gm.is_deleted = 0 " +
"ORDER BY gm.role DESC, gm.join_time ASC " +
"LIMIT ?, ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, groupId, offset, pageSize);
// 处理返回数据
for (Map<String, Object> item : list) {
item.put("userId", item.get("user_id"));
item.put("avatarUrl", item.get("avatar"));
item.put("joinTime", item.get("join_time"));
Integer role = item.get("role") != null ? ((Number) item.get("role")).intValue() : 0;
item.put("isOwner", role == 2);
item.put("isAdmin", role >= 1);
}
String countSql = "SELECT COUNT(*) FROM eb_group_member WHERE group_id = ? AND is_deleted = 0";
Long total = jdbcTemplate.queryForObject(countSql, Long.class, groupId);
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total : 0L);
result.setPage(page);
result.setLimit(pageSize);
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取群组成员列表失败", e);
return CommonResult.failed("获取群组成员列表失败");
}
}
/**
* 添加群组成员
*/
@ApiOperation(value = "添加群组成员")
@PostMapping("/{groupId}/members")
public CommonResult<Boolean> addGroupMembers(
@PathVariable Long groupId,
@RequestBody Map<String, Object> body) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
@SuppressWarnings("unchecked")
List<Integer> userIds = (List<Integer>) body.get("userIds");
if (userIds == null || userIds.isEmpty()) {
return CommonResult.failed("请选择要添加的成员");
}
// 检查权限
String checkSql = "SELECT role FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> members = jdbcTemplate.queryForList(checkSql, groupId, userId);
if (members.isEmpty()) {
return CommonResult.failed("您不是群成员");
}
// 添加成员
for (Integer uid : userIds) {
// 检查是否已经是成员
String existSql = "SELECT id FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> exists = jdbcTemplate.queryForList(existSql, groupId, uid);
if (exists.isEmpty()) {
jdbcTemplate.update(
"INSERT INTO eb_group_member (group_id, user_id, role, join_time, update_time) VALUES (?, ?, 0, NOW(), NOW())",
groupId, uid);
}
}
// 更新成员数量
jdbcTemplate.update(
"UPDATE eb_group SET member_count = (SELECT COUNT(*) FROM eb_group_member WHERE group_id = ? AND is_deleted = 0), update_time = NOW() WHERE id = ?",
groupId, groupId);
return CommonResult.success(true);
} catch (Exception e) {
log.error("添加群组成员失败", e);
return CommonResult.failed("添加群组成员失败");
}
}
/**
* 移除群组成员
*/
@ApiOperation(value = "移除群组成员")
@DeleteMapping("/{groupId}/members/{memberId}")
public CommonResult<Boolean> removeGroupMember(
@PathVariable Long groupId,
@PathVariable Integer memberId) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
// 检查权限只有管理员和群主可以移除成员
String checkSql = "SELECT role FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> members = jdbcTemplate.queryForList(checkSql, groupId, userId);
if (members.isEmpty()) {
return CommonResult.failed("您不是群成员");
}
Integer role = ((Number) members.get(0).get("role")).intValue();
if (role < 1) {
return CommonResult.failed("您没有权限移除成员");
}
// 不能移除群主
String ownerSql = "SELECT owner_id FROM eb_group WHERE id = ? AND is_deleted = 0";
List<Map<String, Object>> groups = jdbcTemplate.queryForList(ownerSql, groupId);
if (!groups.isEmpty()) {
Integer ownerId = (Integer) groups.get(0).get("owner_id");
if (memberId.equals(ownerId)) {
return CommonResult.failed("不能移除群主");
}
}
// 移除成员
jdbcTemplate.update(
"UPDATE eb_group_member SET is_deleted = 1, update_time = NOW() WHERE group_id = ? AND user_id = ?",
groupId, memberId);
// 更新成员数量
jdbcTemplate.update(
"UPDATE eb_group SET member_count = (SELECT COUNT(*) FROM eb_group_member WHERE group_id = ? AND is_deleted = 0), update_time = NOW() WHERE id = ?",
groupId, groupId);
return CommonResult.success(true);
} catch (Exception e) {
log.error("移除群组成员失败", e);
return CommonResult.failed("移除群组成员失败");
}
}
/**
* 退出群组
*/
@ApiOperation(value = "退出群组")
@PostMapping("/{groupId}/leave")
public CommonResult<Boolean> leaveGroup(@PathVariable Long groupId) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
// 检查是否是群主
String ownerSql = "SELECT owner_id FROM eb_group WHERE id = ? AND is_deleted = 0";
List<Map<String, Object>> groups = jdbcTemplate.queryForList(ownerSql, groupId);
if (!groups.isEmpty()) {
Integer ownerId = (Integer) groups.get(0).get("owner_id");
if (userId.equals(ownerId)) {
return CommonResult.failed("群主不能退出群组,请先转让群主或解散群组");
}
}
// 退出群组
jdbcTemplate.update(
"UPDATE eb_group_member SET is_deleted = 1, update_time = NOW() WHERE group_id = ? AND user_id = ?",
groupId, userId);
// 更新成员数量
jdbcTemplate.update(
"UPDATE eb_group SET member_count = (SELECT COUNT(*) FROM eb_group_member WHERE group_id = ? AND is_deleted = 0), update_time = NOW() WHERE id = ?",
groupId, groupId);
return CommonResult.success(true);
} catch (Exception e) {
log.error("退出群组失败", e);
return CommonResult.failed("退出群组失败");
}
}
/**
* 转让群主
*/
@ApiOperation(value = "转让群主")
@PostMapping("/{groupId}/transfer")
public CommonResult<Boolean> transferGroup(
@PathVariable Long groupId,
@RequestBody Map<String, Object> body) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
Integer newOwnerId = (Integer) body.get("newOwnerId");
if (newOwnerId == null) {
return CommonResult.failed("请选择新群主");
}
// 检查是否是群主
String ownerSql = "SELECT owner_id FROM eb_group WHERE id = ? AND is_deleted = 0";
List<Map<String, Object>> groups = jdbcTemplate.queryForList(ownerSql, groupId);
if (groups.isEmpty()) {
return CommonResult.failed("群组不存在");
}
Integer ownerId = (Integer) groups.get(0).get("owner_id");
if (!userId.equals(ownerId)) {
return CommonResult.failed("只有群主可以转让群组");
}
// 检查新群主是否是群成员
String memberSql = "SELECT id FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> members = jdbcTemplate.queryForList(memberSql, groupId, newOwnerId);
if (members.isEmpty()) {
return CommonResult.failed("新群主必须是群成员");
}
// 转让群主
jdbcTemplate.update("UPDATE eb_group SET owner_id = ?, update_time = NOW() WHERE id = ?", newOwnerId, groupId);
// 更新角色
jdbcTemplate.update("UPDATE eb_group_member SET role = 0, update_time = NOW() WHERE group_id = ? AND user_id = ?", groupId, userId);
jdbcTemplate.update("UPDATE eb_group_member SET role = 2, update_time = NOW() WHERE group_id = ? AND user_id = ?", groupId, newOwnerId);
return CommonResult.success(true);
} catch (Exception e) {
log.error("转让群主失败", e);
return CommonResult.failed("转让群主失败");
}
}
// ==================== 群消息接口 ====================
/**
* 获取群消息列表
*/
@ApiOperation(value = "获取群消息列表")
@GetMapping("/{groupId}/messages")
public CommonResult<CommonPage<Map<String, Object>>> getGroupMessages(
@PathVariable Long groupId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", defaultValue = "30") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
// 检查群组是否存在
String groupSql = "SELECT id FROM eb_group WHERE id = ? AND is_deleted = 0";
List<Map<String, Object>> groups = jdbcTemplate.queryForList(groupSql, groupId);
if (groups.isEmpty()) {
return CommonResult.failed("群组不存在");
}
// 检查是否是群成员如果不是则自动加入
String memberSql = "SELECT id FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> members = jdbcTemplate.queryForList(memberSql, groupId, userId);
if (members.isEmpty()) {
// 自动将用户加入群组
String insertMemberSql = "INSERT INTO eb_group_member (group_id, user_id, role, join_time, update_time) VALUES (?, ?, 0, NOW(), NOW())";
jdbcTemplate.update(insertMemberSql, groupId, userId);
jdbcTemplate.update("UPDATE eb_group SET member_count = member_count + 1, update_time = NOW() WHERE id = ?", groupId);
}
int offset = (page - 1) * pageSize;
String sql = "SELECT gm.*, u.nickname as senderName, u.avatar as senderAvatar " +
"FROM eb_group_message gm " +
"LEFT JOIN eb_user u ON gm.sender_id = u.uid " +
"WHERE gm.group_id = ? AND gm.is_deleted = 0 " +
"ORDER BY gm.create_time DESC " +
"LIMIT ?, ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, groupId, offset, pageSize);
// 转换字段名并反转顺序让最新消息在最后
List<Map<String, Object>> result = new ArrayList<>();
for (int i = list.size() - 1; i >= 0; i--) {
Map<String, Object> item = list.get(i);
item.put("senderId", item.get("sender_id"));
item.put("messageType", item.get("message_type"));
item.put("createTime", item.get("create_time"));
result.add(item);
}
String countSql = "SELECT COUNT(*) FROM eb_group_message WHERE group_id = ? AND is_deleted = 0";
Long total = jdbcTemplate.queryForObject(countSql, Long.class, groupId);
CommonPage<Map<String, Object>> pageResult = new CommonPage<>();
pageResult.setList(result);
pageResult.setTotal(total != null ? total : 0L);
pageResult.setPage(page);
pageResult.setLimit(pageSize);
return CommonResult.success(pageResult);
} catch (Exception e) {
log.error("获取群消息失败", e);
return CommonResult.failed("获取群消息失败");
}
}
/**
* 发送群消息
*/
@ApiOperation(value = "发送群消息")
@PostMapping("/{groupId}/messages")
public CommonResult<Map<String, Object>> sendGroupMessage(
@PathVariable Long groupId,
@RequestBody Map<String, Object> body) {
Integer userId = userService.getUserId();
if (userId == null || userId <= 0) {
return CommonResult.failed("请先登录");
}
try {
// 检查群组是否存在
String groupSql = "SELECT id FROM eb_group WHERE id = ? AND is_deleted = 0";
List<Map<String, Object>> groups = jdbcTemplate.queryForList(groupSql, groupId);
if (groups.isEmpty()) {
log.warn("群组{}不存在", groupId);
return CommonResult.failed("群组不存在");
}
// 检查是否是群成员
String memberSql = "SELECT id FROM eb_group_member WHERE group_id = ? AND user_id = ? AND is_deleted = 0";
List<Map<String, Object>> members = jdbcTemplate.queryForList(memberSql, groupId, userId);
if (members.isEmpty()) {
log.warn("用户{}不是群组{}的成员,尝试自动加入", userId, groupId);
// 自动将用户加入群组作为普通成员
String insertMemberSql = "INSERT INTO eb_group_member (group_id, user_id, role, join_time, update_time) VALUES (?, ?, 0, NOW(), NOW())";
jdbcTemplate.update(insertMemberSql, groupId, userId);
// 更新群组成员数
jdbcTemplate.update("UPDATE eb_group SET member_count = member_count + 1, update_time = NOW() WHERE id = ?", groupId);
}
String content = (String) body.get("content");
String messageType = body.get("messageType") != null ? (String) body.get("messageType") : "text";
if (content == null || content.trim().isEmpty()) {
return CommonResult.failed("消息内容不能为空");
}
// 插入消息
String insertSql = "INSERT INTO eb_group_message (group_id, sender_id, content, message_type, create_time) " +
"VALUES (?, ?, ?, ?, NOW())";
jdbcTemplate.update(insertSql, groupId, userId, content.trim(), messageType);
// 获取新插入的消息ID
Long messageId = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
// 更新群组最后活跃时间
jdbcTemplate.update("UPDATE eb_group SET update_time = NOW() WHERE id = ?", groupId);
// 获取发送者信息
String userSql = "SELECT nickname, avatar FROM eb_user WHERE uid = ?";
List<Map<String, Object>> users = jdbcTemplate.queryForList(userSql, userId);
String senderName = "";
String senderAvatar = "";
if (!users.isEmpty()) {
senderName = (String) users.get(0).get("nickname");
senderAvatar = (String) users.get(0).get("avatar");
}
Map<String, Object> result = new HashMap<>();
result.put("id", messageId);
result.put("groupId", groupId);
result.put("senderId", userId);
result.put("senderName", senderName);
result.put("senderAvatar", senderAvatar);
result.put("content", content.trim());
result.put("messageType", messageType);
result.put("createTime", new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
return CommonResult.success(result);
} catch (Exception e) {
log.error("发送群消息失败", e);
return CommonResult.failed("发送群消息失败");
}
}
}

View File

@ -93,12 +93,36 @@ public class LiveRoomController {
return CommonResult.success(toResponse(room, resolveHost(request), currentUserId));
}
@Autowired
private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
@ApiOperation(value = "创建直播间(登录)")
@PostMapping("/rooms")
public CommonResult<LiveRoomResponse> create(@RequestBody @Validated CreateLiveRoomRequest req, HttpServletRequest request) {
Integer uid = frontTokenComponent.getUserId();
if (uid == null) return CommonResult.failed("未登录");
// 检查用户是否是认证主播
try {
String sql = "SELECT is_streamer FROM eb_user WHERE uid = ?";
Integer isStreamer = jdbcTemplate.queryForObject(sql, Integer.class, uid);
if (isStreamer == null || isStreamer != 1) {
return CommonResult.failed("只有认证主播才能开播,请先申请主播认证");
}
// 检查主播是否被封禁
String banSql = "SELECT COUNT(*) FROM eb_streamer_ban WHERE user_id = ? AND is_active = 1 AND (ban_end_time IS NULL OR ban_end_time > NOW())";
Integer banCount = jdbcTemplate.queryForObject(banSql, Integer.class, uid);
if (banCount != null && banCount > 0) {
return CommonResult.failed("您的主播资格已被封禁,暂时无法开播");
}
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
return CommonResult.failed("用户不存在");
} catch (Exception e) {
log.error("检查主播资格失败", e);
// 如果字段不存在暂时允许创建兼容旧数据
}
LiveRoom room = liveRoomService.createRoom(
uid,
req.getTitle(),

View File

@ -0,0 +1,181 @@
package com.zbkj.front.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import com.zbkj.common.token.FrontTokenComponent;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 主播认证控制器
*/
@Slf4j
@RestController
@RequestMapping("api/front/streamer")
@Api(tags = "主播认证")
@Validated
public class StreamerController {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private FrontTokenComponent frontTokenComponent;
/**
* 检查当前用户是否是认证主播
*/
@ApiOperation(value = "检查主播资格")
@GetMapping("/check")
public CommonResult<Map<String, Object>> checkStreamerStatus() {
Integer userId = frontTokenComponent.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
try {
String sql = "SELECT is_streamer, streamer_level, streamer_intro, streamer_certified_time FROM eb_user WHERE uid = ?";
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql, userId);
Map<String, Object> result = new HashMap<>();
if (results.isEmpty()) {
result.put("isStreamer", false);
result.put("streamerLevel", 0);
} else {
Map<String, Object> user = results.get(0);
Integer isStreamer = user.get("is_streamer") != null ? ((Number) user.get("is_streamer")).intValue() : 0;
result.put("isStreamer", isStreamer == 1);
result.put("streamerLevel", user.get("streamer_level"));
result.put("streamerIntro", user.get("streamer_intro"));
result.put("certifiedTime", user.get("streamer_certified_time"));
}
// 检查是否有待审核的申请
String pendingSql = "SELECT id, status, create_time FROM eb_streamer_application WHERE user_id = ? ORDER BY create_time DESC LIMIT 1";
List<Map<String, Object>> applications = jdbcTemplate.queryForList(pendingSql, userId);
if (!applications.isEmpty()) {
Map<String, Object> app = applications.get(0);
result.put("hasApplication", true);
result.put("applicationStatus", app.get("status"));
result.put("applicationTime", app.get("create_time"));
} else {
result.put("hasApplication", false);
}
// 检查是否被封禁
String banSql = "SELECT ban_reason, ban_end_time FROM eb_streamer_ban WHERE user_id = ? AND is_active = 1 AND (ban_end_time IS NULL OR ban_end_time > NOW()) LIMIT 1";
List<Map<String, Object>> bans = jdbcTemplate.queryForList(banSql, userId);
if (!bans.isEmpty()) {
result.put("isBanned", true);
result.put("banReason", bans.get(0).get("ban_reason"));
result.put("banEndTime", bans.get(0).get("ban_end_time"));
} else {
result.put("isBanned", false);
}
return CommonResult.success(result);
} catch (Exception e) {
log.error("检查主播资格失败", e);
// 如果字段不存在返回默认值
Map<String, Object> result = new HashMap<>();
result.put("isStreamer", false);
result.put("streamerLevel", 0);
result.put("hasApplication", false);
result.put("isBanned", false);
return CommonResult.success(result);
}
}
/**
* 提交主播认证申请
*/
@ApiOperation(value = "提交主播认证申请")
@PostMapping("/apply")
public CommonResult<Map<String, Object>> applyStreamer(@RequestBody Map<String, Object> body) {
Integer userId = frontTokenComponent.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
try {
// 检查是否已经是主播
String checkSql = "SELECT is_streamer FROM eb_user WHERE uid = ?";
List<Map<String, Object>> users = jdbcTemplate.queryForList(checkSql, userId);
if (!users.isEmpty()) {
Integer isStreamer = users.get(0).get("is_streamer") != null ?
((Number) users.get(0).get("is_streamer")).intValue() : 0;
if (isStreamer == 1) {
return CommonResult.failed("您已经是认证主播");
}
}
// 检查是否有待审核的申请
String pendingSql = "SELECT id FROM eb_streamer_application WHERE user_id = ? AND status = 0";
List<Map<String, Object>> pending = jdbcTemplate.queryForList(pendingSql, userId);
if (!pending.isEmpty()) {
return CommonResult.failed("您已有待审核的申请,请耐心等待");
}
// 获取申请信息
String realName = (String) body.get("realName");
String idCard = (String) body.get("idCard");
String idCardFront = (String) body.get("idCardFront");
String idCardBack = (String) body.get("idCardBack");
String intro = (String) body.get("intro");
String experience = (String) body.get("experience");
String categoryIds = (String) body.get("categoryIds");
if (realName == null || realName.trim().isEmpty()) {
return CommonResult.failed("请填写真实姓名");
}
if (idCard == null || idCard.trim().isEmpty()) {
return CommonResult.failed("请填写身份证号");
}
// 插入申请记录
String insertSql = "INSERT INTO eb_streamer_application (user_id, real_name, id_card, id_card_front, id_card_back, intro, experience, category_ids, status, create_time) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NOW())";
jdbcTemplate.update(insertSql, userId, realName.trim(), idCard.trim(), idCardFront, idCardBack, intro, experience, categoryIds);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "申请已提交,请等待审核");
return CommonResult.success(result);
} catch (Exception e) {
log.error("提交主播认证申请失败", e);
return CommonResult.failed("提交申请失败:" + e.getMessage());
}
}
/**
* 获取我的申请记录
*/
@ApiOperation(value = "获取我的申请记录")
@GetMapping("/applications")
public CommonResult<List<Map<String, Object>>> getMyApplications() {
Integer userId = frontTokenComponent.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
try {
String sql = "SELECT id, real_name as realName, status, reject_reason as rejectReason, " +
"create_time as createTime, review_time as reviewTime " +
"FROM eb_streamer_application WHERE user_id = ? ORDER BY create_time DESC";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId);
return CommonResult.success(list);
} catch (Exception e) {
log.error("获取申请记录失败", e);
return CommonResult.failed("获取申请记录失败");
}
}
}

View File

@ -100,6 +100,7 @@ public class FriendServiceImpl implements FriendService {
friendRequest.setToUserId(targetUserId);
friendRequest.setMessage(message);
friendRequest.setStatus(0);
friendRequest.setCreateTime(new Date()); // MyBatis-Plus不会触发@PrePersist需要手动设置
friendRequestDao.insert(friendRequest);
return true;
@ -144,16 +145,20 @@ public class FriendServiceImpl implements FriendService {
friendRequestDao.updateById(friendRequest);
// 创建双向好友关系
Date now = new Date();
Friend friend1 = new Friend();
friend1.setUserId(currentUserId);
friend1.setFriendId(fromUserId);
friend1.setStatus(1);
friend1.setCreateTime(now); // MyBatis-Plus不会触发@PrePersist需要手动设置
friendDao.insert(friend1);
Friend friend2 = new Friend();
friend2.setUserId(fromUserId);
friend2.setFriendId(currentUserId);
friend2.setStatus(1);
friend2.setCreateTime(now); // MyBatis-Plus不会触发@PrePersist需要手动设置
friendDao.insert(friend2);
// 自动创建私聊会话
@ -229,6 +234,7 @@ public class FriendServiceImpl implements FriendService {
UserBlacklist blacklist = new UserBlacklist();
blacklist.setUserId(currentUserId);
blacklist.setBlockedUserId(friendId);
blacklist.setCreateTime(new Date()); // MyBatis-Plus不会触发@PrePersist需要手动设置
userBlacklistDao.insert(blacklist);
// 删除好友关系如果存在

34
add_streamer_menu.sql Normal file
View File

@ -0,0 +1,34 @@
-- 添加主播管理菜单到直播管理下
-- 请在数据库中执行
-- 1. 先查找直播管理的菜单ID
SELECT id, name FROM eb_system_menu WHERE name = '直播管理';
-- 2. 插入主播管理菜单假设直播管理的ID需要你替换下面的 XXX
-- 先执行上面的查询获取直播管理的ID然后替换下面的数字
-- 如果直播管理ID是某个数字比如查出来是 100就把下面的 pid 改成 100
INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
VALUES (
(SELECT id FROM eb_system_menu WHERE name = '直播管理' LIMIT 1),
'主播管理',
'',
'admin:streamer:list',
'/liveManage/streamer/list',
'C',
1,
1,
0,
NOW(),
NOW()
);
-- 3. 给超级管理员分配权限
INSERT INTO eb_system_role_menu (rid, menu_id)
SELECT 1, id FROM eb_system_menu WHERE name = '主播管理' AND component = '/liveManage/streamer/list';
-- 4. 验证
SELECT * FROM eb_system_menu WHERE name = '主播管理';
-- 5. 清除Redis缓存在Redis中执行
-- DEL menuList

View File

@ -232,6 +232,18 @@
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name="com.example.livestreaming.GroupChatActivity"
android:exported="false"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<!-- 主播认证相关Activity -->
<activity
android:name="com.example.livestreaming.StreamerApplyActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>

View File

@ -4,6 +4,9 @@ import java.util.Objects;
public class ConversationItem {
public static final int TYPE_PRIVATE = 0; // 私聊
public static final int TYPE_GROUP = 1; // 群聊
private final String id;
private final String title;
private final String lastMessage;
@ -12,6 +15,8 @@ public class ConversationItem {
private final boolean muted;
private int otherUserId;
private String avatarUrl;
private int type = TYPE_PRIVATE; // 默认私聊
private long groupId; // 群组ID群聊时使用
public ConversationItem(String id, String title, String lastMessage, String timeText, int unreadCount, boolean muted) {
this.id = id;
@ -22,6 +27,11 @@ public class ConversationItem {
this.muted = muted;
}
public ConversationItem(String id, String title, String lastMessage, String timeText, int unreadCount, boolean muted, int type) {
this(id, title, lastMessage, timeText, unreadCount, muted);
this.type = type;
}
public String getId() {
return id;
}
@ -62,6 +72,26 @@ public class ConversationItem {
this.avatarUrl = avatarUrl;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public boolean isGroup() {
return type == TYPE_GROUP;
}
public long getGroupId() {
return groupId;
}
public void setGroupId(long groupId) {
this.groupId = groupId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -91,6 +91,9 @@ public class FishPondActivity extends AppCompatActivity {
finish();
return true;
}
if (id == R.id.nav_friends) {
return true;
}
if (id == R.id.nav_wish_tree) {
WishTreeActivity.start(this);
finish();

View File

@ -0,0 +1,403 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityGroupChatBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.AuthStore;
import com.example.livestreaming.net.GroupMessageResponse;
import com.example.livestreaming.net.PageResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 群聊页面
*/
public class GroupChatActivity extends AppCompatActivity {
private static final String TAG = "GroupChatActivity";
private static final String EXTRA_GROUP_ID = "group_id";
private static final String EXTRA_GROUP_NAME = "group_name";
private static final String EXTRA_GROUP_AVATAR = "group_avatar";
private ActivityGroupChatBinding binding;
private GroupChatAdapter chatAdapter;
private long groupId;
private String groupName;
private String groupAvatar;
private int currentUserId;
private final List<GroupChatMessage> messages = new ArrayList<>();
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMore = true;
private Handler refreshHandler;
private Runnable refreshRunnable;
private static final long REFRESH_INTERVAL = 5000; // 5秒刷新一次
public static void start(Context context, long groupId, String groupName, String groupAvatar) {
Intent intent = new Intent(context, GroupChatActivity.class);
intent.putExtra(EXTRA_GROUP_ID, groupId);
intent.putExtra(EXTRA_GROUP_NAME, groupName);
intent.putExtra(EXTRA_GROUP_AVATAR, groupAvatar);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityGroupChatBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
groupId = getIntent().getLongExtra(EXTRA_GROUP_ID, 0);
groupName = getIntent().getStringExtra(EXTRA_GROUP_NAME);
groupAvatar = getIntent().getStringExtra(EXTRA_GROUP_AVATAR);
if (groupId == 0) {
Toast.makeText(this, "群组ID无效", Toast.LENGTH_SHORT).show();
finish();
return;
}
String userIdStr = AuthStore.getUserId(this);
try {
currentUserId = Integer.parseInt(userIdStr != null ? userIdStr : "0");
} catch (NumberFormatException e) {
currentUserId = 0;
}
setupViews();
loadMessages();
startAutoRefresh();
}
private void setupViews() {
binding.backButton.setOnClickListener(v -> finish());
binding.titleText.setText(groupName != null ? groupName : "群聊");
binding.groupInfoButton.setOnClickListener(v -> {
GroupDetailActivity.start(this, groupId);
});
// 聊天列表
chatAdapter = new GroupChatAdapter(currentUserId);
chatAdapter.setOnAvatarClickListener(message -> {
if (message != null && message.getSenderId() != currentUserId) {
UserProfileReadOnlyActivity.start(this,
String.valueOf(message.getSenderId()),
message.getSenderName(),
"", "", 0);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd(true);
binding.messagesRecyclerView.setLayoutManager(layoutManager);
binding.messagesRecyclerView.setAdapter(chatAdapter);
// 发送按钮
binding.sendButton.setOnClickListener(v -> sendMessage());
binding.sendButton.setEnabled(false);
// 输入框监听
binding.messageInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
binding.sendButton.setEnabled(s != null && s.toString().trim().length() > 0);
}
});
// 下拉加载更多
binding.swipeRefresh.setOnRefreshListener(() -> {
if (hasMore && !isLoading) {
currentPage++;
loadMessages();
} else {
binding.swipeRefresh.setRefreshing(false);
}
});
}
private void loadMessages() {
if (isLoading) return;
isLoading = true;
if (currentPage == 1) {
binding.loadingProgress.setVisibility(View.VISIBLE);
}
ApiService apiService = ApiClient.getService(this);
apiService.getGroupMessages(groupId, currentPage, 30).enqueue(new Callback<ApiResponse<PageResponse<GroupMessageResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<GroupMessageResponse>>> call,
Response<ApiResponse<PageResponse<GroupMessageResponse>>> response) {
isLoading = false;
binding.loadingProgress.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
if (response.isSuccessful() && response.body() != null) {
ApiResponse<PageResponse<GroupMessageResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
PageResponse<GroupMessageResponse> pageData = apiResponse.getData();
List<GroupMessageResponse> msgList = pageData.getList();
if (currentPage == 1) {
messages.clear();
}
if (msgList != null && !msgList.isEmpty()) {
List<GroupChatMessage> newMessages = new ArrayList<>();
for (GroupMessageResponse m : msgList) {
GroupChatMessage msg = new GroupChatMessage(
m.getId(),
m.getSenderId(),
m.getSenderName(),
m.getSenderAvatar(),
m.getContent(),
m.getMessageType(),
m.getCreateTime()
);
newMessages.add(msg);
}
if (currentPage == 1) {
messages.addAll(newMessages);
} else {
// 加载更多时添加到列表开头
messages.addAll(0, newMessages);
}
hasMore = msgList.size() >= 30;
} else {
hasMore = false;
}
chatAdapter.submitList(new ArrayList<>(messages));
if (currentPage == 1 && !messages.isEmpty()) {
binding.messagesRecyclerView.scrollToPosition(messages.size() - 1);
}
updateEmptyState();
}
}
}
@Override
public void onFailure(Call<ApiResponse<PageResponse<GroupMessageResponse>>> call, Throwable t) {
isLoading = false;
binding.loadingProgress.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
Log.e(TAG, "加载消息失败", t);
}
});
}
private void sendMessage() {
String content = binding.messageInput.getText() != null ?
binding.messageInput.getText().toString().trim() : "";
if (content.isEmpty()) return;
binding.sendButton.setEnabled(false);
binding.messageInput.setText("");
Map<String, Object> body = new HashMap<>();
body.put("content", content);
body.put("messageType", "text");
ApiService apiService = ApiClient.getService(this);
apiService.sendGroupMessage(groupId, body).enqueue(new Callback<ApiResponse<GroupMessageResponse>>() {
@Override
public void onResponse(Call<ApiResponse<GroupMessageResponse>> call,
Response<ApiResponse<GroupMessageResponse>> response) {
Log.d(TAG, "发送消息响应码: " + response.code());
if (response.isSuccessful() && response.body() != null) {
ApiResponse<GroupMessageResponse> apiResponse = response.body();
Log.d(TAG, "发送消息API响应: code=" + apiResponse.getCode() + ", message=" + apiResponse.getMessage());
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
GroupMessageResponse m = apiResponse.getData();
GroupChatMessage msg = new GroupChatMessage(
m.getId(),
m.getSenderId(),
m.getSenderName(),
m.getSenderAvatar(),
m.getContent(),
m.getMessageType(),
m.getCreateTime()
);
messages.add(msg);
chatAdapter.submitList(new ArrayList<>(messages));
binding.messagesRecyclerView.scrollToPosition(messages.size() - 1);
updateEmptyState();
} else {
String errorMsg = apiResponse.getMessage() != null ? apiResponse.getMessage() : "发送失败";
Log.e(TAG, "发送消息失败: " + errorMsg);
Toast.makeText(GroupChatActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
}
} else {
try {
String errorBody = response.errorBody() != null ? response.errorBody().string() : "无错误信息";
Log.e(TAG, "发送消息HTTP错误: " + response.code() + ", body=" + errorBody);
} catch (Exception e) {
Log.e(TAG, "读取错误信息失败", e);
}
Toast.makeText(GroupChatActivity.this, "发送失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<GroupMessageResponse>> call, Throwable t) {
Log.e(TAG, "发送消息失败", t);
Toast.makeText(GroupChatActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
}
private void updateEmptyState() {
if (messages.isEmpty()) {
binding.emptyStateView.setVisibility(View.VISIBLE);
binding.emptyStateView.setEmptyState("暂无消息,发送第一条消息吧", R.drawable.ic_chat_24);
} else {
binding.emptyStateView.setVisibility(View.GONE);
}
}
private void startAutoRefresh() {
refreshHandler = new Handler(Looper.getMainLooper());
refreshRunnable = new Runnable() {
@Override
public void run() {
refreshNewMessages();
refreshHandler.postDelayed(this, REFRESH_INTERVAL);
}
};
refreshHandler.postDelayed(refreshRunnable, REFRESH_INTERVAL);
}
private void refreshNewMessages() {
// 只刷新第一页获取新消息
ApiService apiService = ApiClient.getService(this);
apiService.getGroupMessages(groupId, 1, 30).enqueue(new Callback<ApiResponse<PageResponse<GroupMessageResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<GroupMessageResponse>>> call,
Response<ApiResponse<PageResponse<GroupMessageResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<PageResponse<GroupMessageResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
PageResponse<GroupMessageResponse> pageData = apiResponse.getData();
List<GroupMessageResponse> msgList = pageData.getList();
if (msgList != null && !msgList.isEmpty()) {
// 检查是否有新消息
long lastMsgId = messages.isEmpty() ? 0 : messages.get(messages.size() - 1).getId();
boolean hasNewMessage = false;
for (GroupMessageResponse m : msgList) {
if (m.getId() > lastMsgId) {
GroupChatMessage msg = new GroupChatMessage(
m.getId(),
m.getSenderId(),
m.getSenderName(),
m.getSenderAvatar(),
m.getContent(),
m.getMessageType(),
m.getCreateTime()
);
messages.add(msg);
hasNewMessage = true;
}
}
if (hasNewMessage) {
chatAdapter.submitList(new ArrayList<>(messages));
binding.messagesRecyclerView.scrollToPosition(messages.size() - 1);
updateEmptyState();
}
}
}
}
}
@Override
public void onFailure(Call<ApiResponse<PageResponse<GroupMessageResponse>>> call, Throwable t) {
// 静默失败
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (refreshHandler != null && refreshRunnable != null) {
refreshHandler.removeCallbacks(refreshRunnable);
}
}
/**
* 群聊消息实体
*/
public static class GroupChatMessage {
private long id;
private int senderId;
private String senderName;
private String senderAvatar;
private String content;
private String messageType;
private String createTime;
public GroupChatMessage(long id, int senderId, String senderName, String senderAvatar,
String content, String messageType, String createTime) {
this.id = id;
this.senderId = senderId;
this.senderName = senderName;
this.senderAvatar = senderAvatar;
this.content = content;
this.messageType = messageType;
this.createTime = createTime;
}
public long getId() { return id; }
public int getSenderId() { return senderId; }
public String getSenderName() { return senderName; }
public String getSenderAvatar() { return senderAvatar; }
public String getContent() { return content; }
public String getMessageType() { return messageType; }
public String getCreateTime() { return createTime; }
}
}

View File

@ -0,0 +1,179 @@
package com.example.livestreaming;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
/**
* 群聊消息适配器
*/
public class GroupChatAdapter extends ListAdapter<GroupChatActivity.GroupChatMessage, RecyclerView.ViewHolder> {
private static final int TYPE_SENT = 1;
private static final int TYPE_RECEIVED = 2;
private final int currentUserId;
private OnAvatarClickListener avatarClickListener;
public interface OnAvatarClickListener {
void onAvatarClick(GroupChatActivity.GroupChatMessage message);
}
public GroupChatAdapter(int currentUserId) {
super(DIFF_CALLBACK);
this.currentUserId = currentUserId;
}
public void setOnAvatarClickListener(OnAvatarClickListener listener) {
this.avatarClickListener = listener;
}
private static final DiffUtil.ItemCallback<GroupChatActivity.GroupChatMessage> DIFF_CALLBACK =
new DiffUtil.ItemCallback<GroupChatActivity.GroupChatMessage>() {
@Override
public boolean areItemsTheSame(@NonNull GroupChatActivity.GroupChatMessage oldItem,
@NonNull GroupChatActivity.GroupChatMessage newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull GroupChatActivity.GroupChatMessage oldItem,
@NonNull GroupChatActivity.GroupChatMessage newItem) {
return oldItem.getContent().equals(newItem.getContent());
}
};
@Override
public int getItemViewType(int position) {
GroupChatActivity.GroupChatMessage message = getItem(position);
return message.getSenderId() == currentUserId ? TYPE_SENT : TYPE_RECEIVED;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == TYPE_SENT) {
View view = inflater.inflate(R.layout.item_group_chat_sent, parent, false);
return new SentMessageViewHolder(view);
} else {
View view = inflater.inflate(R.layout.item_group_chat_received, parent, false);
return new ReceivedMessageViewHolder(view, avatarClickListener);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
GroupChatActivity.GroupChatMessage message = getItem(position);
if (holder instanceof SentMessageViewHolder) {
((SentMessageViewHolder) holder).bind(message);
} else if (holder instanceof ReceivedMessageViewHolder) {
((ReceivedMessageViewHolder) holder).bind(message);
}
}
/**
* 发送消息ViewHolder
*/
static class SentMessageViewHolder extends RecyclerView.ViewHolder {
private final TextView messageText;
private final TextView timeText;
private final ImageView avatar;
SentMessageViewHolder(View itemView) {
super(itemView);
messageText = itemView.findViewById(R.id.messageText);
timeText = itemView.findViewById(R.id.timeText);
avatar = itemView.findViewById(R.id.avatar);
}
void bind(GroupChatActivity.GroupChatMessage message) {
messageText.setText(message.getContent());
timeText.setText(formatTime(message.getCreateTime()));
String avatarUrl = message.getSenderAvatar();
if (avatarUrl != null && !avatarUrl.isEmpty()) {
Glide.with(avatar.getContext())
.load(avatarUrl)
.placeholder(R.drawable.ic_person_24)
.error(R.drawable.ic_person_24)
.circleCrop()
.into(avatar);
} else {
avatar.setImageResource(R.drawable.ic_person_24);
}
}
private String formatTime(String time) {
if (time == null) return "";
// 简单处理只显示时间部分
if (time.length() > 16) {
return time.substring(11, 16);
}
return time;
}
}
/**
* 接收消息ViewHolder
*/
static class ReceivedMessageViewHolder extends RecyclerView.ViewHolder {
private final TextView messageText;
private final TextView timeText;
private final TextView senderName;
private final ImageView avatar;
private final OnAvatarClickListener listener;
private GroupChatActivity.GroupChatMessage currentMessage;
ReceivedMessageViewHolder(View itemView, OnAvatarClickListener listener) {
super(itemView);
this.listener = listener;
messageText = itemView.findViewById(R.id.messageText);
timeText = itemView.findViewById(R.id.timeText);
senderName = itemView.findViewById(R.id.senderName);
avatar = itemView.findViewById(R.id.avatar);
avatar.setOnClickListener(v -> {
if (listener != null && currentMessage != null) {
listener.onAvatarClick(currentMessage);
}
});
}
void bind(GroupChatActivity.GroupChatMessage message) {
currentMessage = message;
messageText.setText(message.getContent());
timeText.setText(formatTime(message.getCreateTime()));
senderName.setText(message.getSenderName() != null ? message.getSenderName() : "未知用户");
String avatarUrl = message.getSenderAvatar();
if (avatarUrl != null && !avatarUrl.isEmpty()) {
Glide.with(avatar.getContext())
.load(avatarUrl)
.placeholder(R.drawable.ic_person_24)
.error(R.drawable.ic_person_24)
.circleCrop()
.into(avatar);
} else {
avatar.setImageResource(R.drawable.ic_person_24);
}
}
private String formatTime(String time) {
if (time == null) return "";
if (time.length() > 16) {
return time.substring(11, 16);
}
return time;
}
}
}

View File

@ -87,8 +87,12 @@ public class GroupDetailActivity extends AppCompatActivity {
binding.leaveGroupButton.setOnClickListener(v -> showLeaveGroupDialog());
binding.sendMessageButton.setOnClickListener(v -> {
// TODO: 打开群聊页面
Toast.makeText(this, "群聊功能开发中", Toast.LENGTH_SHORT).show();
// 打开群聊页面
if (groupInfo != null) {
GroupChatActivity.start(this, groupId,
groupInfo.getName(),
groupInfo.getAvatarUrl());
}
});
// 成员列表适配器

View File

@ -348,6 +348,17 @@ public class MainActivity extends AppCompatActivity {
}
});
// 设置搜索按钮点击事件
View searchButton = findViewById(R.id.searchButton);
if (searchButton != null) {
searchButton.setOnClickListener(new DebounceClickListener() {
@Override
public void onDebouncedClick(View v) {
SearchActivity.start(MainActivity.this);
}
});
}
// 设置通知图标点击事件如果存在
try {
View notificationIcon = findViewById(R.id.notificationIcon);
@ -925,6 +936,93 @@ public class MainActivity extends AppCompatActivity {
}
private void showCreateRoomDialog() {
// 先检查登录状态
if (!AuthHelper.requireLogin(this, "创建直播间需要登录")) {
return;
}
// 检查主播资格
checkStreamerStatusAndShowDialog();
}
private void checkStreamerStatusAndShowDialog() {
// 显示加载提示
Toast.makeText(this, "正在检查主播资格...", Toast.LENGTH_SHORT).show();
ApiClient.getService(getApplicationContext()).checkStreamerStatus()
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (!response.isSuccessful() || response.body() == null) {
// 接口调用失败可能是旧版本后端允许继续创建
showCreateRoomDialogInternal();
return;
}
ApiResponse<Map<String, Object>> body = response.body();
if (body.getCode() != 200 || body.getData() == null) {
// 接口返回错误可能是旧版本后端允许继续创建
showCreateRoomDialogInternal();
return;
}
Map<String, Object> data = body.getData();
Boolean isStreamer = data.get("isStreamer") != null && (Boolean) data.get("isStreamer");
Boolean isBanned = data.get("isBanned") != null && (Boolean) data.get("isBanned");
Boolean hasApplication = data.get("hasApplication") != null && (Boolean) data.get("hasApplication");
Object appStatusObj = data.get("applicationStatus");
Integer applicationStatus = appStatusObj != null ? ((Number) appStatusObj).intValue() : null;
if (isBanned) {
// 被封禁
String banReason = (String) data.get("banReason");
new AlertDialog.Builder(MainActivity.this)
.setTitle("无法开播")
.setMessage("您的主播资格已被封禁" + (banReason != null ? "" + banReason : ""))
.setPositiveButton("确定", null)
.show();
return;
}
if (!isStreamer) {
// 不是主播
if (hasApplication && applicationStatus != null && applicationStatus == 0) {
// 有待审核的申请
new AlertDialog.Builder(MainActivity.this)
.setTitle("申请审核中")
.setMessage("您的主播认证申请正在审核中,请耐心等待")
.setPositiveButton("确定", null)
.show();
} else {
// 没有申请或申请被拒绝提示申请认证
new AlertDialog.Builder(MainActivity.this)
.setTitle("需要主播认证")
.setMessage("只有认证主播才能开播,是否现在申请主播认证?")
.setPositiveButton("去申请", (d, w) -> {
// 跳转到主播认证申请页面
StreamerApplyActivity.start(MainActivity.this);
})
.setNegativeButton("取消", null)
.show();
}
return;
}
// 是认证主播显示创建直播间对话框
showCreateRoomDialogInternal();
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
// 网络错误可能是旧版本后端允许继续创建
Log.w(TAG, "检查主播资格失败,允许继续创建", t);
showCreateRoomDialogInternal();
}
});
}
private void showCreateRoomDialogInternal() {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null);
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView);
@ -949,12 +1047,6 @@ public class MainActivity extends AppCompatActivity {
dialog.setOnShowListener(d -> {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
// 检查登录状态创建直播间需要登录
if (!AuthHelper.requireLogin(this, "创建直播间需要登录")) {
dialog.dismiss();
return;
}
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";

View File

@ -137,7 +137,14 @@ public class MessagesActivity extends AppCompatActivity {
conversationsAdapter = new ConversationsAdapter(item -> {
if (item == null) return;
try {
// 启动会话页面传递未读数量和对方用户ID
// 判断是群聊还是私聊
if (item.isGroup()) {
// 群聊跳转到群聊页面
GroupChatActivity.start(this, item.getGroupId(),
item.getTitle().replace("[群] ", ""),
item.getAvatarUrl());
} else {
// 私聊启动会话页面
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra("extra_conversation_id", item.getId());
intent.putExtra("extra_conversation_title", item.getTitle());
@ -147,12 +154,10 @@ public class MessagesActivity extends AppCompatActivity {
// 用户点击会话时减少该会话的未读数量
if (item.getUnreadCount() > 0) {
// 更新该会话的未读数量为0在实际应用中这里应该更新数据源
// 然后更新总未读数量
UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
// 更新列表中的未读数量显示
updateConversationUnreadCount(item.getId(), 0);
}
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "打开会话失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
@ -175,8 +180,20 @@ public class MessagesActivity extends AppCompatActivity {
return;
}
// 清空列表准备重新加载
allConversations.clear();
conversations.clear();
// 加载私聊会话
loadPrivateConversations(token);
// 加载群组列表
loadGroupConversations(token);
}
private void loadPrivateConversations(String token) {
String url = ApiConfig.getBaseUrl() + "/api/front/conversations";
Log.d(TAG, "加载会话列表: " + url);
Log.d(TAG, "加载私聊会话列表: " + url);
Request request = new Request.Builder()
.url(url)
@ -187,45 +204,87 @@ public class MessagesActivity extends AppCompatActivity {
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "加载会话列表失败", e);
runOnUiThread(() -> {
Toast.makeText(MessagesActivity.this, "加载失败,请重试", Toast.LENGTH_SHORT).show();
updateEmptyState();
});
Log.e(TAG, "加载私聊会话列表失败", e);
runOnUiThread(() -> updateConversationsList());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "会话列表响应: " + body);
Log.d(TAG, "私聊会话列表响应: " + body);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
JSONArray data = json.optJSONArray("data");
parseConversations(data);
} else {
String msg = json.optString("message", "加载失败");
Toast.makeText(MessagesActivity.this, msg, Toast.LENGTH_SHORT).show();
updateEmptyState();
parsePrivateConversations(data);
}
} catch (Exception e) {
Log.e(TAG, "解析会话列表失败", e);
updateEmptyState();
Log.e(TAG, "解析私聊会话列表失败", e);
}
updateConversationsList();
});
}
});
}
private void parseConversations(JSONArray data) {
allConversations.clear();
private void loadGroupConversations(String token) {
String url = ApiConfig.getBaseUrl() + "/api/front/groups/list?page=1&pageSize=100";
Log.d(TAG, "加载群组列表: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.get()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "加载群组列表失败", e);
runOnUiThread(() -> updateConversationsList());
}
@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) {
JSONObject data = json.optJSONObject("data");
if (data != null) {
JSONArray list = data.optJSONArray("list");
parseGroupConversations(list);
}
}
} catch (Exception e) {
Log.e(TAG, "解析群组列表失败", e);
}
updateConversationsList();
});
}
});
}
private void parsePrivateConversations(JSONArray data) {
if (data != null) {
for (int i = 0; i < data.length(); i++) {
try {
JSONObject item = data.getJSONObject(i);
String id = String.valueOf(item.opt("id"));
// 后端返回的字段名是 title不是 otherUserName
// 检查是否已存在避免重复
boolean exists = false;
for (ConversationItem conv : allConversations) {
if (conv.getId().equals(id)) {
exists = true;
break;
}
}
if (exists) continue;
String title = item.optString("title", item.optString("otherUserName", "未知用户"));
String lastMessage = item.optString("lastMessage", "");
String timeText = item.optString("timeText", item.optString("lastMessageTime", ""));
@ -234,15 +293,54 @@ public class MessagesActivity extends AppCompatActivity {
String avatarUrl = item.optString("avatarUrl", item.optString("otherUserAvatar", ""));
int otherUserId = item.optInt("otherUserId", 0);
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted);
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted, ConversationItem.TYPE_PRIVATE);
convItem.setOtherUserId(otherUserId);
convItem.setAvatarUrl(avatarUrl);
allConversations.add(convItem);
} catch (Exception e) {
Log.e(TAG, "解析会话项失败", e);
Log.e(TAG, "解析私聊会话项失败", e);
}
}
}
}
private void parseGroupConversations(JSONArray data) {
if (data != null) {
for (int i = 0; i < data.length(); i++) {
try {
JSONObject item = data.getJSONObject(i);
long groupId = item.optLong("id", 0);
String id = "group_" + groupId; // 使用前缀区分群组
// 检查是否已存在避免重复
boolean exists = false;
for (ConversationItem conv : allConversations) {
if (conv.getId().equals(id)) {
exists = true;
break;
}
}
if (exists) continue;
String title = "[群] " + item.optString("name", "未知群组");
String lastMessage = ""; // 群组暂不显示最后消息
String timeText = item.optString("createTime", "");
int unreadCount = 0; // 群组暂不统计未读
boolean isMuted = false;
String avatarUrl = item.optString("avatarUrl", "");
ConversationItem convItem = new ConversationItem(id, title, lastMessage, timeText, unreadCount, isMuted, ConversationItem.TYPE_GROUP);
convItem.setGroupId(groupId);
convItem.setAvatarUrl(avatarUrl);
allConversations.add(convItem);
} catch (Exception e) {
Log.e(TAG, "解析群组项失败", e);
}
}
}
}
private void updateConversationsList() {
conversations.clear();
conversations.addAll(allConversations);
conversationsAdapter.submitList(new ArrayList<>(conversations));
@ -741,14 +839,19 @@ public class MessagesActivity extends AppCompatActivity {
ConversationItem item = conversations.get(i);
if (item != null && item.getId().equals(conversationId)) {
// 创建新的 ConversationItem更新未读数量
conversations.set(i, new ConversationItem(
ConversationItem newItem = new ConversationItem(
item.getId(),
item.getTitle(),
item.getLastMessage(),
item.getTimeText(),
newUnreadCount,
item.isMuted()
));
item.isMuted(),
item.getType()
);
newItem.setOtherUserId(item.getOtherUserId());
newItem.setAvatarUrl(item.getAvatarUrl());
newItem.setGroupId(item.getGroupId());
conversations.set(i, newItem);
break;
}
}

View File

@ -123,6 +123,9 @@ public class ProfileActivity extends AppCompatActivity {
finish();
return true;
}
if (id == R.id.nav_profile) {
return true;
}
return true;
});
}

View File

@ -0,0 +1,187 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.databinding.ActivityStreamerApplyBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import java.util.HashMap;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 主播认证申请页面
*/
public class StreamerApplyActivity extends AppCompatActivity {
private ActivityStreamerApplyBinding binding;
public static void start(Context context) {
Intent intent = new Intent(context, StreamerApplyActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityStreamerApplyBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setupViews();
checkCurrentStatus();
}
private void setupViews() {
binding.backButton.setOnClickListener(v -> finish());
binding.submitButton.setOnClickListener(v -> submitApplication());
}
private void checkCurrentStatus() {
binding.loadingProgress.setVisibility(View.VISIBLE);
binding.contentLayout.setVisibility(View.GONE);
binding.statusLayout.setVisibility(View.GONE);
ApiClient.getService(this).checkStreamerStatus()
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
binding.loadingProgress.setVisibility(View.GONE);
if (!response.isSuccessful() || response.body() == null ||
response.body().getCode() != 200 || response.body().getData() == null) {
// 显示申请表单
binding.contentLayout.setVisibility(View.VISIBLE);
return;
}
Map<String, Object> data = response.body().getData();
Boolean isStreamer = data.get("isStreamer") != null && (Boolean) data.get("isStreamer");
Boolean hasApplication = data.get("hasApplication") != null && (Boolean) data.get("hasApplication");
Object appStatusObj = data.get("applicationStatus");
Integer applicationStatus = appStatusObj != null ? ((Number) appStatusObj).intValue() : null;
if (isStreamer) {
// 已经是主播
binding.statusLayout.setVisibility(View.VISIBLE);
binding.statusIcon.setImageResource(R.drawable.ic_check_circle_24);
binding.statusTitle.setText("您已是认证主播");
binding.statusMessage.setText("您已通过主播认证,可以开始直播了");
binding.statusButton.setText("返回");
binding.statusButton.setOnClickListener(v -> finish());
} else if (hasApplication && applicationStatus != null) {
binding.statusLayout.setVisibility(View.VISIBLE);
if (applicationStatus == 0) {
// 待审核
binding.statusIcon.setImageResource(R.drawable.ic_pending_24);
binding.statusTitle.setText("申请审核中");
binding.statusMessage.setText("您的主播认证申请正在审核中,请耐心等待");
binding.statusButton.setText("返回");
binding.statusButton.setOnClickListener(v -> finish());
} else if (applicationStatus == 2) {
// 被拒绝可以重新申请
binding.contentLayout.setVisibility(View.VISIBLE);
binding.statusLayout.setVisibility(View.GONE);
Toast.makeText(StreamerApplyActivity.this,
"您的上次申请被拒绝,可以重新提交申请", Toast.LENGTH_LONG).show();
} else {
binding.contentLayout.setVisibility(View.VISIBLE);
}
} else {
// 没有申请显示申请表单
binding.contentLayout.setVisibility(View.VISIBLE);
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
binding.loadingProgress.setVisibility(View.GONE);
binding.contentLayout.setVisibility(View.VISIBLE);
}
});
}
private void submitApplication() {
String realName = binding.realNameEdit.getText() != null ?
binding.realNameEdit.getText().toString().trim() : "";
String idCard = binding.idCardEdit.getText() != null ?
binding.idCardEdit.getText().toString().trim() : "";
String intro = binding.introEdit.getText() != null ?
binding.introEdit.getText().toString().trim() : "";
String experience = binding.experienceEdit.getText() != null ?
binding.experienceEdit.getText().toString().trim() : "";
// 验证必填字段
if (TextUtils.isEmpty(realName)) {
binding.realNameLayout.setError("请填写真实姓名");
return;
} else {
binding.realNameLayout.setError(null);
}
if (TextUtils.isEmpty(idCard)) {
binding.idCardLayout.setError("请填写身份证号");
return;
} else if (idCard.length() != 18) {
binding.idCardLayout.setError("身份证号格式不正确");
return;
} else {
binding.idCardLayout.setError(null);
}
// 禁用提交按钮
binding.submitButton.setEnabled(false);
binding.submitButton.setText("提交中...");
// 构建请求参数
Map<String, Object> body = new HashMap<>();
body.put("realName", realName);
body.put("idCard", idCard);
body.put("intro", intro);
body.put("experience", experience);
ApiClient.getService(this).applyStreamer(body)
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
binding.submitButton.setEnabled(true);
binding.submitButton.setText("提交申请");
if (response.isSuccessful() && response.body() != null &&
response.body().getCode() == 200) {
// 申请成功
binding.contentLayout.setVisibility(View.GONE);
binding.statusLayout.setVisibility(View.VISIBLE);
binding.statusIcon.setImageResource(R.drawable.ic_pending_24);
binding.statusTitle.setText("申请已提交");
binding.statusMessage.setText("您的主播认证申请已提交我们将在1-3个工作日内完成审核");
binding.statusButton.setText("返回");
binding.statusButton.setOnClickListener(v -> finish());
} else {
String msg = response.body() != null ? response.body().getMessage() : "提交失败";
Toast.makeText(StreamerApplyActivity.this, msg, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
binding.submitButton.setEnabled(true);
binding.submitButton.setText("提交申请");
Toast.makeText(StreamerApplyActivity.this, "网络错误,请重试", Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@ -13,8 +13,12 @@ import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import com.example.livestreaming.net.Room;
import java.util.Random;
public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapter.RoomVH> {
public interface OnRoomClickListener {
@ -23,6 +27,9 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
private final OnRoomClickListener onRoomClick;
// 预定义的图片高度比例模拟瀑布流效果
private static final float[] HEIGHT_RATIOS = {1.0f, 1.2f, 0.9f, 1.3f, 1.1f, 0.85f, 1.15f, 1.25f};
public WaterfallRoomsAdapter(OnRoomClickListener onRoomClick) {
super(DIFF);
this.onRoomClick = onRoomClick;
@ -38,7 +45,7 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
@Override
public void onBindViewHolder(@NonNull RoomVH holder, int position) {
holder.bind(getItem(position));
holder.bind(getItem(position), position);
}
static class RoomVH extends RecyclerView.ViewHolder {
@ -50,7 +57,10 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
private final TextView roomTitle;
private final ImageView streamerAvatar;
private final TextView streamerName;
private final ImageView likeIcon;
private final TextView likeCount;
private final TextView hotBadge;
private final ImageView playIcon;
private final OnRoomClickListener onRoomClick;
RoomVH(View itemView, OnRoomClickListener onRoomClick) {
@ -64,54 +74,62 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
roomTitle = itemView.findViewById(R.id.roomTitle);
streamerAvatar = itemView.findViewById(R.id.streamerAvatar);
streamerName = itemView.findViewById(R.id.streamerName);
likeIcon = itemView.findViewById(R.id.likeIcon);
likeCount = itemView.findViewById(R.id.likeCount);
hotBadge = itemView.findViewById(R.id.hotBadge);
playIcon = itemView.findViewById(R.id.playIcon);
}
void bind(Room room) {
// TODO: 接入后端接口 - 从后端获取房间封面图片URL
// 接口路径: GET /api/rooms/{roomId}/cover
// 请求参数: roomId (路径参数)
// 返回数据格式: ApiResponse<{coverUrl: string}>
// 或者Room对象应包含coverUrl字段直接从room.getCoverUrl()获取
// TODO: 接入后端接口 - 从后端获取主播头像URL
// 接口路径: GET /api/user/profile/{streamerId}
// 请求参数: streamerId (路径参数从Room对象中获取streamerId)
// 返回数据格式: ApiResponse<{avatarUrl: string}>
// TODO: 接入后端接口 - 获取房间观看人数
// 接口路径: GET /api/rooms/{roomId}/viewers/count
// 请求参数: roomId (路径参数)
// 返回数据格式: ApiResponse<{viewerCount: number}>
// 或者Room对象应包含viewerCount字段直接从room.getViewerCount()获取
void bind(Room room, int position) {
if (room == null) return;
// 设置标题
roomTitle.setText(room.getTitle() != null ? room.getTitle() : "(无标题)");
String title = room.getTitle();
if (title == null || title.isEmpty()) {
title = generateRandomTitle(position);
}
roomTitle.setText(title);
// 设置主播名称
streamerName.setText(room.getStreamerName() != null ? room.getStreamerName() : "");
String name = room.getStreamerName();
if (name == null || name.isEmpty()) {
name = generateRandomName(position);
}
streamerName.setText(name);
// 加载封面图片
String seed = room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
// 计算随机高度实现瀑布流效果
String seed = room.getId() != null ? room.getId() : String.valueOf(position);
int h = Math.abs(seed.hashCode());
int imgIndex = (h % 10);
String[] colors = {"ff6b6b", "4ecdc4", "45b7d1", "96ceb4", "ffeaa7", "dfe6e9", "fd79a8", "a29bfe", "00b894", "e17055"};
String color = colors[imgIndex];
String imageUrl = "https://placehold.co/600x450/" + color + "/ffffff?text=LIVE";
Glide.with(coverImage)
.load(imageUrl)
.placeholder(R.drawable.bg_cover_placeholder)
.centerCrop()
.into(coverImage);
float ratio = HEIGHT_RATIOS[h % HEIGHT_RATIOS.length];
int baseHeight = (int) (itemView.getContext().getResources().getDisplayMetrics().density * 150);
int imageHeight = (int) (baseHeight * ratio);
ViewGroup.LayoutParams params = coverImage.getLayoutParams();
params.height = imageHeight;
coverImage.setLayoutParams(params);
// 加载封面图片 - 使用更真实的图片
loadCoverImage(room, position, h);
// 加载主播头像
loadAvatarImage(position);
// 设置点赞数
int likes = (h % 500) + 10;
likeCount.setText(formatNumber(likes));
likeIcon.setVisibility(View.VISIBLE);
likeCount.setVisibility(View.VISIBLE);
// 设置直播状态
if (room.isLive()) {
liveBadge.setVisibility(View.VISIBLE);
viewerCountLayout.setVisibility(View.VISIBLE);
int viewers = getViewerCount(room);
viewerCount.setText(String.valueOf(viewers));
int viewers = (h % 380) + 5;
viewerCount.setText(formatNumber(viewers));
playIcon.setVisibility(View.GONE);
// 如果观看人数超过100显示热门标签
if (viewers > 100) {
// 热门标签
if (viewers > 200) {
hotBadge.setVisibility(View.VISIBLE);
} else {
hotBadge.setVisibility(View.GONE);
@ -119,6 +137,7 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
} else {
liveBadge.setVisibility(View.GONE);
viewerCountLayout.setVisibility(View.GONE);
playIcon.setVisibility(View.VISIBLE);
hotBadge.setVisibility(View.GONE);
}
@ -130,14 +149,69 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
});
}
private int getViewerCount(Room room) {
try {
String seed = room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
int h = Math.abs(seed.hashCode());
return (h % 380) + 5;
} catch (Exception ignored) {
return 0;
private void loadCoverImage(Room room, int position, int hash) {
// 使用多种图片源让内容更丰富
String[] imageUrls = {
"https://picsum.photos/seed/" + (hash % 1000) + "/400/500",
"https://picsum.photos/seed/" + ((hash + 100) % 1000) + "/400/600",
"https://picsum.photos/seed/" + ((hash + 200) % 1000) + "/400/450",
};
String imageUrl = imageUrls[position % imageUrls.length];
Glide.with(coverImage.getContext())
.load(imageUrl)
.apply(new RequestOptions()
.placeholder(R.drawable.bg_cover_placeholder)
.error(R.drawable.bg_cover_placeholder)
.centerCrop())
.into(coverImage);
}
private void loadAvatarImage(int position) {
String avatarUrl = "https://i.pravatar.cc/100?img=" + ((position % 70) + 1);
Glide.with(streamerAvatar.getContext())
.load(avatarUrl)
.apply(new RequestOptions()
.placeholder(R.drawable.ic_account_circle_24)
.error(R.drawable.ic_account_circle_24)
.circleCrop())
.into(streamerAvatar);
}
private String generateRandomTitle(int position) {
String[] titles = {
"#健身穿搭 #一万种健与美 #健身女孩 #完美身材",
"避雷秀厢附近租房",
"找线下收U换现 可长期合作 有实力的私面聊",
"我提笔不为离愁 只为你转身回眸",
"今日穿搭分享 #日常穿搭 #时尚",
"周末vlog #生活记录 #美好生活",
"美食探店 #吃货日常 #美食推荐",
"旅行日记 #风景 #旅行攻略",
"护肤心得分享 #护肤 #美妆",
"读书笔记 #好书推荐 #阅读"
};
return titles[position % titles.length];
}
private String generateRandomName(int position) {
String[] names = {
"小桃兔兔", "别管我了", "火火", "RicLei",
"甜甜圈", "小确幸", "追光者", "星河漫步",
"清风徐来", "月光宝盒"
};
return names[position % names.length];
}
private String formatNumber(int num) {
if (num >= 10000) {
return String.format("%.1fw", num / 10000.0);
} else if (num >= 1000) {
return String.format("%.1fk", num / 1000.0);
}
return String.valueOf(num);
}
}

View File

@ -471,4 +471,43 @@ public interface ApiService {
*/
@GET("api/front/wishtree/backgrounds")
Call<ApiResponse<List<WishtreeResponse.Background>>> getWishtreeBackgrounds();
// ==================== 群消息接口 ====================
/**
* 获取群消息列表
*/
@GET("api/front/groups/{groupId}/messages")
Call<ApiResponse<PageResponse<GroupMessageResponse>>> getGroupMessages(
@Path("groupId") long groupId,
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 发送群消息
*/
@POST("api/front/groups/{groupId}/messages")
Call<ApiResponse<GroupMessageResponse>> sendGroupMessage(
@Path("groupId") long groupId,
@Body Map<String, Object> body);
// ==================== 主播认证接口 ====================
/**
* 检查主播资格
*/
@GET("api/front/streamer/check")
Call<ApiResponse<Map<String, Object>>> checkStreamerStatus();
/**
* 提交主播认证申请
*/
@POST("api/front/streamer/apply")
Call<ApiResponse<Map<String, Object>>> applyStreamer(@Body Map<String, Object> body);
/**
* 获取我的申请记录
*/
@GET("api/front/streamer/applications")
Call<ApiResponse<List<Map<String, Object>>>> getStreamerApplications();
}

View File

@ -0,0 +1,42 @@
package com.example.livestreaming.net;
import com.google.gson.annotations.SerializedName;
/**
* 群消息响应
*/
public class GroupMessageResponse {
@SerializedName("id")
private Long id;
@SerializedName("groupId")
private Long groupId;
@SerializedName("senderId")
private Integer senderId;
@SerializedName("senderName")
private String senderName;
@SerializedName("senderAvatar")
private String senderAvatar;
@SerializedName("content")
private String content;
@SerializedName("messageType")
private String messageType;
@SerializedName("createTime")
private String createTime;
public Long getId() { return id != null ? id : 0; }
public Long getGroupId() { return groupId; }
public Integer getSenderId() { return senderId != null ? senderId : 0; }
public String getSenderName() { return senderName; }
public String getSenderAvatar() { return senderAvatar; }
public String getContent() { return content; }
public String getMessageType() { return messageType != null ? messageType : "text"; }
public String getCreateTime() { return createTime; }
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/purple_500" android:state_checked="true" />
<item android:color="#8A8A8A" />
<item android:color="#FF4757" android:state_checked="true" />
<item android:color="#999999" />
</selector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners
android:topLeftRadius="4dp"
android:topRightRadius="16dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#7C4DFF" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="4dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp" />
</shape>

View File

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#E9ECEF" />
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#FFE8E8"
android:endColor="#E8F4FF"
android:angle="135" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFF0F0" />
<corners android:radius="4dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF4757" />
<corners android:radius="10dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFF8E1" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#66000000" />
<corners android:radius="10dp" />
</shape>

View 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="#4CAF50"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View 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="#999999"
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
</vector>

View 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="#FF9800"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2V7h2v2z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?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="#FF9800"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
<path
android:fillColor="#FF9800"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

View 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="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"/>
</vector>

View File

@ -0,0 +1,11 @@
<?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"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@ -0,0 +1,11 @@
<?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"
android:tint="@android:color/white">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@ -0,0 +1,116 @@
<?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="@color/background_light">
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="8dp"
android:background="@color/white"
android:elevation="2dp">
<ImageButton
android:id="@+id/backButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_arrow_back_24"
android:contentDescription="返回"
app:tint="@color/text_primary" />
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="群聊"
android:textSize="18sp"
android:textColor="@color/text_primary"
android:textStyle="bold"
android:gravity="center"
android:maxLines="1"
android:ellipsize="end" />
<ImageButton
android:id="@+id/groupInfoButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_group_24"
android:contentDescription="群组信息"
app:tint="@color/text_primary" />
</LinearLayout>
<!-- 消息列表 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messagesRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.example.livestreaming.EmptyStateView
android:id="@+id/emptyStateView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<ProgressBar
android:id="@+id/loadingProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
<!-- 输入栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="@color/white"
android:elevation="4dp">
<EditText
android:id="@+id/messageInput"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:background="@drawable/bg_edit_text"
android:paddingHorizontal="16dp"
android:hint="输入消息..."
android:textSize="14sp"
android:maxLines="3"
android:inputType="textMultiLine" />
<ImageButton
android:id="@+id/sendButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:background="@drawable/bg_button_primary"
android:src="@drawable/ic_send_24"
android:contentDescription="发送"
app:tint="@color/white" />
</LinearLayout>
</LinearLayout>

View File

@ -14,7 +14,8 @@
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white">
android:background="@android:color/white"
app:elevation="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -38,13 +39,14 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:tabIndicatorColor="@color/purple_500"
app:tabIndicatorColor="#FF4757"
app:tabIndicatorFullWidth="false"
app:tabIndicatorHeight="0dp"
app:tabSelectedTextColor="#666666"
app:tabTextColor="#666666"
app:tabIndicatorHeight="3dp"
app:tabSelectedTextColor="#333333"
app:tabTextColor="#999999"
app:tabRippleColor="@android:color/transparent"
app:layout_constraintEnd_toStartOf="@id/avatarButton"
app:tabTextAppearance="@style/TabTextAppearance"
app:layout_constraintEnd_toStartOf="@id/searchButton"
app:layout_constraintStart_toEndOf="@id/menuButton"
app:layout_constraintTop_toTopOf="parent">
@ -64,6 +66,18 @@
android:text="附近" />
</com.google.android.material.tabs.TabLayout>
<ImageView
android:id="@+id/searchButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:contentDescription="搜索"
android:src="@drawable/ic_search_24"
app:tint="#333333"
app:layout_constraintBottom_toBottomOf="@id/topTabs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/topTabs" />
<ImageView
android:id="@+id/avatarButton"
android:layout_width="28dp"
@ -74,10 +88,12 @@
android:contentDescription="avatar"
android:scaleType="centerCrop"
android:src="@drawable/ic_account_circle_24"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/topTabs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/topTabs" />
<!-- 搜索框 - 隐藏,点击搜索图标时跳转搜索页 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/searchContainer"
android:layout_width="0dp"
@ -86,6 +102,7 @@
android:layout_marginEnd="16dp"
android:layout_marginTop="10dp"
android:background="@drawable/bg_search"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topTabs">
@ -134,14 +151,15 @@
android:id="@+id/categoryTabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginTop="6dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:tabIndicatorColor="@color/purple_500"
android:visibility="gone"
app:tabIndicatorColor="#FF4757"
app:tabIndicatorFullWidth="false"
app:tabMode="scrollable"
app:tabSelectedTextColor="#111111"
app:tabTextColor="#666666"
app:tabSelectedTextColor="#333333"
app:tabTextColor="#999999"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/searchContainer">
@ -222,13 +240,17 @@
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddLive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="88dp"
android:contentDescription="添加直播"
android:contentDescription="发布"
android:src="@drawable/ic_add_24"
app:backgroundTint="@color/purple_500"
android:visibility="gone"
app:backgroundTint="#FF4757"
app:elevation="6dp"
app:fabSize="normal"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Fab.Round"
app:tint="@android:color/white" />
<include

View File

@ -0,0 +1,229 @@
<?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:background="@android:color/white"
android:orientation="vertical">
<!-- 顶部标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/backButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="返回"
android:src="@drawable/ic_arrow_back_24"
app:tint="#333333" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:text="主播认证"
android:textColor="#333333"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#F0F0F0" />
<!-- 加载中 -->
<ProgressBar
android:id="@+id/loadingProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="100dp"
android:visibility="gone" />
<!-- 状态显示 -->
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
android:visibility="gone">
<ImageView
android:id="@+id/statusIcon"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_pending_24"
app:tint="#FF4757" />
<TextView
android:id="@+id/statusTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="申请审核中"
android:textColor="#333333"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/statusMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="您的主播认证申请正在审核中"
android:textColor="#666666"
android:textSize="14sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/statusButton"
android:layout_width="200dp"
android:layout_height="48dp"
android:layout_marginTop="32dp"
android:text="返回"
app:backgroundTint="#FF4757"
app:cornerRadius="24dp" />
</LinearLayout>
<!-- 申请表单 -->
<ScrollView
android:id="@+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- 提示信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_tip_card"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_info_24"
app:tint="#FF9800" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="成为认证主播后您可以创建直播间进行直播。请如实填写以下信息我们将在1-3个工作日内完成审核。"
android:textColor="#666666"
android:textSize="13sp" />
</LinearLayout>
<!-- 真实姓名 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/realNameLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:hint="真实姓名 *">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/realNameEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:maxLength="20" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 身份证号 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/idCardLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="身份证号 *">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/idCardEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapCharacters"
android:maxLength="18" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 主播简介 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/introLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="主播简介">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/introEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:maxLength="200"
android:minLines="3" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 直播经验 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/experienceLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="直播经验(选填)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/experienceEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:maxLength="500"
android:minLines="3" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 提交按钮 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/submitButton"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="32dp"
android:text="提交申请"
android:textSize="16sp"
app:backgroundTint="#FF4757"
app:cornerRadius="26dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="提交申请即表示您同意《主播服务协议》"
android:textColor="#999999"
android:textSize="12sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="start"
android:paddingVertical="4dp">
<ImageView
android:id="@+id/avatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/ic_person_24"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="2dp">
<TextView
android:id="@+id/senderName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="@color/text_secondary" />
<TextView
android:id="@+id/timeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/text_hint"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView
android:id="@+id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:padding="12dp"
android:background="@drawable/bg_chat_bubble_received"
android:textSize="14sp"
android:textColor="@color/text_primary" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:paddingVertical="4dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:layout_marginEnd="8dp">
<TextView
android:id="@+id/timeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/text_hint"
android:layout_marginBottom="2dp" />
<TextView
android:id="@+id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:padding="12dp"
android:background="@drawable/bg_chat_bubble_sent"
android:textSize="14sp"
android:textColor="@color/white" />
</LinearLayout>
<ImageView
android:id="@+id/avatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/ic_person_24"
android:scaleType="centerCrop" />
</LinearLayout>

View File

@ -1,148 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="6dp"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
app:cardElevation="0dp"
app:strokeWidth="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 封面图区域 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 封面图 -->
<ImageView
android:id="@+id/coverImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="封面"
android:minHeight="120dp"
android:scaleType="centerCrop"
android:src="@android:drawable/ic_menu_gallery"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
tools:src="@android:drawable/ic_menu_gallery" />
<!-- 直播标签 -->
<!-- 直播标签 - 左上角 -->
<TextView
android:id="@+id/liveBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_margin="8dp"
android:background="@drawable/live_badge_background"
android:background="@drawable/bg_live_badge_red"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:paddingVertical="3dp"
android:text="直播中"
android:textColor="@android:color/white"
android:textSize="10sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
tools:visibility="visible" />
<!-- 观看人数 -->
<!-- 观看人数 - 右上角 -->
<LinearLayout
android:id="@+id/viewerCountLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:background="@drawable/viewer_count_background"
android:gravity="center"
android:minWidth="56dp"
android:background="@drawable/bg_viewer_count"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:paddingHorizontal="6dp"
android:paddingVertical="3dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
tools:visibility="visible">
<ImageView
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_width="10dp"
android:layout_height="10dp"
android:contentDescription="观看"
android:src="@android:drawable/ic_menu_view"
android:tint="@android:color/white" />
android:src="@drawable/ic_visibility_white"
app:tint="@android:color/white" />
<TextView
android:id="@+id/viewerCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:minEms="3"
android:layout_marginStart="3dp"
android:text="0"
android:textColor="@android:color/white"
android:textSize="10sp" />
android:textSize="10sp"
tools:text="359" />
</LinearLayout>
<!-- 视频播放图标 - 右下角 -->
<ImageView
android:id="@+id/playIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:contentDescription="播放"
android:src="@drawable/ic_play_circle"
android:visibility="gone"
app:tint="@android:color/white" />
</FrameLayout>
<!-- 底部信息区域 -->
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/coverImage">
android:paddingHorizontal="10dp"
android:paddingTop="10dp"
android:paddingBottom="12dp">
<!-- 标题 -->
<!-- 标题/内容 -->
<TextView
android:id="@+id/roomTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lineSpacingExtra="2dp"
android:maxLines="2"
android:text="直播间标题"
android:textColor="#333333"
android:textSize="14sp"
android:textStyle="bold" />
tools:text="#健身穿搭 #一万种健与美 #健身女孩 #完美身材 #..." />
<!-- 主播信息 -->
<!-- 主播信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- 主播头像 -->
<ImageView
android:id="@+id/streamerAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="主播头像"
android:src="@android:drawable/ic_menu_myplaces"
android:tint="@color/purple_500" />
android:layout_width="20dp"
android:layout_height="20dp"
android:background="@drawable/bg_avatar_circle"
android:clipToOutline="true"
android:contentDescription="头像"
android:scaleType="centerCrop"
android:src="@drawable/ic_account_circle_24" />
<!-- 主播名称 -->
<TextView
android:id="@+id/streamerName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="主播名称"
android:textColor="#666666"
android:textSize="12sp" />
android:textColor="#999999"
android:textSize="12sp"
tools:text="小桃兔兔" />
<!-- 热度标签 -->
<!-- 点赞图标 -->
<ImageView
android:id="@+id/likeIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:contentDescription="点赞"
android:src="@drawable/ic_favorite_border"
app:tint="#999999" />
<!-- 点赞数 -->
<TextView
android:id="@+id/likeCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:textColor="#999999"
android:textSize="12sp"
tools:text="359" />
<!-- 热门标签 -->
<TextView
android:id="@+id/hotBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/hot_badge_background"
android:paddingHorizontal="6dp"
android:paddingVertical="2dp"
android:layout_marginStart="6dp"
android:background="@drawable/bg_hot_badge"
android:paddingHorizontal="5dp"
android:paddingVertical="1dp"
android:text="热"
android:textColor="@color/live_red"
android:textSize="10sp"
android:textColor="#FF6B6B"
android:textSize="9sp"
android:textStyle="bold"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -9,7 +9,7 @@
<item
android:id="@+id/nav_friends"
android:icon="@drawable/ic_people_24"
android:title="缘池" />
android:title="市集" />
<item
android:id="@+id/nav_wish_tree"
@ -24,6 +24,6 @@
<item
android:id="@+id/nav_profile"
android:icon="@drawable/ic_person_24"
android:title="我" />
android:title="我" />
</menu>

View File

@ -13,5 +13,7 @@
<color name="green">#34C759</color>
<color name="text_primary">#212121</color>
<color name="text_secondary">#757575</color>
<color name="text_hint">#9E9E9E</color>
<color name="background_color">#F5F5F5</color>
<color name="background_light">#FAFAFA</color>
</resources>

View File

@ -1,8 +1,8 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.LiveStreaming" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">#FF4757</item>
<item name="colorPrimaryVariant">#E84152</item>
<item name="colorOnPrimary">@android:color/white</item>
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
@ -35,9 +35,20 @@
<item name="cornerSize">50%</item>
</style>
<!-- FAB圆形样式 -->
<style name="ShapeAppearanceOverlay.Fab.Round" parent="">
<item name="cornerSize">50%</item>
</style>
<!-- 底部导航栏文字样式 -->
<style name="BottomNavigationView.TextAppearance" parent="TextAppearance.AppCompat">
<item name="android:textSize">10sp</item>
</style>
<!-- 顶部Tab文字样式 -->
<style name="TabTextAppearance" parent="TextAppearance.Design.Tab">
<item name="android:textSize">16sp</item>
<item name="textAllCaps">false</item>
</style>
</resources>

68
group_tables.sql Normal file
View File

@ -0,0 +1,68 @@
-- 群组系统数据库表
-- 请在服务器数据库中执行此脚本
-- 1. 群组表
CREATE TABLE IF NOT EXISTS `eb_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '群组ID',
`group_name` varchar(100) NOT NULL COMMENT '群组名称',
`avatar` varchar(500) DEFAULT NULL COMMENT '群头像',
`description` varchar(500) DEFAULT NULL COMMENT '群描述',
`announcement` varchar(1000) DEFAULT NULL COMMENT '群公告',
`owner_id` int(11) NOT NULL COMMENT '群主用户ID',
`member_count` int(11) NOT NULL DEFAULT 1 COMMENT '成员数量',
`max_members` int(11) NOT NULL DEFAULT 500 COMMENT '最大成员数',
`mute_all` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否全员禁言',
`allow_member_invite` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否允许成员邀请',
`is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_owner_id` (`owner_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群组表';
-- 2. 群组成员表
CREATE TABLE IF NOT EXISTS `eb_group_member` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` bigint(20) NOT NULL COMMENT '群组ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`role` tinyint(1) NOT NULL DEFAULT 0 COMMENT '角色0-普通成员 1-管理员 2-群主',
`nickname` varchar(50) DEFAULT NULL COMMENT '群内昵称',
`is_muted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否被禁言',
`is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已退出',
`join_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_user` (`group_id`, `user_id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群组成员表';
-- 3. 群消息表
CREATE TABLE IF NOT EXISTS `eb_group_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '消息ID',
`group_id` bigint(20) NOT NULL COMMENT '群组ID',
`sender_id` int(11) NOT NULL COMMENT '发送者用户ID',
`content` text NOT NULL COMMENT '消息内容',
`message_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '消息类型text/image/voice/video/file',
`extra` varchar(1000) DEFAULT NULL COMMENT '扩展信息JSON格式',
`is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间',
PRIMARY KEY (`id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_sender_id` (`sender_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群消息表';
-- 4. 群消息已读记录表(可选,用于已读回执)
CREATE TABLE IF NOT EXISTS `eb_group_message_read` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` bigint(20) NOT NULL COMMENT '群组ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`last_read_message_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '最后已读消息ID',
`read_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_user` (`group_id`, `user_id`),
KEY `idx_group_id` (`group_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='群消息已读记录表';

74
streamer_menu.sql Normal file
View File

@ -0,0 +1,74 @@
-- 主播管理菜单配置
-- 请在数据库中执行此脚本
-- 1. 首先查找直播管理的菜单ID
SELECT id, pid, name, component FROM eb_system_menu WHERE name = '直播管理' OR component LIKE '%liveManage%';
-- 2. 查找直播管理下的子菜单,确认结构
SELECT id, pid, name, component, sort FROM eb_system_menu WHERE pid IN (
SELECT id FROM eb_system_menu WHERE name = '直播管理' OR component LIKE '%liveManage%'
) ORDER BY sort DESC;
-- 3. 添加主播管理菜单(需要根据上面查询结果修改 pid 值)
-- 假设直播管理的菜单ID是 @live_manage_id请根据实际查询结果替换
-- 方式一如果知道直播管理的ID例如是 XXX直接插入
-- INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
-- VALUES (XXX, '主播管理', '', 'admin:streamer:list', '/liveManage/streamer/list', 'C', 10, 1, 0, NOW(), NOW());
-- 方式二:使用变量(推荐)
SET @live_manage_id = (SELECT id FROM eb_system_menu WHERE name = '直播管理' LIMIT 1);
-- 检查是否已存在主播管理菜单
SELECT * FROM eb_system_menu WHERE name = '主播管理' AND pid = @live_manage_id;
-- 如果不存在,插入主播管理菜单
INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
SELECT @live_manage_id, '主播管理', '', 'admin:streamer:list', '/liveManage/streamer/list', 'C', 5, 1, 0, NOW(), NOW()
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM eb_system_menu WHERE name = '主播管理' AND pid = @live_manage_id);
-- 4. 获取新插入的菜单ID
SET @streamer_menu_id = (SELECT id FROM eb_system_menu WHERE name = '主播管理' AND pid = @live_manage_id LIMIT 1);
-- 5. 添加主播管理的权限按钮(可选)
INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
SELECT @streamer_menu_id, '主播列表', '', 'admin:streamer:list', '', 'A', 1, 1, 0, NOW(), NOW()
FROM DUAL
WHERE @streamer_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM eb_system_menu WHERE perms = 'admin:streamer:list' AND menu_type = 'A');
INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
SELECT @streamer_menu_id, '主播详情', '', 'admin:streamer:detail', '', 'A', 2, 1, 0, NOW(), NOW()
FROM DUAL
WHERE @streamer_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM eb_system_menu WHERE perms = 'admin:streamer:detail');
INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
SELECT @streamer_menu_id, '封禁主播', '', 'admin:streamer:ban', '', 'A', 3, 1, 0, NOW(), NOW()
FROM DUAL
WHERE @streamer_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM eb_system_menu WHERE perms = 'admin:streamer:ban');
INSERT INTO `eb_system_menu` (`pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `create_time`, `update_time`)
SELECT @streamer_menu_id, '解封主播', '', 'admin:streamer:unban', '', 'A', 4, 1, 0, NOW(), NOW()
FROM DUAL
WHERE @streamer_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM eb_system_menu WHERE perms = 'admin:streamer:unban');
-- 6. 给超级管理员角色分配权限角色ID通常是1
-- 先查询角色菜单关联表
SELECT * FROM eb_system_role_menu WHERE rid = 1 ORDER BY menu_id DESC LIMIT 10;
-- 添加菜单权限到超级管理员角色
INSERT IGNORE INTO eb_system_role_menu (rid, menu_id)
SELECT 1, id FROM eb_system_menu WHERE name = '主播管理' OR perms LIKE 'admin:streamer:%';
-- 7. 验证结果
SELECT m.id, m.pid, m.name, m.perms, m.component, m.menu_type
FROM eb_system_menu m
WHERE m.name = '主播管理' OR m.perms LIKE 'admin:streamer:%'
ORDER BY m.id;
-- 8. 清除Redis缓存需要在Redis中执行
-- DEL menuList
-- 提示执行完SQL后需要清除Redis缓存才能看到新菜单
-- 可以在Redis中执行: DEL menuList
-- 或者重启后端服务

61
streamer_tables.sql Normal file
View File

@ -0,0 +1,61 @@
-- 主播认证系统数据库表
-- 请在服务器数据库中执行此脚本
-- 1. 在用户表中添加主播相关字段
-- 注意:如果字段已存在会报错,可以忽略
-- 添加主播认证状态字段
ALTER TABLE `eb_user` ADD COLUMN `is_streamer` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否是认证主播0-否 1-是';
-- 添加主播认证时间
ALTER TABLE `eb_user` ADD COLUMN `streamer_certified_time` datetime DEFAULT NULL COMMENT '主播认证时间';
-- 添加主播等级
ALTER TABLE `eb_user` ADD COLUMN `streamer_level` int(11) NOT NULL DEFAULT 0 COMMENT '主播等级0-普通 1-初级 2-中级 3-高级 4-金牌';
-- 添加主播简介
ALTER TABLE `eb_user` ADD COLUMN `streamer_intro` varchar(500) DEFAULT NULL COMMENT '主播简介';
-- 添加主播标签
ALTER TABLE `eb_user` ADD COLUMN `streamer_tags` varchar(200) DEFAULT NULL COMMENT '主播标签,逗号分隔';
-- 2. 主播认证申请表
CREATE TABLE IF NOT EXISTS `eb_streamer_application` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '申请ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`real_name` varchar(50) NOT NULL COMMENT '真实姓名',
`id_card` varchar(20) NOT NULL COMMENT '身份证号',
`id_card_front` varchar(500) DEFAULT NULL COMMENT '身份证正面照片',
`id_card_back` varchar(500) DEFAULT NULL COMMENT '身份证背面照片',
`intro` varchar(500) DEFAULT NULL COMMENT '主播简介',
`experience` varchar(1000) DEFAULT NULL COMMENT '直播经验描述',
`category_ids` varchar(100) DEFAULT NULL COMMENT '擅长分类ID逗号分隔',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态0-待审核 1-审核通过 2-审核拒绝',
`reject_reason` varchar(200) DEFAULT NULL COMMENT '拒绝原因',
`reviewer_id` int(11) DEFAULT NULL COMMENT '审核人ID',
`review_time` datetime DEFAULT NULL COMMENT '审核时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主播认证申请表';
-- 3. 主播封禁记录表
CREATE TABLE IF NOT EXISTS `eb_streamer_ban` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录ID',
`user_id` int(11) NOT NULL COMMENT '主播用户ID',
`ban_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '封禁类型1-临时封禁 2-永久封禁',
`ban_reason` varchar(500) NOT NULL COMMENT '封禁原因',
`ban_start_time` datetime NOT NULL COMMENT '封禁开始时间',
`ban_end_time` datetime DEFAULT NULL COMMENT '封禁结束时间(永久封禁为空)',
`operator_id` int(11) NOT NULL COMMENT '操作人ID',
`is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否生效0-已解除 1-生效中',
`unban_time` datetime DEFAULT NULL COMMENT '解封时间',
`unban_operator_id` int(11) DEFAULT NULL COMMENT '解封操作人ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_is_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主播封禁记录表';