diff --git a/Zhibo/admin/src/api/userActivity.js b/Zhibo/admin/src/api/userActivity.js new file mode 100644 index 00000000..c483bcff --- /dev/null +++ b/Zhibo/admin/src/api/userActivity.js @@ -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 + }); +} diff --git a/Zhibo/admin/src/views/user/list/userDetails.vue b/Zhibo/admin/src/views/user/list/userDetails.vue index 57ad253f..16bf198f 100644 --- a/Zhibo/admin/src/views/user/list/userDetails.vue +++ b/Zhibo/admin/src/views/user/list/userDetails.vue @@ -278,6 +278,17 @@ + + + + + + + + + + +
{ - this.tableData = res.data.list || []; - this.paginationData.total = res.data.total || 0; + this.tableData = res.list || []; + this.paginationData.total = res.total || 0; }).catch(() => { this.tableData = []; this.paginationData.total = 0; @@ -421,17 +434,13 @@ export default { }, // 获取点赞记录 getLikeRecords() { - this.$http({ - url: '/admin/user/like/records', - method: 'get', - params: { - userId: this.userNo, - page: this.paginationData.page, - limit: this.paginationData.limit, - }, + fetchLikeRecords({ + userId: this.userNo, + page: this.paginationData.page, + limit: this.paginationData.limit, }).then((res) => { - this.tableData = res.data.list || []; - this.paginationData.total = res.data.total || 0; + this.tableData = res.list || []; + this.paginationData.total = res.total || 0; }).catch(() => { this.tableData = []; this.paginationData.total = 0; @@ -439,17 +448,27 @@ export default { }, // 获取查看历史 getViewHistory() { - this.$http({ - url: '/admin/user/view/history', - method: 'get', - params: { - userId: this.userNo, - page: this.paginationData.page, - limit: this.paginationData.limit, - }, + fetchViewHistory({ + userId: this.userNo, + page: this.paginationData.page, + limit: this.paginationData.limit, }).then((res) => { - this.tableData = res.data.list || []; - this.paginationData.total = res.data.total || 0; + this.tableData = res.list || []; + 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(() => { this.tableData = []; this.paginationData.total = 0; diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/UserActivityController.java b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/UserActivityController.java index 9f6d639a..57745fc0 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/UserActivityController.java +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/UserActivityController.java @@ -2,8 +2,10 @@ package com.zbkj.admin.controller; import com.zbkj.common.page.CommonPage; import com.zbkj.common.result.CommonResult; +import com.zbkj.service.service.UserActivityRecordService; 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.jdbc.core.JdbcTemplate; @@ -15,7 +17,7 @@ import java.util.List; import java.util.Map; /** - * 用户活动记录控制器 + * 用户活动记录控制器(管理后台) * 包括关注记录、点赞记录、查看历史等 */ @Slf4j @@ -28,6 +30,9 @@ public class UserActivityController { @Autowired private JdbcTemplate jdbcTemplate; + @Autowired + private UserActivityRecordService userActivityRecordService; + /** * 获取用户的关注记录 */ @@ -38,38 +43,7 @@ public class UserActivityController { @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit) { try { - // 计算偏移量 - 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> list = jdbcTemplate.queryForList(sql, userId, limit, offset); - - // 封装分页结果 - CommonPage> 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)); - + CommonPage> result = userActivityRecordService.getFollowRecords(userId, page, limit); return CommonResult.success(result); } catch (Exception e) { log.error("获取用户关注记录失败: userId={}", userId, e); @@ -84,76 +58,12 @@ public class UserActivityController { @GetMapping("/like/records") public CommonResult>> getLikeRecords( @RequestParam Integer userId, + @ApiParam(value = "目标类型:room-直播间, work-作品, wish-心愿") + @RequestParam(required = false) String targetType, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit) { try { - // 计算偏移量 - 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> list = jdbcTemplate.queryForList(sql.toString(), userId, userId, userId, limit, offset); - - // 封装分页结果 - CommonPage> result = new CommonPage<>(); - result.setList(list); - result.setTotal((long) total); - result.setPage(page); - result.setLimit(limit); - result.setTotalPage((int) Math.ceil((double) total / limit)); - + CommonPage> result = userActivityRecordService.getLikeRecords(userId, targetType, page, limit); return CommonResult.success(result); } catch (Exception e) { log.error("获取用户点赞记录失败: userId={}", userId, e); @@ -168,56 +78,49 @@ public class UserActivityController { @GetMapping("/view/history") public CommonResult>> getViewHistory( @RequestParam Integer userId, + @ApiParam(value = "目标类型:room-直播间, work-作品, profile-用户主页") + @RequestParam(required = false) String targetType, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit) { try { - // 计算偏移量 - 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> 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> list = jdbcTemplate.queryForList(sql, userId, limit, offset); - - // 封装分页结果 - CommonPage> 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)); - + CommonPage> result = userActivityRecordService.getViewHistory(userId, targetType, page, limit); return CommonResult.success(result); } catch (Exception e) { log.error("获取用户查看历史失败: userId={}", userId, e); return CommonResult.failed("获取查看历史失败"); } } + + /** + * 获取用户收藏的作品 + */ + @ApiOperation(value = "获取用户收藏的作品") + @GetMapping("/collect/works") + public CommonResult>> getCollectedWorks( + @RequestParam Integer userId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer limit) { + try { + CommonPage> 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> getUserActivityStats(@RequestParam Integer userId) { + try { + Map stats = userActivityRecordService.getUserActivityStats(userId); + return CommonResult.success(stats); + } catch (Exception e) { + log.error("获取用户活动统计失败: userId={}", userId, e); + return CommonResult.failed("获取统计失败"); + } + } } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserActivityRecordController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserActivityRecordController.java new file mode 100644 index 00000000..02e3af69 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserActivityRecordController.java @@ -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> recordViewHistory(@RequestBody Map 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 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>> 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> result = userActivityRecordService.getViewHistory(userId, targetType, page, pageSize); + return CommonResult.success(result); + } + + /** + * 删除单条观看历史 + */ + @ApiOperation(value = "删除单条观看历史") + @DeleteMapping("/view/history/{historyId}") + public CommonResult 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 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>> 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> result = userActivityRecordService.getLikeRecords(userId, targetType, page, pageSize); + return CommonResult.success(result); + } + + /** + * 获取点赞的直播间列表 + */ + @ApiOperation(value = "获取点赞的直播间列表") + @GetMapping("/like/rooms") + public CommonResult>> getLikedRooms( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize) { + + Integer userId = userService.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + CommonPage> result = userActivityRecordService.getLikedRooms(userId, page, pageSize); + return CommonResult.success(result); + } + + /** + * 获取点赞的作品列表 + */ + @ApiOperation(value = "获取点赞的作品列表") + @GetMapping("/like/works") + public CommonResult>> getLikedWorks( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize) { + + Integer userId = userService.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + CommonPage> result = userActivityRecordService.getLikedWorks(userId, page, pageSize); + return CommonResult.success(result); + } + + /** + * 获取点赞的心愿列表 + */ + @ApiOperation(value = "获取点赞的心愿列表") + @GetMapping("/like/wishes") + public CommonResult>> getLikedWishes( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize) { + + Integer userId = userService.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + CommonPage> result = userActivityRecordService.getLikedWishes(userId, page, pageSize); + return CommonResult.success(result); + } + + // ==================== 关注记录 ==================== + + /** + * 获取关注记录列表 + */ + @ApiOperation(value = "获取关注记录列表") + @GetMapping("/follow/records") + public CommonResult>> getFollowRecords( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize) { + + Integer userId = userService.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + CommonPage> result = userActivityRecordService.getFollowRecords(userId, page, pageSize); + return CommonResult.success(result); + } + + // ==================== 收藏记录 ==================== + + /** + * 获取收藏的作品列表 + */ + @ApiOperation(value = "获取收藏的作品列表") + @GetMapping("/collect/works") + public CommonResult>> getCollectedWorks( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize) { + + Integer userId = userService.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + CommonPage> result = userActivityRecordService.getCollectedWorks(userId, page, pageSize); + return CommonResult.success(result); + } + + // ==================== 统计信息 ==================== + + /** + * 获取用户活动统计 + */ + @ApiOperation(value = "获取用户活动统计") + @GetMapping("/stats") + public CommonResult> getUserActivityStats() { + Integer userId = userService.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + Map stats = userActivityRecordService.getUserActivityStats(userId); + return CommonResult.success(stats); + } +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/UserActivityRecordService.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/UserActivityRecordService.java new file mode 100644 index 00000000..2e05d0ae --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/UserActivityRecordService.java @@ -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> 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> getLikeRecords(Integer userId, String targetType, Integer page, Integer pageSize); + + /** + * 获取用户点赞的直播间列表 + */ + CommonPage> getLikedRooms(Integer userId, Integer page, Integer pageSize); + + /** + * 获取用户点赞的作品列表 + */ + CommonPage> getLikedWorks(Integer userId, Integer page, Integer pageSize); + + /** + * 获取用户点赞的心愿列表 + */ + CommonPage> getLikedWishes(Integer userId, Integer page, Integer pageSize); + + // ==================== 关注记录 ==================== + + /** + * 获取用户关注记录列表 + * @param userId 用户ID + * @param page 页码 + * @param pageSize 每页数量 + * @return 关注记录列表 + */ + CommonPage> getFollowRecords(Integer userId, Integer page, Integer pageSize); + + // ==================== 收藏记录 ==================== + + /** + * 获取用户收藏的作品列表 + */ + CommonPage> getCollectedWorks(Integer userId, Integer page, Integer pageSize); + + // ==================== 统计信息 ==================== + + /** + * 获取用户活动统计 + * @param userId 用户ID + * @return 统计信息(观看数、点赞数、关注数、收藏数等) + */ + Map getUserActivityStats(Integer userId); +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserActivityRecordServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserActivityRecordServiceImpl.java new file mode 100644 index 00000000..296c981e --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserActivityRecordServiceImpl.java @@ -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> 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> 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 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> 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> list) { + for (Map 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> 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> 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> 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 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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 getUserActivityStats(Integer userId) { + Map 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> emptyPage(Integer page, Integer pageSize) { + CommonPage> result = new CommonPage<>(); + result.setList(new ArrayList<>()); + result.setTotal(0L); + result.setPage(page); + result.setLimit(pageSize); + result.setTotalPage(0); + return result; + } + + private CommonPage> buildPage(List> list, int total, + Integer page, Integer pageSize) { + CommonPage> 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; + } +} diff --git a/Zhibo/zhibo-h/sql/user_activity_records_update.sql b/Zhibo/zhibo-h/sql/user_activity_records_update.sql new file mode 100644 index 00000000..e22271e7 --- /dev/null +++ b/Zhibo/zhibo-h/sql/user_activity_records_update.sql @@ -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; diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 8492b3a4..24765448 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -67,6 +67,10 @@ android:name="com.example.livestreaming.WatchHistoryActivity" android:exported="false" /> + + diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyRecordsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyRecordsActivity.java new file mode 100644 index 00000000..8ba5f76e --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/MyRecordsActivity.java @@ -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> 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>>> 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>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + loadingView.setVisibility(View.GONE); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> 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>>> 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 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 { + + private final String recordType; + private List> data = new ArrayList<>(); + private final OnItemClickListener listener; + + interface OnItemClickListener { + void onItemClick(Map item); + } + + RecordAdapter(String recordType, OnItemClickListener listener) { + this.recordType = recordType; + this.listener = listener; + } + + void setData(List> 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 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 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 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 ""; + } + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java index d1459879..5e972a29 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java @@ -419,6 +419,13 @@ public class ProfileActivity extends AppCompatActivity { startActivity(new Intent(this, LikedRoomsActivity.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 -> { // 检查登录状态,编辑资料需要登录 diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java index dd08fdb2..91db0c63 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java @@ -934,22 +934,28 @@ public class RoomDetailActivity extends AppCompatActivity { ApiService apiService = ApiClient.getService(getApplicationContext()); + // 使用新的统一观看历史API java.util.Map body = new java.util.HashMap<>(); - body.put("roomId", roomId); - body.put("watchTime", System.currentTimeMillis()); + body.put("targetType", "room"); + body.put("targetId", roomId); + body.put("targetTitle", roomTitle != null ? roomTitle : "直播间"); + body.put("duration", 0); // 初始时长为0,后续可更新 - Call>> call = apiService.recordWatchHistory(body); + Call>> call = apiService.recordViewHistoryNew(body); call.enqueue(new Callback>>() { @Override public void onResponse(Call>> call, Response>> response) { - // 忽略结果,接口可能不存在 + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + android.util.Log.d("RoomDetail", "观看历史记录成功"); + } } @Override public void onFailure(Call>> call, Throwable t) { - // 忽略错误,接口可能不存在 + // 忽略错误,不影响直播观看 + android.util.Log.w("RoomDetail", "记录观看历史失败: " + t.getMessage()); } }); } catch (Exception e) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java index 66d7740d..9175df46 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java @@ -3,19 +3,42 @@ package com.example.livestreaming; import android.content.Context; import android.content.Intent; 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.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.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.Room; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * 观看历史页面 + */ public class WatchHistoryActivity extends AppCompatActivity { private ActivityWatchHistoryBinding binding; + private RoomsAdapter adapter; + private final List historyRooms = new ArrayList<>(); + + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; public static void start(Context context) { Intent intent = new Intent(context, WatchHistoryActivity.class); @@ -28,34 +51,202 @@ public class WatchHistoryActivity extends AppCompatActivity { binding = ActivityWatchHistoryBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - binding.backButton.setOnClickListener(v -> finish()); + initViews(); + setupRecyclerView(); + loadWatchHistory(); + } - RoomsAdapter adapter = new RoomsAdapter(room -> { + private void initViews() { + binding.backButton.setOnClickListener(v -> finish()); + + // 清空历史按钮 + binding.clearButton.setOnClickListener(v -> showClearConfirmDialog()); + + // 下拉刷新 + binding.swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadWatchHistory(); + }); + } + + private void setupRecyclerView() { + adapter = new RoomsAdapter(room -> { if (room == null) return; Intent intent = new Intent(WatchHistoryActivity.this, RoomDetailActivity.class); intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); startActivity(intent); }); - // TODO: 接入后端接口 - 获取观看历史 - // 接口路径: GET /api/watch/history - // 请求参数: - // - userId: 当前用户ID(从token中获取) - // - page (可选): 页码 - // - pageSize (可选): 每页数量 - // 返回数据格式: ApiResponse> - // 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.setLayoutManager(new LinearLayoutManager(this)); 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 buildDemoHistory(int count) { - // 不再使用模拟数据,只从后端接口获取真实观看历史数据 - return new ArrayList<>(); + private void loadWatchHistory() { + if (isLoading) return; + isLoading = true; + + if (currentPage == 1) { + showLoading(); + } + + // 调用API获取观看历史(只获取直播间类型) + ApiClient.getService(this) + .getViewHistory("room", currentPage, 20) + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + hideLoading(); + binding.swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + List 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>>> 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 convertToRooms(List> maps) { + List rooms = new ArrayList<>(); + for (Map 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>() { + @Override + public void onResponse(Call> call, Response> 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> 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); + } } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java index a27a1319..d1cc948c 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java @@ -652,4 +652,108 @@ public interface ApiService { Call>> getMatchList( @Query("page") int page, @Query("limit") int limit); + + // ==================== 用户活动记录接口 ==================== + + /** + * 记录观看历史 + */ + @POST("api/front/activity/view/record") + Call>> recordViewHistoryNew(@Body Map body); + + /** + * 获取观看历史列表 + */ + @GET("api/front/activity/view/history") + Call>>> getViewHistory( + @Query("targetType") String targetType, + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 删除单条观看历史 + */ + @DELETE("api/front/activity/view/history/{historyId}") + Call> deleteViewHistory(@Path("historyId") long historyId); + + /** + * 清空观看历史 + */ + @DELETE("api/front/activity/view/history") + Call> clearViewHistory(@Query("targetType") String targetType); + + /** + * 获取点赞记录列表(全部类型) + */ + @GET("api/front/activity/like/records") + Call>>> getLikeRecords( + @Query("targetType") String targetType, + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取点赞的直播间列表 + */ + @GET("api/front/activity/like/rooms") + Call>>> getLikedRoomsNew( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取点赞的作品列表 + */ + @GET("api/front/activity/like/works") + Call>>> getLikedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取点赞的心愿列表 + */ + @GET("api/front/activity/like/wishes") + Call>>> getLikedWishes( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取关注记录列表 + */ + @GET("api/front/activity/follow/records") + Call>>> getFollowRecords( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取收藏的作品列表 + */ + @GET("api/front/activity/collect/works") + Call>>> getCollectedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取用户活动统计 + */ + @GET("api/front/activity/stats") + Call>> getUserActivityStats(); + + /** + * 获取搜索历史 + */ + @GET("api/front/search/history") + Call>> getSearchHistory( + @Query("searchType") Integer searchType, + @Query("limit") int limit); + + /** + * 清除搜索历史 + */ + @DELETE("api/front/search/history") + Call> clearSearchHistory(@Query("searchType") Integer searchType); + + /** + * 删除单条搜索历史 + */ + @DELETE("api/front/search/history/{historyId}") + Call> deleteSearchHistoryItem(@Path("historyId") long historyId); } diff --git a/android-app/app/src/main/res/drawable/ic_history_24.xml b/android-app/app/src/main/res/drawable/ic_history_24.xml new file mode 100644 index 00000000..c193462e --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_history_24.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android-app/app/src/main/res/layout/activity_my_records.xml b/android-app/app/src/main/res/layout/activity_my_records.xml new file mode 100644 index 00000000..440025c5 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_my_records.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_profile.xml b/android-app/app/src/main/res/layout/activity_profile.xml index 05e5d5f1..dad61a4e 100644 --- a/android-app/app/src/main/res/layout/activity_profile.xml +++ b/android-app/app/src/main/res/layout/activity_profile.xml @@ -475,6 +475,7 @@ android:id="@+id/action3" android:layout_width="wrap_content" android:layout_height="64dp" + android:layout_marginEnd="16dp" android:gravity="center_vertical" android:orientation="horizontal"> @@ -519,6 +520,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_watch_history.xml b/android-app/app/src/main/res/layout/activity_watch_history.xml index 47deeaa2..41c54bb1 100644 --- a/android-app/app/src/main/res/layout/activity_watch_history.xml +++ b/android-app/app/src/main/res/layout/activity_watch_history.xml @@ -33,6 +33,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="12dp" + android:layout_marginEnd="12dp" android:ellipsize="end" android:maxLines="1" android:text="观看历史" @@ -40,23 +41,102 @@ android:textSize="18sp" android:textStyle="bold" 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_constraintTop_toTopOf="@id/backButton" /> + + - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_record_list.xml b/android-app/app/src/main/res/layout/fragment_record_list.xml new file mode 100644 index 00000000..8209180b --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_record_list.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_record.xml b/android-app/app/src/main/res/layout/item_record.xml new file mode 100644 index 00000000..7c0d7ab0 --- /dev/null +++ b/android-app/app/src/main/res/layout/item_record.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + diff --git a/用户活动记录功能说明.md b/用户活动记录功能说明.md index 9c6cf093..9531aa9f 100644 --- a/用户活动记录功能说明.md +++ b/用户活动记录功能说明.md @@ -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` - 获取用户活动统计 -**新增标签页**: -- 标签页 7:关注记录 -- 标签页 8:点赞记录 -- 标签页 9:查看历史 - -**功能特性**: -- 支持分页显示 -- 数据实时加载 -- 状态标签显示(关注状态、内容类型等) -- 时间排序(最新的在前) - -### 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 - } -} -``` +- **管理端 API**: `UserActivityController.java` + - 路径前缀: `/api/admin/user/` + - 接口列表: + - `GET /follow/records` - 获取用户关注记录 + - `GET /like/records` - 获取用户点赞记录 + - `GET /view/history` - 获取用户查看历史 + - `GET /collect/works` - 获取用户收藏的作品 + - `GET /activity/stats` - 获取用户活动统计 ### 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) -```sql -CREATE TABLE `eb_live_room_like` ( - `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`) -); -``` +### 2. 页面 +- `MyRecordsActivity.java` - 我的记录页面(整合所有记录类型) +- `WatchHistoryActivity.java` - 观看历史页面(已更新使用新 API) -#### 3.2 作品点赞记录表 (eb_work_like) -```sql -CREATE TABLE `eb_work_like` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL COMMENT '用户ID', - `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. 布局文件 +- `activity_my_records.xml` - 我的记录页面布局 +- `fragment_record_list.xml` - 记录列表 Fragment 布局 +- `item_record.xml` - 记录列表项布局 +- `activity_watch_history.xml` - 观看历史页面布局 -#### 3.3 心愿点赞记录表 (eb_wish_like) -```sql -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`) -); -``` +### 4. 入口 +在 `ProfileActivity` 中添加了"我的记录"快捷入口(action4) -#### 3.4 查看历史记录表 (eb_view_history) -```sql -CREATE TABLE `eb_view_history` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL COMMENT '用户ID', - `target_type` varchar(20) NOT NULL COMMENT '目标类型', - `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, - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`) -); -``` +## 管理端前端实现 + +### 1. API 文件 +- `src/api/userActivity.js` - 用户活动记录 API + +### 2. 页面更新 +- `src/views/user/list/userDetails.vue` - 用户详情页面 + - 添加了"关注记录"标签页 + - 添加了"点赞记录"标签页 + - 添加了"查看历史"标签页 + - 添加了"收藏记录"标签页 ## 部署步骤 -### 1. 创建数据库表 -```bash -mysql -u root -p zhibo < user_activity_tables.sql -``` +1. **数据库更新** + ```sql + -- 执行 SQL 脚本 + source sql/user_activity_records_update.sql + ``` -### 2. 编译后端代码 -```bash -cd Zhibo/zhibo-h -mvn clean package -DskipTests -pl crmeb-admin -am -``` +2. **后端部署** + - 重新编译并部署 Java 后端服务 -### 3. 重启后端服务 -```bash -cd /root/zhibo/Zhibo/zhibo-h/crmeb-admin -./restart.sh -``` +3. **管理端前端部署** + - 重新构建并部署管理端前端 -### 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 --- 插入点赞记录 -INSERT INTO eb_live_room_like (user_id, room_id, create_time) -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'); -``` +4. **Android 端** + - 重新编译 APK 并安装 ## 注意事项 -1. **性能优化**: - - 所有表都添加了索引,确保查询性能 - - 使用分页查询,避免一次加载过多数据 - -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` +1. 后端使用 JdbcTemplate 直接操作数据库,确保数据库表已创建 +2. Android 端需要登录后才能使用记录功能 +3. 管理端可以查看任意用户的活动记录