feat: 实现管理端与移动端历史记录功能互通

This commit is contained in:
cxytw 2026-01-04 10:13:32 +08:00
parent 4890c98f85
commit 87674d736e
20 changed files with 2364 additions and 479 deletions

View File

@ -0,0 +1,65 @@
/**
* 用户活动记录 API
* 包括观看历史点赞记录关注记录收藏记录等
*/
import request from '@/utils/request';
/**
* 获取用户关注记录
* @param {Object} params - { userId, page, limit }
*/
export function getFollowRecords(params) {
return request({
url: '/admin/user/follow/records',
method: 'get',
params
});
}
/**
* 获取用户点赞记录
* @param {Object} params - { userId, targetType, page, limit }
*/
export function getLikeRecords(params) {
return request({
url: '/admin/user/like/records',
method: 'get',
params
});
}
/**
* 获取用户查看历史
* @param {Object} params - { userId, targetType, page, limit }
*/
export function getViewHistory(params) {
return request({
url: '/admin/user/view/history',
method: 'get',
params
});
}
/**
* 获取用户收藏的作品
* @param {Object} params - { userId, page, limit }
*/
export function getCollectedWorks(params) {
return request({
url: '/admin/user/collect/works',
method: 'get',
params
});
}
/**
* 获取用户活动统计
* @param {Object} params - { userId }
*/
export function getUserActivityStats(params) {
return request({
url: '/admin/user/activity/stats',
method: 'get',
params
});
}

View File

@ -278,6 +278,17 @@
<el-table-column prop="createTime" label="查看时间" width="180" /> <el-table-column prop="createTime" label="查看时间" width="180" />
</el-table> </el-table>
</el-tab-pane> </el-tab-pane>
<!-- 收藏记录 -->
<el-tab-pane name="10" label="收藏记录">
<el-table :data="tableData" size="small" class="mt20">
<el-table-column prop="workId" label="作品ID" width="100" />
<el-table-column prop="title" label="作品标题" min-width="200" />
<el-table-column prop="authorName" label="作者" width="120" />
<el-table-column prop="likeCount" label="点赞数" width="100" />
<el-table-column prop="collectCount" label="收藏数" width="100" />
<el-table-column prop="collectTime" label="收藏时间" width="180" />
</el-table>
</el-tab-pane>
</el-tabs> </el-tabs>
<div class="block" v-if="tabsVal != '0'"> <div class="block" v-if="tabsVal != '0'">
<el-pagination <el-pagination
@ -309,6 +320,7 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
import { infobyconditionApi, topdetailApi } from '@/api/user'; import { infobyconditionApi, topdetailApi } from '@/api/user';
import { integralListApi } from '@/api/marketing'; import { integralListApi } from '@/api/marketing';
import { getFollowRecords as fetchFollowRecords, getLikeRecords as fetchLikeRecords, getViewHistory as fetchViewHistory, getCollectedWorks as fetchCollectedWorks } from '@/api/userActivity';
export default { export default {
name: 'detailUser', name: 'detailUser',
props: { props: {
@ -349,6 +361,9 @@ export default {
} else if (val == '9') { } else if (val == '9') {
// //
this.getViewHistory(); this.getViewHistory();
} else if (val == '10') {
//
this.getCollectedWorks();
} else { } else {
this.getListData(); this.getListData();
} }
@ -365,6 +380,8 @@ export default {
this.getLikeRecords(); this.getLikeRecords();
} else if (this.tabsVal == '9') { } else if (this.tabsVal == '9') {
this.getViewHistory(); this.getViewHistory();
} else if (this.tabsVal == '10') {
this.getCollectedWorks();
} else { } else {
this.getListData(); this.getListData();
} }
@ -403,17 +420,13 @@ export default {
}, },
// //
getFollowRecords() { getFollowRecords() {
this.$http({ fetchFollowRecords({
url: '/admin/user/follow/records',
method: 'get',
params: {
userId: this.userNo, userId: this.userNo,
page: this.paginationData.page, page: this.paginationData.page,
limit: this.paginationData.limit, limit: this.paginationData.limit,
},
}).then((res) => { }).then((res) => {
this.tableData = res.data.list || []; this.tableData = res.list || [];
this.paginationData.total = res.data.total || 0; this.paginationData.total = res.total || 0;
}).catch(() => { }).catch(() => {
this.tableData = []; this.tableData = [];
this.paginationData.total = 0; this.paginationData.total = 0;
@ -421,17 +434,13 @@ export default {
}, },
// //
getLikeRecords() { getLikeRecords() {
this.$http({ fetchLikeRecords({
url: '/admin/user/like/records',
method: 'get',
params: {
userId: this.userNo, userId: this.userNo,
page: this.paginationData.page, page: this.paginationData.page,
limit: this.paginationData.limit, limit: this.paginationData.limit,
},
}).then((res) => { }).then((res) => {
this.tableData = res.data.list || []; this.tableData = res.list || [];
this.paginationData.total = res.data.total || 0; this.paginationData.total = res.total || 0;
}).catch(() => { }).catch(() => {
this.tableData = []; this.tableData = [];
this.paginationData.total = 0; this.paginationData.total = 0;
@ -439,17 +448,27 @@ export default {
}, },
// //
getViewHistory() { getViewHistory() {
this.$http({ fetchViewHistory({
url: '/admin/user/view/history',
method: 'get',
params: {
userId: this.userNo, userId: this.userNo,
page: this.paginationData.page, page: this.paginationData.page,
limit: this.paginationData.limit, limit: this.paginationData.limit,
},
}).then((res) => { }).then((res) => {
this.tableData = res.data.list || []; this.tableData = res.list || [];
this.paginationData.total = res.data.total || 0; this.paginationData.total = res.total || 0;
}).catch(() => {
this.tableData = [];
this.paginationData.total = 0;
});
},
//
getCollectedWorks() {
fetchCollectedWorks({
userId: this.userNo,
page: this.paginationData.page,
limit: this.paginationData.limit,
}).then((res) => {
this.tableData = res.list || [];
this.paginationData.total = res.total || 0;
}).catch(() => { }).catch(() => {
this.tableData = []; this.tableData = [];
this.paginationData.total = 0; this.paginationData.total = 0;

View File

@ -2,8 +2,10 @@ package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage; import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult; import com.zbkj.common.result.CommonResult;
import com.zbkj.service.service.UserActivityRecordService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@ -15,7 +17,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* 用户活动记录控制器 * 用户活动记录控制器管理后台
* 包括关注记录点赞记录查看历史等 * 包括关注记录点赞记录查看历史等
*/ */
@Slf4j @Slf4j
@ -28,6 +30,9 @@ public class UserActivityController {
@Autowired @Autowired
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
@Autowired
private UserActivityRecordService userActivityRecordService;
/** /**
* 获取用户的关注记录 * 获取用户的关注记录
*/ */
@ -38,38 +43,7 @@ public class UserActivityController {
@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit) { @RequestParam(defaultValue = "10") Integer limit) {
try { try {
// 计算偏移量 CommonPage<Map<String, Object>> result = userActivityRecordService.getFollowRecords(userId, page, limit);
int offset = (page - 1) * limit;
// 查询总数
String countSql = "SELECT COUNT(*) FROM eb_follow_record WHERE follower_id = ?";
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
// 查询列表
String sql = "SELECT " +
"fr.id, " +
"fr.follower_id as followerId, " +
"fr.follower_nickname as followerNickname, " +
"fr.followed_id as followedId, " +
"fr.followed_nickname as followedNickname, " +
"fr.follow_status as followStatus, " +
"fr.is_deleted as isDeleted, " +
"fr.create_time as createTime " +
"FROM eb_follow_record fr " +
"WHERE fr.follower_id = ? " +
"ORDER BY fr.create_time DESC " +
"LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, limit, offset);
// 封装分页结果
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result); return CommonResult.success(result);
} catch (Exception e) { } catch (Exception e) {
log.error("获取用户关注记录失败: userId={}", userId, e); log.error("获取用户关注记录失败: userId={}", userId, e);
@ -84,76 +58,12 @@ public class UserActivityController {
@GetMapping("/like/records") @GetMapping("/like/records")
public CommonResult<CommonPage<Map<String, Object>>> getLikeRecords( public CommonResult<CommonPage<Map<String, Object>>> getLikeRecords(
@RequestParam Integer userId, @RequestParam Integer userId,
@ApiParam(value = "目标类型room-直播间, work-作品, wish-心愿")
@RequestParam(required = false) String targetType,
@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit) { @RequestParam(defaultValue = "10") Integer limit) {
try { try {
// 计算偏移量 CommonPage<Map<String, Object>> result = userActivityRecordService.getLikeRecords(userId, targetType, page, limit);
int offset = (page - 1) * limit;
// 查询总数从多个表统计
int totalRoomLikes = 0;
int totalWorkLikes = 0;
int totalWishLikes = 0;
try {
String countRoomSql = "SELECT COUNT(*) FROM eb_live_room_like WHERE user_id = ?";
totalRoomLikes = jdbcTemplate.queryForObject(countRoomSql, Integer.class, userId);
} catch (Exception e) {
log.debug("eb_live_room_like表不存在或查询失败");
}
try {
String countWorkSql = "SELECT COUNT(*) FROM eb_work_like WHERE user_id = ?";
totalWorkLikes = jdbcTemplate.queryForObject(countWorkSql, Integer.class, userId);
} catch (Exception e) {
log.debug("eb_work_like表不存在或查询失败");
}
try {
String countWishSql = "SELECT COUNT(*) FROM eb_wish_like WHERE user_id = ?";
totalWishLikes = jdbcTemplate.queryForObject(countWishSql, Integer.class, userId);
} catch (Exception e) {
log.debug("eb_wish_like表不存在或查询失败");
}
int total = totalRoomLikes + totalWorkLikes + totalWishLikes;
// 联合查询所有点赞记录
StringBuilder sql = new StringBuilder();
// 直播间点赞
sql.append("SELECT 'room' as targetType, lr.id as targetId, lr.title as targetTitle, rl.create_time as createTime ");
sql.append("FROM eb_live_room_like rl ");
sql.append("LEFT JOIN eb_live_room lr ON rl.room_id = lr.id ");
sql.append("WHERE rl.user_id = ? ");
// 作品点赞
sql.append("UNION ALL ");
sql.append("SELECT 'work' as targetType, w.id as targetId, w.title as targetTitle, wl.create_time as createTime ");
sql.append("FROM eb_work_like wl ");
sql.append("LEFT JOIN eb_works w ON wl.work_id = w.id ");
sql.append("WHERE wl.user_id = ? ");
// 心愿点赞
sql.append("UNION ALL ");
sql.append("SELECT 'wish' as targetType, w.id as targetId, w.content as targetTitle, wl.create_time as createTime ");
sql.append("FROM eb_wish_like wl ");
sql.append("LEFT JOIN eb_wish w ON wl.wish_id = w.id ");
sql.append("WHERE wl.user_id = ? ");
sql.append("ORDER BY createTime DESC ");
sql.append("LIMIT ? OFFSET ?");
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), userId, userId, userId, limit, offset);
// 封装分页结果
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal((long) total);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) total / limit));
return CommonResult.success(result); return CommonResult.success(result);
} catch (Exception e) { } catch (Exception e) {
log.error("获取用户点赞记录失败: userId={}", userId, e); log.error("获取用户点赞记录失败: userId={}", userId, e);
@ -168,56 +78,49 @@ public class UserActivityController {
@GetMapping("/view/history") @GetMapping("/view/history")
public CommonResult<CommonPage<Map<String, Object>>> getViewHistory( public CommonResult<CommonPage<Map<String, Object>>> getViewHistory(
@RequestParam Integer userId, @RequestParam Integer userId,
@ApiParam(value = "目标类型room-直播间, work-作品, profile-用户主页")
@RequestParam(required = false) String targetType,
@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit) { @RequestParam(defaultValue = "10") Integer limit) {
try { try {
// 计算偏移量 CommonPage<Map<String, Object>> result = userActivityRecordService.getViewHistory(userId, targetType, page, limit);
int offset = (page - 1) * limit;
// 查询总数
String countSql = "SELECT COUNT(*) FROM eb_view_history WHERE user_id = ?";
Integer total = 0;
try {
total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
} catch (Exception e) {
log.debug("eb_view_history表不存在返回空数据");
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(new java.util.ArrayList<>());
result.setTotal(0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage(0);
return CommonResult.success(result);
}
// 查询列表
String sql = "SELECT " +
"vh.id, " +
"vh.user_id as userId, " +
"vh.target_type as targetType, " +
"vh.target_id as targetId, " +
"vh.target_title as targetTitle, " +
"vh.view_duration as viewDuration, " +
"vh.create_time as createTime " +
"FROM eb_view_history vh " +
"WHERE vh.user_id = ? " +
"ORDER BY vh.create_time DESC " +
"LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, limit, offset);
// 封装分页结果
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result); return CommonResult.success(result);
} catch (Exception e) { } catch (Exception e) {
log.error("获取用户查看历史失败: userId={}", userId, e); log.error("获取用户查看历史失败: userId={}", userId, e);
return CommonResult.failed("获取查看历史失败"); return CommonResult.failed("获取查看历史失败");
} }
} }
/**
* 获取用户收藏的作品
*/
@ApiOperation(value = "获取用户收藏的作品")
@GetMapping("/collect/works")
public CommonResult<CommonPage<Map<String, Object>>> getCollectedWorks(
@RequestParam Integer userId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit) {
try {
CommonPage<Map<String, Object>> result = userActivityRecordService.getCollectedWorks(userId, page, limit);
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取用户收藏作品失败: userId={}", userId, e);
return CommonResult.failed("获取收藏作品失败");
}
}
/**
* 获取用户活动统计
*/
@ApiOperation(value = "获取用户活动统计")
@GetMapping("/activity/stats")
public CommonResult<Map<String, Object>> getUserActivityStats(@RequestParam Integer userId) {
try {
Map<String, Object> stats = userActivityRecordService.getUserActivityStats(userId);
return CommonResult.success(stats);
} catch (Exception e) {
log.error("获取用户活动统计失败: userId={}", userId, e);
return CommonResult.failed("获取统计失败");
}
}
} }

View File

@ -0,0 +1,254 @@
package com.zbkj.front.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import com.zbkj.service.service.UserActivityRecordService;
import com.zbkj.service.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 用户活动记录控制器前端API
* 提供观看历史点赞记录关注记录收藏记录等功能
*/
@Slf4j
@RestController
@RequestMapping("api/front/activity")
@Api(tags = "用户活动记录")
@Validated
public class UserActivityRecordController {
@Autowired
private UserActivityRecordService userActivityRecordService;
@Autowired
private UserService userService;
// ==================== 观看历史 ====================
/**
* 记录观看历史
*/
@ApiOperation(value = "记录观看历史")
@PostMapping("/view/record")
public CommonResult<Map<String, Object>> recordViewHistory(@RequestBody Map<String, Object> body) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
String targetType = body.get("targetType") != null ? body.get("targetType").toString() : null;
String targetId = body.get("targetId") != null ? body.get("targetId").toString() : null;
String targetTitle = body.get("targetTitle") != null ? body.get("targetTitle").toString() : null;
Integer duration = body.get("duration") != null ? Integer.valueOf(body.get("duration").toString()) : 0;
if (targetType == null || targetId == null) {
return CommonResult.failed("参数不完整");
}
boolean success = userActivityRecordService.recordViewHistory(userId, targetType, targetId, targetTitle, duration);
if (success) {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "记录成功");
return CommonResult.success(result);
} else {
return CommonResult.failed("记录失败");
}
}
/**
* 获取观看历史列表
*/
@ApiOperation(value = "获取观看历史列表")
@GetMapping("/view/history")
public CommonResult<CommonPage<Map<String, Object>>> getViewHistory(
@ApiParam(value = "目标类型room-直播间, work-作品, profile-用户主页")
@RequestParam(required = false) String targetType,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getViewHistory(userId, targetType, page, pageSize);
return CommonResult.success(result);
}
/**
* 删除单条观看历史
*/
@ApiOperation(value = "删除单条观看历史")
@DeleteMapping("/view/history/{historyId}")
public CommonResult<String> deleteViewHistory(@PathVariable Long historyId) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
boolean success = userActivityRecordService.deleteViewHistory(userId, historyId);
return success ? CommonResult.success("删除成功") : CommonResult.failed("删除失败");
}
/**
* 清空观看历史
*/
@ApiOperation(value = "清空观看历史")
@DeleteMapping("/view/history")
public CommonResult<String> clearViewHistory(
@ApiParam(value = "目标类型可选room-直播间, work-作品, profile-用户主页")
@RequestParam(required = false) String targetType) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
boolean success = userActivityRecordService.clearViewHistory(userId, targetType);
return success ? CommonResult.success("清空成功") : CommonResult.failed("清空失败");
}
// ==================== 点赞记录 ====================
/**
* 获取点赞记录列表
*/
@ApiOperation(value = "获取点赞记录列表")
@GetMapping("/like/records")
public CommonResult<CommonPage<Map<String, Object>>> getLikeRecords(
@ApiParam(value = "目标类型room-直播间, work-作品, wish-心愿,不传则获取全部")
@RequestParam(required = false) String targetType,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getLikeRecords(userId, targetType, page, pageSize);
return CommonResult.success(result);
}
/**
* 获取点赞的直播间列表
*/
@ApiOperation(value = "获取点赞的直播间列表")
@GetMapping("/like/rooms")
public CommonResult<CommonPage<Map<String, Object>>> getLikedRooms(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getLikedRooms(userId, page, pageSize);
return CommonResult.success(result);
}
/**
* 获取点赞的作品列表
*/
@ApiOperation(value = "获取点赞的作品列表")
@GetMapping("/like/works")
public CommonResult<CommonPage<Map<String, Object>>> getLikedWorks(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getLikedWorks(userId, page, pageSize);
return CommonResult.success(result);
}
/**
* 获取点赞的心愿列表
*/
@ApiOperation(value = "获取点赞的心愿列表")
@GetMapping("/like/wishes")
public CommonResult<CommonPage<Map<String, Object>>> getLikedWishes(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getLikedWishes(userId, page, pageSize);
return CommonResult.success(result);
}
// ==================== 关注记录 ====================
/**
* 获取关注记录列表
*/
@ApiOperation(value = "获取关注记录列表")
@GetMapping("/follow/records")
public CommonResult<CommonPage<Map<String, Object>>> getFollowRecords(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getFollowRecords(userId, page, pageSize);
return CommonResult.success(result);
}
// ==================== 收藏记录 ====================
/**
* 获取收藏的作品列表
*/
@ApiOperation(value = "获取收藏的作品列表")
@GetMapping("/collect/works")
public CommonResult<CommonPage<Map<String, Object>>> getCollectedWorks(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
CommonPage<Map<String, Object>> result = userActivityRecordService.getCollectedWorks(userId, page, pageSize);
return CommonResult.success(result);
}
// ==================== 统计信息 ====================
/**
* 获取用户活动统计
*/
@ApiOperation(value = "获取用户活动统计")
@GetMapping("/stats")
public CommonResult<Map<String, Object>> getUserActivityStats() {
Integer userId = userService.getUserId();
if (userId == null) {
return CommonResult.failed("请先登录");
}
Map<String, Object> stats = userActivityRecordService.getUserActivityStats(userId);
return CommonResult.success(stats);
}
}

View File

@ -0,0 +1,106 @@
package com.zbkj.service.service;
import com.zbkj.common.page.CommonPage;
import java.util.List;
import java.util.Map;
/**
* 用户活动记录服务接口
* 统一管理用户的各类历史记录观看历史点赞记录搜索历史等
*/
public interface UserActivityRecordService {
// ==================== 观看历史 ====================
/**
* 记录观看历史
* @param userId 用户ID
* @param targetType 目标类型room-直播间, work-作品, profile-用户主页
* @param targetId 目标ID
* @param targetTitle 目标标题
* @param duration 观看时长
* @return 是否成功
*/
boolean recordViewHistory(Integer userId, String targetType, String targetId, String targetTitle, Integer duration);
/**
* 获取用户观看历史列表
* @param userId 用户ID
* @param targetType 目标类型可选null表示全部
* @param page 页码
* @param pageSize 每页数量
* @return 观看历史列表
*/
CommonPage<Map<String, Object>> getViewHistory(Integer userId, String targetType, Integer page, Integer pageSize);
/**
* 删除单条观看历史
* @param userId 用户ID
* @param historyId 历史记录ID
* @return 是否成功
*/
boolean deleteViewHistory(Integer userId, Long historyId);
/**
* 清空用户观看历史
* @param userId 用户ID
* @param targetType 目标类型可选null表示清空全部
* @return 是否成功
*/
boolean clearViewHistory(Integer userId, String targetType);
// ==================== 点赞记录 ====================
/**
* 获取用户点赞记录列表包含直播间作品心愿等
* @param userId 用户ID
* @param targetType 目标类型可选room-直播间, work-作品, wish-心愿
* @param page 页码
* @param pageSize 每页数量
* @return 点赞记录列表
*/
CommonPage<Map<String, Object>> getLikeRecords(Integer userId, String targetType, Integer page, Integer pageSize);
/**
* 获取用户点赞的直播间列表
*/
CommonPage<Map<String, Object>> getLikedRooms(Integer userId, Integer page, Integer pageSize);
/**
* 获取用户点赞的作品列表
*/
CommonPage<Map<String, Object>> getLikedWorks(Integer userId, Integer page, Integer pageSize);
/**
* 获取用户点赞的心愿列表
*/
CommonPage<Map<String, Object>> getLikedWishes(Integer userId, Integer page, Integer pageSize);
// ==================== 关注记录 ====================
/**
* 获取用户关注记录列表
* @param userId 用户ID
* @param page 页码
* @param pageSize 每页数量
* @return 关注记录列表
*/
CommonPage<Map<String, Object>> getFollowRecords(Integer userId, Integer page, Integer pageSize);
// ==================== 收藏记录 ====================
/**
* 获取用户收藏的作品列表
*/
CommonPage<Map<String, Object>> getCollectedWorks(Integer userId, Integer page, Integer pageSize);
// ==================== 统计信息 ====================
/**
* 获取用户活动统计
* @param userId 用户ID
* @return 统计信息观看数点赞数关注数收藏数等
*/
Map<String, Object> getUserActivityStats(Integer userId);
}

View File

@ -0,0 +1,488 @@
package com.zbkj.service.service.impl;
import com.zbkj.common.page.CommonPage;
import com.zbkj.service.service.UserActivityRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
* 用户活动记录服务实现
*/
@Slf4j
@Service
public class UserActivityRecordServiceImpl implements UserActivityRecordService {
@Autowired
private JdbcTemplate jdbcTemplate;
// ==================== 观看历史 ====================
@Override
@Transactional(rollbackFor = Exception.class)
public boolean recordViewHistory(Integer userId, String targetType, String targetId,
String targetTitle, Integer duration) {
if (userId == null || targetType == null || targetId == null) {
return false;
}
try {
// 检查是否已存在相同记录同一用户同一目标
String checkSql = "SELECT id FROM eb_view_history WHERE user_id = ? AND target_type = ? AND target_id = ?";
List<Map<String, Object>> existing = jdbcTemplate.queryForList(checkSql, userId, targetType, targetId);
if (!existing.isEmpty()) {
// 更新已有记录
String updateSql = "UPDATE eb_view_history SET target_title = ?, view_duration = COALESCE(view_duration, 0) + ?, " +
"update_time = NOW() WHERE user_id = ? AND target_type = ? AND target_id = ?";
jdbcTemplate.update(updateSql, targetTitle, duration != null ? duration : 0, userId, targetType, targetId);
} else {
// 插入新记录
String insertSql = "INSERT INTO eb_view_history (user_id, target_type, target_id, target_title, view_duration, create_time, update_time) " +
"VALUES (?, ?, ?, ?, ?, NOW(), NOW())";
jdbcTemplate.update(insertSql, userId, targetType, targetId, targetTitle, duration != null ? duration : 0);
}
return true;
} catch (Exception e) {
log.error("记录观看历史失败: userId={}, targetType={}, targetId={}", userId, targetType, targetId, e);
return false;
}
}
@Override
public CommonPage<Map<String, Object>> getViewHistory(Integer userId, String targetType,
Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
try {
int offset = (page - 1) * pageSize;
// 构建查询条件
StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM eb_view_history WHERE user_id = ?");
StringBuilder querySql = new StringBuilder(
"SELECT id, user_id as userId, target_type as targetType, target_id as targetId, " +
"target_title as targetTitle, view_duration as viewDuration, create_time as createTime, " +
"update_time as updateTime FROM eb_view_history WHERE user_id = ?");
List<Object> params = new ArrayList<>();
params.add(userId);
if (targetType != null && !targetType.isEmpty()) {
countSql.append(" AND target_type = ?");
querySql.append(" AND target_type = ?");
params.add(targetType);
}
querySql.append(" ORDER BY update_time DESC LIMIT ? OFFSET ?");
// 查询总数
Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, params.toArray());
// 查询列表
params.add(pageSize);
params.add(offset);
List<Map<String, Object>> list = jdbcTemplate.queryForList(querySql.toString(), params.toArray());
// 补充目标详情
enrichViewHistoryDetails(list);
return buildPage(list, total != null ? total : 0, page, pageSize);
} catch (Exception e) {
log.error("获取观看历史失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
/**
* 补充观看历史的目标详情
*/
private void enrichViewHistoryDetails(List<Map<String, Object>> list) {
for (Map<String, Object> item : list) {
String targetType = (String) item.get("targetType");
String targetId = String.valueOf(item.get("targetId"));
try {
if ("room".equals(targetType)) {
// 查询直播间信息
String sql = "SELECT title, streamer_name as streamerName, is_live as isLive, cover_image as coverImage " +
"FROM eb_live_room WHERE id = ?";
List<Map<String, Object>> rooms = jdbcTemplate.queryForList(sql, Integer.parseInt(targetId));
if (!rooms.isEmpty()) {
item.putAll(rooms.get(0));
}
} else if ("work".equals(targetType)) {
// 查询作品信息
String sql = "SELECT title, cover_image as coverImage, user_id as authorId FROM eb_works WHERE id = ?";
List<Map<String, Object>> works = jdbcTemplate.queryForList(sql, Long.parseLong(targetId));
if (!works.isEmpty()) {
item.putAll(works.get(0));
}
} else if ("profile".equals(targetType)) {
// 查询用户信息
String sql = "SELECT nickname, avatar FROM eb_user WHERE uid = ?";
List<Map<String, Object>> users = jdbcTemplate.queryForList(sql, Integer.parseInt(targetId));
if (!users.isEmpty()) {
item.putAll(users.get(0));
}
}
} catch (Exception e) {
log.warn("补充观看历史详情失败: targetType={}, targetId={}", targetType, targetId, e);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteViewHistory(Integer userId, Long historyId) {
if (userId == null || historyId == null) {
return false;
}
try {
String sql = "DELETE FROM eb_view_history WHERE id = ? AND user_id = ?";
int rows = jdbcTemplate.update(sql, historyId, userId);
return rows > 0;
} catch (Exception e) {
log.error("删除观看历史失败: userId={}, historyId={}", userId, historyId, e);
return false;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean clearViewHistory(Integer userId, String targetType) {
if (userId == null) {
return false;
}
try {
StringBuilder sql = new StringBuilder("DELETE FROM eb_view_history WHERE user_id = ?");
List<Object> params = new ArrayList<>();
params.add(userId);
if (targetType != null && !targetType.isEmpty()) {
sql.append(" AND target_type = ?");
params.add(targetType);
}
jdbcTemplate.update(sql.toString(), params.toArray());
return true;
} catch (Exception e) {
log.error("清空观看历史失败: userId={}", userId, e);
return false;
}
}
// ==================== 点赞记录 ====================
@Override
public CommonPage<Map<String, Object>> getLikeRecords(Integer userId, String targetType,
Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
// 根据类型调用不同方法
if ("room".equals(targetType)) {
return getLikedRooms(userId, page, pageSize);
} else if ("work".equals(targetType)) {
return getLikedWorks(userId, page, pageSize);
} else if ("wish".equals(targetType)) {
return getLikedWishes(userId, page, pageSize);
}
// 获取全部点赞记录联合查询
return getAllLikeRecords(userId, page, pageSize);
}
/**
* 获取全部点赞记录联合查询
*/
private CommonPage<Map<String, Object>> getAllLikeRecords(Integer userId, Integer page, Integer pageSize) {
try {
int offset = (page - 1) * pageSize;
// 统计各类点赞总数
int roomLikes = countTableRows("eb_live_room_like", "user_id", userId);
int workLikes = countTableRows("eb_works_relation", "uid", userId, "type", "like");
int wishLikes = countTableRows("eb_wish_like", "user_id", userId);
int total = roomLikes + workLikes + wishLikes;
// 联合查询
String sql = "(" +
"SELECT 'room' as targetType, CAST(room_id AS CHAR) as targetId, lr.title as targetTitle, " +
"lr.cover_image as coverImage, lr.streamer_name as streamerName, lr.is_live as isLive, " +
"rl.create_time as createTime " +
"FROM eb_live_room_like rl " +
"LEFT JOIN eb_live_room lr ON rl.room_id = lr.id " +
"WHERE rl.user_id = ?" +
") UNION ALL (" +
"SELECT 'work' as targetType, CAST(wr.works_id AS CHAR) as targetId, w.title as targetTitle, " +
"w.cover_image as coverImage, NULL as streamerName, NULL as isLive, " +
"wr.create_time as createTime " +
"FROM eb_works_relation wr " +
"LEFT JOIN eb_works w ON wr.works_id = w.id " +
"WHERE wr.uid = ? AND wr.type = 'like'" +
") UNION ALL (" +
"SELECT 'wish' as targetType, CAST(wl.wish_id AS CHAR) as targetId, ws.content as targetTitle, " +
"NULL as coverImage, NULL as streamerName, NULL as isLive, " +
"wl.create_time as createTime " +
"FROM eb_wish_like wl " +
"LEFT JOIN eb_wishtree_wish ws ON wl.wish_id = ws.id " +
"WHERE wl.user_id = ?" +
") ORDER BY createTime DESC LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, userId, userId, pageSize, offset);
return buildPage(list, total, page, pageSize);
} catch (Exception e) {
log.error("获取全部点赞记录失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
@Override
public CommonPage<Map<String, Object>> getLikedRooms(Integer userId, Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
try {
int offset = (page - 1) * pageSize;
String countSql = "SELECT COUNT(*) FROM eb_live_room_like WHERE user_id = ?";
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
String sql = "SELECT rl.id, rl.room_id as roomId, lr.title as roomTitle, " +
"lr.streamer_name as streamerName, lr.uid as streamerId, lr.cover_image as coverImage, " +
"lr.is_live as isLive, lr.like_count as likeCount, lr.view_count as viewCount, " +
"rl.create_time as likeTime " +
"FROM eb_live_room_like rl " +
"LEFT JOIN eb_live_room lr ON rl.room_id = lr.id " +
"WHERE rl.user_id = ? " +
"ORDER BY rl.create_time DESC LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, pageSize, offset);
return buildPage(list, total != null ? total : 0, page, pageSize);
} catch (Exception e) {
log.error("获取点赞直播间列表失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
@Override
public CommonPage<Map<String, Object>> getLikedWorks(Integer userId, Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
try {
int offset = (page - 1) * pageSize;
String countSql = "SELECT COUNT(*) FROM eb_works_relation WHERE uid = ? AND type = 'like'";
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
String sql = "SELECT wr.id, wr.works_id as workId, w.title, w.description, " +
"w.cover_image as coverImage, w.video_url as videoUrl, w.user_id as authorId, " +
"u.nickname as authorName, u.avatar as authorAvatar, " +
"w.like_count as likeCount, w.view_count as viewCount, w.comment_count as commentCount, " +
"wr.create_time as likeTime " +
"FROM eb_works_relation wr " +
"LEFT JOIN eb_works w ON wr.works_id = w.id " +
"LEFT JOIN eb_user u ON w.user_id = u.uid " +
"WHERE wr.uid = ? AND wr.type = 'like' " +
"ORDER BY wr.create_time DESC LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, pageSize, offset);
return buildPage(list, total != null ? total : 0, page, pageSize);
} catch (Exception e) {
log.error("获取点赞作品列表失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
@Override
public CommonPage<Map<String, Object>> getLikedWishes(Integer userId, Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
try {
int offset = (page - 1) * pageSize;
String countSql = "SELECT COUNT(*) FROM eb_wish_like WHERE user_id = ?";
Integer total = 0;
try {
total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
} catch (Exception e) {
log.debug("eb_wish_like表可能不存在");
return emptyPage(page, pageSize);
}
String sql = "SELECT wl.id, wl.wish_id as wishId, ws.content, ws.user_id as authorId, " +
"u.nickname as authorName, u.avatar as authorAvatar, " +
"ws.like_count as likeCount, wl.create_time as likeTime " +
"FROM eb_wish_like wl " +
"LEFT JOIN eb_wishtree_wish ws ON wl.wish_id = ws.id " +
"LEFT JOIN eb_user u ON ws.user_id = u.uid " +
"WHERE wl.user_id = ? " +
"ORDER BY wl.create_time DESC LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, pageSize, offset);
return buildPage(list, total != null ? total : 0, page, pageSize);
} catch (Exception e) {
log.error("获取点赞心愿列表失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
// ==================== 关注记录 ====================
@Override
public CommonPage<Map<String, Object>> getFollowRecords(Integer userId, Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
try {
int offset = (page - 1) * pageSize;
String countSql = "SELECT COUNT(*) FROM eb_follow_record WHERE follower_id = ? AND follow_status = 1";
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
String sql = "SELECT fr.id, fr.followed_id as followedId, fr.followed_nickname as followedNickname, " +
"u.avatar as followedAvatar, u.phone, " +
"fr.create_time as followTime " +
"FROM eb_follow_record fr " +
"LEFT JOIN eb_user u ON fr.followed_id = u.uid " +
"WHERE fr.follower_id = ? AND fr.follow_status = 1 " +
"ORDER BY fr.create_time DESC LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, pageSize, offset);
return buildPage(list, total != null ? total : 0, page, pageSize);
} catch (Exception e) {
log.error("获取关注记录失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
// ==================== 收藏记录 ====================
@Override
public CommonPage<Map<String, Object>> getCollectedWorks(Integer userId, Integer page, Integer pageSize) {
if (userId == null) {
return emptyPage(page, pageSize);
}
try {
int offset = (page - 1) * pageSize;
String countSql = "SELECT COUNT(*) FROM eb_works_relation WHERE uid = ? AND type = 'collect'";
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, userId);
String sql = "SELECT wr.id, wr.works_id as workId, w.title, w.description, " +
"w.cover_image as coverImage, w.video_url as videoUrl, w.user_id as authorId, " +
"u.nickname as authorName, u.avatar as authorAvatar, " +
"w.like_count as likeCount, w.collect_count as collectCount, " +
"wr.create_time as collectTime " +
"FROM eb_works_relation wr " +
"LEFT JOIN eb_works w ON wr.works_id = w.id " +
"LEFT JOIN eb_user u ON w.user_id = u.uid " +
"WHERE wr.uid = ? AND wr.type = 'collect' " +
"ORDER BY wr.create_time DESC LIMIT ? OFFSET ?";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, userId, pageSize, offset);
return buildPage(list, total != null ? total : 0, page, pageSize);
} catch (Exception e) {
log.error("获取收藏作品列表失败: userId={}", userId, e);
return emptyPage(page, pageSize);
}
}
// ==================== 统计信息 ====================
@Override
public Map<String, Object> getUserActivityStats(Integer userId) {
Map<String, Object> stats = new HashMap<>();
if (userId == null) {
return stats;
}
try {
// 观看历史数
stats.put("viewCount", countTableRows("eb_view_history", "user_id", userId));
// 点赞直播间数
stats.put("likedRoomCount", countTableRows("eb_live_room_like", "user_id", userId));
// 点赞作品数
stats.put("likedWorkCount", countTableRows("eb_works_relation", "uid", userId, "type", "like"));
// 收藏作品数
stats.put("collectedWorkCount", countTableRows("eb_works_relation", "uid", userId, "type", "collect"));
// 关注数
stats.put("followingCount", countTableRows("eb_follow_record", "follower_id", userId, "follow_status", 1));
// 粉丝数
stats.put("followerCount", countTableRows("eb_follow_record", "followed_id", userId, "follow_status", 1));
// 搜索历史数
stats.put("searchHistoryCount", countTableRows("eb_search_history", "user_id", userId, "is_deleted", 0));
} catch (Exception e) {
log.error("获取用户活动统计失败: userId={}", userId, e);
}
return stats;
}
// ==================== 工具方法 ====================
private int countTableRows(String tableName, String column, Object value) {
try {
String sql = "SELECT COUNT(*) FROM " + tableName + " WHERE " + column + " = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, value);
return count != null ? count : 0;
} catch (Exception e) {
log.debug("统计表{}行数失败", tableName);
return 0;
}
}
private int countTableRows(String tableName, String column1, Object value1, String column2, Object value2) {
try {
String sql = "SELECT COUNT(*) FROM " + tableName + " WHERE " + column1 + " = ? AND " + column2 + " = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, value1, value2);
return count != null ? count : 0;
} catch (Exception e) {
log.debug("统计表{}行数失败", tableName);
return 0;
}
}
private CommonPage<Map<String, Object>> emptyPage(Integer page, Integer pageSize) {
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(new ArrayList<>());
result.setTotal(0L);
result.setPage(page);
result.setLimit(pageSize);
result.setTotalPage(0);
return result;
}
private CommonPage<Map<String, Object>> buildPage(List<Map<String, Object>> list, int total,
Integer page, Integer pageSize) {
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal((long) total);
result.setPage(page);
result.setLimit(pageSize);
result.setTotalPage((int) Math.ceil((double) total / pageSize));
return result;
}
}

View File

@ -0,0 +1,111 @@
-- 用户活动记录相关表更新脚本
-- 确保管理端与移动端的"历史记录"功能互通
-- 1. 查看历史记录表(如果不存在则创建)
CREATE TABLE IF NOT EXISTS `eb_view_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`target_type` varchar(20) NOT NULL COMMENT '目标类型room-直播间, work-作品, profile-用户主页',
`target_id` varchar(50) NOT NULL COMMENT '目标ID',
`target_title` varchar(255) DEFAULT NULL COMMENT '目标标题',
`view_duration` int(11) DEFAULT 0 COMMENT '观看时长(秒)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_target` (`target_type`, `target_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_update_time` (`update_time`),
UNIQUE KEY `uk_user_target` (`user_id`, `target_type`, `target_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查看历史记录表';
-- 2. 直播间点赞记录表(如果不存在则创建)
CREATE TABLE IF NOT EXISTS `eb_live_room_like` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`room_id` int(11) NOT NULL COMMENT '直播间ID',
`like_count` int(11) DEFAULT 1 COMMENT '点赞次数',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '首次点赞时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后点赞时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_room` (`user_id`, `room_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_room_id` (`room_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直播间点赞记录表';
-- 3. 心愿点赞记录表(如果不存在则创建)
CREATE TABLE IF NOT EXISTS `eb_wish_like` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`wish_id` bigint(20) NOT NULL COMMENT '心愿ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '点赞时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_wish` (`user_id`, `wish_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_wish_id` (`wish_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心愿点赞记录表';
-- 4. 搜索历史表(如果不存在则创建)
CREATE TABLE IF NOT EXISTS `eb_search_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`keyword` varchar(200) NOT NULL COMMENT '搜索关键词',
`search_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '搜索类型0-综合 1-用户 2-直播间 3-作品 4-消息',
`search_count` int(11) NOT NULL DEFAULT 1 COMMENT '搜索次数',
`is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除0-未删除 1-已删除',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`ext_field1` varchar(200) DEFAULT NULL COMMENT '扩展字段1',
`ext_field2` varchar(200) DEFAULT NULL COMMENT '扩展字段2',
`ext_field3` varchar(200) DEFAULT NULL COMMENT '扩展字段3',
`ext_field4` varchar(200) DEFAULT NULL COMMENT '扩展字段4',
`ext_field5` varchar(200) DEFAULT NULL COMMENT '扩展字段5',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_search_type` (`search_type`),
KEY `idx_is_deleted` (`is_deleted`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='搜索历史表';
-- 5. 热门搜索表(如果不存在则创建)
CREATE TABLE IF NOT EXISTS `eb_hot_search` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`keyword` varchar(200) NOT NULL COMMENT '搜索关键词',
`search_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '搜索类型0-综合 1-用户 2-直播间 3-作品',
`hot_score` int(11) NOT NULL DEFAULT 0 COMMENT '热度分数',
`search_count` int(11) NOT NULL DEFAULT 0 COMMENT '搜索次数',
`sort_order` int(11) NOT NULL DEFAULT 0 COMMENT '排序权重',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态0-禁用 1-启用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_keyword_type` (`keyword`, `search_type`),
KEY `idx_search_type` (`search_type`),
KEY `idx_hot_score` (`hot_score`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='热门搜索表';
-- 6. 确保关注记录表有必要的字段
-- 如果表已存在,添加缺失的字段
-- ALTER TABLE `eb_follow_record` ADD COLUMN IF NOT EXISTS `follower_nickname` varchar(100) DEFAULT NULL COMMENT '关注者昵称';
-- ALTER TABLE `eb_follow_record` ADD COLUMN IF NOT EXISTS `followed_nickname` varchar(100) DEFAULT NULL COMMENT '被关注者昵称';
-- 7. 确保作品关系表存在(点赞、收藏)
CREATE TABLE IF NOT EXISTS `eb_works_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`uid` int(11) NOT NULL COMMENT '用户ID',
`works_id` bigint(20) NOT NULL COMMENT '作品ID',
`type` varchar(20) NOT NULL COMMENT '关系类型like-点赞, collect-收藏',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_works_type` (`uid`, `works_id`, `type`),
KEY `idx_uid` (`uid`),
KEY `idx_works_id` (`works_id`),
KEY `idx_type` (`type`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作品关系表(点赞、收藏)';
-- 完成提示
SELECT '用户活动记录表结构更新完成' AS message;

View File

@ -67,6 +67,10 @@
android:name="com.example.livestreaming.WatchHistoryActivity" android:name="com.example.livestreaming.WatchHistoryActivity"
android:exported="false" /> android:exported="false" />
<activity
android:name="com.example.livestreaming.MyRecordsActivity"
android:exported="false" />
<activity <activity
android:name="com.example.livestreaming.MyFriendsActivity" android:name="com.example.livestreaming.MyFriendsActivity"
android:exported="false" /> android:exported="false" />

View File

@ -0,0 +1,474 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.Glide;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.PageResponse;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 我的记录页面 - 整合观看历史点赞记录收藏记录等
*/
public class MyRecordsActivity extends AppCompatActivity {
private TabLayout tabLayout;
private ViewPager2 viewPager;
private ImageView backButton;
private final String[] tabTitles = {"观看历史", "点赞记录", "收藏记录", "关注记录"};
public static void start(Context context) {
Intent intent = new Intent(context, MyRecordsActivity.class);
context.startActivity(intent);
}
public static void start(Context context, int tabIndex) {
Intent intent = new Intent(context, MyRecordsActivity.class);
intent.putExtra("tabIndex", tabIndex);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_records);
initViews();
setupViewPager();
// 处理跳转到指定Tab
int tabIndex = getIntent().getIntExtra("tabIndex", 0);
if (tabIndex >= 0 && tabIndex < tabTitles.length) {
viewPager.setCurrentItem(tabIndex, false);
}
}
private void initViews() {
backButton = findViewById(R.id.backButton);
tabLayout = findViewById(R.id.tabLayout);
viewPager = findViewById(R.id.viewPager);
backButton.setOnClickListener(v -> finish());
}
private void setupViewPager() {
RecordsPagerAdapter adapter = new RecordsPagerAdapter(this);
viewPager.setAdapter(adapter);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
tab.setText(tabTitles[position]);
}).attach();
}
/**
* ViewPager适配器
*/
private static class RecordsPagerAdapter extends FragmentStateAdapter {
public RecordsPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return RecordListFragment.newInstance("view");
case 1:
return RecordListFragment.newInstance("like");
case 2:
return RecordListFragment.newInstance("collect");
case 3:
return RecordListFragment.newInstance("follow");
default:
return RecordListFragment.newInstance("view");
}
}
@Override
public int getItemCount() {
return 4;
}
}
/**
* 记录列表Fragment
*/
public static class RecordListFragment extends Fragment {
private static final String ARG_TYPE = "type";
private String recordType;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private View loadingView;
private View emptyView;
private RecordAdapter adapter;
private final List<Map<String, Object>> records = new ArrayList<>();
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMore = true;
public static RecordListFragment newInstance(String type) {
RecordListFragment fragment = new RecordListFragment();
Bundle args = new Bundle();
args.putString(ARG_TYPE, type);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
recordType = getArguments().getString(ARG_TYPE, "view");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_record_list, container, false);
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout);
recyclerView = view.findViewById(R.id.recyclerView);
loadingView = view.findViewById(R.id.loadingView);
emptyView = view.findViewById(R.id.emptyView);
setupRecyclerView();
setupSwipeRefresh();
loadRecords();
return view;
}
private void setupRecyclerView() {
adapter = new RecordAdapter(recordType, item -> {
// 点击跳转
handleItemClick(item);
});
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0 && !isLoading && hasMore) {
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
if (lm != null) {
int visible = lm.getChildCount();
int total = lm.getItemCount();
int first = lm.findFirstVisibleItemPosition();
if ((visible + first) >= total - 2) {
loadMore();
}
}
}
}
});
}
private void setupSwipeRefresh() {
swipeRefreshLayout.setOnRefreshListener(() -> {
currentPage = 1;
hasMore = true;
loadRecords();
});
}
private void loadRecords() {
if (isLoading || getContext() == null) return;
isLoading = true;
if (currentPage == 1) {
loadingView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
}
Call<ApiResponse<PageResponse<Map<String, Object>>>> call;
switch (recordType) {
case "view":
call = ApiClient.getService(getContext()).getViewHistory(null, currentPage, 20);
break;
case "like":
call = ApiClient.getService(getContext()).getLikeRecords(null, currentPage, 20);
break;
case "collect":
call = ApiClient.getService(getContext()).getCollectedWorks(currentPage, 20);
break;
case "follow":
call = ApiClient.getService(getContext()).getFollowRecords(currentPage, 20);
break;
default:
call = ApiClient.getService(getContext()).getViewHistory(null, currentPage, 20);
}
call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
isLoading = false;
loadingView.setVisibility(View.GONE);
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<Map<String, Object>> data = response.body().getData();
if (data != null && data.getList() != null) {
if (currentPage == 1) {
records.clear();
}
records.addAll(data.getList());
adapter.setData(new ArrayList<>(records));
hasMore = data.getList().size() >= 20;
}
}
updateEmptyState();
}
@Override
public void onFailure(Call<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
isLoading = false;
loadingView.setVisibility(View.GONE);
swipeRefreshLayout.setRefreshing(false);
updateEmptyState();
if (getContext() != null) {
Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show();
}
}
});
}
private void loadMore() {
currentPage++;
loadRecords();
}
private void updateEmptyState() {
if (records.isEmpty()) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}
private void handleItemClick(Map<String, Object> item) {
if (getContext() == null) return;
String targetType = (String) item.get("targetType");
if (targetType == null) {
// 根据记录类型判断
if ("follow".equals(recordType)) {
// 跳转到用户主页
Object followedId = item.get("followedId");
if (followedId != null) {
Intent intent = new Intent(getContext(), UserProfileReadOnlyActivity.class);
intent.putExtra("userId", ((Number) followedId).intValue());
startActivity(intent);
}
} else if ("collect".equals(recordType) || "like".equals(recordType)) {
// 跳转到作品详情
Object workId = item.get("workId");
if (workId != null) {
Intent intent = new Intent(getContext(), WorkDetailActivity.class);
intent.putExtra("workId", ((Number) workId).longValue());
startActivity(intent);
}
}
return;
}
String targetId = String.valueOf(item.get("targetId"));
switch (targetType) {
case "room":
Intent roomIntent = new Intent(getContext(), RoomDetailActivity.class);
roomIntent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, targetId);
startActivity(roomIntent);
break;
case "work":
Intent workIntent = new Intent(getContext(), WorkDetailActivity.class);
workIntent.putExtra("workId", Long.parseLong(targetId));
startActivity(workIntent);
break;
case "profile":
Intent profileIntent = new Intent(getContext(), UserProfileReadOnlyActivity.class);
profileIntent.putExtra("userId", Integer.parseInt(targetId));
startActivity(profileIntent);
break;
}
}
}
/**
* 记录列表适配器
*/
private static class RecordAdapter extends RecyclerView.Adapter<RecordAdapter.ViewHolder> {
private final String recordType;
private List<Map<String, Object>> data = new ArrayList<>();
private final OnItemClickListener listener;
interface OnItemClickListener {
void onItemClick(Map<String, Object> item);
}
RecordAdapter(String recordType, OnItemClickListener listener) {
this.recordType = recordType;
this.listener = listener;
}
void setData(List<Map<String, Object>> data) {
this.data = data;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_record, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Map<String, Object> item = data.get(position);
holder.bind(item, recordType);
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onItemClick(item);
}
});
}
@Override
public int getItemCount() {
return data.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
TextView tvSubtitle;
TextView tvTime;
ImageView ivIcon;
ViewHolder(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tvTitle);
tvSubtitle = itemView.findViewById(R.id.tvSubtitle);
tvTime = itemView.findViewById(R.id.tvTime);
ivIcon = itemView.findViewById(R.id.ivIcon);
}
void bind(Map<String, Object> item, String recordType) {
String title = "";
String subtitle = "";
String time = "";
String imageUrl = "";
switch (recordType) {
case "view":
title = getStringValue(item, "targetTitle", "title");
String targetType = (String) item.get("targetType");
subtitle = "room".equals(targetType) ? "直播间" :
"work".equals(targetType) ? "作品" : "用户主页";
time = formatTime(item.get("updateTime"));
imageUrl = getStringValue(item, "coverImage", "avatar");
break;
case "like":
title = getStringValue(item, "targetTitle", "roomTitle", "title", "content");
String likeType = (String) item.get("targetType");
subtitle = "room".equals(likeType) ? "直播间" :
"work".equals(likeType) ? "作品" : "心愿";
time = formatTime(item.get("createTime"), item.get("likeTime"));
imageUrl = getStringValue(item, "coverImage");
break;
case "collect":
title = getStringValue(item, "title");
subtitle = "作品 · " + getStringValue(item, "authorName");
time = formatTime(item.get("collectTime"));
imageUrl = getStringValue(item, "coverImage");
break;
case "follow":
title = getStringValue(item, "followedNickname");
subtitle = "已关注";
time = formatTime(item.get("followTime"));
imageUrl = getStringValue(item, "followedAvatar");
break;
}
tvTitle.setText(title.isEmpty() ? "未知" : title);
tvSubtitle.setText(subtitle);
tvTime.setText(time);
// 加载图片
if (!imageUrl.isEmpty()) {
Glide.with(itemView.getContext())
.load(imageUrl)
.placeholder(R.drawable.ic_history_24)
.error(R.drawable.ic_history_24)
.centerCrop()
.into(ivIcon);
} else {
ivIcon.setImageResource(R.drawable.ic_history_24);
}
}
private String getStringValue(Map<String, Object> item, String... keys) {
for (String key : keys) {
Object value = item.get(key);
if (value != null && !value.toString().isEmpty()) {
return value.toString();
}
}
return "";
}
private String formatTime(Object... times) {
for (Object time : times) {
if (time != null) {
String timeStr = time.toString();
if (timeStr.length() > 10) {
return timeStr.substring(0, 10);
}
return timeStr;
}
}
return "";
}
}
}
}

View File

@ -419,6 +419,13 @@ public class ProfileActivity extends AppCompatActivity {
startActivity(new Intent(this, LikedRoomsActivity.class)); startActivity(new Intent(this, LikedRoomsActivity.class));
}); });
binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class))); binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class)));
binding.action4.setOnClickListener(v -> {
// 我的记录 - 跳转到统一记录页面
if (!AuthHelper.requireLogin(this, "查看记录需要登录")) {
return;
}
MyRecordsActivity.start(this);
});
binding.editProfile.setOnClickListener(v -> { binding.editProfile.setOnClickListener(v -> {
// 检查登录状态编辑资料需要登录 // 检查登录状态编辑资料需要登录

View File

@ -934,22 +934,28 @@ public class RoomDetailActivity extends AppCompatActivity {
ApiService apiService = ApiClient.getService(getApplicationContext()); ApiService apiService = ApiClient.getService(getApplicationContext());
// 使用新的统一观看历史API
java.util.Map<String, Object> body = new java.util.HashMap<>(); java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("roomId", roomId); body.put("targetType", "room");
body.put("watchTime", System.currentTimeMillis()); body.put("targetId", roomId);
body.put("targetTitle", roomTitle != null ? roomTitle : "直播间");
body.put("duration", 0); // 初始时长为0后续可更新
Call<ApiResponse<java.util.Map<String, Object>>> call = apiService.recordWatchHistory(body); Call<ApiResponse<java.util.Map<String, Object>>> call = apiService.recordViewHistoryNew(body);
call.enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() { call.enqueue(new Callback<ApiResponse<java.util.Map<String, Object>>>() {
@Override @Override
public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call, public void onResponse(Call<ApiResponse<java.util.Map<String, Object>>> call,
Response<ApiResponse<java.util.Map<String, Object>>> response) { Response<ApiResponse<java.util.Map<String, Object>>> response) {
// 忽略结果接口可能不存在 if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
android.util.Log.d("RoomDetail", "观看历史记录成功");
}
} }
@Override @Override
public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) { public void onFailure(Call<ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
// 忽略错误接口可能不存在 // 忽略错误不影响直播观看
android.util.Log.w("RoomDetail", "记录观看历史失败: " + t.getMessage());
} }
}); });
} catch (Exception e) { } catch (Exception e) {

View File

@ -3,19 +3,42 @@ package com.example.livestreaming;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.StaggeredGridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.example.livestreaming.databinding.ActivityWatchHistoryBinding; import com.example.livestreaming.databinding.ActivityWatchHistoryBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.PageResponse;
import com.example.livestreaming.net.Room; import com.example.livestreaming.net.Room;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 观看历史页面
*/
public class WatchHistoryActivity extends AppCompatActivity { public class WatchHistoryActivity extends AppCompatActivity {
private ActivityWatchHistoryBinding binding; private ActivityWatchHistoryBinding binding;
private RoomsAdapter adapter;
private final List<Room> historyRooms = new ArrayList<>();
private int currentPage = 1;
private boolean isLoading = false;
private boolean hasMore = true;
public static void start(Context context) { public static void start(Context context) {
Intent intent = new Intent(context, WatchHistoryActivity.class); Intent intent = new Intent(context, WatchHistoryActivity.class);
@ -28,34 +51,202 @@ public class WatchHistoryActivity extends AppCompatActivity {
binding = ActivityWatchHistoryBinding.inflate(getLayoutInflater()); binding = ActivityWatchHistoryBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot()); setContentView(binding.getRoot());
initViews();
setupRecyclerView();
loadWatchHistory();
}
private void initViews() {
binding.backButton.setOnClickListener(v -> finish()); binding.backButton.setOnClickListener(v -> finish());
RoomsAdapter adapter = new RoomsAdapter(room -> { // 清空历史按钮
binding.clearButton.setOnClickListener(v -> showClearConfirmDialog());
// 下拉刷新
binding.swipeRefreshLayout.setOnRefreshListener(() -> {
currentPage = 1;
hasMore = true;
loadWatchHistory();
});
}
private void setupRecyclerView() {
adapter = new RoomsAdapter(room -> {
if (room == null) return; if (room == null) return;
Intent intent = new Intent(WatchHistoryActivity.this, RoomDetailActivity.class); Intent intent = new Intent(WatchHistoryActivity.this, RoomDetailActivity.class);
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
startActivity(intent); startActivity(intent);
}); });
// TODO: 接入后端接口 - 获取观看历史 binding.roomsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 接口路径: GET /api/watch/history
// 请求参数:
// - userId: 当前用户ID从token中获取
// - page (可选): 页码
// - pageSize (可选): 每页数量
// 返回数据格式: ApiResponse<List<Room>>
// Room对象应包含: id, title, streamerName, type, isLive, coverUrl, watchTime观看时间等字段
// 列表应按观看时间倒序排列最近观看的在前面
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
binding.roomsRecyclerView.setLayoutManager(glm);
binding.roomsRecyclerView.setAdapter(adapter); binding.roomsRecyclerView.setAdapter(adapter);
adapter.submitList(buildDemoHistory(18)); // 滚动加载更多
binding.roomsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0 && !isLoading && hasMore) {
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount - 2) {
loadMore();
}
}
}
}
});
} }
private List<Room> buildDemoHistory(int count) { private void loadWatchHistory() {
// 不再使用模拟数据只从后端接口获取真实观看历史数据 if (isLoading) return;
return new ArrayList<>(); isLoading = true;
if (currentPage == 1) {
showLoading();
}
// 调用API获取观看历史只获取直播间类型
ApiClient.getService(this)
.getViewHistory("room", currentPage, 20)
.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
isLoading = false;
hideLoading();
binding.swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<Map<String, Object>> pageData = response.body().getData();
if (pageData != null && pageData.getList() != null) {
List<Room> rooms = convertToRooms(pageData.getList());
if (currentPage == 1) {
historyRooms.clear();
}
historyRooms.addAll(rooms);
adapter.submitList(new ArrayList<>(historyRooms));
hasMore = rooms.size() >= 20;
updateEmptyState();
}
} else {
if (currentPage == 1) {
updateEmptyState();
}
Toast.makeText(WatchHistoryActivity.this, "加载失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
isLoading = false;
hideLoading();
binding.swipeRefreshLayout.setRefreshing(false);
if (currentPage == 1) {
updateEmptyState();
}
Toast.makeText(WatchHistoryActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
}
private void loadMore() {
currentPage++;
loadWatchHistory();
}
/**
* 将Map转换为Room对象
*/
private List<Room> convertToRooms(List<Map<String, Object>> maps) {
List<Room> rooms = new ArrayList<>();
for (Map<String, Object> map : maps) {
Room room = new Room();
// 从观看历史数据中提取信息
if (map.containsKey("targetId")) {
room.setId(map.get("targetId"));
}
if (map.containsKey("targetTitle")) {
room.setTitle((String) map.get("targetTitle"));
}
if (map.containsKey("title")) {
room.setTitle((String) map.get("title"));
}
if (map.containsKey("streamerName")) {
room.setStreamerName((String) map.get("streamerName"));
}
if (map.containsKey("coverImage")) {
room.setCoverImage((String) map.get("coverImage"));
}
if (map.containsKey("isLive")) {
Object isLive = map.get("isLive");
if (isLive instanceof Number) {
room.setLive(((Number) isLive).intValue() == 1);
} else if (isLive instanceof Boolean) {
room.setLive((Boolean) isLive);
}
}
rooms.add(room);
}
return rooms;
}
private void showClearConfirmDialog() {
new AlertDialog.Builder(this)
.setTitle("清空观看历史")
.setMessage("确定要清空所有观看历史吗?此操作不可恢复。")
.setPositiveButton("确定", (dialog, which) -> clearWatchHistory())
.setNegativeButton("取消", null)
.show();
}
private void clearWatchHistory() {
ApiClient.getService(this)
.clearViewHistory("room")
.enqueue(new Callback<ApiResponse<String>>() {
@Override
public void onResponse(Call<ApiResponse<String>> call, Response<ApiResponse<String>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
historyRooms.clear();
adapter.submitList(new ArrayList<>());
updateEmptyState();
Toast.makeText(WatchHistoryActivity.this, "已清空观看历史", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(WatchHistoryActivity.this, "清空失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<String>> call, Throwable t) {
Toast.makeText(WatchHistoryActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
}
private void showLoading() {
binding.loadingView.setVisibility(View.VISIBLE);
binding.roomsRecyclerView.setVisibility(View.GONE);
binding.emptyView.setVisibility(View.GONE);
}
private void hideLoading() {
binding.loadingView.setVisibility(View.GONE);
}
private void updateEmptyState() {
if (historyRooms.isEmpty()) {
binding.emptyView.setVisibility(View.VISIBLE);
binding.roomsRecyclerView.setVisibility(View.GONE);
} else {
binding.emptyView.setVisibility(View.GONE);
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
}
} }
} }

View File

@ -652,4 +652,108 @@ public interface ApiService {
Call<ApiResponse<List<CommunityResponse.MatchUser>>> getMatchList( Call<ApiResponse<List<CommunityResponse.MatchUser>>> getMatchList(
@Query("page") int page, @Query("page") int page,
@Query("limit") int limit); @Query("limit") int limit);
// ==================== 用户活动记录接口 ====================
/**
* 记录观看历史
*/
@POST("api/front/activity/view/record")
Call<ApiResponse<Map<String, Object>>> recordViewHistoryNew(@Body Map<String, Object> body);
/**
* 获取观看历史列表
*/
@GET("api/front/activity/view/history")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getViewHistory(
@Query("targetType") String targetType,
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 删除单条观看历史
*/
@DELETE("api/front/activity/view/history/{historyId}")
Call<ApiResponse<String>> deleteViewHistory(@Path("historyId") long historyId);
/**
* 清空观看历史
*/
@DELETE("api/front/activity/view/history")
Call<ApiResponse<String>> clearViewHistory(@Query("targetType") String targetType);
/**
* 获取点赞记录列表全部类型
*/
@GET("api/front/activity/like/records")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getLikeRecords(
@Query("targetType") String targetType,
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取点赞的直播间列表
*/
@GET("api/front/activity/like/rooms")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getLikedRoomsNew(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取点赞的作品列表
*/
@GET("api/front/activity/like/works")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getLikedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取点赞的心愿列表
*/
@GET("api/front/activity/like/wishes")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getLikedWishes(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取关注记录列表
*/
@GET("api/front/activity/follow/records")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getFollowRecords(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取收藏的作品列表
*/
@GET("api/front/activity/collect/works")
Call<ApiResponse<PageResponse<Map<String, Object>>>> getCollectedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取用户活动统计
*/
@GET("api/front/activity/stats")
Call<ApiResponse<Map<String, Object>>> getUserActivityStats();
/**
* 获取搜索历史
*/
@GET("api/front/search/history")
Call<ApiResponse<List<SearchHistoryResponse>>> getSearchHistory(
@Query("searchType") Integer searchType,
@Query("limit") int limit);
/**
* 清除搜索历史
*/
@DELETE("api/front/search/history")
Call<ApiResponse<String>> clearSearchHistory(@Query("searchType") Integer searchType);
/**
* 删除单条搜索历史
*/
@DELETE("api/front/search/history/{historyId}")
Call<ApiResponse<String>> deleteSearchHistoryItem(@Path("historyId") long historyId);
} }

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#999999"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
</vector>

View File

@ -0,0 +1,65 @@
<?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">
<!-- 顶部标题栏 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/backButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="返回"
android:src="@drawable/ic_arrow_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的记录"
android:textColor="#111111"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Tab栏 -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/white"
app:tabGravity="fill"
app:tabIndicatorColor="#FF6B6B"
app:tabIndicatorHeight="3dp"
app:tabMode="fixed"
app:tabSelectedTextColor="#FF6B6B"
app:tabTextColor="#666666" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#EEEEEE" />
<!-- ViewPager -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

View File

@ -475,6 +475,7 @@
android:id="@+id/action3" android:id="@+id/action3"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="64dp" android:layout_height="64dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
@ -519,6 +520,54 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/action4"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/bg_gray_12">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_history_24"
android:tint="#2196F3" />
</FrameLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的记录"
android:textColor="#111111"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/recordsCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="查看全部"
android:textColor="#999999"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
</HorizontalScrollView> </HorizontalScrollView>

View File

@ -33,6 +33,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:text="观看历史" android:text="观看历史"
@ -40,14 +41,36 @@
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/backButton" app:layout_constraintBottom_toBottomOf="@id/backButton"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/clearButton"
app:layout_constraintStart_toEndOf="@id/backButton" app:layout_constraintStart_toEndOf="@id/backButton"
app:layout_constraintTop_toTopOf="@id/backButton" /> app:layout_constraintTop_toTopOf="@id/backButton" />
<TextView
android:id="@+id/clearButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:text="清空"
android:textColor="#FF6B6B"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomsRecyclerView" android:id="@+id/roomsRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -56,7 +79,64 @@
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="16dp" android:paddingBottom="16dp" />
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- 加载中 -->
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="加载中..."
android:textColor="#999999"
android:textSize="14sp" />
</LinearLayout>
<!-- 空状态 -->
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:alpha="0.5"
android:src="@drawable/ic_history_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无观看历史"
android:textColor="#999999"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="去看看精彩直播吧"
android:textColor="#BBBBBB"
android:textSize="14sp" />
</LinearLayout>
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="16dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 加载中 -->
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="加载中..."
android:textColor="#999999"
android:textSize="14sp" />
</LinearLayout>
<!-- 空状态 -->
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:alpha="0.4"
android:src="@drawable/ic_history_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无记录"
android:textColor="#999999"
android:textSize="16sp" />
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:paddingBottom="12dp">
<ImageView
android:id="@+id/ivIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="#F5F5F5"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="标题"
android:textColor="#333333"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/tvTime"
app:layout_constraintStart_toEndOf="@id/ivIcon"
app:layout_constraintTop_toTopOf="@id/ivIcon" />
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="副标题"
android:textColor="#999999"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@id/tvTime"
app:layout_constraintStart_toEndOf="@id/ivIcon"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="时间"
android:textColor="#BBBBBB"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvTitle" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="#F0F0F0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/tvTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,297 +2,106 @@
## 功能概述 ## 功能概述
在管理后台的用户详情页面添加了三个新的标签页,用于查看用户的活动记录: 实现了管理端与移动端的"历史记录"功能互通,包括:
1. **关注记录** - 查看用户关注了哪些人 - 观看历史(直播间、作品、用户主页)
2. **点赞记录** - 查看用户点赞了哪些内容(直播间、作品、心愿等) - 点赞记录(直播间、作品、心愿)
3. **查看历史** - 查看用户的浏览历史记录 - 关注记录
- 收藏记录
## 实现内容 ## 后端实现
### 1. 前端修改 ### 1. 服务层
- `UserActivityRecordService.java` - 服务接口
- `UserActivityRecordServiceImpl.java` - 服务实现
**文件**: `Zhibo/admin/src/views/user/list/userDetails.vue` ### 2. API 控制器
- **前端 API**: `UserActivityRecordController.java`
- 路径前缀: `/api/front/activity/`
- 接口列表:
- `POST /view/record` - 记录观看历史
- `GET /view/history` - 获取观看历史
- `DELETE /view/history/{historyId}` - 删除单条观看历史
- `DELETE /view/history` - 清空观看历史
- `GET /like/records` - 获取点赞记录
- `GET /like/rooms` - 获取点赞的直播间
- `GET /like/works` - 获取点赞的作品
- `GET /like/wishes` - 获取点赞的心愿
- `GET /follow/records` - 获取关注记录
- `GET /collect/works` - 获取收藏的作品
- `GET /stats` - 获取用户活动统计
**新增标签页**: - **管理端 API**: `UserActivityController.java`
- 标签页 7关注记录 - 路径前缀: `/api/admin/user/`
- 标签页 8点赞记录 - 接口列表:
- 标签页 9查看历史 - `GET /follow/records` - 获取用户关注记录
- `GET /like/records` - 获取用户点赞记录
**功能特性**: - `GET /view/history` - 获取用户查看历史
- 支持分页显示 - `GET /collect/works` - 获取用户收藏的作品
- 数据实时加载 - `GET /activity/stats` - 获取用户活动统计
- 状态标签显示(关注状态、内容类型等)
- 时间排序(最新的在前)
### 2. 后端接口
**文件**: `Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/UserActivityController.java`
**新增接口**:
#### 2.1 获取关注记录
```
GET /api/admin/user/follow/records
参数:
- userId: 用户ID
- page: 页码默认1
- limit: 每页数量默认10
```
返回数据:
```json
{
"code": 200,
"data": {
"list": [
{
"followedId": 41,
"followedNickname": "夏至已至",
"followStatus": "1",
"createTime": "2026-01-03 14:19:16"
}
],
"total": 1,
"page": 1,
"limit": 10
}
}
```
#### 2.2 获取点赞记录
```
GET /api/admin/user/like/records
参数:
- userId: 用户ID
- page: 页码默认1
- limit: 每页数量默认10
```
返回数据:
```json
{
"code": 200,
"data": {
"list": [
{
"targetType": "room",
"targetId": "8",
"targetTitle": "火影忍者",
"createTime": "2026-01-03 14:30:00"
}
],
"total": 1,
"page": 1,
"limit": 10
}
}
```
#### 2.3 获取查看历史
```
GET /api/admin/user/view/history
参数:
- userId: 用户ID
- page: 页码默认1
- limit: 每页数量默认10
```
返回数据:
```json
{
"code": 200,
"data": {
"list": [
{
"targetType": "room",
"targetId": "8",
"targetTitle": "火影忍者",
"viewDuration": 1200,
"createTime": "2026-01-03 14:25:00"
}
],
"total": 1,
"page": 1,
"limit": 10
}
}
```
### 3. 数据库表 ### 3. 数据库表
执行 `sql/user_activity_records_update.sql` 创建以下表:
- `eb_view_history` - 查看历史记录表
- `eb_live_room_like` - 直播间点赞记录表
- `eb_wish_like` - 心愿点赞记录表
- `eb_search_history` - 搜索历史表
- `eb_hot_search` - 热门搜索表
- `eb_works_relation` - 作品关系表(点赞、收藏)
**文件**: `user_activity_tables.sql` ## Android 端实现
**新增表**: ### 1. API 接口
`ApiService.java` 中添加了以下接口:
- 观看历史相关接口
- 点赞记录相关接口
- 关注记录接口
- 收藏记录接口
- 用户活动统计接口
#### 3.1 直播间点赞记录表 (eb_live_room_like) ### 2. 页面
```sql - `MyRecordsActivity.java` - 我的记录页面(整合所有记录类型)
CREATE TABLE `eb_live_room_like` ( - `WatchHistoryActivity.java` - 观看历史页面(已更新使用新 API
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
`room_id` varchar(50) NOT NULL COMMENT '直播间ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_room` (`user_id`, `room_id`)
);
```
#### 3.2 作品点赞记录表 (eb_work_like) ### 3. 布局文件
```sql - `activity_my_records.xml` - 我的记录页面布局
CREATE TABLE `eb_work_like` ( - `fragment_record_list.xml` - 记录列表 Fragment 布局
`id` bigint(20) NOT NULL AUTO_INCREMENT, - `item_record.xml` - 记录列表项布局
`user_id` int(11) NOT NULL COMMENT '用户ID', - `activity_watch_history.xml` - 观看历史页面布局
`work_id` bigint(20) NOT NULL COMMENT '作品ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_work` (`user_id`, `work_id`)
);
```
#### 3.3 心愿点赞记录表 (eb_wish_like) ### 4. 入口
```sql `ProfileActivity` 中添加了"我的记录"快捷入口action4
CREATE TABLE `eb_wish_like` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
`wish_id` bigint(20) NOT NULL COMMENT '心愿ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_wish` (`user_id`, `wish_id`)
);
```
#### 3.4 查看历史记录表 (eb_view_history) ## 管理端前端实现
```sql
CREATE TABLE `eb_view_history` ( ### 1. API 文件
`id` bigint(20) NOT NULL AUTO_INCREMENT, - `src/api/userActivity.js` - 用户活动记录 API
`user_id` int(11) NOT NULL COMMENT '用户ID',
`target_type` varchar(20) NOT NULL COMMENT '目标类型', ### 2. 页面更新
`target_id` varchar(50) NOT NULL COMMENT '目标ID', - `src/views/user/list/userDetails.vue` - 用户详情页面
`target_title` varchar(255) DEFAULT NULL COMMENT '目标标题', - 添加了"关注记录"标签页
`view_duration` int(11) DEFAULT 0 COMMENT '观看时长(秒)', - 添加了"点赞记录"标签页
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - 添加了"查看历史"标签页
PRIMARY KEY (`id`), - 添加了"收藏记录"标签页
KEY `idx_user_id` (`user_id`)
);
```
## 部署步骤 ## 部署步骤
### 1. 创建数据库表 1. **数据库更新**
```bash
mysql -u root -p zhibo < user_activity_tables.sql
```
### 2. 编译后端代码
```bash
cd Zhibo/zhibo-h
mvn clean package -DskipTests -pl crmeb-admin -am
```
### 3. 重启后端服务
```bash
cd /root/zhibo/Zhibo/zhibo-h/crmeb-admin
./restart.sh
```
### 4. 前端无需重新编译
前端代码修改后,刷新浏览器即可看到新功能。
## 使用说明
### 1. 查看用户活动记录
1. 登录管理后台
2. 进入"用户管理" -> "用户列表"
3. 点击某个用户的"详情"按钮
4. 在弹出的抽屉中,可以看到新增的三个标签页:
- 关注记录
- 点赞记录
- 查看历史
### 2. 数据说明
**关注记录**:
- 显示用户关注了哪些人
- 包含关注状态(已关注/已取消)
- 按关注时间倒序排列
**点赞记录**:
- 显示用户点赞的所有内容
- 包含类型标签(直播间/作品/心愿)
- 按点赞时间倒序排列
**查看历史**:
- 显示用户的浏览记录
- 包含观看时长
- 按查看时间倒序排列
### 3. 数据收集
这些数据需要在应用中主动记录:
**点赞记录**:
- 用户点赞直播间时,插入 `eb_live_room_like`
- 用户点赞作品时,插入 `eb_work_like`
- 用户点赞心愿时,插入 `eb_wish_like`
**查看历史**:
- 用户进入直播间时,记录到 `eb_view_history`
- 用户查看作品时,记录到 `eb_view_history`
- 用户访问他人主页时,记录到 `eb_view_history`
## 测试数据
可以使用以下 SQL 插入测试数据:
```sql ```sql
-- 插入点赞记录 -- 执行 SQL 脚本
INSERT INTO eb_live_room_like (user_id, room_id, create_time) source sql/user_activity_records_update.sql
VALUES (43, '8', '2026-01-03 14:30:00');
-- 插入查看历史
INSERT INTO eb_view_history (user_id, target_type, target_id, target_title, view_duration, create_time)
VALUES (43, 'room', '8', '火影忍者', 1200, '2026-01-03 14:25:00');
``` ```
2. **后端部署**
- 重新编译并部署 Java 后端服务
3. **管理端前端部署**
- 重新构建并部署管理端前端
4. **Android 端**
- 重新编译 APK 并安装
## 注意事项 ## 注意事项
1. **性能优化**: 1. 后端使用 JdbcTemplate 直接操作数据库,确保数据库表已创建
- 所有表都添加了索引,确保查询性能 2. Android 端需要登录后才能使用记录功能
- 使用分页查询,避免一次加载过多数据 3. 管理端可以查看任意用户的活动记录
2. **数据一致性**:
- 点赞表使用唯一索引,防止重复点赞
- 查看历史可以重复记录,用于统计观看次数
3. **扩展性**:
- 可以轻松添加更多类型的点赞记录
- 查看历史支持多种目标类型
4. **隐私保护**:
- 只有管理员可以查看用户的活动记录
- 前端用户无法查看其他用户的私密数据
## 后续优化建议
1. **数据统计**:
- 添加用户活跃度统计
- 生成用户行为分析报告
2. **数据导出**:
- 支持导出用户活动记录为 Excel
- 用于数据分析和备份
3. **实时更新**:
- 使用 WebSocket 实时推送新的活动记录
- 提升管理员的监控体验
4. **数据清理**:
- 定期清理过期的查看历史记录
- 避免数据库膨胀
## 相关文件
- 前端页面: `Zhibo/admin/src/views/user/list/userDetails.vue`
- 后端控制器: `Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/UserActivityController.java`
- 数据库脚本: `user_activity_tables.sql`
- 说明文档: `用户活动记录功能说明.md`