接口的编写和接入

This commit is contained in:
ShiQi 2025-12-31 18:05:04 +08:00
parent 162b44bf4f
commit 9f6b72dfcc
24 changed files with 1670 additions and 331 deletions

View File

@ -1,167 +0,0 @@
# 待完成接入接口清单
> 更新时间: 2024-12-30
## 📊 总体情况
| 类别 | 后端接口数 | Android已接入 | 待接入 |
|------|-----------|--------------|--------|
| 总计 | 131 | ~85 | ~46 |
---
## ✅ 已接入模块 (无需处理)
| 模块 | 接口数 | 状态 |
|------|--------|------|
| 用户认证 | 4 | ✅ 全部接入 |
| 用户信息 | 2 | ✅ 全部接入 |
| 直播间 | 6 | ✅ 全部接入 |
| 直播弹幕 | 2 | ✅ 全部接入 |
| 礼物打赏 | 5 | ✅ 全部接入 |
| 私聊会话 | 8 | ✅ 全部接入 |
| 好友管理 | 9 | ✅ 全部接入 |
| 文件上传 | 2 | ✅ 全部接入 |
| 在线状态 | 5 | ✅ 全部接入 |
| 离线消息 | 3 | ✅ 全部接入 |
| 消息表情回应 | 4 | ✅ 全部接入 |
| 关注功能 | 7 | ✅ 全部接入 |
| 作品管理 | 13 | ✅ 全部接入 |
| 搜索功能 | 9 | ✅ 全部接入 |
| 支付集成 | 4 | ✅ 全部接入 |
| 通话功能 | 10 | ✅ 全部接入 |
---
## ❌ 待接入模块
### 1. 群组管理 (10个接口) - 🔴 高优先级
后端已完成Android端未定义接口。
| 接口 | 路径 | 说明 |
|------|------|------|
| 创建群组 | `POST /api/front/groups/create` | 创建新群组 |
| 群组列表 | `GET /api/front/groups/list` | 获取我的群组 |
| 群组详情 | `GET /api/front/groups/{groupId}` | 获取群组信息 |
| 更新群组 | `PUT /api/front/groups/{groupId}` | 修改群组信息 |
| 解散群组 | `DELETE /api/front/groups/{groupId}` | 解散群组 |
| 添加成员 | `POST /api/front/groups/{groupId}/members` | 邀请成员 |
| 移除成员 | `DELETE /api/front/groups/{groupId}/members/{userId}` | 踢出成员 |
| 成员列表 | `GET /api/front/groups/{groupId}/members` | 获取成员 |
| 退出群组 | `POST /api/front/groups/{groupId}/leave` | 主动退群 |
| 转让群主 | `POST /api/front/groups/{groupId}/transfer` | 转让群主 |
---
### 2. 群组消息 (4个接口) - 🔴 高优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 发送群消息 | `POST /api/front/groups/{groupId}/messages` | 发送消息 |
| 群消息历史 | `GET /api/front/groups/{groupId}/messages` | 获取历史 |
| 撤回消息 | `DELETE /api/front/groups/{groupId}/messages/{messageId}` | 撤回消息 |
| 转发消息 | `POST /api/front/groups/{groupId}/messages/{messageId}/forward` | 转发消息 |
---
### 3. 消息搜索 (3个接口) - 🟡 中优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 搜索会话 | `GET /api/front/messages/search/conversations` | 搜索会话 |
| 搜索消息 | `GET /api/front/messages/search/messages` | 搜索消息内容 |
| 全局搜索 | `GET /api/front/messages/search/global` | 全局搜索 |
---
### 4. 评论功能 (8个接口) - 🟡 中优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 发布评论 | `POST /api/front/works/comment/publish` | 发布评论 |
| 评论列表 | `GET /api/front/works/comment/list/{worksId}` | 获取评论 |
| 点赞评论 | `POST /api/front/works/comment/like/{commentId}` | 点赞 |
| 取消点赞 | `POST /api/front/works/comment/unlike/{commentId}` | 取消点赞 |
| 删除评论 | `POST /api/front/works/comment/delete/{commentId}` | 删除评论 |
| 回复列表 | `GET /api/front/works/comment/reply/list/{commentId}` | 获取回复 |
| 评论详情 | `GET /api/front/works/comment/detail/{commentId}` | 评论详情 |
| 检查点赞 | `GET /api/front/works/comment/check-liked/{commentId}` | 检查状态 |
---
### 5. 通知推送 (9个接口) - 🟡 中优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 通知列表 | `GET /api/front/notification/list` | 获取通知 |
| 未读数量 | `GET /api/front/notification/unread-count` | 未读数 |
| 标记已读 | `POST /api/front/notification/mark-read/{id}` | 单条已读 |
| 全部已读 | `POST /api/front/notification/mark-all-read` | 全部已读 |
| 注册FCM | `POST /api/front/notification/fcm/register` | 注册推送 |
| 移除FCM | `POST /api/front/notification/fcm/remove` | 移除推送 |
| 删除通知 | `DELETE /api/front/notification/{id}` | 删除单条 |
| 清空通知 | `DELETE /api/front/notification/clear-all` | 清空全部 |
| 按类型统计 | `GET /api/front/notification/unread-count-by-type` | 分类统计 |
---
### 6. 分类管理 (7个接口) - 🟢 低优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 直播间分类 | `GET /api/front/category/live-room` | 直播分类 |
| 作品分类 | `GET /api/front/category/work` | 作品分类 |
| 分类列表 | `GET /api/front/category/list` | 全部分类 |
| 分类详情 | `GET /api/front/category/{id}` | 分类详情 |
| 分类统计 | `GET /api/front/category/statistics` | 统计数据 |
| 热门分类 | `GET /api/front/category/hot` | 热门分类 |
| 子分类 | `GET /api/front/category/{parentId}/children` | 子分类 |
---
### 7. 文件上传补充 (3个接口) - 🟢 低优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 通用图片上传 | `POST /api/upload/image` | 通用图片 |
| 通用文件上传 | `POST /api/upload/file` | 通用文件 |
| 语音上传 | `POST /api/upload/chat/voice` | 语音消息 |
---
### 8. 直播间补充 (4个接口) - 🟢 低优先级
| 接口 | 路径 | 说明 |
|------|------|------|
| 开始直播 | `POST /api/front/live/room/{id}/start` | 开播 |
| 结束直播 | `POST /api/front/live/room/{id}/stop` | 停播 |
| 观众列表 | `GET /api/rooms/{roomId}/viewers` | 观众列表 |
| 手动广播人数 | `POST /api/live/online/broadcast/{roomId}` | 广播人数 |
---
## 📋 接入优先级建议
### 第一优先级 (核心社交)
1. **群组管理** - 10个接口
2. **群组消息** - 4个接口
### 第二优先级 (内容互动)
3. **评论功能** - 8个接口
4. **通知推送** - 9个接口
### 第三优先级 (辅助功能)
5. **消息搜索** - 3个接口
6. **分类管理** - 7个接口
7. **文件上传补充** - 3个接口
8. **直播间补充** - 4个接口
---
## 📝 备注
- 后端接口已全部完成 (131个)
- Android端已接入约85个接口
- 待接入约46个接口
- 核心功能已基本完成,待接入的主要是群组和辅助功能

View File

@ -73,6 +73,9 @@ public class User implements Serializable {
@ApiModelProperty(value = "用户头像") @ApiModelProperty(value = "用户头像")
private String avatar; private String avatar;
@ApiModelProperty(value = "个人签名")
private String bio;
@ApiModelProperty(value = "手机号码") @ApiModelProperty(value = "手机号码")
private String phone; private String phone;

View File

@ -0,0 +1,38 @@
package com.zbkj.common.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
/**
* 用户批量操作Request
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UserBatchOperationRequest对象", description="用户批量操作")
public class UserBatchOperationRequest implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "用户ID列表", required = true)
@NotEmpty(message = "用户ID列表不能为空")
private List<Integer> uids;
@ApiModelProperty(value = "操作类型1-启用2-禁用3-删除4-设置分组5-设置标签", required = true)
@NotNull(message = "操作类型不能为空")
private Integer operationType;
@ApiModelProperty(value = "分组ID操作类型为4时必填")
private String groupId;
@ApiModelProperty(value = "标签ID操作类型为5时必填")
private String tagId;
}

View File

@ -0,0 +1,79 @@
package com.zbkj.common.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
/**
* 用户创建Request
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UserCreateRequest对象", description="创建用户")
public class UserCreateRequest implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "用户账号", required = true)
@NotBlank(message = "请填写用户账号")
@Length(max = 32, message = "用户账号不能超过32个字符")
private String account;
@ApiModelProperty(value = "用户密码", required = true)
@NotBlank(message = "请填写用户密码")
@Length(min = 6, max = 32, message = "密码长度为6-32个字符")
private String pwd;
@ApiModelProperty(value = "用户昵称", required = true)
@NotBlank(message = "请填写用户昵称")
@Length(max = 255, message = "用户昵称不能超过255个字符")
private String nickname;
@ApiModelProperty(value = "手机号码")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号码格式不正确")
private String phone;
@ApiModelProperty(value = "用户头像")
@Length(max = 256, message = "用户头像不能超过256个字符")
private String avatar;
@ApiModelProperty(value = "真实姓名")
@Length(max = 25, message = "真实姓名不能超过25个字符")
private String realName;
@ApiModelProperty(value = "生日")
private String birthday;
@ApiModelProperty(value = "性别0未知1男2女3保密")
private Integer sex;
@ApiModelProperty(value = "用户备注")
@Length(max = 255, message = "用户备注不能超过255个字符")
private String mark;
@ApiModelProperty(value = "用户分组id")
private String groupId;
@ApiModelProperty(value = "标签id")
private String tagId;
@ApiModelProperty(value = "推广员id")
private Integer spreadUid;
@ApiModelProperty(value = "是否为推广员0否1是")
private Integer isPromoter;
@ApiModelProperty(value = "用户等级")
private Integer level;
@ApiModelProperty(value = "状态1正常0禁用")
private Integer status;
}

View File

@ -36,7 +36,21 @@ public class UserEditRequest implements Serializable {
private String nickname; private String nickname;
@ApiModelProperty(value = "用户头像") @ApiModelProperty(value = "用户头像")
@NotBlank(message = "请上传用户头像")
@Length(max = 255, message = "用户头像不能超过255个字符") @Length(max = 255, message = "用户头像不能超过255个字符")
private String avatar; private String avatar;
@ApiModelProperty(value = "个人签名")
@Length(max = 500, message = "个人签名不能超过500个字符")
private String bio;
@ApiModelProperty(value = "生日")
private String birthday;
@ApiModelProperty(value = "性别")
@Length(max = 10, message = "性别不能超过10个字符")
private String gender;
@ApiModelProperty(value = "所在地")
@Length(max = 100, message = "所在地不能超过100个字符")
private String location;
} }

View File

@ -0,0 +1,69 @@
package com.zbkj.common.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 用户查询Request
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UserQueryRequest对象", description="用户查询条件")
public class UserQueryRequest implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "用户账号")
private String account;
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "用户分组id")
private String groupId;
@ApiModelProperty(value = "标签id")
private String tagId;
@ApiModelProperty(value = "用户等级")
private Integer level;
@ApiModelProperty(value = "状态1正常0禁用")
private Integer status;
@ApiModelProperty(value = "是否为推广员0否1是")
private Integer isPromoter;
@ApiModelProperty(value = "推广员id")
private Integer spreadUid;
@ApiModelProperty(value = "性别0未知1男2女3保密")
private Integer sex;
@ApiModelProperty(value = "用户类型")
private String userType;
@ApiModelProperty(value = "关键字搜索(账号/昵称/手机号)")
private String keywords;
@ApiModelProperty(value = "开始时间")
private String startTime;
@ApiModelProperty(value = "结束时间")
private String endTime;
@ApiModelProperty(value = "排序字段")
private String orderBy;
@ApiModelProperty(value = "排序方式asc升序desc降序")
private String orderType;
}

View File

@ -88,4 +88,19 @@ public class UserCenterResponse implements Serializable {
@ApiModelProperty(value = "用户收藏数量") @ApiModelProperty(value = "用户收藏数量")
private Integer collectCount; private Integer collectCount;
@ApiModelProperty(value = "个人签名")
private String bio;
@ApiModelProperty(value = "生日")
private String birthday;
@ApiModelProperty(value = "性别0未知1男2女3保密")
private Integer sex;
@ApiModelProperty(value = "详细地址/所在地")
private String addres;
@ApiModelProperty(value = "真实姓名")
private String realName;
} }

View File

@ -0,0 +1,155 @@
package com.zbkj.common.response;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 用户详情Response
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UserDetailResponse对象", description="用户详情")
public class UserDetailResponse implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "用户id")
private Integer uid;
@ApiModelProperty(value = "用户账号")
private String account;
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ApiModelProperty(value = "用户头像")
private String avatar;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "真实姓名")
private String realName;
@ApiModelProperty(value = "生日")
private String birthday;
@ApiModelProperty(value = "身份证号码")
private String cardId;
@ApiModelProperty(value = "性别0未知1男2女3保密")
private Integer sex;
@ApiModelProperty(value = "用户备注")
private String mark;
@ApiModelProperty(value = "合伙人id")
private Integer partnerId;
@ApiModelProperty(value = "用户分组id")
private String groupId;
@ApiModelProperty(value = "标签id")
private String tagId;
@ApiModelProperty(value = "用户余额")
private BigDecimal nowMoney;
@ApiModelProperty(value = "佣金金额")
private BigDecimal brokeragePrice;
@ApiModelProperty(value = "用户剩余积分")
private Integer integral;
@ApiModelProperty(value = "用户剩余经验")
private Integer experience;
@ApiModelProperty(value = "连续签到天数")
private Integer signNum;
@ApiModelProperty(value = "状态1正常0禁用")
private Integer status;
@ApiModelProperty(value = "用户等级")
private Integer level;
@ApiModelProperty(value = "推广员id")
private Integer spreadUid;
@ApiModelProperty(value = "推广员昵称")
private String spreadNickname;
@ApiModelProperty(value = "推广员关联时间")
private Date spreadTime;
@ApiModelProperty(value = "用户类型")
private String userType;
@ApiModelProperty(value = "是否为推广员0否1是")
private Integer isPromoter;
@ApiModelProperty(value = "用户购买次数")
private Integer payCount;
@ApiModelProperty(value = "下级人数")
private Integer spreadCount;
@ApiModelProperty(value = "详细地址")
private String addres;
@ApiModelProperty(value = "登陆类型")
private String loginType;
@ApiModelProperty(value = "创建时间")
private Date createTime;
@ApiModelProperty(value = "更新时间")
private Date updateTime;
@ApiModelProperty(value = "最后一次登录时间")
private Date lastLoginTime;
@ApiModelProperty(value = "推广等级记录")
private String path;
@ApiModelProperty(value = "是否关注公众号0否1是")
private Integer subscribe;
@ApiModelProperty(value = "关注公众号时间")
private Date subscribeTime;
@ApiModelProperty(value = "国家")
private String country;
@ApiModelProperty(value = "成为分销员时间")
private Date promoterTime;
@ApiModelProperty(value = "是否注销0未注销1已注销")
private Integer isLogoff;
@ApiModelProperty(value = "注销时间")
private Date logoffTime;
@ApiModelProperty(value = "扩展字段1")
private String extField1;
@ApiModelProperty(value = "扩展字段2")
private String extField2;
@ApiModelProperty(value = "扩展字段3")
private String extField3;
@ApiModelProperty(value = "扩展字段4")
private Integer extField4;
@ApiModelProperty(value = "扩展字段5")
private BigDecimal extField5;
}

View File

@ -0,0 +1,58 @@
package com.zbkj.common.response;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 用户统计Response
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UserStatisticsResponse对象", description="用户统计数据")
public class UserStatisticsResponse implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "总用户数")
private Integer totalUsers;
@ApiModelProperty(value = "今日新增用户数")
private Integer todayNewUsers;
@ApiModelProperty(value = "本周新增用户数")
private Integer weekNewUsers;
@ApiModelProperty(value = "本月新增用户数")
private Integer monthNewUsers;
@ApiModelProperty(value = "正常用户数")
private Integer activeUsers;
@ApiModelProperty(value = "禁用用户数")
private Integer disabledUsers;
@ApiModelProperty(value = "推广员数量")
private Integer promoterCount;
@ApiModelProperty(value = "总余额")
private BigDecimal totalBalance;
@ApiModelProperty(value = "总佣金")
private BigDecimal totalBrokerage;
@ApiModelProperty(value = "总积分")
private Long totalIntegral;
@ApiModelProperty(value = "付费用户数")
private Integer paidUsers;
@ApiModelProperty(value = "付费率")
private String paidRate;
}

View File

@ -68,10 +68,27 @@ public class UserController {
@ApiOperation(value = "修改个人资料") @ApiOperation(value = "修改个人资料")
@RequestMapping(value = "/user/edit", method = RequestMethod.POST) @RequestMapping(value = "/user/edit", method = RequestMethod.POST)
public CommonResult<Object> personInfo(@RequestBody @Validated UserEditRequest request) { public CommonResult<Object> personInfo(@RequestBody @Validated UserEditRequest request) {
if (userService.editUser(request)) { log.info("========== 收到修改个人资料请求 ==========");
return CommonResult.success(); log.info("请求参数: {}", request);
log.info("昵称: {}", request.getNickname());
log.info("头像: {}", request.getAvatar());
log.info("个人签名: {}", request.getBio());
log.info("生日: {}", request.getBirthday());
log.info("性别: {}", request.getGender());
log.info("所在地: {}", request.getLocation());
try {
boolean result = userService.editUser(request);
log.info("修改结果: {}", result);
if (result) {
return CommonResult.success();
}
return CommonResult.failed();
} catch (Exception e) {
log.error("修改个人资料失败", e);
return CommonResult.failed("修改失败:" + e.getMessage());
} }
return CommonResult.failed();
} }
/** /**
@ -80,7 +97,16 @@ public class UserController {
@ApiOperation(value = "个人中心-用户信息") @ApiOperation(value = "个人中心-用户信息")
@RequestMapping(value = "/user", method = RequestMethod.GET) @RequestMapping(value = "/user", method = RequestMethod.GET)
public CommonResult<UserCenterResponse> getUserCenter() { public CommonResult<UserCenterResponse> getUserCenter() {
return CommonResult.success(userService.getUserCenter()); log.info("========== 收到获取用户信息请求 ==========");
UserCenterResponse response = userService.getUserCenter();
log.info("========== 返回用户信息 ==========");
log.info("昵称: {}", response.getNickname());
log.info("个人签名: {}", response.getBio());
log.info("生日: {}", response.getBirthday());
log.info("性别: {}", response.getSex());
log.info("所在地: {}", response.getAddres());
log.info("真实姓名: {}", response.getRealName());
return CommonResult.success(response);
} }
/** /**

View File

@ -30,6 +30,7 @@ import com.zbkj.common.utils.*;
import com.zbkj.common.vo.DateLimitUtilVo; import com.zbkj.common.vo.DateLimitUtilVo;
import com.zbkj.service.dao.UserDao; import com.zbkj.service.dao.UserDao;
import com.zbkj.service.service.*; import com.zbkj.service.service.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -56,6 +57,7 @@ import java.util.stream.Collectors;
* | Author: CRMEB Team <admin@crmeb.com> * | Author: CRMEB Team <admin@crmeb.com>
* +---------------------------------------------------------------------- * +----------------------------------------------------------------------
*/ */
@Slf4j
@Service @Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService { public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService {
@ -556,8 +558,29 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
if (ObjectUtil.isNull(currentUser)) { if (ObjectUtil.isNull(currentUser)) {
throw new CrmebException("您的登录已过期,请先登录"); throw new CrmebException("您的登录已过期,请先登录");
} }
// 添加日志打印用户原始数据
log.info("========== getUserCenter 开始 ==========");
log.info("用户ID: {}", currentUser.getUid());
log.info("昵称: {}", currentUser.getNickname());
log.info("个人签名: {}", currentUser.getBio());
log.info("生日: {}", currentUser.getBirthday());
log.info("性别: {}", currentUser.getSex());
log.info("所在地: {}", currentUser.getAddres());
log.info("真实姓名: {}", currentUser.getRealName());
UserCenterResponse userCenterResponse = new UserCenterResponse(); UserCenterResponse userCenterResponse = new UserCenterResponse();
BeanUtils.copyProperties(currentUser, userCenterResponse); BeanUtils.copyProperties(currentUser, userCenterResponse);
// 添加日志打印复制后的数据
log.info("========== 复制后的数据 ==========");
log.info("响应对象-昵称: {}", userCenterResponse.getNickname());
log.info("响应对象-个人签名: {}", userCenterResponse.getBio());
log.info("响应对象-生日: {}", userCenterResponse.getBirthday());
log.info("响应对象-性别: {}", userCenterResponse.getSex());
log.info("响应对象-所在地: {}", userCenterResponse.getAddres());
log.info("响应对象-真实姓名: {}", userCenterResponse.getRealName());
// 设置用户ID // 设置用户ID
userCenterResponse.setUid(currentUser.getUid()); userCenterResponse.setUid(currentUser.getUid());
// 优惠券数量 // 优惠券数量
@ -593,6 +616,7 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
visitRecord.setVisitType(4); visitRecord.setVisitType(4);
userVisitRecordService.save(visitRecord); userVisitRecordService.save(visitRecord);
log.info("========== getUserCenter 结束 ==========");
return userCenterResponse; return userCenterResponse;
} }
@ -1710,11 +1734,67 @@ public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserS
*/ */
@Override @Override
public Boolean editUser(UserEditRequest request) { public Boolean editUser(UserEditRequest request) {
log.info("========== UserService.editUser 开始执行 ==========");
User user = getInfo(); User user = getInfo();
log.info("当前用户 uid: {}, nickname: {}", user.getUid(), user.getNickname());
log.info("原始用户数据: {}", user);
user.setAvatar(systemAttachmentService.clearPrefix(request.getAvatar())); user.setAvatar(systemAttachmentService.clearPrefix(request.getAvatar()));
user.setNickname(request.getNickname()); user.setNickname(request.getNickname());
// 更新个人签名
if (request.getBio() != null) {
log.info("更新个人签名: {}", request.getBio());
user.setBio(request.getBio());
}
// 更新生日
if (request.getBirthday() != null) {
log.info("更新生日: {}", request.getBirthday());
user.setBirthday(request.getBirthday());
}
// 更新性别将文字转换为数字=1=2保密=0
if (request.getGender() != null && !request.getGender().isEmpty()) {
int sex = 0; // 默认保密
if ("".equals(request.getGender())) {
sex = 1;
} else if ("".equals(request.getGender())) {
sex = 2;
}
log.info("更新性别: {} -> {}", request.getGender(), sex);
user.setSex(sex);
}
// 更新所在地
if (request.getLocation() != null) {
log.info("更新所在地: {}", request.getLocation());
user.setAddres(request.getLocation());
}
user.setUpdateTime(DateUtil.date()); user.setUpdateTime(DateUtil.date());
return updateById(user);
log.info("准备更新数据库,用户信息: uid={}, nickname={}, bio={}, birthday={}, sex={}, addres={}",
user.getUid(), user.getNickname(), user.getBio(),
user.getBirthday(), user.getSex(), user.getAddres());
log.info("完整用户对象: {}", user);
try {
boolean result = updateById(user);
log.info("数据库更新结果: {}", result);
// 立即查询验证
User updatedUser = getById(user.getUid());
log.info("更新后查询结果: uid={}, nickname={}, bio={}, birthday={}, sex={}, addres={}",
updatedUser.getUid(), updatedUser.getNickname(), updatedUser.getBio(),
updatedUser.getBirthday(), updatedUser.getSex(), updatedUser.getAddres());
return result;
} catch (Exception e) {
log.error("更新用户信息时发生异常", e);
throw e;
}
} }
/** /**

View File

@ -71,10 +71,10 @@
<!-- 获取用户的关注和粉丝统计 --> <!-- 获取用户的关注和粉丝统计 -->
<select id="getFollowStats" resultType="java.util.HashMap"> <select id="getFollowStats" resultType="java.util.HashMap">
SELECT SELECT
(SELECT COUNT(*) FROM eb_follow_record CAST((SELECT COUNT(*) FROM eb_follow_record
WHERE follower_id = #{userId} AND follow_status = 1 AND is_deleted = 0) as followingCount, WHERE follower_id = #{userId} AND follow_status = 1 AND is_deleted = 0) AS SIGNED) as followingCount,
(SELECT COUNT(*) FROM eb_follow_record CAST((SELECT COUNT(*) FROM eb_follow_record
WHERE followed_id = #{userId} AND follow_status = 1 AND is_deleted = 0) as followersCount WHERE followed_id = #{userId} AND follow_status = 1 AND is_deleted = 0) AS SIGNED) as followersCount
</select> </select>
</mapper> </mapper>

View File

@ -28,6 +28,9 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.example.livestreaming.databinding.ActivityEditProfileBinding; import com.example.livestreaming.databinding.ActivityEditProfileBinding;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.UserInfoResponse;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
import android.widget.NumberPicker; import android.widget.NumberPicker;
@ -106,7 +109,9 @@ public class EditProfileActivity extends AppCompatActivity {
binding.backButton.setOnClickListener(v -> finish()); binding.backButton.setOnClickListener(v -> finish());
binding.cancelButton.setOnClickListener(v -> finish()); binding.cancelButton.setOnClickListener(v -> finish());
loadFromPrefs(); // 先从服务器加载用户信息如果失败则从本地加载
loadUserInfoFromServer();
setupGenderDropdown(); setupGenderDropdown();
setupLocationDropdown(); setupLocationDropdown();
setupBirthdayPicker(); setupBirthdayPicker();
@ -143,24 +148,6 @@ public class EditProfileActivity extends AppCompatActivity {
}); });
binding.saveButton.setOnClickListener(v -> { binding.saveButton.setOnClickListener(v -> {
// TODO: 接入后端接口 - 上传头像
// 接口路径: POST /api/users/{userId}/avatar
// 请求参数:
// - userId: 用户ID从token中获取
// - avatar: 头像文件multipart/form-data
// 返回数据格式: ApiResponse<{avatarUrl: string}>
// 上传成功后保存avatarUrl到本地并更新界面显示
// TODO: 接入后端接口 - 更新用户资料
// 接口路径: PUT /api/users/{userId}/profile
// 请求参数:
// - userId: 用户ID从token中获取
// - name: 昵称
// - bio: 个人签名
// - birthday: 生日格式yyyy-MM-dd
// - gender: 性别//保密
// - location: 所在地格式省份-城市
// 返回数据格式: ApiResponse<UserProfile>
// 更新成功后同步更新本地缓存和界面显示
String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : ""; String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : "";
String bio = binding.inputBio.getText() != null ? binding.inputBio.getText().toString().trim() : ""; String bio = binding.inputBio.getText() != null ? binding.inputBio.getText().toString().trim() : "";
String birthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString().trim() : ""; String birthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString().trim() : "";
@ -172,6 +159,7 @@ public class EditProfileActivity extends AppCompatActivity {
return; return;
} }
// 先保存到本地SharedPreferences
getSharedPreferences(PREFS_NAME, MODE_PRIVATE) getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
.edit() .edit()
.putString(KEY_NAME, name) .putString(KEY_NAME, name)
@ -183,6 +171,8 @@ public class EditProfileActivity extends AppCompatActivity {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIO, bio).apply(); getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIO, bio).apply();
} }
// 处理头像
String avatarUrl = null;
if (selectedAvatarUri != null) { if (selectedAvatarUri != null) {
Uri persisted = persistAvatarToInternalStorage(selectedAvatarUri); Uri persisted = persistAvatarToInternalStorage(selectedAvatarUri);
if (persisted != null) { if (persisted != null) {
@ -191,15 +181,17 @@ public class EditProfileActivity extends AppCompatActivity {
.putString(KEY_AVATAR_URI, persisted.toString()) .putString(KEY_AVATAR_URI, persisted.toString())
.remove(KEY_AVATAR_RES) .remove(KEY_AVATAR_RES)
.apply(); .apply();
avatarUrl = persisted.toString();
} }
} else {
// 使用现有头像
avatarUrl = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, "");
} }
// 生日已经在日期选择器中实时保存这里只需要确保同步 // 保存其他字段到本地
// 如果输入框为空则清除
if (TextUtils.isEmpty(birthday)) { if (TextUtils.isEmpty(birthday)) {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_BIRTHDAY).apply(); getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_BIRTHDAY).apply();
} else { } else {
// 确保使用输入框中的最新值
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIRTHDAY, birthday).apply(); getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIRTHDAY, birthday).apply();
} }
@ -215,9 +207,8 @@ public class EditProfileActivity extends AppCompatActivity {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_LOCATION, location).apply(); getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_LOCATION, location).apply();
} }
// 设置结果通知ProfileActivity需要刷新 // 调用后端接口保存到数据库
setResult(RESULT_OK); saveProfileToServer(name, avatarUrl, bio, birthday, gender, location);
finish();
}); });
} }
@ -296,6 +287,122 @@ public class EditProfileActivity extends AppCompatActivity {
} }
} }
/**
* 从服务器加载用户信息
*/
private void loadUserInfoFromServer() {
android.util.Log.d("EditProfile", "开始从服务器加载用户信息");
ApiClient.getService(getApplicationContext()).getUserInfo()
.enqueue(new retrofit2.Callback<ApiResponse<UserInfoResponse>>() {
@Override
public void onResponse(retrofit2.Call<ApiResponse<UserInfoResponse>> call,
retrofit2.Response<ApiResponse<UserInfoResponse>> response) {
android.util.Log.d("EditProfile", "收到响应: " + response.code());
if (response.isSuccessful() && response.body() != null) {
android.util.Log.d("EditProfile", "响应成功,检查数据");
android.util.Log.d("EditProfile", "响应体: " + response.body());
if (response.body().isOk()) {
UserInfoResponse userInfo = response.body().getData();
android.util.Log.d("EditProfile", "成功获取用户信息");
android.util.Log.d("EditProfile", "昵称: " + userInfo.getNickname());
android.util.Log.d("EditProfile", "个人签名: " + userInfo.getBio());
android.util.Log.d("EditProfile", "生日: " + userInfo.getBirthday());
android.util.Log.d("EditProfile", "性别: " + userInfo.getSex());
android.util.Log.d("EditProfile", "所在地: " + userInfo.getAddres());
android.util.Log.d("EditProfile", "头像: " + userInfo.getAvatar());
// 填充表单
if (userInfo.getNickname() != null) {
binding.inputName.setText(userInfo.getNickname());
android.util.Log.d("EditProfile", "已设置昵称");
}
if (userInfo.getBio() != null && !userInfo.getBio().isEmpty()) {
binding.inputBio.setText(userInfo.getBio());
android.util.Log.d("EditProfile", "已设置个人签名");
} else {
android.util.Log.d("EditProfile", "个人签名为空");
}
if (userInfo.getBirthday() != null && !userInfo.getBirthday().isEmpty()) {
binding.inputBirthday.setText(userInfo.getBirthday());
android.util.Log.d("EditProfile", "已设置生日");
} else {
android.util.Log.d("EditProfile", "生日为空");
}
if (userInfo.getSex() != null) {
String genderText = userInfo.getGenderText();
binding.inputGender.setText(genderText);
android.util.Log.d("EditProfile", "已设置性别: " + genderText);
} else {
android.util.Log.d("EditProfile", "性别为空");
}
if (userInfo.getAddres() != null && !userInfo.getAddres().isEmpty()) {
binding.inputLocation.setText(userInfo.getAddres());
android.util.Log.d("EditProfile", "已设置所在地");
} else {
android.util.Log.d("EditProfile", "所在地为空");
}
// 加载头像
if (userInfo.getAvatar() != null && !userInfo.getAvatar().isEmpty()) {
String avatarUrl = userInfo.getAvatar();
// 如果是相对路径添加服务器地址
if (!avatarUrl.startsWith("http")) {
avatarUrl = ApiClient.getCurrentBaseUrl(getApplicationContext()) + avatarUrl;
}
android.util.Log.d("EditProfile", "加载头像: " + avatarUrl);
Glide.with(EditProfileActivity.this)
.load(avatarUrl)
.circleCrop()
.placeholder(R.drawable.ic_account_circle_24)
.error(R.drawable.ic_account_circle_24)
.into(binding.avatarPreview);
// 保存到本地
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
.edit()
.putString(KEY_AVATAR_URI, avatarUrl)
.apply();
} else {
android.util.Log.d("EditProfile", "头像为空");
}
android.util.Log.d("EditProfile", "用户信息填充完成");
} else {
android.util.Log.w("EditProfile", "响应不OK: " + response.body().getMessage());
loadFromPrefs();
}
} else {
android.util.Log.w("EditProfile", "响应失败或响应体为空");
if (response.errorBody() != null) {
try {
String errorBody = response.errorBody().string();
android.util.Log.e("EditProfile", "错误响应: " + errorBody);
} catch (Exception e) {
android.util.Log.e("EditProfile", "无法读取错误响应", e);
}
}
// 如果服务器获取失败从本地加载
loadFromPrefs();
}
}
@Override
public void onFailure(retrofit2.Call<ApiResponse<UserInfoResponse>> call, Throwable t) {
android.util.Log.e("EditProfile", "获取用户信息失败", t);
android.util.Log.e("EditProfile", "错误消息: " + t.getMessage());
// 如果网络请求失败从本地加载
loadFromPrefs();
}
});
}
private void showAvatarBottomSheet() { private void showAvatarBottomSheet() {
BottomSheetDialog dialog = new BottomSheetDialog(this); BottomSheetDialog dialog = new BottomSheetDialog(this);
android.view.View view = getLayoutInflater().inflate(R.layout.bottom_sheet_avatar_picker, null); android.view.View view = getLayoutInflater().inflate(R.layout.bottom_sheet_avatar_picker, null);
@ -592,5 +699,204 @@ public class EditProfileActivity extends AppCompatActivity {
cal.set(year, month - 1, 1); // Calendar.MONTH 从0开始 cal.set(year, month - 1, 1); // Calendar.MONTH 从0开始
return cal.getActualMaximum(Calendar.DAY_OF_MONTH); return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
} }
/**
* 保存个人资料到服务器
*/
private void saveProfileToServer(String nickname, String avatar, String bio,
String birthday, String gender, String location) {
// 检查登录状态
if (!AuthHelper.isLoggedIn(this)) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
return;
}
// 显示加载提示
android.app.ProgressDialog progressDialog = new android.app.ProgressDialog(this);
progressDialog.setMessage("保存中...");
progressDialog.setCancelable(false);
progressDialog.show();
// 如果有选择新头像先上传头像
if (selectedAvatarUri != null && avatar != null && avatar.startsWith("content://")) {
android.util.Log.d("EditProfile", "检测到本地头像,开始上传...");
uploadAvatarAndSave(nickname, bio, birthday, gender, location, progressDialog);
} else {
// 没有新头像或头像已经是URL直接保存
android.util.Log.d("EditProfile", "使用现有头像URL直接保存");
saveUserInfo(nickname, avatar, bio, birthday, gender, location, progressDialog);
}
}
/**
* 上传头像并保存用户信息
*/
private void uploadAvatarAndSave(String nickname, String bio, String birthday,
String gender, String location,
android.app.ProgressDialog progressDialog) {
try {
// URI 转换为文件
java.io.File file = new java.io.File(getFilesDir(), "temp_avatar.jpg");
java.io.InputStream inputStream = getContentResolver().openInputStream(selectedAvatarUri);
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(file);
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
inputStream.close();
// 创建 RequestBody
okhttp3.RequestBody requestFile = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("image/*"), file);
okhttp3.MultipartBody.Part body = okhttp3.MultipartBody.Part.createFormData(
"file", file.getName(), requestFile);
okhttp3.RequestBody model = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("text/plain"), "avatar");
okhttp3.RequestBody pid = okhttp3.RequestBody.create(
okhttp3.MediaType.parse("text/plain"), "0");
android.util.Log.d("EditProfile", "开始上传头像文件...");
// 上传头像
ApiClient.getService(getApplicationContext()).uploadImage(body, model, pid)
.enqueue(new retrofit2.Callback<ApiResponse<com.example.livestreaming.net.FileUploadResponse>>() {
@Override
public void onResponse(retrofit2.Call<ApiResponse<com.example.livestreaming.net.FileUploadResponse>> call,
retrofit2.Response<ApiResponse<com.example.livestreaming.net.FileUploadResponse>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
String avatarUrl = response.body().getData().getUrl();
android.util.Log.d("EditProfile", "头像上传成功: " + avatarUrl);
// 删除临时文件
file.delete();
// 使用上传后的URL保存用户信息
saveUserInfo(nickname, avatarUrl, bio, birthday, gender, location, progressDialog);
} else {
progressDialog.dismiss();
android.util.Log.e("EditProfile", "头像上传失败");
Toast.makeText(EditProfileActivity.this,
"头像上传失败,请重试", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(retrofit2.Call<ApiResponse<com.example.livestreaming.net.FileUploadResponse>> call,
Throwable t) {
progressDialog.dismiss();
file.delete();
android.util.Log.e("EditProfile", "头像上传失败", t);
Toast.makeText(EditProfileActivity.this,
"头像上传失败:" + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
progressDialog.dismiss();
android.util.Log.e("EditProfile", "准备上传头像时出错", e);
Toast.makeText(this, "头像处理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* 保存用户信息到服务器
*/
private void saveUserInfo(String nickname, String avatar, String bio,
String birthday, String gender, String location,
android.app.ProgressDialog progressDialog) {
// 添加日志打印要保存的数据
android.util.Log.d("EditProfile", "========== 开始保存个人资料 ==========");
android.util.Log.d("EditProfile", "昵称: " + nickname);
android.util.Log.d("EditProfile", "头像: " + avatar);
android.util.Log.d("EditProfile", "个人签名: " + bio);
android.util.Log.d("EditProfile", "生日: " + birthday);
android.util.Log.d("EditProfile", "性别: " + gender);
android.util.Log.d("EditProfile", "所在地: " + location);
// 如果头像为空使用默认头像URL
final String finalAvatar;
if (TextUtils.isEmpty(avatar)) {
finalAvatar = "https://example.com/default-avatar.jpg";
} else {
finalAvatar = avatar;
}
// 检查 API 地址
String apiUrl = ApiClient.getCurrentBaseUrl(getApplicationContext());
android.util.Log.d("EditProfile", "API 地址: " + apiUrl);
// 检查 Token
String token = com.example.livestreaming.net.AuthStore.getToken(getApplicationContext());
android.util.Log.d("EditProfile", "Token: " + (token != null ? token.substring(0, Math.min(20, token.length())) + "..." : "null"));
// 构建请求对象
com.example.livestreaming.net.UserEditRequest request =
new com.example.livestreaming.net.UserEditRequest();
request.setNickname(nickname);
request.setAvatar(finalAvatar);
request.setBio(bio);
request.setBirthday(birthday);
request.setGender(gender);
request.setLocation(location);
android.util.Log.d("EditProfile", "开始发送请求...");
// 调用API
ApiClient.getService(getApplicationContext()).updateUserInfo(request)
.enqueue(new retrofit2.Callback<ApiResponse<Object>>() {
@Override
public void onResponse(retrofit2.Call<ApiResponse<Object>> call,
retrofit2.Response<ApiResponse<Object>> response) {
progressDialog.dismiss();
android.util.Log.d("EditProfile", "收到响应");
android.util.Log.d("EditProfile", "响应码: " + response.code());
android.util.Log.d("EditProfile", "响应成功: " + response.isSuccessful());
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Object> apiResponse = response.body();
android.util.Log.d("EditProfile", "响应消息: " + apiResponse.getMessage());
android.util.Log.d("EditProfile", "是否成功: " + apiResponse.isOk());
if (apiResponse.isOk()) {
Toast.makeText(EditProfileActivity.this,
"保存成功", Toast.LENGTH_SHORT).show();
// 设置结果通知ProfileActivity需要刷新
setResult(RESULT_OK);
finish();
} else {
Toast.makeText(EditProfileActivity.this,
"保存失败:" + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
android.util.Log.e("EditProfile", "响应失败或响应体为空");
if (response.errorBody() != null) {
try {
String errorBody = response.errorBody().string();
android.util.Log.e("EditProfile", "错误响应: " + errorBody);
} catch (Exception e) {
android.util.Log.e("EditProfile", "无法读取错误响应", e);
}
}
Toast.makeText(EditProfileActivity.this,
"保存失败:服务器错误 (code: " + response.code() + ")",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(retrofit2.Call<ApiResponse<Object>> call, Throwable t) {
progressDialog.dismiss();
android.util.Log.e("EditProfile", "请求失败", t);
android.util.Log.e("EditProfile", "错误消息: " + t.getMessage());
Toast.makeText(EditProfileActivity.this,
"保存失败:" + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
} }

View File

@ -1576,12 +1576,6 @@ public class MainActivity extends AppCompatActivity {
Log.d(TAG, "loadLiveTypesForDialog() 开始从后端加载直播类型"); Log.d(TAG, "loadLiveTypesForDialog() 开始从后端加载直播类型");
// 设置下拉框为只读不允许手动输入只能从列表中选择
typeSpinner.setKeyListener(null);
typeSpinner.setFocusable(false);
typeSpinner.setClickable(true);
typeSpinner.setFocusableInTouchMode(false);
// 调用后端接口获取直播类型列表 // 调用后端接口获取直播类型列表
ApiClient.getService(getApplicationContext()).getLiveTypes() ApiClient.getService(getApplicationContext()).getLiveTypes()
.enqueue(new Callback<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>>() { .enqueue(new Callback<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>>() {
@ -1611,11 +1605,12 @@ public class MainActivity extends AppCompatActivity {
typeNames[i] = types.get(i).getName(); typeNames[i] = types.get(i).getName();
} }
// 使用 simple_list_item_1 而不是 simple_dropdown_item_1line
ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this, ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this,
android.R.layout.simple_dropdown_item_1line, typeNames); android.R.layout.simple_list_item_1, typeNames);
typeSpinner.setAdapter(adapter); typeSpinner.setAdapter(adapter);
// 设置默认选中第一项 // 设置默认选中第一项使用 false 参数避免触发过滤
if (typeNames.length > 0) { if (typeNames.length > 0) {
typeSpinner.setText(typeNames[0], false); typeSpinner.setText(typeNames[0], false);
Log.d(TAG, "loadLiveTypesForDialog() 默认选中: " + typeNames[0]); Log.d(TAG, "loadLiveTypesForDialog() 默认选中: " + typeNames[0]);
@ -1648,7 +1643,7 @@ public class MainActivity extends AppCompatActivity {
private void useDefaultLiveTypes(MaterialAutoCompleteTextView typeSpinner) { private void useDefaultLiveTypes(MaterialAutoCompleteTextView typeSpinner) {
String[] defaultTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"}; String[] defaultTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this, ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this,
android.R.layout.simple_dropdown_item_1line, defaultTypes); android.R.layout.simple_list_item_1, defaultTypes);
typeSpinner.setAdapter(adapter); typeSpinner.setAdapter(adapter);
// 设置默认选中第一项 // 设置默认选中第一项

View File

@ -24,6 +24,7 @@ import com.bumptech.glide.Glide;
import com.example.livestreaming.BuildConfig; import com.example.livestreaming.BuildConfig;
import com.example.livestreaming.databinding.ActivityProfileBinding; import com.example.livestreaming.databinding.ActivityProfileBinding;
import com.example.livestreaming.ShareUtils; import com.example.livestreaming.ShareUtils;
import com.example.livestreaming.net.AuthStore;
import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
@ -78,17 +79,15 @@ public class ProfileActivity extends AppCompatActivity {
editProfileLauncher = registerForActivityResult( editProfileLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
// 当从EditProfileActivity返回时立即刷新所有数据 // 当从EditProfileActivity返回时从服务器重新加载数据
loadProfileFromPrefs(); loadUserInfoFromServer();
loadAndDisplayTags(); loadFollowStats();
loadProfileInfo();
} }
); );
loadProfileFromPrefs(); // 首次加载从服务器获取
loadAndDisplayTags(); loadUserInfoFromServer();
loadProfileInfo(); loadFollowStats();
loadFollowStats(); // 加载关注统计
setupEditableAreas(); setupEditableAreas();
setupAvatarClick(); setupAvatarClick();
setupNavigationClicks(); setupNavigationClicks();
@ -128,14 +127,7 @@ public class ProfileActivity extends AppCompatActivity {
} }
private void loadProfileFromPrefs() { private void loadProfileFromPrefs() {
// TODO: 接入后端接口 - 获取用户资料 // 从本地缓存加载用户资料已通过 loadUserInfoFromServer 从服务器同步
// 接口路径: GET /api/users/{userId}/profile
// 请求参数:
// - userId: 用户ID路径参数当前用户从token中获取
// 返回数据格式: ApiResponse<UserProfile>
// UserProfile对象应包含: id, name, avatarUrl, bio, level, badge, birthday, gender, location,
// followingCount, fansCount, likesCount等字段
// 首次加载时从接口获取后续可从本地缓存读取
String n = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, null); String n = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, null);
if (!TextUtils.isEmpty(n)) binding.name.setText(n); if (!TextUtils.isEmpty(n)) binding.name.setText(n);
@ -189,18 +181,8 @@ public class ProfileActivity extends AppCompatActivity {
} }
private void setupEditableAreas() { private void setupEditableAreas() {
// TODO: 接入后端接口 - 更新用户资料 // 注意用户资料的完整编辑功能在 EditProfileActivity 中实现
// 接口路径: PUT /api/users/{userId}/profile // 这里保留简单的昵称和签名编辑功能仅本地保存
// 请求参数:
// - userId: 用户ID路径参数从token中获取
// - name (可选): 昵称
// - bio (可选): 个人签名
// - avatarUrl (可选): 头像URL
// - birthday (可选): 生日
// - gender (可选): 性别
// - location (可选): 所在地
// 返回数据格式: ApiResponse<UserProfile>
// 更新成功后同步更新本地缓存和界面显示
binding.name.setOnClickListener(v -> showEditDialog("编辑昵称", binding.name.getText() != null ? binding.name.getText().toString() : "", text -> { binding.name.setOnClickListener(v -> showEditDialog("编辑昵称", binding.name.getText() != null ? binding.name.getText().toString() : "", text -> {
binding.name.setText(text); binding.name.setText(text);
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_NAME, text).apply(); getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_NAME, text).apply();
@ -273,12 +255,7 @@ public class ProfileActivity extends AppCompatActivity {
} }
}); });
// TODO: 接入后端接口 - 获取关注/粉丝/获赞数量 // 关注/粉丝/获赞统计已通过 loadFollowStats() 方法从后端接口获取
// 接口路径: GET /api/users/{userId}/stats
// 请求参数:
// - userId: 用户ID路径参数
// 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
// 在ProfileActivity加载时调用更新关注粉丝获赞数量显示
binding.following.setOnClickListener(v -> { binding.following.setOnClickListener(v -> {
// 检查登录状态查看关注列表需要登录 // 检查登录状态查看关注列表需要登录
if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) { if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) {
@ -356,18 +333,7 @@ public class ProfileActivity extends AppCompatActivity {
} }
}); });
// TODO: 接入后端接口 - 发布作品 // 作品发布功能在 PublishWorkActivity 中实现
// 接口路径: POST /api/works
// 请求参数:
// - userId: 用户ID从token中获取
// - title: 作品标题
// - description: 作品描述可选
// - coverUrl: 封面图片URL必填需要先上传图片
// - videoUrl (可选): 视频URL如果是视频作品
// - images (可选): 图片URL列表如果是图片作品
// 返回数据格式: ApiResponse<WorkItem>
// WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段
// 发布成功后刷新作品列表显示
// 空状态的发布按钮 // 空状态的发布按钮
binding.worksPublishBtn.setOnClickListener(v -> { binding.worksPublishBtn.setOnClickListener(v -> {
// 检查登录状态发布作品需要登录 // 检查登录状态发布作品需要登录
@ -410,24 +376,84 @@ public class ProfileActivity extends AppCompatActivity {
} }
private void loadWorks() { private void loadWorks() {
// TODO: 接入后端接口 - 获取当前用户的作品列表 // 检查登录状态
// 接口路径: GET /api/users/{userId}/works String userIdStr = AuthStore.getUserId(this);
// 请求方法: GET if (userIdStr == null || userIdStr.trim().isEmpty()) {
// 请求头: Authorization: Bearer {token} (必填从AuthStore获取) // 未登录显示空状态
// 路径参数: binding.worksRecycler.setVisibility(View.GONE);
// - userId: String (必填) - 当前用户ID从token中解析获取 binding.worksEmptyState.setVisibility(View.VISIBLE);
// 请求参数Query: return;
// - page: int (可选默认1) - 页码 }
// - pageSize: int (可选默认20) - 每页数量
// 返回数据格式: ApiResponse<PageResult<WorkItem>>
// 实现步骤:
// 1. 从AuthStore获取token解析userId或从用户信息中获取
// 2. 调用接口获取作品列表
// 3. 更新UI显示作品列表或空状态
// 4. 处理错误情况网络错误未登录等
// 注意: 此方法在onResume时也会调用需要避免重复请求
// 临时从本地存储加载等待后端接口 try {
int userId = Integer.parseInt(userIdStr.trim());
// 从后端接口获取作品列表
com.example.livestreaming.net.ApiService apiService =
com.example.livestreaming.net.ApiClient.getService(this);
retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call =
apiService.getUserWorks(userId, 1, 20);
call.enqueue(new retrofit2.Callback<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>>() {
@Override
public void onResponse(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call,
retrofit2.Response<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse> pageResponse = response.body().getData();
if (pageResponse != null && pageResponse.getList() != null && !pageResponse.getList().isEmpty()) {
// 转换为 WorkItem 列表
List<WorkItem> works = new java.util.ArrayList<>();
for (com.example.livestreaming.net.WorksResponse worksResponse : pageResponse.getList()) {
WorkItem item = new WorkItem();
item.setId(String.valueOf(worksResponse.getId()));
item.setTitle(worksResponse.getTitle());
item.setCoverUrl(worksResponse.getCoverUrl());
item.setLikeCount(worksResponse.getLikeCount() != null ? worksResponse.getLikeCount() : 0);
item.setViewCount(worksResponse.getViewCount() != null ? worksResponse.getViewCount() : 0);
// 根据作品类型设置
if (worksResponse.getType() != null) {
if ("VIDEO".equalsIgnoreCase(worksResponse.getType()) || "1".equals(worksResponse.getType())) {
item.setType(WorkItem.WorkType.VIDEO);
item.setVideoUrl(worksResponse.getVideoUrl());
} else if ("IMAGE".equalsIgnoreCase(worksResponse.getType()) || "2".equals(worksResponse.getType())) {
item.setType(WorkItem.WorkType.IMAGE);
item.setImageUrls(worksResponse.getImageUrls());
}
}
works.add(item);
}
binding.worksRecycler.setVisibility(View.VISIBLE);
binding.worksEmptyState.setVisibility(View.GONE);
worksAdapter.submitList(works);
} else {
// 没有作品
binding.worksRecycler.setVisibility(View.GONE);
binding.worksEmptyState.setVisibility(View.VISIBLE);
}
} else {
// 请求失败尝试从本地加载
loadWorksFromLocal();
}
}
@Override
public void onFailure(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call, Throwable t) {
android.util.Log.e("Profile", "加载作品列表失败", t);
// 失败时从本地加载
loadWorksFromLocal();
}
});
} catch (NumberFormatException e) {
android.util.Log.e("Profile", "用户ID格式错误", e);
loadWorksFromLocal();
}
}
/**
* 从本地加载作品列表备用方案
*/
private void loadWorksFromLocal() {
List<WorkItem> works = WorkManager.getAllWorks(this); List<WorkItem> works = WorkManager.getAllWorks(this);
if (works != null && !works.isEmpty()) { if (works != null && !works.isEmpty()) {
binding.worksRecycler.setVisibility(View.VISIBLE); binding.worksRecycler.setVisibility(View.VISIBLE);
@ -440,38 +466,96 @@ public class ProfileActivity extends AppCompatActivity {
} }
private void showTab(int index) { private void showTab(int index) {
// TODO: 接入后端接口 - 获取用户作品列表
// 接口路径: GET /api/users/{userId}/works
// 请求参数:
// - userId: 用户ID从token中获取
// - page (可选): 页码
// - pageSize (可选): 每页数量
// 返回数据格式: ApiResponse<List<WorkItem>>
// WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段
// TODO: 接入后端接口 - 获取用户收藏列表
// 接口路径: GET /api/users/{userId}/favorites
// 请求参数:
// - userId: 用户ID从token中获取
// - page (可选): 页码
// - pageSize (可选): 每页数量
// 返回数据格式: ApiResponse<List<WorkItem>>
// TODO: 接入后端接口 - 获取用户赞过的作品列表
// 接口路径: GET /api/users/{userId}/liked
// 请求参数:
// - userId: 用户ID从token中获取
// - page (可选): 页码
// - pageSize (可选): 每页数量
// 返回数据格式: ApiResponse<List<WorkItem>>
// 标签页顺序0-作品, 1-收藏, 2-赞过 // 标签页顺序0-作品, 1-收藏, 2-赞过
binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE); binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE);
binding.tabFavorites.setVisibility(index == 1 ? View.VISIBLE : View.GONE); binding.tabFavorites.setVisibility(index == 1 ? View.VISIBLE : View.GONE);
binding.tabLiked.setVisibility(index == 2 ? View.VISIBLE : View.GONE); binding.tabLiked.setVisibility(index == 2 ? View.VISIBLE : View.GONE);
// 当切换到作品标签页时重新加载作品列表 // 根据选中的标签页加载对应的数据
if (index == 0) { if (index == 0) {
// 作品标签页
loadWorks(); loadWorks();
} else if (index == 1) {
// 收藏标签页
loadCollectedWorks();
} else if (index == 2) {
// 赞过标签页
loadLikedWorks();
} }
// "资料"标签页已移除 }
/**
* 加载收藏的作品列表
*/
private void loadCollectedWorks() {
// 检查登录状态
if (!AuthHelper.requireLogin(this, "查看收藏列表需要登录")) {
return;
}
com.example.livestreaming.net.ApiService apiService =
com.example.livestreaming.net.ApiClient.getService(this);
retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call =
apiService.getMyCollectedWorks(1, 20);
call.enqueue(new retrofit2.Callback<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>>() {
@Override
public void onResponse(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call,
retrofit2.Response<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse> pageResponse = response.body().getData();
if (pageResponse != null && pageResponse.getList() != null && !pageResponse.getList().isEmpty()) {
// 有收藏的作品显示列表这里可以添加显示逻辑
Toast.makeText(ProfileActivity.this, "已加载 " + pageResponse.getList().size() + " 个收藏", Toast.LENGTH_SHORT).show();
} else {
// 没有收藏
Toast.makeText(ProfileActivity.this, "暂无收藏", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onFailure(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call, Throwable t) {
android.util.Log.e("Profile", "加载收藏列表失败", t);
}
});
}
/**
* 加载点赞的作品列表
*/
private void loadLikedWorks() {
// 检查登录状态
if (!AuthHelper.requireLogin(this, "查看点赞列表需要登录")) {
return;
}
com.example.livestreaming.net.ApiService apiService =
com.example.livestreaming.net.ApiClient.getService(this);
retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call =
apiService.getMyLikedWorks(1, 20);
call.enqueue(new retrofit2.Callback<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>>() {
@Override
public void onResponse(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call,
retrofit2.Response<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse> pageResponse = response.body().getData();
if (pageResponse != null && pageResponse.getList() != null && !pageResponse.getList().isEmpty()) {
// 有点赞的作品显示列表这里可以添加显示逻辑
Toast.makeText(ProfileActivity.this, "已加载 " + pageResponse.getList().size() + " 个点赞", Toast.LENGTH_SHORT).show();
} else {
// 没有点赞
Toast.makeText(ProfileActivity.this, "暂无点赞", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onFailure(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call, Throwable t) {
android.util.Log.e("Profile", "加载点赞列表失败", t);
}
});
} }
private interface OnTextSaved { private interface OnTextSaved {
@ -505,9 +589,7 @@ public class ProfileActivity extends AppCompatActivity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
if (binding != null) { if (binding != null) {
loadProfileFromPrefs(); loadUserInfoFromServer(); // 从服务器加载最新数据
loadAndDisplayTags();
loadProfileInfo();
loadFollowStats(); // 刷新关注统计 loadFollowStats(); // 刷新关注统计
loadWorks(); // 重新加载作品列表 loadWorks(); // 重新加载作品列表
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation; BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
@ -674,6 +756,92 @@ public class ProfileActivity extends AppCompatActivity {
ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧"); ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧");
} }
/**
* 从服务器加载用户信息
*/
private void loadUserInfoFromServer() {
android.util.Log.d("Profile", "开始从服务器加载用户信息");
com.example.livestreaming.net.ApiClient.getService(getApplicationContext()).getUserInfo()
.enqueue(new retrofit2.Callback<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.UserInfoResponse>>() {
@Override
public void onResponse(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.UserInfoResponse>> call,
retrofit2.Response<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.UserInfoResponse>> response) {
android.util.Log.d("Profile", "收到响应: " + response.code());
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
com.example.livestreaming.net.UserInfoResponse userInfo = response.body().getData();
android.util.Log.d("Profile", "成功获取用户信息");
// 保存到本地
android.content.SharedPreferences.Editor editor =
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit();
if (userInfo.getNickname() != null) {
editor.putString(KEY_NAME, userInfo.getNickname());
android.util.Log.d("Profile", "保存昵称: " + userInfo.getNickname());
}
if (userInfo.getBio() != null && !userInfo.getBio().isEmpty()) {
editor.putString(KEY_BIO, userInfo.getBio());
android.util.Log.d("Profile", "保存个人签名: " + userInfo.getBio());
} else {
editor.remove(KEY_BIO);
}
if (userInfo.getBirthday() != null && !userInfo.getBirthday().isEmpty()) {
editor.putString(KEY_BIRTHDAY, userInfo.getBirthday());
android.util.Log.d("Profile", "保存生日: " + userInfo.getBirthday());
}
if (userInfo.getSex() != null) {
String genderText = userInfo.getGenderText();
editor.putString(KEY_GENDER, genderText);
android.util.Log.d("Profile", "保存性别: " + genderText);
}
if (userInfo.getAddres() != null && !userInfo.getAddres().isEmpty()) {
editor.putString(KEY_LOCATION, userInfo.getAddres());
android.util.Log.d("Profile", "保存所在地: " + userInfo.getAddres());
}
if (userInfo.getAvatar() != null && !userInfo.getAvatar().isEmpty()) {
String avatarUrl = userInfo.getAvatar();
if (!avatarUrl.startsWith("http")) {
avatarUrl = com.example.livestreaming.net.ApiClient.getCurrentBaseUrl(getApplicationContext()) + avatarUrl;
}
editor.putString(KEY_AVATAR_URI, avatarUrl);
android.util.Log.d("Profile", "保存头像: " + avatarUrl);
}
editor.apply();
android.util.Log.d("Profile", "数据已保存到本地");
// 刷新界面
loadProfileFromPrefs();
loadAndDisplayTags();
loadProfileInfo();
android.util.Log.d("Profile", "界面已刷新");
} else {
android.util.Log.w("Profile", "响应失败或响应体为空");
// 失败时使用本地缓存
loadProfileFromPrefs();
loadAndDisplayTags();
loadProfileInfo();
}
}
@Override
public void onFailure(retrofit2.Call<com.example.livestreaming.net.ApiResponse<com.example.livestreaming.net.UserInfoResponse>> call, Throwable t) {
android.util.Log.e("Profile", "获取用户信息失败", t);
// 失败时使用本地缓存
loadProfileFromPrefs();
loadAndDisplayTags();
loadProfileInfo();
}
});
}
/** /**
* 加载关注统计数据 * 加载关注统计数据
*/ */
@ -695,13 +863,33 @@ public class ProfileActivity extends AppCompatActivity {
// 更新关注数 // 更新关注数
Object followingCount = stats.get("followingCount"); Object followingCount = stats.get("followingCount");
if (followingCount != null) { if (followingCount != null) {
binding.following.setText(String.valueOf(followingCount) + "\n关注"); int count = 0;
if (followingCount instanceof Number) {
count = ((Number) followingCount).intValue();
} else {
try {
count = Integer.parseInt(String.valueOf(followingCount));
} catch (NumberFormatException e) {
count = 0;
}
}
binding.following.setText(count + "\n关注");
} }
// 更新粉丝数 // 更新粉丝数
Object followersCount = stats.get("followersCount"); Object followersCount = stats.get("followersCount");
if (followersCount != null) { if (followersCount != null) {
binding.followers.setText(String.valueOf(followersCount) + "\n粉丝"); int count = 0;
if (followersCount instanceof Number) {
count = ((Number) followersCount).intValue();
} else {
try {
count = Integer.parseInt(String.valueOf(followersCount));
} catch (NumberFormatException e) {
count = 0;
}
}
binding.followers.setText(count + "\n粉丝");
} }
} }
} }

View File

@ -196,10 +196,13 @@ public class RoomDetailActivity extends AppCompatActivity {
// 全屏按钮 // 全屏按钮
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen()); binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
// 观众列表按钮
binding.topViewerLayout.setOnClickListener(v -> showViewerListDialog());
// 关注按钮 // 关注按钮
binding.followButton.setOnClickListener(v -> { binding.followButton.setOnClickListener(v -> {
// 检查登录状态关注主播需要登录 // 检查登录状态关注主播需要登录
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) { if (!AuthHelper.requireLogin(this, "关注/取消关注需要登录")) {
return; return;
} }
@ -208,8 +211,12 @@ public class RoomDetailActivity extends AppCompatActivity {
return; return;
} }
// 调用后端接口关注主播 // 根据当前关注状态决定是关注还是取消关注
followStreamerBackend(); if (room.getIsFollowing()) {
unfollowStreamerBackend();
} else {
followStreamerBackend();
}
}); });
// 分享按钮 // 分享按钮
@ -906,6 +913,9 @@ public class RoomDetailActivity extends AppCompatActivity {
binding.roomTitle.setText(title); binding.roomTitle.setText(title);
binding.streamerName.setText(streamer); binding.streamerName.setText(streamer);
// 更新关注按钮状态
updateFollowButtonState(r.getIsFollowing());
// 设置直播状态 // 设置直播状态
if (r.isLive()) { if (r.isLive()) {
binding.liveTag.setVisibility(View.VISIBLE); binding.liveTag.setVisibility(View.VISIBLE);
@ -942,6 +952,17 @@ public class RoomDetailActivity extends AppCompatActivity {
} }
} }
/**
* 更新关注按钮状态
*/
private void updateFollowButtonState(boolean isFollowing) {
if (isFollowing) {
binding.followButton.setText("已关注");
} else {
binding.followButton.setText("关注");
}
}
private void ensurePlayer(String url, String fallbackHlsUrl) { private void ensurePlayer(String url, String fallbackHlsUrl) {
if (TextUtils.isEmpty(url)) return; if (TextUtils.isEmpty(url)) return;
@ -1735,16 +1756,49 @@ public class RoomDetailActivity extends AppCompatActivity {
*/ */
private void followStreamerBackend() { private void followStreamerBackend() {
if (room == null) { if (room == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 检查主播ID是否存在
if (room.getStreamerId() == null) {
Toast.makeText(this, "主播信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 获取当前登录用户ID
String currentUserIdStr = AuthStore.getUserId(this);
android.util.Log.d("RoomDetail", "followStreamerBackend: currentUserIdStr = " + currentUserIdStr);
if (currentUserIdStr == null || currentUserIdStr.trim().isEmpty()) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
return;
}
// 验证用户ID格式
try {
int currentUserId = Integer.parseInt(currentUserIdStr.trim());
int streamerId = room.getStreamerId();
android.util.Log.d("RoomDetail", "followStreamerBackend: currentUserId = " + currentUserId + ", streamerId = " + streamerId);
// 不能关注自己
if (currentUserId == streamerId) {
Toast.makeText(this, "不能关注自己", Toast.LENGTH_SHORT).show();
return;
}
} catch (NumberFormatException e) {
android.util.Log.e("RoomDetail", "用户ID格式错误: currentUserIdStr = '" + currentUserIdStr + "'", e);
Toast.makeText(this, "用户ID格式错误请重新登录", Toast.LENGTH_SHORT).show();
return; return;
} }
ApiService apiService = ApiClient.getService(getApplicationContext()); ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>(); java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("streamerId", roomId); // 使用房间ID作为主播ID body.put("userId", room.getStreamerId()); // 使用主播的用户ID
body.put("action", "follow");
Call<ApiResponse<Map<String, Object>>> call = apiService.followStreamer(body); Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(body);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() { call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override @Override
@ -1752,10 +1806,26 @@ public class RoomDetailActivity extends AppCompatActivity {
Response<ApiResponse<Map<String, Object>>> response) { Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body(); ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200) { if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Toast.makeText(RoomDetailActivity.this, "已关注主播", Toast.LENGTH_SHORT).show(); Map<String, Object> data = apiResponse.getData();
binding.followButton.setText("已关注"); Boolean success = (Boolean) data.get("success");
binding.followButton.setEnabled(false); String message = (String) data.get("message");
if (Boolean.TRUE.equals(success)) {
Toast.makeText(RoomDetailActivity.this,
message != null ? message : "已关注主播",
Toast.LENGTH_SHORT).show();
// 更新UI和状态
binding.followButton.setText("已关注");
if (room != null) {
room.setIsFollowing(true);
}
} else {
Toast.makeText(RoomDetailActivity.this,
message != null ? message : "关注失败",
Toast.LENGTH_SHORT).show();
}
} else { } else {
Toast.makeText(RoomDetailActivity.this, Toast.makeText(RoomDetailActivity.this,
"关注失败: " + apiResponse.getMessage(), "关注失败: " + apiResponse.getMessage(),
@ -1777,6 +1847,164 @@ public class RoomDetailActivity extends AppCompatActivity {
}); });
} }
/**
* 取消关注主播
*/
private void unfollowStreamerBackend() {
if (room == null) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 检查主播ID是否存在
if (room.getStreamerId() == null) {
Toast.makeText(this, "主播信息不可用", Toast.LENGTH_SHORT).show();
return;
}
ApiService apiService = ApiClient.getService(getApplicationContext());
java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("userId", room.getStreamerId()); // 使用主播的用户ID
Call<ApiResponse<Map<String, Object>>> call = apiService.unfollowUser(body);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Map<String, Object> data = apiResponse.getData();
Boolean success = (Boolean) data.get("success");
String message = (String) data.get("message");
if (Boolean.TRUE.equals(success)) {
Toast.makeText(RoomDetailActivity.this,
message != null ? message : "已取消关注",
Toast.LENGTH_SHORT).show();
// 更新UI和状态
binding.followButton.setText("关注");
if (room != null) {
room.setIsFollowing(false);
}
} else {
Toast.makeText(RoomDetailActivity.this,
message != null ? message : "取消关注失败",
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"取消关注失败: " + apiResponse.getMessage(),
Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(RoomDetailActivity.this,
"取消关注失败",
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
Toast.makeText(RoomDetailActivity.this,
"网络错误: " + t.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* 显示观众列表对话框
*/
private void showViewerListDialog() {
if (room == null || TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
return;
}
// 创建对话框
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle("在线观众 (" + binding.topViewerCount.getText() + ")");
// 显示加载中
View loadingView = getLayoutInflater().inflate(android.R.layout.simple_list_item_1, null);
((android.widget.TextView) loadingView.findViewById(android.R.id.text1)).setText("加载中...");
builder.setView(loadingView);
androidx.appcompat.app.AlertDialog dialog = builder.create();
dialog.show();
// 调用后端接口获取观众列表
ApiService apiService = ApiClient.getService(getApplicationContext());
Call<ApiResponse<Map<String, Object>>> call = apiService.getRoomOnlineUsers(roomId);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (!isFinishing() && dialog.isShowing()) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Map<String, Object> data = apiResponse.getData();
// 获取观众列表
Object usersObj = data.get("users");
if (usersObj instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> users = (List<Map<String, Object>>) usersObj;
if (users.isEmpty()) {
// 没有观众
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
.setText("暂无观众");
} else {
// 创建观众列表视图
String[] userNames = new String[users.size()];
for (int i = 0; i < users.size(); i++) {
Map<String, Object> user = users.get(i);
String nickname = user.get("nickname") != null ?
user.get("nickname").toString() : "匿名用户";
userNames[i] = nickname;
}
runOnUiThread(() -> {
dialog.dismiss();
new MaterialAlertDialogBuilder(RoomDetailActivity.this)
.setTitle("在线观众 (" + users.size() + ")")
.setItems(userNames, null)
.setPositiveButton("关闭", null)
.show();
});
}
} else {
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
.setText("数据格式错误");
}
} else {
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
.setText("获取失败: " + apiResponse.getMessage());
}
} else {
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
.setText("获取失败");
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
if (!isFinishing() && dialog.isShowing()) {
((android.widget.TextView) loadingView.findViewById(android.R.id.text1))
.setText("网络错误: " + t.getMessage());
}
}
});
}
/** /**
* 开始直播 * 开始直播
* 接口: POST /api/front/live/room/{id}/start * 接口: POST /api/front/live/room/{id}/start

View File

@ -311,10 +311,10 @@ public interface ApiService {
@GET("api/front/follow/check/{userId}") @GET("api/front/follow/check/{userId}")
Call<ApiResponse<Map<String, Object>>> checkFollowStatus(@Path("userId") int userId); Call<ApiResponse<Map<String, Object>>> checkFollowStatus(@Path("userId") int userId);
@POST("api/front/follow") @POST("api/front/follow/follow")
Call<ApiResponse<Map<String, Object>>> followUser(@Body Map<String, Object> body); Call<ApiResponse<Map<String, Object>>> followUser(@Body Map<String, Object> body);
@POST("api/front/follow/cancel") @POST("api/front/follow/unfollow")
Call<ApiResponse<Map<String, Object>>> unfollowUser(@Body Map<String, Object> body); Call<ApiResponse<Map<String, Object>>> unfollowUser(@Body Map<String, Object> body);
// ==================== 作品接口 ==================== // ==================== 作品接口 ====================
@ -347,6 +347,31 @@ public interface ApiService {
@DELETE("api/front/works/{id}/collect") @DELETE("api/front/works/{id}/collect")
Call<ApiResponse<Boolean>> uncollectWork(@Path("id") long id); Call<ApiResponse<Boolean>> uncollectWork(@Path("id") long id);
/**
* 获取指定用户的作品列表
*/
@GET("api/front/works/user/{userId}")
Call<ApiResponse<PageResponse<WorksResponse>>> getUserWorks(
@Path("userId") int userId,
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取我点赞的作品列表
*/
@GET("api/front/works/my/liked")
Call<ApiResponse<PageResponse<WorksResponse>>> getMyLikedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取我收藏的作品列表
*/
@GET("api/front/works/my/collected")
Call<ApiResponse<PageResponse<WorksResponse>>> getMyCollectedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
// ==================== 搜索接口 ==================== // ==================== 搜索接口 ====================
@GET("api/front/live/public/rooms/search") @GET("api/front/live/public/rooms/search")

View File

@ -51,23 +51,55 @@ public final class AuthStore {
public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) { public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) {
if (context == null) return; if (context == null) return;
Log.d(TAG, "setUserInfo: userId=" + userId + ", nickname=" + nickname);
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) // 清理和验证 userId
.edit() String cleanUserId = null;
.putString(KEY_USER_ID, userId) if (userId != null) {
.putString(KEY_NICKNAME, nickname) cleanUserId = userId.trim();
.apply(); // 如果是 "null" 字符串或空字符串设置为 null
if (cleanUserId.isEmpty() || "null".equalsIgnoreCase(cleanUserId)) {
Log.w(TAG, "setUserInfo: invalid userId value: '" + userId + "', will not save");
cleanUserId = null;
}
}
Log.d(TAG, "setUserInfo: userId=" + cleanUserId + ", nickname=" + nickname);
android.content.SharedPreferences.Editor editor = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit();
if (cleanUserId != null) {
editor.putString(KEY_USER_ID, cleanUserId);
} else {
editor.remove(KEY_USER_ID);
}
if (nickname != null && !nickname.trim().isEmpty()) {
editor.putString(KEY_NICKNAME, nickname.trim());
} else {
editor.remove(KEY_NICKNAME);
}
editor.apply();
} }
@Nullable @Nullable
public static String getUserId(Context context) { public static String getUserId(Context context) {
if (context == null) return null; if (context == null) {
Log.w(TAG, "getUserId: context is null");
return null;
}
String userId = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) String userId = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_USER_ID, null); .getString(KEY_USER_ID, null);
// 确保空字符串也返回 null
if (userId != null && userId.trim().isEmpty()) { // 确保空字符串"null" 字符串也返回 null
userId = null; if (userId != null) {
userId = userId.trim();
if (userId.isEmpty() || "null".equalsIgnoreCase(userId)) {
Log.w(TAG, "getUserId: invalid userId value: '" + userId + "', returning null");
userId = null;
}
} }
Log.d(TAG, "getUserId: " + userId); Log.d(TAG, "getUserId: " + userId);
return userId; return userId;
} }

View File

@ -21,7 +21,20 @@ public class LoginResponse {
} }
public String getUid() { public String getUid() {
return uid != null ? String.valueOf(uid) : null; if (uid == null) {
return null;
}
// 如果 uid 是数字类型直接转换
if (uid instanceof Number) {
return String.valueOf(((Number) uid).intValue());
}
// 如果是字符串类型返回字符串
String uidStr = String.valueOf(uid);
// 避免返回 "null" 字符串
if ("null".equalsIgnoreCase(uidStr) || uidStr.trim().isEmpty()) {
return null;
}
return uidStr;
} }
public String getNikeName() { public String getNikeName() {

View File

@ -10,6 +10,18 @@ public class UserEditRequest {
@SerializedName("avatar") @SerializedName("avatar")
private String avatar; private String avatar;
@SerializedName("bio")
private String bio;
@SerializedName("birthday")
private String birthday;
@SerializedName("gender")
private String gender;
@SerializedName("location")
private String location;
public UserEditRequest() {} public UserEditRequest() {}
public UserEditRequest(String nickname, String avatar) { public UserEditRequest(String nickname, String avatar) {
@ -19,6 +31,15 @@ public class UserEditRequest {
public void setNickname(String nickname) { this.nickname = nickname; } public void setNickname(String nickname) { this.nickname = nickname; }
public void setAvatar(String avatar) { this.avatar = avatar; } public void setAvatar(String avatar) { this.avatar = avatar; }
public void setBio(String bio) { this.bio = bio; }
public void setBirthday(String birthday) { this.birthday = birthday; }
public void setGender(String gender) { this.gender = gender; }
public void setLocation(String location) { this.location = location; }
public String getNickname() { return nickname; } public String getNickname() { return nickname; }
public String getAvatar() { return avatar; } public String getAvatar() { return avatar; }
public String getBio() { return bio; }
public String getBirthday() { return birthday; }
public String getGender() { return gender; }
public String getLocation() { return location; }
} }

View File

@ -53,6 +53,21 @@ public class UserInfoResponse {
@SerializedName("collectCount") @SerializedName("collectCount")
private Integer collectCount; private Integer collectCount;
@SerializedName("bio")
private String bio;
@SerializedName("birthday")
private String birthday;
@SerializedName("sex")
private Integer sex;
@SerializedName("addres")
private String addres;
@SerializedName("realName")
private String realName;
public Integer getUid() { return uid; } public Integer getUid() { return uid; }
public String getNickname() { return nickname; } public String getNickname() { return nickname; }
public String getAvatar() { return avatar; } public String getAvatar() { return avatar; }
@ -69,4 +84,22 @@ public class UserInfoResponse {
public String getVipName() { return vipName; } public String getVipName() { return vipName; }
public Boolean getRechargeSwitch() { return rechargeSwitch; } public Boolean getRechargeSwitch() { return rechargeSwitch; }
public Integer getCollectCount() { return collectCount; } public Integer getCollectCount() { return collectCount; }
public String getBio() { return bio; }
public String getBirthday() { return birthday; }
public Integer getSex() { return sex; }
public String getAddres() { return addres; }
public String getRealName() { return realName; }
/**
* 将性别数字转换为文字
* @return //保密
*/
public String getGenderText() {
if (sex == null) return "保密";
switch (sex) {
case 1: return "";
case 2: return "";
default: return "保密";
}
}
} }

View File

@ -23,6 +23,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/typeLayout" android:id="@+id/typeLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
@ -35,7 +36,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="选择直播类型" android:hint="选择直播类型"
android:inputType="none" /> android:inputType="none"
android:editable="false" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,7 +3,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:padding="24dp"
android:background="@android:color/white">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -12,13 +13,61 @@
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:gravity="center" android:gravity="center"
android:textColor="@android:color/black"
android:layout_marginBottom="16dp" /> android:layout_marginBottom="16dp" />
<EditText <EditText
android:id="@+id/editWish" android:id="@+id/editWish"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="120dp"
android:hint="写下你的愿望..." android:hint="写下你的愿望..."
android:minLines="3" android:maxLength="50"
android:gravity="top" /> android:gravity="top|start"
android:padding="12dp"
android:background="@android:drawable/edit_text"
android:textSize="14sp" />
<TextView
android:id="@+id/tvCharCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0/50"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="取消"
android:gravity="center"
android:textSize="16sp"
android:textColor="@android:color/darker_gray"
android:background="?attr/selectableItemBackground"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/btnMakeWish"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="许愿"
android:gravity="center"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#FF6B9D"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@android:color/white">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的愿望"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_centerInParent="true" />
<ImageView
android:id="@+id/btnClose"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless" />
</RelativeLayout>
<TextView
android:id="@+id/tvWishContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:padding="12dp"
android:textSize="14sp"
android:textColor="@android:color/black"
android:background="@android:drawable/edit_text"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:id="@+id/btnDeleteWish"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="删除"
android:gravity="center"
android:textSize="16sp"
android:textColor="@android:color/holo_red_dark"
android:background="?attr/selectableItemBackground"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/btnCompleteWish"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_weight="1"
android:text="完成"
android:gravity="center"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="#FF6B9D"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>