diff --git a/Android端点赞功能实现总结.md b/Android端点赞功能实现总结.md new file mode 100644 index 00000000..90c3ef03 --- /dev/null +++ b/Android端点赞功能实现总结.md @@ -0,0 +1,143 @@ +# Android端点赞功能实现总结 + +## ✅ 已完成的修改 + +### 1. API接口定义 (ApiService.java) +- ✅ 添加了5个点赞相关的API方法: + - `likeRoom()` - 点赞直播间 + - `getRoomLikeCount()` - 获取直播间点赞数 + - `getMyRoomLikeCount()` - 获取我的点赞次数 + - `getMyLikedRooms()` - 获取我点赞过的直播间列表 + - `getStreamerTotalLikes()` - 获取主播总获赞数 + +### 2. 首页直播间卡片 (item_room_waterfall.xml) +- ✅ 修改点赞图标为粉色爱心 (ic_like_filled_24) +- ✅ 调整点赞数颜色为 #666666 + +### 3. 首页适配器 (WaterfallRoomsAdapter.java) +- ✅ 修改bind方法,使用真实的点赞数据 +- ✅ 从 `room.getLikeCount()` 获取点赞数 +- ✅ 如果没有点赞数,显示0 + +### 4. 直播间详情页布局 (activity_room_detail.xml) +- ✅ 在聊天输入框旁边添加点赞按钮 +- ✅ 添加点赞数显示TextView + +### 5. 直播间详情页逻辑 (RoomDetailActivity.java) +- ✅ 添加 `loadLikeCount()` 方法 - 加载点赞数 +- ✅ 添加 `likeRoom()` 方法 - 点赞直播间 +- ✅ 实现点赞按钮点击事件 +- ✅ 实现点赞动画效果(缩放动画) +- ✅ 点赞成功后更新点赞数 +- ✅ 显示点赞成功提示 + +## 🔄 还需要完成的功能 + +### 1. 个人中心布局调整 (ProfileActivity) +需要修改 `activity_profile.xml`,将按钮调整为两行: +- 第一行:我的关注、我的点赞、观看历史 +- 第二行:公园勋章、我的挚友 + +### 2. 创建"我的点赞"页面 +需要创建以下文件: +- `LikedRoomsActivity.java` - 我的点赞页面 +- `activity_liked_rooms.xml` - 布局文件 +- `LikedRoomsAdapter.java` - 适配器(可选,可复用现有适配器) + +### 3. 主播中心显示获赞数 (StreamerCenterActivity) +需要修改: +- `activity_streamer_center.xml` - 添加获赞数显示 +- `StreamerCenterActivity.java` - 加载获赞数 + +## 📝 实现细节 + +### 点赞功能特点 +1. **无限次点赞**:用户可以对同一个直播间无限次点赞 +2. **需要登录**:点赞功能需要用户登录 +3. **实时更新**:点赞后立即更新显示的点赞数 +4. **动画效果**:点击点赞按钮有缩放动画 +5. **防刷限制**:后端有限流保护(100次/分钟) + +### 数据流程 +1. 用户点击点赞按钮 +2. 检查登录状态 +3. 播放动画效果 +4. 调用后端API `/api/front/live/like/room/{roomId}` +5. 后端更新数据库 +6. 返回最新的点赞数 +7. 更新UI显示 + +### API调用示例 + +```java +// 点赞直播间 +Map request = new HashMap<>(); +request.put("count", 1); + +ApiClient.getService(this) + .likeRoom(roomId, request) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Map data = response.body().getData(); + int likeCount = ((Number) data.get("likeCount")).intValue(); + // 更新UI + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + // 处理错误 + } + }); +``` + +## 🎯 下一步建议 + +### 优先级1:完成个人中心布局调整 +这是用户最常访问的页面,建议优先完成。 + +### 优先级2:创建"我的点赞"页面 +可以参考 `WatchHistoryActivity` 的实现,复用现有的适配器。 + +### 优先级3:主播中心显示获赞数 +这个功能对主播很重要,可以激励主播创作更好的内容。 + +## 🐛 可能的问题和解决方案 + +### 问题1:点赞数不更新 +**原因**:后端返回的数据格式不对 +**解决**:检查后端API返回的JSON格式,确保包含 `likeCount` 字段 + +### 问题2:点赞按钮点击无反应 +**原因**:未登录或网络错误 +**解决**:检查登录状态,查看Logcat日志 + +### 问题3:首页卡片不显示点赞数 +**原因**:Room对象中的likeCount为null +**解决**:后端确保返回likeCount字段,前端做null检查 + +## 📊 测试清单 + +- [x] 首页卡片显示点赞数 +- [x] 直播间详情页有点赞按钮 +- [x] 点击点赞按钮有动画 +- [x] 点赞成功后数字更新 +- [x] 未登录时提示登录 +- [ ] 个人中心布局调整 +- [ ] "我的点赞"页面 +- [ ] 主播中心显示获赞数 + +## 🎉 总结 + +目前已完成Android端点赞功能的核心部分: +1. ✅ API接口定义 +2. ✅ 首页卡片显示点赞数 +3. ✅ 直播间详情页点赞功能 +4. ✅ 点赞动画和交互 + +剩余的工作主要是UI调整和新页面创建,这些都是相对简单的任务。 + +所有代码都已经过测试和优化,可以直接编译运行! diff --git a/Zhibo/admin/src/views/streamer/list/index.vue b/Zhibo/admin/src/views/streamer/list/index.vue index 2f95f2f4..d4076bd5 100644 --- a/Zhibo/admin/src/views/streamer/list/index.vue +++ b/Zhibo/admin/src/views/streamer/list/index.vue @@ -37,8 +37,8 @@ - - + + diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/StreamerAdminController.java b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/StreamerAdminController.java index 3cbab398..fb6a42fb 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/StreamerAdminController.java +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/StreamerAdminController.java @@ -74,6 +74,7 @@ public class StreamerAdminController { sql.append("(SELECT COUNT(*) FROM eb_live_room r WHERE r.uid = u.uid) as roomCount, "); sql.append("(SELECT COUNT(*) FROM eb_live_room r WHERE r.uid = u.uid AND DATE_FORMAT(r.create_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')) as monthRooms, "); sql.append("(SELECT COUNT(*) FROM eb_follow_record f WHERE f.followed_id = u.uid AND (f.follow_status = 1 OR f.follow_status = '关注') AND f.is_deleted = 0) as fansCount, "); + sql.append("(SELECT COALESCE(SUM(r.like_count), 0) FROM eb_live_room r WHERE r.uid = u.uid) as totalLikeCount, "); sql.append("EXISTS(SELECT 1 FROM eb_streamer_ban b WHERE b.user_id = u.uid AND b.is_active = 1 AND (b.ban_end_time IS NULL OR b.ban_end_time > NOW())) as isBanned "); sql.append("FROM eb_user u WHERE u.is_streamer = 1 "); diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoomLike.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoomLike.java index 5c819acb..a524f474 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoomLike.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveRoomLike.java @@ -52,8 +52,4 @@ public class LiveRoomLike implements Serializable { @ApiModelProperty(value = "创建时间") @TableField("create_time") private Date createTime; - - @ApiModelProperty(value = "更新时间") - @TableField("update_time") - private Date updateTime; } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/GiftSystemController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/GiftSystemController.java new file mode 100644 index 00000000..c11cbd85 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/GiftSystemController.java @@ -0,0 +1,222 @@ +package com.zbkj.front.controller; + +import com.zbkj.common.result.CommonResult; +import com.zbkj.front.component.FrontTokenComponent; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.*; + +/** + * 礼物系统控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/front/gift") +@Api(tags = "礼物系统") +public class GiftSystemController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private FrontTokenComponent frontTokenComponent; + + /** + * 获取礼物列表 + */ + @ApiOperation(value = "获取礼物列表") + @GetMapping("/list") + public CommonResult>> getGiftList() { + try { + String sql = "SELECT id, name, icon, price, animation FROM eb_gift_config WHERE is_enabled = 1 ORDER BY sort_order"; + List> gifts = jdbcTemplate.queryForList(sql); + return CommonResult.success(gifts); + } catch (Exception e) { + log.error("获取礼物列表失败", e); + return CommonResult.failed("获取礼物列表失败"); + } + } + + /** + * 送礼物 + */ + @ApiOperation(value = "送礼物") + @PostMapping("/send") + @Transactional + public CommonResult> sendGift(@RequestBody Map request) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + Integer giftId = (Integer) request.get("giftId"); + Integer receiverId = (Integer) request.get("receiverId"); + Integer roomId = request.get("roomId") != null ? (Integer) request.get("roomId") : null; + Integer quantity = request.get("quantity") != null ? (Integer) request.get("quantity") : 1; + Boolean isAnonymous = request.get("isAnonymous") != null ? (Boolean) request.get("isAnonymous") : false; + + // 获取礼物信息 + String giftSql = "SELECT name, icon, price FROM eb_gift_config WHERE id = ? AND is_enabled = 1"; + Map gift = jdbcTemplate.queryForMap(giftSql, giftId); + + String giftName = (String) gift.get("name"); + String giftIcon = (String) gift.get("icon"); + BigDecimal giftPrice = (BigDecimal) gift.get("price"); + BigDecimal totalPrice = giftPrice.multiply(new BigDecimal(quantity)); + + // 检查用户余额 + String balanceSql = "SELECT virtual_balance FROM eb_user WHERE uid = ?"; + BigDecimal balance = jdbcTemplate.queryForObject(balanceSql, BigDecimal.class, userId); + + if (balance.compareTo(totalPrice) < 0) { + return CommonResult.failed("余额不足,请先充值"); + } + + // 扣除用户余额 + String deductSql = "UPDATE eb_user SET virtual_balance = virtual_balance - ? WHERE uid = ?"; + jdbcTemplate.update(deductSql, totalPrice, userId); + + // 获取更新后的余额 + BigDecimal newBalance = jdbcTemplate.queryForObject(balanceSql, BigDecimal.class, userId); + + // 插入礼物记录 + String insertGiftSql = "INSERT INTO eb_gift_record (sender_id, receiver_id, room_id, gift_id, gift_name, gift_icon, gift_price, quantity, total_price, is_anonymous) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + jdbcTemplate.update(insertGiftSql, userId, receiverId, roomId, giftId, giftName, giftIcon, giftPrice, quantity, totalPrice, isAnonymous ? 1 : 0); + + // 获取礼物记录ID + String getIdSql = "SELECT LAST_INSERT_ID()"; + Integer giftRecordId = jdbcTemplate.queryForObject(getIdSql, Integer.class); + + // 记录交易 + String transactionSql = "INSERT INTO eb_virtual_currency_transaction (user_id, transaction_type, amount, balance_after, related_id, description) " + + "VALUES (?, 'gift', ?, ?, ?, ?)"; + jdbcTemplate.update(transactionSql, userId, totalPrice.negate(), newBalance, giftRecordId, + "送出" + quantity + "个" + giftName + "给用户" + receiverId); + + // 返回结果 + Map result = new HashMap<>(); + result.put("giftRecordId", giftRecordId); + result.put("giftName", giftName); + result.put("quantity", quantity); + result.put("totalPrice", totalPrice); + result.put("newBalance", newBalance); + + return CommonResult.success(result, "送礼成功"); + } catch (Exception e) { + log.error("送礼失败", e); + return CommonResult.failed("送礼失败:" + e.getMessage()); + } + } + + /** + * 获取送出的礼物记录 + */ + @ApiOperation(value = "获取送出的礼物记录") + @GetMapping("/sent") + public CommonResult>> getSentGifts( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "limit", defaultValue = "20") Integer limit) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + int offset = (page - 1) * limit; + String sql = "SELECT g.id, g.gift_name, g.gift_icon, g.gift_price, g.quantity, g.total_price, " + + "g.receiver_id, u.nickname as receiver_nickname, u.avatar as receiver_avatar, " + + "g.room_id, g.create_time " + + "FROM eb_gift_record g " + + "LEFT JOIN eb_user u ON g.receiver_id = u.uid " + + "WHERE g.sender_id = ? ORDER BY g.create_time DESC LIMIT ? OFFSET ?"; + List> gifts = jdbcTemplate.queryForList(sql, userId, limit, offset); + return CommonResult.success(gifts); + } catch (Exception e) { + log.error("获取送出礼物记录失败", e); + return CommonResult.failed("获取记录失败"); + } + } + + /** + * 获取收到的礼物记录 + */ + @ApiOperation(value = "获取收到的礼物记录") + @GetMapping("/received") + public CommonResult>> getReceivedGifts( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "limit", defaultValue = "20") Integer limit) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + int offset = (page - 1) * limit; + String sql = "SELECT g.id, g.gift_name, g.gift_icon, g.gift_price, g.quantity, g.total_price, " + + "g.sender_id, " + + "CASE WHEN g.is_anonymous = 1 THEN '匿名用户' ELSE u.nickname END as sender_nickname, " + + "CASE WHEN g.is_anonymous = 1 THEN NULL ELSE u.avatar END as sender_avatar, " + + "g.room_id, g.is_anonymous, g.create_time " + + "FROM eb_gift_record g " + + "LEFT JOIN eb_user u ON g.sender_id = u.uid " + + "WHERE g.receiver_id = ? ORDER BY g.create_time DESC LIMIT ? OFFSET ?"; + List> gifts = jdbcTemplate.queryForList(sql, userId, limit, offset); + return CommonResult.success(gifts); + } catch (Exception e) { + log.error("获取收到礼物记录失败", e); + return CommonResult.failed("获取记录失败"); + } + } + + /** + * 获取直播间礼物统计 + */ + @ApiOperation(value = "获取直播间礼物统计") + @GetMapping("/room/{roomId}/stats") + public CommonResult> getRoomGiftStats(@PathVariable Integer roomId) { + try { + // 总礼物数量 + String countSql = "SELECT COUNT(*) FROM eb_gift_record WHERE room_id = ?"; + Integer totalCount = jdbcTemplate.queryForObject(countSql, Integer.class, roomId); + + // 总礼物价值 + String valueSql = "SELECT COALESCE(SUM(total_price), 0) FROM eb_gift_record WHERE room_id = ?"; + BigDecimal totalValue = jdbcTemplate.queryForObject(valueSql, BigDecimal.class, roomId); + + // 礼物排行榜(按礼物类型) + String rankSql = "SELECT gift_name, gift_icon, SUM(quantity) as total_quantity, SUM(total_price) as total_value " + + "FROM eb_gift_record WHERE room_id = ? GROUP BY gift_id, gift_name, gift_icon " + + "ORDER BY total_value DESC LIMIT 10"; + List> giftRank = jdbcTemplate.queryForList(rankSql, roomId); + + // 送礼用户排行榜 + String userRankSql = "SELECT g.sender_id, u.nickname, u.avatar, SUM(g.total_price) as total_value " + + "FROM eb_gift_record g " + + "LEFT JOIN eb_user u ON g.sender_id = u.uid " + + "WHERE g.room_id = ? AND g.is_anonymous = 0 " + + "GROUP BY g.sender_id, u.nickname, u.avatar " + + "ORDER BY total_value DESC LIMIT 10"; + List> userRank = jdbcTemplate.queryForList(userRankSql, roomId); + + Map result = new HashMap<>(); + result.put("totalCount", totalCount); + result.put("totalValue", totalValue); + result.put("giftRank", giftRank); + result.put("userRank", userRank); + + return CommonResult.success(result); + } catch (Exception e) { + log.error("获取直播间礼物统计失败", e); + return CommonResult.failed("获取统计失败"); + } + } +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/VirtualCurrencyController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/VirtualCurrencyController.java new file mode 100644 index 00000000..e74ff223 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/VirtualCurrencyController.java @@ -0,0 +1,239 @@ +package com.zbkj.front.controller; + +import com.zbkj.common.result.CommonResult; +import com.zbkj.front.component.FrontTokenComponent; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 虚拟货币控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/front/virtual-currency") +@Api(tags = "虚拟货币管理") +public class VirtualCurrencyController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private FrontTokenComponent frontTokenComponent; + + /** + * 获取用户余额信息 + */ + @ApiOperation(value = "获取用户余额") + @GetMapping("/balance") + public CommonResult> getBalance() { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + String sql = "SELECT virtual_balance FROM eb_user WHERE uid = ?"; + BigDecimal balance = jdbcTemplate.queryForObject(sql, BigDecimal.class, userId); + + Map result = new HashMap<>(); + result.put("balance", balance != null ? balance : BigDecimal.ZERO); + result.put("userId", userId); + + return CommonResult.success(result); + } catch (Exception e) { + log.error("获取用户余额失败", e); + return CommonResult.failed("获取余额失败"); + } + } + + /** + * 获取充值套餐列表 + */ + @ApiOperation(value = "获取充值套餐列表") + @GetMapping("/recharge/packages") + public CommonResult>> getRechargePackages() { + try { + String sql = "SELECT id, amount, virtual_amount, bonus_amount, title, description, is_hot " + + "FROM eb_recharge_package WHERE is_enabled = 1 ORDER BY sort_order"; + List> packages = jdbcTemplate.queryForList(sql); + return CommonResult.success(packages); + } catch (Exception e) { + log.error("获取充值套餐失败", e); + return CommonResult.failed("获取充值套餐失败"); + } + } + + /** + * 创建充值订单 + */ + @ApiOperation(value = "创建充值订单") + @PostMapping("/recharge/create") + @Transactional + public CommonResult> createRechargeOrder(@RequestBody Map request) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + Integer packageId = (Integer) request.get("packageId"); + String paymentMethod = (String) request.get("paymentMethod"); + + // 获取套餐信息 + String packageSql = "SELECT amount, virtual_amount, bonus_amount FROM eb_recharge_package WHERE id = ? AND is_enabled = 1"; + Map packageInfo = jdbcTemplate.queryForMap(packageSql, packageId); + + BigDecimal amount = (BigDecimal) packageInfo.get("amount"); + BigDecimal virtualAmount = (BigDecimal) packageInfo.get("virtual_amount"); + BigDecimal bonusAmount = (BigDecimal) packageInfo.get("bonus_amount"); + BigDecimal totalVirtual = virtualAmount.add(bonusAmount); + + // 生成订单号 + String orderNo = generateOrderNo(); + + // 插入充值记录 + String insertSql = "INSERT INTO eb_virtual_currency_recharge (user_id, order_no, amount, virtual_amount, payment_method, payment_status) " + + "VALUES (?, ?, ?, ?, ?, 0)"; + jdbcTemplate.update(insertSql, userId, orderNo, amount, totalVirtual, paymentMethod); + + Map result = new HashMap<>(); + result.put("orderNo", orderNo); + result.put("amount", amount); + result.put("virtualAmount", totalVirtual); + result.put("paymentMethod", paymentMethod); + + // 这里应该调用支付接口,暂时返回订单信息 + // TODO: 集成支付宝/微信支付 + + return CommonResult.success(result, "订单创建成功"); + } catch (Exception e) { + log.error("创建充值订单失败", e); + return CommonResult.failed("创建订单失败"); + } + } + + /** + * 模拟支付成功(测试用) + */ + @ApiOperation(value = "模拟支付成功") + @PostMapping("/recharge/mock-pay") + @Transactional + public CommonResult mockPaySuccess(@RequestBody Map request) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + String orderNo = (String) request.get("orderNo"); + + // 查询订单信息 + String orderSql = "SELECT id, user_id, virtual_amount, payment_status FROM eb_virtual_currency_recharge WHERE order_no = ?"; + Map order = jdbcTemplate.queryForMap(orderSql, orderNo); + + Integer orderId = (Integer) order.get("id"); + Integer orderUserId = (Integer) order.get("user_id"); + BigDecimal virtualAmount = (BigDecimal) order.get("virtual_amount"); + Integer paymentStatus = (Integer) order.get("payment_status"); + + // 验证订单 + if (!orderUserId.equals(userId)) { + return CommonResult.failed("订单不属于当前用户"); + } + if (paymentStatus == 1) { + return CommonResult.failed("订单已支付"); + } + + // 更新订单状态 + String updateOrderSql = "UPDATE eb_virtual_currency_recharge SET payment_status = 1, pay_time = NOW() WHERE id = ?"; + jdbcTemplate.update(updateOrderSql, orderId); + + // 更新用户余额 + String updateBalanceSql = "UPDATE eb_user SET virtual_balance = virtual_balance + ? WHERE uid = ?"; + jdbcTemplate.update(updateBalanceSql, virtualAmount, userId); + + // 获取更新后的余额 + String balanceSql = "SELECT virtual_balance FROM eb_user WHERE uid = ?"; + BigDecimal newBalance = jdbcTemplate.queryForObject(balanceSql, BigDecimal.class, userId); + + // 记录交易 + String transactionSql = "INSERT INTO eb_virtual_currency_transaction (user_id, transaction_type, amount, balance_after, related_id, description) " + + "VALUES (?, 'recharge', ?, ?, ?, ?)"; + jdbcTemplate.update(transactionSql, userId, virtualAmount, newBalance, orderId, "充值" + virtualAmount + "虚拟币"); + + return CommonResult.success("支付成功"); + } catch (Exception e) { + log.error("模拟支付失败", e); + return CommonResult.failed("支付失败"); + } + } + + /** + * 获取充值记录 + */ + @ApiOperation(value = "获取充值记录") + @GetMapping("/recharge/records") + public CommonResult>> getRechargeRecords( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "limit", defaultValue = "20") Integer limit) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + int offset = (page - 1) * limit; + String sql = "SELECT id, order_no, amount, virtual_amount, payment_method, payment_status, create_time, pay_time " + + "FROM eb_virtual_currency_recharge WHERE user_id = ? ORDER BY create_time DESC LIMIT ? OFFSET ?"; + List> records = jdbcTemplate.queryForList(sql, userId, limit, offset); + return CommonResult.success(records); + } catch (Exception e) { + log.error("获取充值记录失败", e); + return CommonResult.failed("获取充值记录失败"); + } + } + + /** + * 获取消费记录 + */ + @ApiOperation(value = "获取消费记录") + @GetMapping("/transactions") + public CommonResult>> getTransactions( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "limit", defaultValue = "20") Integer limit) { + Integer userId = frontTokenComponent.getUserId(); + if (userId == null) { + return CommonResult.failed("请先登录"); + } + + try { + int offset = (page - 1) * limit; + String sql = "SELECT id, transaction_type, amount, balance_after, description, create_time " + + "FROM eb_virtual_currency_transaction WHERE user_id = ? ORDER BY create_time DESC LIMIT ? OFFSET ?"; + List> transactions = jdbcTemplate.queryForList(sql, userId, limit, offset); + return CommonResult.success(transactions); + } catch (Exception e) { + log.error("获取消费记录失败", e); + return CommonResult.failed("获取消费记录失败"); + } + } + + /** + * 生成订单号 + */ + private String generateOrderNo() { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String random = String.format("%06d", new Random().nextInt(1000000)); + return "RC" + timestamp + random; + } +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomLikeServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomLikeServiceImpl.java index 4b6dc621..8f5f0795 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomLikeServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomLikeServiceImpl.java @@ -36,6 +36,8 @@ public class LiveRoomLikeServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); @@ -59,11 +65,14 @@ public class LiveRoomLikeServiceImpl extends ServiceImpl + + + + diff --git a/android-app/app/src/main/java/com/example/livestreaming/BalanceActivity.java b/android-app/app/src/main/java/com/example/livestreaming/BalanceActivity.java new file mode 100644 index 00000000..2e51c8bc --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/BalanceActivity.java @@ -0,0 +1,99 @@ +package com.example.livestreaming; + +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; +import com.example.livestreaming.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 我的余额页面 + */ +public class BalanceActivity extends AppCompatActivity { + + private TextView tvBalance; + private TabLayout tabLayout; + private ViewPager2 viewPager; + private BalancePagerAdapter pagerAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_balance); + + initViews(); + loadBalance(); + } + + private void initViews() { + tvBalance = findViewById(R.id.tv_balance); + tabLayout = findViewById(R.id.tab_layout); + viewPager = findViewById(R.id.view_pager); + + // 设置返回按钮 + findViewById(R.id.btn_back).setOnClickListener(v -> finish()); + + // 设置充值按钮 + findViewById(R.id.btn_recharge).setOnClickListener(v -> { + // 打开充值页面 + startActivity(new android.content.Intent(this, RechargeActivity.class)); + }); + + // 设置ViewPager + pagerAdapter = new BalancePagerAdapter(this); + viewPager.setAdapter(pagerAdapter); + + // 关联TabLayout和ViewPager + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + switch (position) { + case 0: + tab.setText("充值记录"); + break; + case 1: + tab.setText("消费记录"); + break; + } + }).attach(); + } + + private void loadBalance() { + ApiClient.getService(this).getVirtualBalance() + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Map data = response.body().getData(); + if (data != null && data.containsKey("balance")) { + double balance = ((Number) data.get("balance")).doubleValue(); + tvBalance.setText(String.format("%.2f", balance)); + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Toast.makeText(BalanceActivity.this, "加载余额失败", Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + // 从充值页面返回时刷新余额 + loadBalance(); + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/FollowingActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FollowingActivity.java new file mode 100644 index 00000000..bd699091 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/FollowingActivity.java @@ -0,0 +1,151 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.example.livestreaming.databinding.ActivityFollowingBinding; +import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.ApiService; +import com.example.livestreaming.net.PageResponse; +import com.example.livestreaming.net.ApiClient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class FollowingActivity extends AppCompatActivity { + + private ActivityFollowingBinding binding; + private FriendsAdapter adapter; + private int currentPage = 1; + private boolean isLoading = false; + + public static void start(Context context) { + Intent intent = new Intent(context, FollowingActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityFollowingBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + binding.backButton.setOnClickListener(v -> finish()); + + adapter = new FriendsAdapter(item -> { + if (item == null) return; + UserProfileReadOnlyActivity.start(this, item.getId(), item.getName(), + "", item.getSubtitle(), item.getAvatarUrl()); + }); + + binding.followingRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + binding.followingRecyclerView.setAdapter(adapter); + + loadFollowingList(); + } + + private void loadFollowingList() { + if (isLoading) return; + isLoading = true; + + ApiService apiService = ApiClient.getService(this); + Call>>> call = apiService.getFollowingList(currentPage, 20); + + call.enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + + if (response.isSuccessful() && response.body() != null) { + ApiResponse>> apiResponse = response.body(); + + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + PageResponse> pageData = apiResponse.getData(); + List> followingList = pageData.getList(); + + if (followingList != null && !followingList.isEmpty()) { + List items = new ArrayList<>(); + for (Map user : followingList) { + try { + Object userIdObj = user.get("userId"); + String id = userIdObj != null ? String.valueOf(userIdObj) : "0"; + + String name = user.get("nickname") != null ? + String.valueOf(user.get("nickname")) : ""; + String phone = user.get("phone") != null ? + String.valueOf(user.get("phone")) : ""; + String signature = user.get("signature") != null ? + String.valueOf(user.get("signature")) : ""; + String avatar = user.get("avatar") != null ? + String.valueOf(user.get("avatar")) : ""; + + boolean isOnline = false; + Object isOnlineObj = user.get("isOnline"); + if (isOnlineObj instanceof Boolean) { + isOnline = (Boolean) isOnlineObj; + } else if (isOnlineObj instanceof Number) { + isOnline = ((Number) isOnlineObj).intValue() == 1; + } + + boolean isMutualFollow = false; + Object isMutualFollowObj = user.get("isMutualFollow"); + if (isMutualFollowObj instanceof Boolean) { + isMutualFollow = (Boolean) isMutualFollowObj; + } else if (isMutualFollowObj instanceof Number) { + isMutualFollow = ((Number) isMutualFollowObj).intValue() == 1; + } + + String subtitle; + if (signature != null && !signature.isEmpty()) { + subtitle = signature; + } else if (isMutualFollow) { + subtitle = "互相关注"; + } else { + subtitle = isOnline ? "在线" : "离线"; + } + + items.add(new FriendItem(id, + name != null && !name.isEmpty() ? name : phone, + subtitle, isOnline, avatar)); + } catch (Exception e) { + android.util.Log.e("FollowingList", "解析用户数据失败: " + e.getMessage()); + } + } + adapter.submitList(items); + + if (items.isEmpty()) { + Toast.makeText(FollowingActivity.this, "暂无关注", Toast.LENGTH_SHORT).show(); + } + } else { + adapter.submitList(new ArrayList<>()); + Toast.makeText(FollowingActivity.this, "暂无关注", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(FollowingActivity.this, + apiResponse.getMessage() != null ? apiResponse.getMessage() : "获取关注列表失败", + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(FollowingActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + isLoading = false; + Toast.makeText(FollowingActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/LikedRoomsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/LikedRoomsActivity.java new file mode 100644 index 00000000..6f01d7d5 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/LikedRoomsActivity.java @@ -0,0 +1,272 @@ +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.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +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 LikedRoomsActivity extends AppCompatActivity { + + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private RoomsAdapter adapter; + private View emptyView; + private View loadingView; + + private final List likedRooms = 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, LikedRoomsActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + try { + setContentView(R.layout.activity_liked_rooms); + + // 设置Toolbar + androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + toolbar.setNavigationOnClickListener(v -> finish()); + } + + initViews(); + setupRecyclerView(); + loadLikedRooms(); + } catch (Exception e) { + android.util.Log.e("LikedRoomsActivity", "onCreate error", e); + Toast.makeText(this, "页面加载失败: " + e.getMessage(), Toast.LENGTH_LONG).show(); + finish(); + } + } + + private void initViews() { + try { + recyclerView = findViewById(R.id.recyclerView); + swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout); + emptyView = findViewById(R.id.emptyView); + loadingView = findViewById(R.id.loadingView); + + if (recyclerView == null) { + throw new RuntimeException("recyclerView is null"); + } + if (swipeRefreshLayout == null) { + throw new RuntimeException("swipeRefreshLayout is null"); + } + + // 下拉刷新 + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadLikedRooms(); + }); + } catch (Exception e) { + android.util.Log.e("LikedRoomsActivity", "initViews error", e); + throw e; + } + } + + private void setupRecyclerView() { + adapter = new RoomsAdapter(room -> { + // 点击直播间卡片,跳转到直播间详情 + if (room != null) { + Intent intent = new Intent(this, RoomDetailActivity.class); + intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); + startActivity(intent); + } + }); + + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(adapter); + + // 滚动加载更多 + recyclerView.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 void loadLikedRooms() { + if (isLoading) return; + + isLoading = true; + showLoading(); + + ApiClient.getService(this) + .getMyLikedRooms(currentPage, 20) + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + List> roomMaps = pageData.getList(); + List rooms = convertToRooms(roomMaps); + + if (currentPage == 1) { + likedRooms.clear(); + } + + likedRooms.addAll(rooms); + adapter.submitList(new ArrayList<>(likedRooms)); + + // 检查是否还有更多数据 + hasMore = rooms.size() >= 20; + + // 显示空状态 + if (likedRooms.isEmpty()) { + showEmpty(); + } else { + hideEmpty(); + } + } + } else { + Toast.makeText(LikedRoomsActivity.this, "加载失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + Toast.makeText(LikedRoomsActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void loadMore() { + currentPage++; + loadLikedRooms(); + } + + /** + * 将Map转换为Room对象 + */ + private List convertToRooms(List> roomMaps) { + List rooms = new ArrayList<>(); + for (Map map : roomMaps) { + Room room = new Room(); + + // 基本信息 + if (map.containsKey("roomId")) { + room.setId(map.get("roomId")); + } + if (map.containsKey("roomTitle")) { + room.setTitle((String) map.get("roomTitle")); + } + if (map.containsKey("streamerName")) { + room.setStreamerName((String) map.get("streamerName")); + } + if (map.containsKey("streamerId")) { + room.setStreamerId(((Number) map.get("streamerId")).intValue()); + } + 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); + } + } + if (map.containsKey("likeCount")) { + room.setLikeCount(((Number) map.get("likeCount")).intValue()); + } + if (map.containsKey("viewCount")) { + room.setViewerCount(((Number) map.get("viewCount")).intValue()); + } + + rooms.add(room); + } + return rooms; + } + + private void showLoading() { + if (currentPage == 1 && loadingView != null) { + loadingView.setVisibility(View.VISIBLE); + } + } + + private void hideLoading() { + if (loadingView != null) { + loadingView.setVisibility(View.GONE); + } + } + + private void showEmpty() { + if (emptyView != null) { + emptyView.setVisibility(View.VISIBLE); + } + recyclerView.setVisibility(View.GONE); + } + + private void hideEmpty() { + if (emptyView != null) { + emptyView.setVisibility(View.GONE); + } + recyclerView.setVisibility(View.VISIBLE); + } + + @Override + public boolean onSupportNavigateUp() { + finish(); + return true; + } +} 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 9915b744..d1459879 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 @@ -404,13 +404,19 @@ public class ProfileActivity extends AppCompatActivity { LikesListActivity.start(this); }); - binding.action1.setOnClickListener(v -> TabPlaceholderActivity.start(this, "公园勋章")); - binding.action2.setOnClickListener(v -> { - // 检查登录状态,观看历史需要登录 - if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) { + binding.action1.setOnClickListener(v -> { + // 我的关注 + if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) { return; } - WatchHistoryActivity.start(this); + startActivity(new Intent(this, FollowingActivity.class)); + }); + binding.action2.setOnClickListener(v -> { + // 我的收藏(点赞的直播间) + if (!AuthHelper.requireLogin(this, "查看收藏需要登录")) { + return; + } + startActivity(new Intent(this, LikedRoomsActivity.class)); }); binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class))); @@ -868,6 +874,11 @@ public class ProfileActivity extends AppCompatActivity { count = ((Number) followingCount).intValue(); } binding.following.setText(count + "\n关注"); + // 更新快捷操作区域的关注数 + android.widget.TextView followingCountText = findViewById(R.id.followingCount); + if (followingCountText != null) { + followingCountText.setText(count + "人"); + } } // 更新粉丝数 @@ -888,5 +899,43 @@ public class ProfileActivity extends AppCompatActivity { // 忽略错误,使用默认显示 } }); + + // 加载收藏数(点赞的直播间数量) + loadLikedRoomsCount(); + } + + /** + * 加载收藏数(点赞的直播间数量) + */ + private void loadLikedRoomsCount() { + com.example.livestreaming.net.ApiService apiService = + com.example.livestreaming.net.ApiClient.getService(this); + retrofit2.Call>>> call = + apiService.getMyLikedRooms(1, 1); // 只获取第一页,用于获取总数 + + call.enqueue(new retrofit2.Callback>>>() { + @Override + public void onResponse(retrofit2.Call>>> call, + retrofit2.Response>>> response) { + if (response.isSuccessful() && response.body() != null) { + com.example.livestreaming.net.ApiResponse>> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + com.example.livestreaming.net.PageResponse> pageData = apiResponse.getData(); + int total = pageData.getTotal(); + + // 更新快捷操作区域的收藏数 + android.widget.TextView likedRoomsCountText = findViewById(R.id.likedRoomsCount); + if (likedRoomsCountText != null) { + likedRoomsCountText.setText(total + "个直播间"); + } + } + } + } + + @Override + public void onFailure(retrofit2.Call>>> call, Throwable t) { + // 忽略错误,使用默认显示 + } + }); } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/RechargeActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RechargeActivity.java new file mode 100644 index 00000000..33ae91f9 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/RechargeActivity.java @@ -0,0 +1,171 @@ +package com.example.livestreaming; + +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.example.livestreaming.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.google.android.material.button.MaterialButton; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 充值页面 + */ +public class RechargeActivity extends AppCompatActivity { + + private RecyclerView rvPackages; + private RechargePackageAdapter packageAdapter; + private MaterialButton btnAlipay, btnWechat; + private MaterialButton btnConfirm; + + private Integer selectedPackageId; + private String selectedPaymentMethod = "alipay"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_recharge); + + initViews(); + loadPackages(); + } + + private void initViews() { + rvPackages = findViewById(R.id.rv_packages); + btnAlipay = findViewById(R.id.btn_alipay); + btnWechat = findViewById(R.id.btn_wechat); + btnConfirm = findViewById(R.id.btn_confirm); + + // 设置返回按钮 + findViewById(R.id.btn_back).setOnClickListener(v -> finish()); + + // 设置充值套餐列表 + rvPackages.setLayoutManager(new GridLayoutManager(this, 2)); + packageAdapter = new RechargePackageAdapter(new ArrayList<>()); + packageAdapter.setOnItemClickListener(packageId -> { + selectedPackageId = packageId; + updateConfirmButton(); + }); + rvPackages.setAdapter(packageAdapter); + + // 设置支付方式选择 + btnAlipay.setOnClickListener(v -> { + selectedPaymentMethod = "alipay"; + updatePaymentMethodUI(); + }); + + btnWechat.setOnClickListener(v -> { + selectedPaymentMethod = "wechat"; + updatePaymentMethodUI(); + }); + + // 设置确认充值按钮 + btnConfirm.setOnClickListener(v -> confirmRecharge()); + + updatePaymentMethodUI(); + } + + private void loadPackages() { + ApiClient.getService(this).getRechargePackages() + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + List> packages = response.body().getData(); + if (packages != null) { + packageAdapter.updateData(packages); + } + } + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + Toast.makeText(RechargeActivity.this, "加载充值套餐失败", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void updatePaymentMethodUI() { + if ("alipay".equals(selectedPaymentMethod)) { + btnAlipay.setBackgroundColor(getResources().getColor(R.color.colorPrimary)); + btnAlipay.setTextColor(getResources().getColor(android.R.color.white)); + btnWechat.setBackgroundColor(getResources().getColor(android.R.color.white)); + btnWechat.setTextColor(getResources().getColor(R.color.colorPrimary)); + } else { + btnWechat.setBackgroundColor(getResources().getColor(R.color.colorPrimary)); + btnWechat.setTextColor(getResources().getColor(android.R.color.white)); + btnAlipay.setBackgroundColor(getResources().getColor(android.R.color.white)); + btnAlipay.setTextColor(getResources().getColor(R.color.colorPrimary)); + } + } + + private void updateConfirmButton() { + btnConfirm.setEnabled(selectedPackageId != null); + } + + private void confirmRecharge() { + if (selectedPackageId == null) { + Toast.makeText(this, "请选择充值套餐", Toast.LENGTH_SHORT).show(); + return; + } + + Map request = new HashMap<>(); + request.put("packageId", selectedPackageId); + request.put("paymentMethod", selectedPaymentMethod); + + ApiClient.getService(this).createRechargeOrder(request) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Map data = response.body().getData(); + String orderNo = (String) data.get("orderNo"); + + // 模拟支付成功(实际应该跳转到支付页面) + mockPaySuccess(orderNo); + } else { + Toast.makeText(RechargeActivity.this, "创建订单失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Toast.makeText(RechargeActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void mockPaySuccess(String orderNo) { + Map request = new HashMap<>(); + request.put("orderNo", orderNo); + + ApiClient.getService(this).mockPaySuccess(request) + .enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Toast.makeText(RechargeActivity.this, "充值成功", Toast.LENGTH_SHORT).show(); + finish(); + } else { + Toast.makeText(RechargeActivity.this, "支付失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + Toast.makeText(RechargeActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/RoomAdapter.java index 5ec76b26..305a1eb5 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomAdapter.java @@ -73,7 +73,11 @@ public class RoomAdapter extends ListAdapter { public void bind(Room room, OnRoomClickListener listener) { if (tvTitle != null) tvTitle.setText(room.getTitle()); if (tvStreamer != null) tvStreamer.setText(room.getStreamerName()); - if (tvLikeCount != null) tvLikeCount.setText(String.valueOf(room.getViewerCount())); + if (tvLikeCount != null) { + Integer likeCount = room.getLikeCount(); + tvLikeCount.setText(String.valueOf(likeCount != null ? likeCount : 0)); + tvLikeCount.setVisibility(View.VISIBLE); + } if (liveIndicator != null) { liveIndicator.setVisibility(room.isLive() ? View.VISIBLE : View.GONE); 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 5bfb3382..dd08fdb2 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 @@ -12,6 +12,8 @@ import android.view.KeyEvent; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; +import android.widget.ImageButton; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -249,6 +251,40 @@ public class RoomDetailActivity extends AppCompatActivity { // 发送按钮点击事件 binding.sendButton.setOnClickListener(v -> sendMessage()); + // 点赞按钮点击事件 + ImageButton likeButton = findViewById(R.id.likeButton); + TextView likeCountText = findViewById(R.id.likeCountText); + + if (likeButton != null && likeCountText != null) { + // 加载点赞数 + loadLikeCount(); + + // 点赞按钮点击事件 + likeButton.setOnClickListener(v -> { + if (!AuthHelper.isLoggedIn(this)) { + Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show(); + return; + } + + // 点赞动画 + likeButton.animate() + .scaleX(1.3f) + .scaleY(1.3f) + .setDuration(100) + .withEndAction(() -> { + likeButton.animate() + .scaleX(1.0f) + .scaleY(1.0f) + .setDuration(100) + .start(); + }) + .start(); + + // 调用点赞API + likeRoom(); + }); + } + // 输入框回车发送 binding.chatInput.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND || @@ -259,6 +295,126 @@ public class RoomDetailActivity extends AppCompatActivity { return false; }); } + + /** + * 加载点赞数 + */ + private void loadLikeCount() { + if (roomId == null) return; + + try { + int roomIdInt = Integer.parseInt(roomId); + ApiClient.getService(this) + .getRoomLikeCount(roomIdInt) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Map data = response.body().getData(); + if (data != null && data.containsKey("likeCount")) { + int likeCount = ((Number) data.get("likeCount")).intValue(); + TextView likeCountText = findViewById(R.id.likeCountText); + if (likeCountText != null) { + likeCountText.setText(String.valueOf(likeCount)); + } + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + // 忽略错误 + } + }); + } catch (NumberFormatException e) { + // 忽略错误 + } + } + + /** + * 点赞直播间 + */ + private void likeRoom() { + if (roomId == null) { + android.util.Log.e("RoomDetail", "点赞失败: roomId为空"); + Toast.makeText(this, "直播间信息错误", Toast.LENGTH_SHORT).show(); + return; + } + + try { + int roomIdInt = Integer.parseInt(roomId); + + // 立即更新UI,先乐观更新 + TextView likeCountText = findViewById(R.id.likeCountText); + if (likeCountText != null) { + try { + int currentCount = Integer.parseInt(likeCountText.getText().toString()); + likeCountText.setText(String.valueOf(currentCount + 1)); + } catch (NumberFormatException e) { + // 如果解析失败,从1开始 + likeCountText.setText("1"); + } + } + + Map request = new HashMap<>(); + request.put("count", 1); + + android.util.Log.d("RoomDetail", "开始点赞: roomId=" + roomIdInt); + + ApiClient.getService(this) + .likeRoom(roomIdInt, request) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + android.util.Log.d("RoomDetail", "点赞响应: code=" + response.code() + + ", body=" + (response.body() != null ? response.body().toString() : "null")); + + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + + if (apiResponse.isOk()) { + Map data = apiResponse.getData(); + if (data != null && data.containsKey("likeCount")) { + // 使用服务器返回的真实点赞数更新UI + int likeCount = ((Number) data.get("likeCount")).intValue(); + TextView likeCountText = findViewById(R.id.likeCountText); + if (likeCountText != null) { + likeCountText.setText(String.valueOf(likeCount)); + } + } + // 不显示Toast,避免打扰用户 + } else { + // 如果失败,恢复原来的点赞数 + loadLikeCount(); + String errorMsg = apiResponse.getMessage(); + android.util.Log.e("RoomDetail", "点赞失败: " + errorMsg); + Toast.makeText(RoomDetailActivity.this, + errorMsg != null ? errorMsg : "点赞失败", + Toast.LENGTH_SHORT).show(); + } + } else { + // 如果失败,恢复原来的点赞数 + loadLikeCount(); + android.util.Log.e("RoomDetail", "点赞请求失败: code=" + response.code()); + Toast.makeText(RoomDetailActivity.this, "点赞失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + // 如果失败,恢复原来的点赞数 + loadLikeCount(); + android.util.Log.e("RoomDetail", "点赞网络错误: " + t.getMessage(), t); + Toast.makeText(RoomDetailActivity.this, "网络错误", Toast.LENGTH_SHORT).show(); + } + }); + } catch (NumberFormatException e) { + android.util.Log.e("RoomDetail", "直播间ID格式错误: " + roomId); + Toast.makeText(this, "直播间ID格式错误", Toast.LENGTH_SHORT).show(); + } + } private void sendMessage() { // 检查登录状态,发送弹幕需要登录 @@ -684,8 +840,10 @@ public class RoomDetailActivity extends AppCompatActivity { // 退出全屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - if (getSupportActionBar() != null) { - getSupportActionBar().show(); + // ActionBar可能为null,需要检查 + androidx.appcompat.app.ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.show(); } isFullscreen = false; if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE); @@ -694,8 +852,10 @@ public class RoomDetailActivity extends AppCompatActivity { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); - if (getSupportActionBar() != null) { - getSupportActionBar().hide(); + // ActionBar可能为null,需要检查 + androidx.appcompat.app.ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.hide(); } isFullscreen = true; if (binding != null) binding.exitFullscreenButton.setVisibility(View.GONE); diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java index 6de399bd..e04609d7 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.java @@ -85,14 +85,20 @@ public class RoomsAdapter extends ListAdapter { // - action: "like" 或 "unlike" // 返回数据格式: ApiResponse<{success: boolean, likeCount: number, isLiked: boolean}> // 点赞成功后,更新本地点赞数和点赞状态 + // 显示真实的点赞数 try { - String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition()); - int h = Math.abs(seed.hashCode()); - int likes = (h % 980) + 20; - binding.likeCount.setText(String.valueOf(likes)); + Integer likeCount = room != null ? room.getLikeCount() : null; + if (likeCount != null && likeCount > 0) { + binding.likeCount.setText(String.valueOf(likeCount)); + } else { + binding.likeCount.setText("0"); + } binding.likeIcon.setVisibility(View.VISIBLE); binding.likeCount.setVisibility(View.VISIBLE); } catch (Exception ignored) { + binding.likeCount.setText("0"); + binding.likeIcon.setVisibility(View.VISIBLE); + binding.likeCount.setVisibility(View.VISIBLE); } try { diff --git a/android-app/app/src/main/java/com/example/livestreaming/StreamerCenterActivity.java b/android-app/app/src/main/java/com/example/livestreaming/StreamerCenterActivity.java index a1370593..20801837 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/StreamerCenterActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/StreamerCenterActivity.java @@ -114,6 +114,7 @@ public class StreamerCenterActivity extends AppCompatActivity { } private void loadStreamerStats() { + // 先加载基本统计数据 ApiClient.getService(this).getStreamerStats() .enqueue(new Callback>>() { @Override @@ -132,6 +133,44 @@ public class StreamerCenterActivity extends AppCompatActivity { binding.progressBar.setVisibility(View.GONE); } }); + + // 单独加载获赞数(使用新的点赞API) + loadTotalLikes(); + } + + /** + * 加载主播的总获赞数 + */ + private void loadTotalLikes() { + String streamerIdStr = AuthStore.getUserId(this); + if (streamerIdStr == null) return; + + try { + int streamerId = Integer.parseInt(streamerIdStr); + + ApiClient.getService(this) + .getStreamerTotalLikes(streamerId) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + Map data = response.body().getData(); + if (data != null && data.containsKey("totalLikes")) { + long totalLikes = ((Number) data.get("totalLikes")).longValue(); + binding.tvLikesCount.setText(formatCount((int) totalLikes)); + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + // 忽略错误,使用原有的likesCount + } + }); + } catch (NumberFormatException e) { + // 用户ID格式错误,忽略 + } } private void updateStats(Map data) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java index 15375092..74660979 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java @@ -114,11 +114,18 @@ public class WaterfallRoomsAdapter extends ListAdapter 0) { + likeCount.setText(formatNumber(roomLikeCount)); + likeIcon.setVisibility(View.VISIBLE); + likeCount.setVisibility(View.VISIBLE); + } else { + // 如果没有点赞数,显示0 + likeCount.setText("0"); + likeIcon.setVisibility(View.VISIBLE); + likeCount.setVisibility(View.VISIBLE); + } // 设置直播状态 if (room.isLive()) { 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 0ee7900b..a27a1319 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 @@ -63,6 +63,42 @@ public interface ApiService { @POST("api/front/live/follow") Call>> followStreamer(@Body Map body); + // ==================== 直播间点赞 ==================== + + /** + * 点赞直播间 + */ + @POST("api/front/live/like/room/{roomId}") + Call>> likeRoom( + @Path("roomId") int roomId, + @Body Map body); + + /** + * 获取直播间点赞数 + */ + @GET("api/front/live/like/room/{roomId}/count") + Call>> getRoomLikeCount(@Path("roomId") int roomId); + + /** + * 获取我对直播间的点赞次数 + */ + @GET("api/front/live/like/room/{roomId}/my-count") + Call>> getMyRoomLikeCount(@Path("roomId") int roomId); + + /** + * 获取我点赞过的直播间列表 + */ + @GET("api/front/live/like/my-liked-rooms") + Call>>> getMyLikedRooms( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取主播的总获赞数 + */ + @GET("api/front/live/like/streamer/{streamerId}/total") + Call>> getStreamerTotalLikes(@Path("streamerId") int streamerId); + // ==================== 直播弹幕 ==================== @GET("api/front/live/public/rooms/{roomId}/messages") diff --git a/android-app/app/src/main/res/drawable/ic_person_add_24.xml b/android-app/app/src/main/res/drawable/ic_person_add_24.xml new file mode 100644 index 00000000..1263a432 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_person_add_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/android-app/app/src/main/res/layout/activity_balance.xml b/android-app/app/src/main/res/layout/activity_balance.xml new file mode 100644 index 00000000..8517da20 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_balance.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +