diff --git a/0-待完成接入接口.md b/0-待完成接入接口.md deleted file mode 100644 index b14c635c..00000000 --- a/0-待完成接入接口.md +++ /dev/null @@ -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个接口 -- 核心功能已基本完成,待接入的主要是群组和辅助功能 diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java index a9e83578..870eb816 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java @@ -73,6 +73,9 @@ public class User implements Serializable { @ApiModelProperty(value = "用户头像") private String avatar; + @ApiModelProperty(value = "个人签名") + private String bio; + @ApiModelProperty(value = "手机号码") private String phone; diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserBatchOperationRequest.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserBatchOperationRequest.java new file mode 100644 index 00000000..3df46539 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserBatchOperationRequest.java @@ -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 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; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserCreateRequest.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserCreateRequest.java new file mode 100644 index 00000000..72c58715 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserCreateRequest.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserEditRequest.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserEditRequest.java index bcd4514c..4ae3c7e4 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserEditRequest.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserEditRequest.java @@ -36,7 +36,21 @@ public class UserEditRequest implements Serializable { private String nickname; @ApiModelProperty(value = "用户头像") - @NotBlank(message = "请上传用户头像") @Length(max = 255, message = "用户头像不能超过255个字符") 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; } diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserQueryRequest.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserQueryRequest.java new file mode 100644 index 00000000..834cd61f --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/UserQueryRequest.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserCenterResponse.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserCenterResponse.java index d1ac6069..2e6f18d2 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserCenterResponse.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserCenterResponse.java @@ -88,4 +88,19 @@ public class UserCenterResponse implements Serializable { @ApiModelProperty(value = "用户收藏数量") 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; } diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserDetailResponse.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserDetailResponse.java new file mode 100644 index 00000000..19507a79 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserDetailResponse.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserStatisticsResponse.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserStatisticsResponse.java new file mode 100644 index 00000000..d5ac2630 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/UserStatisticsResponse.java @@ -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; +} diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserController.java index 661f2cd2..61ae2886 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/UserController.java @@ -68,10 +68,27 @@ public class UserController { @ApiOperation(value = "修改个人资料") @RequestMapping(value = "/user/edit", method = RequestMethod.POST) public CommonResult personInfo(@RequestBody @Validated UserEditRequest request) { - if (userService.editUser(request)) { - return CommonResult.success(); + log.info("========== 收到修改个人资料请求 =========="); + 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 = "个人中心-用户信息") @RequestMapping(value = "/user", method = RequestMethod.GET) public CommonResult 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); } /** diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserServiceImpl.java index 8dd901e8..46cef4e8 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserServiceImpl.java @@ -30,6 +30,7 @@ import com.zbkj.common.utils.*; import com.zbkj.common.vo.DateLimitUtilVo; import com.zbkj.service.dao.UserDao; import com.zbkj.service.service.*; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -56,6 +57,7 @@ import java.util.stream.Collectors; * | Author: CRMEB Team * +---------------------------------------------------------------------- */ +@Slf4j @Service public class UserServiceImpl extends ServiceImpl implements UserService { @@ -556,8 +558,29 @@ public class UserServiceImpl extends ServiceImpl implements UserS if (ObjectUtil.isNull(currentUser)) { 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(); 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 userCenterResponse.setUid(currentUser.getUid()); // 优惠券数量 @@ -593,6 +616,7 @@ public class UserServiceImpl extends ServiceImpl implements UserS visitRecord.setVisitType(4); userVisitRecordService.save(visitRecord); + log.info("========== getUserCenter 结束 =========="); return userCenterResponse; } @@ -1710,11 +1734,67 @@ public class UserServiceImpl extends ServiceImpl implements UserS */ @Override public Boolean editUser(UserEditRequest request) { + log.info("========== UserService.editUser 开始执行 =========="); + User user = getInfo(); + log.info("当前用户 uid: {}, nickname: {}", user.getUid(), user.getNickname()); + log.info("原始用户数据: {}", user); + user.setAvatar(systemAttachmentService.clearPrefix(request.getAvatar())); 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()); - 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; + } } /** diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/resources/mapper/FollowRecordDao.xml b/Zhibo/zhibo-h/crmeb-service/src/main/resources/mapper/FollowRecordDao.xml index 0b5881f9..461a1b78 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/resources/mapper/FollowRecordDao.xml +++ b/Zhibo/zhibo-h/crmeb-service/src/main/resources/mapper/FollowRecordDao.xml @@ -71,10 +71,10 @@ diff --git a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java index b7ba206e..a8ba9889 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java @@ -28,6 +28,9 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; 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 android.widget.NumberPicker; @@ -106,7 +109,9 @@ public class EditProfileActivity extends AppCompatActivity { binding.backButton.setOnClickListener(v -> finish()); binding.cancelButton.setOnClickListener(v -> finish()); - loadFromPrefs(); + // 先从服务器加载用户信息,如果失败则从本地加载 + loadUserInfoFromServer(); + setupGenderDropdown(); setupLocationDropdown(); setupBirthdayPicker(); @@ -143,24 +148,6 @@ public class EditProfileActivity extends AppCompatActivity { }); 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 - // 更新成功后,同步更新本地缓存和界面显示 String name = binding.inputName.getText() != null ? binding.inputName.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() : ""; @@ -172,6 +159,7 @@ public class EditProfileActivity extends AppCompatActivity { return; } + // 先保存到本地SharedPreferences getSharedPreferences(PREFS_NAME, MODE_PRIVATE) .edit() .putString(KEY_NAME, name) @@ -183,6 +171,8 @@ public class EditProfileActivity extends AppCompatActivity { getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_BIO, bio).apply(); } + // 处理头像 + String avatarUrl = null; if (selectedAvatarUri != null) { Uri persisted = persistAvatarToInternalStorage(selectedAvatarUri); if (persisted != null) { @@ -191,15 +181,17 @@ public class EditProfileActivity extends AppCompatActivity { .putString(KEY_AVATAR_URI, persisted.toString()) .remove(KEY_AVATAR_RES) .apply(); + avatarUrl = persisted.toString(); } + } else { + // 使用现有头像 + avatarUrl = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, ""); } - // 生日已经在日期选择器中实时保存,这里只需要确保同步 - // 如果输入框为空,则清除 + // 保存其他字段到本地 if (TextUtils.isEmpty(birthday)) { getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().remove(KEY_BIRTHDAY).apply(); } else { - // 确保使用输入框中的最新值 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(); } - // 设置结果,通知ProfileActivity需要刷新 - setResult(RESULT_OK); - finish(); + // 调用后端接口保存到数据库 + saveProfileToServer(name, avatarUrl, bio, birthday, gender, location); }); } @@ -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>() { + @Override + public void onResponse(retrofit2.Call> call, + retrofit2.Response> 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> call, Throwable t) { + android.util.Log.e("EditProfile", "获取用户信息失败", t); + android.util.Log.e("EditProfile", "错误消息: " + t.getMessage()); + // 如果网络请求失败,从本地加载 + loadFromPrefs(); + } + }); + } + private void showAvatarBottomSheet() { BottomSheetDialog dialog = new BottomSheetDialog(this); 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开始 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>() { + @Override + public void onResponse(retrofit2.Call> call, + retrofit2.Response> 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> 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>() { + @Override + public void onResponse(retrofit2.Call> call, + retrofit2.Response> 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 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> 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(); + } + }); + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 1920f54d..83ab937b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -1576,12 +1576,6 @@ public class MainActivity extends AppCompatActivity { Log.d(TAG, "loadLiveTypesForDialog() 开始从后端加载直播类型"); - // 设置下拉框为只读,不允许手动输入,只能从列表中选择 - typeSpinner.setKeyListener(null); - typeSpinner.setFocusable(false); - typeSpinner.setClickable(true); - typeSpinner.setFocusableInTouchMode(false); - // 调用后端接口获取直播类型列表 ApiClient.getService(getApplicationContext()).getLiveTypes() .enqueue(new Callback>>() { @@ -1611,11 +1605,12 @@ public class MainActivity extends AppCompatActivity { typeNames[i] = types.get(i).getName(); } + // 使用 simple_list_item_1 而不是 simple_dropdown_item_1line ArrayAdapter adapter = new ArrayAdapter<>(MainActivity.this, - android.R.layout.simple_dropdown_item_1line, typeNames); + android.R.layout.simple_list_item_1, typeNames); typeSpinner.setAdapter(adapter); - // 设置默认选中第一项 + // 设置默认选中第一项,使用 false 参数避免触发过滤 if (typeNames.length > 0) { typeSpinner.setText(typeNames[0], false); Log.d(TAG, "loadLiveTypesForDialog() 默认选中: " + typeNames[0]); @@ -1648,7 +1643,7 @@ public class MainActivity extends AppCompatActivity { private void useDefaultLiveTypes(MaterialAutoCompleteTextView typeSpinner) { String[] defaultTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"}; ArrayAdapter adapter = new ArrayAdapter<>(MainActivity.this, - android.R.layout.simple_dropdown_item_1line, defaultTypes); + android.R.layout.simple_list_item_1, defaultTypes); typeSpinner.setAdapter(adapter); // 设置默认选中第一项 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 cd98c15b..b43e295d 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 @@ -24,6 +24,7 @@ import com.bumptech.glide.Glide; import com.example.livestreaming.BuildConfig; import com.example.livestreaming.databinding.ActivityProfileBinding; import com.example.livestreaming.ShareUtils; +import com.example.livestreaming.net.AuthStore; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -78,17 +79,15 @@ public class ProfileActivity extends AppCompatActivity { editProfileLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { - // 当从EditProfileActivity返回时,立即刷新所有数据 - loadProfileFromPrefs(); - loadAndDisplayTags(); - loadProfileInfo(); + // 当从EditProfileActivity返回时,从服务器重新加载数据 + loadUserInfoFromServer(); + loadFollowStats(); } ); - loadProfileFromPrefs(); - loadAndDisplayTags(); - loadProfileInfo(); - loadFollowStats(); // 加载关注统计 + // 首次加载从服务器获取 + loadUserInfoFromServer(); + loadFollowStats(); setupEditableAreas(); setupAvatarClick(); setupNavigationClicks(); @@ -128,14 +127,7 @@ public class ProfileActivity extends AppCompatActivity { } private void loadProfileFromPrefs() { - // TODO: 接入后端接口 - 获取用户资料 - // 接口路径: GET /api/users/{userId}/profile - // 请求参数: - // - userId: 用户ID(路径参数,当前用户从token中获取) - // 返回数据格式: ApiResponse - // UserProfile对象应包含: id, name, avatarUrl, bio, level, badge, birthday, gender, location, - // followingCount, fansCount, likesCount等字段 - // 首次加载时从接口获取,后续可从本地缓存读取 + // 从本地缓存加载用户资料(已通过 loadUserInfoFromServer 从服务器同步) String n = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, null); if (!TextUtils.isEmpty(n)) binding.name.setText(n); @@ -189,18 +181,8 @@ public class ProfileActivity extends AppCompatActivity { } private void setupEditableAreas() { - // TODO: 接入后端接口 - 更新用户资料 - // 接口路径: PUT /api/users/{userId}/profile - // 请求参数: - // - userId: 用户ID(路径参数,从token中获取) - // - name (可选): 昵称 - // - bio (可选): 个人签名 - // - avatarUrl (可选): 头像URL - // - birthday (可选): 生日 - // - gender (可选): 性别 - // - location (可选): 所在地 - // 返回数据格式: ApiResponse - // 更新成功后,同步更新本地缓存和界面显示 + // 注意:用户资料的完整编辑功能在 EditProfileActivity 中实现 + // 这里保留简单的昵称和签名编辑功能(仅本地保存) binding.name.setOnClickListener(v -> showEditDialog("编辑昵称", binding.name.getText() != null ? binding.name.getText().toString() : "", text -> { binding.name.setText(text); getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_NAME, text).apply(); @@ -273,12 +255,7 @@ public class ProfileActivity extends AppCompatActivity { } }); - // TODO: 接入后端接口 - 获取关注/粉丝/获赞数量 - // 接口路径: GET /api/users/{userId}/stats - // 请求参数: - // - userId: 用户ID(路径参数) - // 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}> - // 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示 + // 关注/粉丝/获赞统计已通过 loadFollowStats() 方法从后端接口获取 binding.following.setOnClickListener(v -> { // 检查登录状态,查看关注列表需要登录 if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) { @@ -356,18 +333,7 @@ public class ProfileActivity extends AppCompatActivity { } }); - // TODO: 接入后端接口 - 发布作品 - // 接口路径: POST /api/works - // 请求参数: - // - userId: 用户ID(从token中获取) - // - title: 作品标题 - // - description: 作品描述(可选) - // - coverUrl: 封面图片URL(必填,需要先上传图片) - // - videoUrl (可选): 视频URL(如果是视频作品) - // - images (可选): 图片URL列表(如果是图片作品) - // 返回数据格式: ApiResponse - // WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段 - // 发布成功后,刷新作品列表显示 + // 作品发布功能在 PublishWorkActivity 中实现 // 空状态的发布按钮 binding.worksPublishBtn.setOnClickListener(v -> { // 检查登录状态,发布作品需要登录 @@ -410,24 +376,84 @@ public class ProfileActivity extends AppCompatActivity { } private void loadWorks() { - // TODO: 接入后端接口 - 获取当前用户的作品列表 - // 接口路径: GET /api/users/{userId}/works - // 请求方法: GET - // 请求头: Authorization: Bearer {token} (必填,从AuthStore获取) - // 路径参数: - // - userId: String (必填) - 当前用户ID(从token中解析获取) - // 请求参数(Query): - // - page: int (可选,默认1) - 页码 - // - pageSize: int (可选,默认20) - 每页数量 - // 返回数据格式: ApiResponse> - // 实现步骤: - // 1. 从AuthStore获取token,解析userId(或从用户信息中获取) - // 2. 调用接口获取作品列表 - // 3. 更新UI显示作品列表或空状态 - // 4. 处理错误情况(网络错误、未登录等) - // 注意: 此方法在onResume时也会调用,需要避免重复请求 + // 检查登录状态 + String userIdStr = AuthStore.getUserId(this); + if (userIdStr == null || userIdStr.trim().isEmpty()) { + // 未登录,显示空状态 + binding.worksRecycler.setVisibility(View.GONE); + binding.worksEmptyState.setVisibility(View.VISIBLE); + return; + } - // 临时:从本地存储加载(等待后端接口) + try { + int userId = Integer.parseInt(userIdStr.trim()); + + // 从后端接口获取作品列表 + com.example.livestreaming.net.ApiService apiService = + com.example.livestreaming.net.ApiClient.getService(this); + retrofit2.Call>> call = + apiService.getUserWorks(userId, 1, 20); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(retrofit2.Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + com.example.livestreaming.net.PageResponse pageResponse = response.body().getData(); + if (pageResponse != null && pageResponse.getList() != null && !pageResponse.getList().isEmpty()) { + // 转换为 WorkItem 列表 + List 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>> 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 works = WorkManager.getAllWorks(this); if (works != null && !works.isEmpty()) { binding.worksRecycler.setVisibility(View.VISIBLE); @@ -440,38 +466,96 @@ public class ProfileActivity extends AppCompatActivity { } private void showTab(int index) { - // TODO: 接入后端接口 - 获取用户作品列表 - // 接口路径: GET /api/users/{userId}/works - // 请求参数: - // - userId: 用户ID(从token中获取) - // - page (可选): 页码 - // - pageSize (可选): 每页数量 - // 返回数据格式: ApiResponse> - // WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段 - // TODO: 接入后端接口 - 获取用户收藏列表 - // 接口路径: GET /api/users/{userId}/favorites - // 请求参数: - // - userId: 用户ID(从token中获取) - // - page (可选): 页码 - // - pageSize (可选): 每页数量 - // 返回数据格式: ApiResponse> - // TODO: 接入后端接口 - 获取用户赞过的作品列表 - // 接口路径: GET /api/users/{userId}/liked - // 请求参数: - // - userId: 用户ID(从token中获取) - // - page (可选): 页码 - // - pageSize (可选): 每页数量 - // 返回数据格式: ApiResponse> // 标签页顺序:0-作品, 1-收藏, 2-赞过 binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE); binding.tabFavorites.setVisibility(index == 1 ? View.VISIBLE : View.GONE); binding.tabLiked.setVisibility(index == 2 ? View.VISIBLE : View.GONE); - // 当切换到作品标签页时,重新加载作品列表 + // 根据选中的标签页加载对应的数据 if (index == 0) { + // 作品标签页 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>> call = + apiService.getMyCollectedWorks(1, 20); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(retrofit2.Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + com.example.livestreaming.net.PageResponse 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>> 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>> call = + apiService.getMyLikedWorks(1, 20); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(retrofit2.Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + com.example.livestreaming.net.PageResponse 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>> call, Throwable t) { + android.util.Log.e("Profile", "加载点赞列表失败", t); + } + }); } private interface OnTextSaved { @@ -505,9 +589,7 @@ public class ProfileActivity extends AppCompatActivity { protected void onResume() { super.onResume(); if (binding != null) { - loadProfileFromPrefs(); - loadAndDisplayTags(); - loadProfileInfo(); + loadUserInfoFromServer(); // 从服务器加载最新数据 loadFollowStats(); // 刷新关注统计 loadWorks(); // 重新加载作品列表 BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation; @@ -674,6 +756,92 @@ public class ProfileActivity extends AppCompatActivity { ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧"); } + /** + * 从服务器加载用户信息 + */ + private void loadUserInfoFromServer() { + android.util.Log.d("Profile", "开始从服务器加载用户信息"); + + com.example.livestreaming.net.ApiClient.getService(getApplicationContext()).getUserInfo() + .enqueue(new retrofit2.Callback>() { + @Override + public void onResponse(retrofit2.Call> call, + retrofit2.Response> 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> 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"); 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"); 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粉丝"); } } } 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 eb0afe5e..4527b062 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 @@ -196,10 +196,13 @@ public class RoomDetailActivity extends AppCompatActivity { // 全屏按钮 binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen()); + // 观众列表按钮 + binding.topViewerLayout.setOnClickListener(v -> showViewerListDialog()); + // 关注按钮 binding.followButton.setOnClickListener(v -> { // 检查登录状态,关注主播需要登录 - if (!AuthHelper.requireLogin(this, "关注主播需要登录")) { + if (!AuthHelper.requireLogin(this, "关注/取消关注需要登录")) { return; } @@ -208,8 +211,12 @@ public class RoomDetailActivity extends AppCompatActivity { return; } - // 调用后端接口关注主播 - followStreamerBackend(); + // 根据当前关注状态决定是关注还是取消关注 + if (room.getIsFollowing()) { + unfollowStreamerBackend(); + } else { + followStreamerBackend(); + } }); // 分享按钮 @@ -906,6 +913,9 @@ public class RoomDetailActivity extends AppCompatActivity { binding.roomTitle.setText(title); binding.streamerName.setText(streamer); + // 更新关注按钮状态 + updateFollowButtonState(r.getIsFollowing()); + // 设置直播状态 if (r.isLive()) { 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) { if (TextUtils.isEmpty(url)) return; @@ -1735,16 +1756,49 @@ public class RoomDetailActivity extends AppCompatActivity { */ private void followStreamerBackend() { 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; } ApiService apiService = ApiClient.getService(getApplicationContext()); java.util.Map body = new java.util.HashMap<>(); - body.put("streamerId", roomId); // 使用房间ID作为主播ID - body.put("action", "follow"); + body.put("userId", room.getStreamerId()); // 使用主播的用户ID - Call>> call = apiService.followStreamer(body); + Call>> call = apiService.followUser(body); call.enqueue(new Callback>>() { @Override @@ -1752,10 +1806,26 @@ public class RoomDetailActivity extends AppCompatActivity { Response>> response) { if (response.isSuccessful() && response.body() != null) { ApiResponse> apiResponse = response.body(); - if (apiResponse.getCode() == 200) { - Toast.makeText(RoomDetailActivity.this, "已关注主播", Toast.LENGTH_SHORT).show(); - binding.followButton.setText("已关注"); - binding.followButton.setEnabled(false); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map 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(true); + } + } else { + Toast.makeText(RoomDetailActivity.this, + message != null ? message : "关注失败", + Toast.LENGTH_SHORT).show(); + } } else { Toast.makeText(RoomDetailActivity.this, "关注失败: " + 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 body = new java.util.HashMap<>(); + body.put("userId", room.getStreamerId()); // 使用主播的用户ID + + Call>> call = apiService.unfollowUser(body); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map 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>> 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>> call = apiService.getRoomOnlineUsers(roomId); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (!isFinishing() && dialog.isShowing()) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map data = apiResponse.getData(); + + // 获取观众列表 + Object usersObj = data.get("users"); + if (usersObj instanceof List) { + @SuppressWarnings("unchecked") + List> users = (List>) 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 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>> 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 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 a0cdefb8..8f0b13cd 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 @@ -311,10 +311,10 @@ public interface ApiService { @GET("api/front/follow/check/{userId}") Call>> checkFollowStatus(@Path("userId") int userId); - @POST("api/front/follow") + @POST("api/front/follow/follow") Call>> followUser(@Body Map body); - @POST("api/front/follow/cancel") + @POST("api/front/follow/unfollow") Call>> unfollowUser(@Body Map body); // ==================== 作品接口 ==================== @@ -347,6 +347,31 @@ public interface ApiService { @DELETE("api/front/works/{id}/collect") Call> uncollectWork(@Path("id") long id); + /** + * 获取指定用户的作品列表 + */ + @GET("api/front/works/user/{userId}") + Call>> getUserWorks( + @Path("userId") int userId, + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取我点赞的作品列表 + */ + @GET("api/front/works/my/liked") + Call>> getMyLikedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取我收藏的作品列表 + */ + @GET("api/front/works/my/collected") + Call>> getMyCollectedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + // ==================== 搜索接口 ==================== @GET("api/front/live/public/rooms/search") diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java b/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java index cbc551d0..9ebde64a 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/AuthStore.java @@ -51,23 +51,55 @@ public final class AuthStore { public static void setUserInfo(Context context, @Nullable String userId, @Nullable String nickname) { if (context == null) return; - Log.d(TAG, "setUserInfo: userId=" + userId + ", nickname=" + nickname); - context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .edit() - .putString(KEY_USER_ID, userId) - .putString(KEY_NICKNAME, nickname) - .apply(); + + // 清理和验证 userId + String cleanUserId = null; + if (userId != null) { + cleanUserId = userId.trim(); + // 如果是 "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 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) .getString(KEY_USER_ID, null); - // 确保空字符串也返回 null - if (userId != null && userId.trim().isEmpty()) { - userId = null; + + // 确保空字符串、"null" 字符串也返回 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); return userId; } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java b/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java index 1165d2d5..c431f093 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/LoginResponse.java @@ -21,7 +21,20 @@ public class LoginResponse { } 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() { diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/UserEditRequest.java b/android-app/app/src/main/java/com/example/livestreaming/net/UserEditRequest.java index 4a4d08c9..cfb27fe6 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/UserEditRequest.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/UserEditRequest.java @@ -10,6 +10,18 @@ public class UserEditRequest { @SerializedName("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(String nickname, String avatar) { @@ -19,6 +31,15 @@ public class UserEditRequest { public void setNickname(String nickname) { this.nickname = nickname; } 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 getAvatar() { return avatar; } + public String getBio() { return bio; } + public String getBirthday() { return birthday; } + public String getGender() { return gender; } + public String getLocation() { return location; } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/UserInfoResponse.java b/android-app/app/src/main/java/com/example/livestreaming/net/UserInfoResponse.java index f62752e8..98728ba7 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/UserInfoResponse.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/UserInfoResponse.java @@ -53,6 +53,21 @@ public class UserInfoResponse { @SerializedName("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 String getNickname() { return nickname; } public String getAvatar() { return avatar; } @@ -69,4 +84,22 @@ public class UserInfoResponse { public String getVipName() { return vipName; } public Boolean getRechargeSwitch() { return rechargeSwitch; } 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 "保密"; + } + } } diff --git a/android-app/app/src/main/res/layout/dialog_create_room.xml b/android-app/app/src/main/res/layout/dialog_create_room.xml index c2a4a921..2e14fd3a 100644 --- a/android-app/app/src/main/res/layout/dialog_create_room.xml +++ b/android-app/app/src/main/res/layout/dialog_create_room.xml @@ -23,6 +23,7 @@ + android:inputType="none" + android:editable="false" /> diff --git a/android-app/app/src/main/res/layout/dialog_make_wish.xml b/android-app/app/src/main/res/layout/dialog_make_wish.xml index da09d37d..a0d013db 100644 --- a/android-app/app/src/main/res/layout/dialog_make_wish.xml +++ b/android-app/app/src/main/res/layout/dialog_make_wish.xml @@ -3,7 +3,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="16dp"> + android:padding="24dp" + android:background="@android:color/white"> + android:maxLength="50" + android:gravity="top|start" + android:padding="12dp" + android:background="@android:drawable/edit_text" + android:textSize="14sp" /> + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/dialog_view_wish.xml b/android-app/app/src/main/res/layout/dialog_view_wish.xml new file mode 100644 index 00000000..00ad37d7 --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_view_wish.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + +