feat: 添加主播管理、群组管理功能及Android端相关页面
This commit is contained in:
parent
f7a0291b8c
commit
3209a6b1bc
43
Zhibo/admin/src/api/group.js
Normal file
43
Zhibo/admin/src/api/group.js
Normal 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'
|
||||
})
|
||||
}
|
||||
77
Zhibo/admin/src/api/streamer.js
Normal file
77
Zhibo/admin/src/api/streamer.js
Normal 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'
|
||||
})
|
||||
}
|
||||
|
|
@ -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: '' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: '' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
261
Zhibo/admin/src/views/group/list/index.vue
Normal file
261
Zhibo/admin/src/views/group/list/index.vue
Normal 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>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
188
Zhibo/admin/src/views/streamer/list/index.vue
Normal file
188
Zhibo/admin/src/views/streamer/list/index.vue
Normal 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>
|
||||
|
|
@ -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);
|
||||
|
||||
// 待处理请求数
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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("发送群消息失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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("获取申请记录失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
34
add_streamer_menu.sql
Normal 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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
// 成员列表适配器
|
||||
|
|
|
|||
|
|
@ -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() : "";
|
||||
|
||||
|
|
|
|||
|
|
@ -137,21 +137,26 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
conversationsAdapter = new ConversationsAdapter(item -> {
|
||||
if (item == null) return;
|
||||
try {
|
||||
// 启动会话页面,传递未读数量和对方用户ID
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra("extra_conversation_id", item.getId());
|
||||
intent.putExtra("extra_conversation_title", item.getTitle());
|
||||
intent.putExtra("extra_unread_count", item.getUnreadCount());
|
||||
intent.putExtra("other_user_id", item.getOtherUserId());
|
||||
startActivity(intent);
|
||||
|
||||
// 用户点击会话时,减少该会话的未读数量
|
||||
if (item.getUnreadCount() > 0) {
|
||||
// 更新该会话的未读数量为0(在实际应用中,这里应该更新数据源)
|
||||
// 然后更新总未读数量
|
||||
UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
|
||||
// 更新列表中的未读数量显示
|
||||
updateConversationUnreadCount(item.getId(), 0);
|
||||
// 判断是群聊还是私聊
|
||||
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());
|
||||
intent.putExtra("extra_unread_count", item.getUnreadCount());
|
||||
intent.putExtra("other_user_id", item.getOtherUserId());
|
||||
startActivity(intent);
|
||||
|
||||
// 用户点击会话时,减少该会话的未读数量
|
||||
if (item.getUnreadCount() > 0) {
|
||||
UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
|
||||
updateConversationUnreadCount(item.getId(), 0);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
|
@ -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 loadGroupConversations(String token) {
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/groups/list?page=1&pageSize=100";
|
||||
Log.d(TAG, "加载群组列表: " + url);
|
||||
|
||||
private void parseConversations(JSONArray data) {
|
||||
allConversations.clear();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
finish();
|
||||
return true;
|
||||
}
|
||||
if (id == R.id.nav_profile) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -22,6 +26,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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
6
android-app/app/src/main/res/drawable/bg_hot_badge.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_hot_badge.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFF0F0" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FF4757" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
6
android-app/app/src/main/res/drawable/bg_tip_card.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_tip_card.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFF8E1" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#66000000" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
10
android-app/app/src/main/res/drawable/ic_check_circle_24.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_check_circle_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#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>
|
||||
10
android-app/app/src/main/res/drawable/ic_favorite_border.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_favorite_border.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#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>
|
||||
10
android-app/app/src/main/res/drawable/ic_info_24.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_info_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#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>
|
||||
13
android-app/app/src/main/res/drawable/ic_pending_24.xml
Normal file
13
android-app/app/src/main/res/drawable/ic_pending_24.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="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>
|
||||
10
android-app/app/src/main/res/drawable/ic_play_circle.xml
Normal file
10
android-app/app/src/main/res/drawable/ic_play_circle.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#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>
|
||||
11
android-app/app/src/main/res/drawable/ic_send_24.xml
Normal file
11
android-app/app/src/main/res/drawable/ic_send_24.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
116
android-app/app/src/main/res/layout/activity_group_chat.xml
Normal file
116
android-app/app/src/main/res/layout/activity_group_chat.xml
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
229
android-app/app/src/main/res/layout/activity_streamer_apply.xml
Normal file
229
android-app/app/src/main/res/layout/activity_streamer_apply.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
41
android-app/app/src/main/res/layout/item_group_chat_sent.xml
Normal file
41
android-app/app/src/main/res/layout/item_group_chat_sent.xml
Normal 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>
|
||||
|
|
@ -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:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 封面图 -->
|
||||
<ImageView
|
||||
android:id="@+id/coverImage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="封面"
|
||||
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" />
|
||||
|
||||
<!-- 直播标签 -->
|
||||
<TextView
|
||||
android:id="@+id/liveBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/live_badge_background"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp"
|
||||
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" />
|
||||
|
||||
<!-- 观看人数 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/viewerCountLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/viewer_count_background"
|
||||
android:gravity="center"
|
||||
android:minWidth="56dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<!-- 封面图区域 -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:contentDescription="观看"
|
||||
android:src="@android:drawable/ic_menu_view"
|
||||
android:tint="@android:color/white" />
|
||||
android:id="@+id/coverImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="封面"
|
||||
android:minHeight="120dp"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@android:drawable/ic_menu_gallery" />
|
||||
|
||||
<!-- 直播标签 - 左上角 -->
|
||||
<TextView
|
||||
android:id="@+id/viewerCount"
|
||||
android:id="@+id/liveBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:minEms="3"
|
||||
android:text="0"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/bg_live_badge_red"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:text="直播中"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
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/bg_viewer_count"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="6dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:contentDescription="观看"
|
||||
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="3dp"
|
||||
android:text="0"
|
||||
android:textColor="@android:color/white"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
68
group_tables.sql
Normal 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
74
streamer_menu.sql
Normal 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
61
streamer_tables.sql
Normal 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='主播封禁记录表';
|
||||
Loading…
Reference in New Issue
Block a user