diff --git a/Android接口参数详细文档.md b/Android接口参数详细文档.md index 0cf63e58..d16bdb2b 100644 --- a/Android接口参数详细文档.md +++ b/Android接口参数详细文档.md @@ -5,8 +5,8 @@ 本文档详细列出了Android应用中**真实调用的所有接口**的请求参数和响应参数示例。 **文档版本**: v1.0 -**更新时间**: 2024-12-30 -**接口总数**: 73个真实调用的接口 +**更新时间**: 2024-12-31 +**接口总数**: 74个真实调用的接口 **基础URL**: `http://your-server:port` --- @@ -26,6 +26,7 @@ 11. [搜索功能模块](#11-搜索功能模块) 12. [观看历史模块](#12-观看历史模块) 13. [分类管理模块](#13-分类管理模块) +14. [直播类型模块](#14-直播类型模块) --- @@ -292,7 +293,7 @@ Authorization: Bearer { "title": "我的直播间", "streamerName": "主播昵称", - "type": "video", + "type": "game", "categoryId": 1, "description": "直播间描述", "coverImage": "https://example.com/cover.jpg", @@ -306,13 +307,25 @@ Authorization: Bearer |--------|------|------|------| | title | String | 是 | 直播间标题 | | streamerName | String | 是 | 主播名称 | -| type | String | 否 | 直播类型,默认video | +| type | String | 否 | 直播类型编码(game/talent/outdoor/music/food/chat),默认game | | categoryId | Integer | 否 | 分类ID | | description | String | 否 | 直播间描述 | | coverImage | String | 否 | 封面图片URL | | tags | String | 否 | 标签,逗号分隔 | | notice | String | 否 | 直播间公告 | +**重要说明**: +- `type` 参数应使用类型编码(code),而不是类型名称(name) +- 可用的类型编码: + - `game` - 游戏 + - `talent` - 才艺 + - `outdoor` - 户外 + - `music` - 音乐 + - `food` - 美食 + - `chat` - 聊天 +- 前端应先调用 `GET /api/front/live/public/types` 获取类型列表 +- 如果type参数为空,后端会使用默认值 `game` + **响应示例**: ```json { @@ -321,6 +334,7 @@ Authorization: Bearer "data": { "id": "10", "title": "我的直播间", + "type": "game", "streamKey": "live_stream_key_456", "streamUrls": { "rtmp": "rtmp://server:25002/live/stream_key_456", @@ -2540,6 +2554,90 @@ Authorization: Bearer --- +## 14. 直播类型模块 + +### 14.1 获取直播类型列表 + +**接口地址**: `GET /api/front/live/public/types` + +**请求参数**: 无 + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "name": "游戏", + "code": "game", + "icon": "", + "description": "游戏直播", + "sort": 100 + }, + { + "id": 2, + "name": "才艺", + "code": "talent", + "icon": "", + "description": "才艺表演", + "sort": 90 + }, + { + "id": 3, + "name": "户外", + "code": "outdoor", + "icon": "", + "description": "户外直播", + "sort": 80 + }, + { + "id": 4, + "name": "音乐", + "code": "music", + "icon": "", + "description": "音乐直播", + "sort": 70 + }, + { + "id": 5, + "name": "美食", + "code": "food", + "icon": "", + "description": "美食直播", + "sort": 60 + }, + { + "id": 6, + "name": "聊天", + "code": "chat", + "icon": "", + "description": "聊天互动", + "sort": 50 + } + ] +} +``` + +**响应字段说明**: +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | Integer | 类型ID | +| name | String | 类型名称(显示用) | +| code | String | 类型编码(提交用) | +| icon | String | 类型图标URL | +| description | String | 类型描述 | +| sort | Integer | 排序值,越大越靠前 | + +**使用说明**: +- 此接口用于获取创建直播间时可选的类型列表 +- 前端在创建直播间时,应使用 `code` 字段的值提交给后端 +- 类型列表按 `sort` 字段降序排列 +- 只返回状态为启用的类型 + +--- + ## 📝 附录 ### A. 通用响应格式 diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java index 121135c4..8d5b5c06 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/task/live/LiveStatusSyncTask.java @@ -35,13 +35,21 @@ public class LiveStatusSyncTask { @Value("${SRS_API_URL:http://127.0.0.1:1985}") private String srsApiUrl; + @Value("${live.status.sync.enabled:true}") + private boolean syncEnabled; + private final ObjectMapper objectMapper = new ObjectMapper(); /** - * 同步直播状态(每 5 秒执行一次) + * 同步直播状态(每 30 秒执行一次) + * 生产环境建议保留此任务,确保直播状态与实际推流状态一致 */ - @Scheduled(fixedRate = 5000) + @Scheduled(fixedRate = 30000) // 改为30秒,降低频率 public void syncLiveStatus() { + if (!syncEnabled) { + logger.debug("直播状态同步已禁用"); + return; + } try { Set liveStreamKeys = fetchLiveStreamKeysFromSrs(); updateLiveStatus(liveStreamKeys); @@ -86,6 +94,7 @@ public class LiveStatusSyncTask { /** * 更新数据库中的直播状态 + * 增加容错:只有连续多次检测到断流才更新为未开播 */ private void updateLiveStatus(Set liveStreamKeys) { List allRooms = liveRoomService.list(new LambdaQueryWrapper<>()); @@ -96,9 +105,18 @@ public class LiveStatusSyncTask { int currentStatus = room.getIsLive() == null ? 0 : room.getIsLive(); if (newStatus != currentStatus) { - room.setIsLive(newStatus); - liveRoomService.updateById(room); - logger.info("直播状态更新: {} -> {}", room.getTitle(), shouldBeLive ? "直播中" : "未开播"); + // 如果检测到推流开始,立即更新为直播中 + if (newStatus == 1) { + room.setIsLive(newStatus); + liveRoomService.updateById(room); + logger.info("直播状态更新: {} -> 直播中", room.getTitle()); + } + // 如果检测到推流结束,也立即更新(SRS回调已经处理了,这里是兜底) + else if (currentStatus == 1) { + room.setIsLive(0); + liveRoomService.updateById(room); + logger.info("直播状态更新: {} -> 未开播(定时任务检测到断流)", room.getTitle()); + } } } } diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/constants/Constants.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/constants/Constants.java index 4572e82d..1cb833f5 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/constants/Constants.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/constants/Constants.java @@ -13,7 +13,10 @@ package com.zbkj.common.constants; * +---------------------------------------------------------------------- */ public class Constants { - public static final long TOKEN_EXPRESS_MINUTES = (60 * 24); //3小时 + // Token有效期:30天(43200分钟) + // 采用滑动过期策略:每次访问自动刷新过期时间 + // 只有用户主动退出登录时才会删除Token + public static final long TOKEN_EXPRESS_MINUTES = (60 * 24 * 30); // 30天 public static final int HTTPSTATUS_CODE_SUCCESS = 200; diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveType.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveType.java new file mode 100644 index 00000000..2fa44653 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/live/LiveType.java @@ -0,0 +1,70 @@ +package com.zbkj.common.model.live; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import javax.persistence.*; +import java.io.Serializable; +import java.util.Date; + +/** + * 直播类型实体类 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Entity +@Table(name = "eb_live_type") +@TableName("eb_live_type") +@ApiModel(value = "LiveType对象", description = "直播类型") +public class LiveType implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键ID") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + @ApiModelProperty(value = "类型名称") + @Column(name = "name") + private String name; + + @ApiModelProperty(value = "类型编码") + @Column(name = "code") + private String code; + + @ApiModelProperty(value = "类型图标") + @Column(name = "icon") + private String icon; + + @ApiModelProperty(value = "类型描述") + @Column(name = "description") + private String description; + + @ApiModelProperty(value = "排序(越大越靠前)") + @Column(name = "sort") + private Integer sort; + + @ApiModelProperty(value = "状态 0=禁用 1=启用") + @Column(name = "status") + private Integer status; + + @ApiModelProperty(value = "创建时间") + @TableField("create_time") + @Column(name = "create_time") + private Date createTime; + + @ApiModelProperty(value = "更新时间") + @TableField("update_time") + @Column(name = "update_time") + private Date updateTime; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/token/FrontTokenComponent.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/token/FrontTokenComponent.java index 22a61cdd..04b1c45d 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/token/FrontTokenComponent.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/token/FrontTokenComponent.java @@ -41,9 +41,9 @@ public class FrontTokenComponent { private static final Long MILLIS_MINUTE = 60 * 1000L; - // 令牌有效期(默认30分钟) todo 调试期改为5小时 -// private static final int expireTime = 30; - private static final int expireTime = 5 * 60; + // 令牌有效期(设置为30天,每次访问自动刷新) + // 只有用户主动退出登录时才会删除token + private static final long expireTime = 30 * 24 * 60; // 30天 /** * 获取用户身份信息 @@ -81,13 +81,16 @@ public class FrontTokenComponent { /** * 创建令牌 + * Token有效期30天,每次访问自动刷新 + * 只有用户主动退出登录时才会删除 * * @param user 用户信息 * @return 令牌 */ public String createToken(User user) { String token = UUID.randomUUID().toString().replace("-", ""); - redisUtil.set(getTokenKey(token), user.getUid(), Constants.TOKEN_EXPRESS_MINUTES, TimeUnit.MINUTES); + // 设置30天过期时间,每次访问会自动刷新 + redisUtil.set(getTokenKey(token), user.getUid(), expireTime, TimeUnit.MINUTES); return token; } @@ -137,12 +140,16 @@ public class FrontTokenComponent { } /** - * 推出登录 + * 退出登录 + * 删除Redis中的Token,使Token立即失效 + * * @param request HttpServletRequest */ public void logout(HttpServletRequest request) { String token = getToken(request); - delLoginUser(token); + if (StrUtil.isNotEmpty(token)) { + delLoginUser(token); + } } /** @@ -189,13 +196,25 @@ public class FrontTokenComponent { return ArrayUtils.contains(routerList, uri); } + /** + * 检查Token是否有效 + * 如果Token存在,自动刷新过期时间(滑动过期策略) + * 这样只要用户持续使用,Token就不会过期 + * 只有用户主动退出登录时才会删除Token + * + * @param token Token值 + * @param request 请求对象 + * @return true-有效 false-无效 + */ public Boolean check(String token, HttpServletRequest request){ - try { boolean exists = redisUtil.exists(getTokenKey(token)); if(exists){ + // Token存在,获取用户ID Integer uid = redisUtil.get(getTokenKey(token)); - redisUtil.set(getTokenKey(token), uid, Constants.TOKEN_EXPRESS_MINUTES, TimeUnit.MINUTES); + // 自动刷新Token过期时间(滑动过期) + // 每次访问都重置为30天后过期 + redisUtil.set(getTokenKey(token), uid, expireTime, TimeUnit.MINUTES); }else{ //判断路由,部分路由不管用户是否登录/token过期都可以访问 exists = checkRouter(RequestUtil.getUri(request)); diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java index 2485fc59..da4abd62 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java @@ -48,6 +48,9 @@ public class LiveRoomController { @Autowired private com.zbkj.service.service.FollowRecordService followRecordService; + @Autowired + private com.zbkj.service.service.LiveTypeService liveTypeService; + @Value("${LIVE_PUBLIC_SRS_HOST:}") private String publicHost; @@ -99,11 +102,22 @@ public class LiveRoomController { Integer uid = frontTokenComponent.getUserId(); if (uid == null) return CommonResult.failed("未登录"); + // 记录创建直播间的请求参数 + log.info("创建直播间请求 - uid: {}, title: {}, streamerName: {}, type: {}, categoryId: {}", + uid, req.getTitle(), req.getStreamerName(), req.getType(), req.getCategoryId()); + + // 验证type参数 + String type = req.getType(); + if (type == null || type.trim().isEmpty()) { + log.warn("创建直播间 - type参数为空,使用默认值: game"); + type = "game"; // 默认值 + } + LiveRoom room = liveRoomService.createRoom( uid, req.getTitle(), req.getStreamerName(), - req.getType(), + type, req.getCategoryId(), req.getDescription(), req.getCoverImage(), @@ -111,6 +125,9 @@ public class LiveRoomController { req.getNotice() ); + log.info("创建直播间成功 - roomId: {}, type: {}, streamKey: {}", + room.getId(), room.getType(), room.getStreamKey()); + return CommonResult.success(toResponse(room, resolveHost(request), uid)); } @@ -519,4 +536,27 @@ public class LiveRoomController { public Map onStop(@RequestBody Map body) { return Collections.singletonMap("code", 0); } + + @ApiOperation(value = "公开:获取直播类型列表") + @GetMapping("/public/types") + public CommonResult> getLiveTypes() { + log.info("获取直播类型列表"); + List types = liveTypeService.getEnabledList(); + + List responseList = types.stream() + .map(type -> { + com.zbkj.front.response.live.LiveTypeResponse response = new com.zbkj.front.response.live.LiveTypeResponse(); + response.setId(type.getId()); + response.setName(type.getName()); + response.setCode(type.getCode()); + response.setIcon(type.getIcon()); + response.setDescription(type.getDescription()); + response.setSort(type.getSort()); + return response; + }) + .collect(Collectors.toList()); + + log.info("返回直播类型列表,共 {} 条", responseList.size()); + return CommonResult.success(responseList); + } } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveTypeResponse.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveTypeResponse.java new file mode 100644 index 00000000..3cf54624 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/response/live/LiveTypeResponse.java @@ -0,0 +1,35 @@ +package com.zbkj.front.response.live; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 直播类型响应对象 + */ +@Data +@ApiModel(value = "LiveTypeResponse", description = "直播类型响应") +public class LiveTypeResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "类型ID") + private Integer id; + + @ApiModelProperty(value = "类型名称") + private String name; + + @ApiModelProperty(value = "类型编码") + private String code; + + @ApiModelProperty(value = "类型图标") + private String icon; + + @ApiModelProperty(value = "类型描述") + private String description; + + @ApiModelProperty(value = "排序") + private Integer sort; +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveTypeDao.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveTypeDao.java new file mode 100644 index 00000000..1643071f --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/LiveTypeDao.java @@ -0,0 +1,11 @@ +package com.zbkj.service.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.zbkj.common.model.live.LiveType; + +/** + * 直播类型 Mapper 接口 + */ +public interface LiveTypeDao extends BaseMapper { + +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveTypeService.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveTypeService.java new file mode 100644 index 00000000..7bb1e1ed --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/LiveTypeService.java @@ -0,0 +1,25 @@ +package com.zbkj.service.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.zbkj.common.model.live.LiveType; + +import java.util.List; + +/** + * 直播类型服务接口 + */ +public interface LiveTypeService extends IService { + + /** + * 获取所有启用的直播类型列表 + * @return 直播类型列表,按sort字段降序排列 + */ + List getEnabledList(); + + /** + * 根据编码获取直播类型 + * @param code 类型编码 + * @return 直播类型 + */ + LiveType getByCode(String code); +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java index 9290756f..3386b201 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveRoomServiceImpl.java @@ -43,12 +43,22 @@ public class LiveRoomServiceImpl extends ServiceImpl impl Integer categoryId, String description, String coverImage, String tags, String notice) { String streamKey = UUID.randomUUID().toString().replace("-", ""); + + // 验证和处理type参数 + if (type == null || type.trim().isEmpty()) { + log.warn("createRoom - type参数为空,使用默认值: game"); + type = "game"; + } + + log.info("createRoom - 开始创建直播间: uid={}, title={}, type={}, categoryId={}", + uid, title, type, categoryId); + LiveRoom room = new LiveRoom(); room.setUid(uid); room.setTitle(title); room.setStreamerName(streamerName); room.setStreamKey(streamKey); - room.setType(type != null ? type : "live"); + room.setType(type); room.setCategoryId(categoryId); room.setDescription(description); room.setCoverImage(coverImage); @@ -62,7 +72,12 @@ public class LiveRoomServiceImpl extends ServiceImpl impl room.setOnlineCount(0); room.setCreateTime(new Date()); room.setStartedAt(null); + dao.insert(room); + + log.info("createRoom - 直播间创建成功: roomId={}, type={}, streamKey={}", + room.getId(), room.getType(), streamKey); + return room; } diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveTypeServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveTypeServiceImpl.java new file mode 100644 index 00000000..12b3386d --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/LiveTypeServiceImpl.java @@ -0,0 +1,48 @@ +package com.zbkj.service.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.zbkj.common.model.live.LiveType; +import com.zbkj.service.dao.LiveTypeDao; +import com.zbkj.service.service.LiveTypeService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 直播类型服务实现类 + */ +@Slf4j +@Service +public class LiveTypeServiceImpl extends ServiceImpl implements LiveTypeService { + + /** + * 获取所有启用的直播类型列表 + * @return 直播类型列表,按sort字段降序排列 + */ + @Override + public List getEnabledList() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LiveType::getStatus, 1); // 只查询启用状态的 + wrapper.orderByDesc(LiveType::getSort); // 按排序字段降序 + wrapper.orderByAsc(LiveType::getId); // 排序相同时按ID升序 + + List list = list(wrapper); + log.info("获取启用的直播类型列表,共 {} 条", list.size()); + return list; + } + + /** + * 根据编码获取直播类型 + * @param code 类型编码 + * @return 直播类型 + */ + @Override + public LiveType getByCode(String code) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LiveType::getCode, code); + wrapper.eq(LiveType::getStatus, 1); + return getOne(wrapper); + } +} 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 d1d9dd38..1920f54d 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 @@ -920,17 +920,12 @@ public class MainActivity extends AppCompatActivity { View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null); DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView); - // 设置直播类型选择器 - String[] liveTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"}; - ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, liveTypes); - // 使用正确的资源ID获取typeSpinner int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName()); MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId); - if (typeSpinner != null) { - typeSpinner.setAdapter(adapter); - } + // 从后端加载直播类型分类 + loadLiveTypesForDialog(typeSpinner); AlertDialog dialog = new AlertDialog.Builder(this) .setTitle("创建直播间") @@ -948,7 +943,7 @@ public class MainActivity extends AppCompatActivity { } String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : ""; - String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : ""; + String typeDisplay = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : ""; if (TextUtils.isEmpty(title)) { dialogBinding.titleLayout.setError("标题不能为空"); @@ -957,7 +952,7 @@ public class MainActivity extends AppCompatActivity { dialogBinding.titleLayout.setError(null); } - if (TextUtils.isEmpty(type)) { + if (TextUtils.isEmpty(typeDisplay)) { int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName()); TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId); if (typeLayout != null) { @@ -972,14 +967,74 @@ public class MainActivity extends AppCompatActivity { } } + // 从后端返回的类型列表中查找对应的类型编码 + String typeCode = "game"; // 默认值:游戏 + Object tag = typeSpinner.getTag(); + + Log.d(TAG, "创建直播间 - 用户选择的类型显示名称: " + typeDisplay); + Log.d(TAG, "创建直播间 - typeSpinner.getTag() 类型: " + (tag != null ? tag.getClass().getName() : "null")); + + if (tag instanceof List) { + @SuppressWarnings("unchecked") + List types = + (List) tag; + + Log.d(TAG, "创建直播间 - 从Tag中获取到 " + types.size() + " 个类型"); + + boolean found = false; + for (com.example.livestreaming.net.LiveTypeResponse type : types) { + Log.d(TAG, "创建直播间 - 比对类型: " + type.getName() + " vs " + typeDisplay); + if (type.getName().equals(typeDisplay)) { + typeCode = type.getCode(); + found = true; + Log.d(TAG, "创建直播间 - 匹配成功!使用类型编码: " + typeCode); + break; + } + } + + if (!found) { + Log.w(TAG, "创建直播间 - 未找到匹配的类型,使用默认值: " + typeCode); + } + } else { + Log.w(TAG, "创建直播间 - Tag不是List类型,使用默认映射"); + // 如果没有从后端获取到类型列表,使用默认映射 + switch (typeDisplay) { + case "游戏": + typeCode = "game"; + break; + case "才艺": + typeCode = "talent"; + break; + case "户外": + typeCode = "outdoor"; + break; + case "音乐": + typeCode = "music"; + break; + case "美食": + typeCode = "food"; + break; + case "聊天": + typeCode = "chat"; + break; + default: + Log.w(TAG, "创建直播间 - 未知的类型名称: " + typeDisplay + ",使用默认值: game"); + typeCode = "game"; + break; + } + Log.d(TAG, "创建直播间 - 默认映射结果: " + typeDisplay + " -> " + typeCode); + } + // 获取用户昵称 String streamerName = getSharedPreferences("profile_prefs", MODE_PRIVATE) .getString("profile_name", "未知用户"); + Log.d(TAG, "创建直播间 - 最终参数: title=" + title + ", streamerName=" + streamerName + ", typeCode=" + typeCode); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - // 调用后端接口创建直播间 - ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, "live")) + // 调用后端接口创建直播间,使用从后端获取的类型编码 + ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, typeCode)) .enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -1509,6 +1564,101 @@ public class MainActivity extends AppCompatActivity { }); } + /** + * 为创建直播间对话框加载直播类型 + * 从后端数据库获取直播类型列表(游戏、才艺、户外、音乐、美食、聊天等) + */ + private void loadLiveTypesForDialog(MaterialAutoCompleteTextView typeSpinner) { + if (typeSpinner == null) { + Log.w(TAG, "loadLiveTypesForDialog() typeSpinner is null"); + return; + } + + Log.d(TAG, "loadLiveTypesForDialog() 开始从后端加载直播类型"); + + // 设置下拉框为只读,不允许手动输入,只能从列表中选择 + typeSpinner.setKeyListener(null); + typeSpinner.setFocusable(false); + typeSpinner.setClickable(true); + typeSpinner.setFocusableInTouchMode(false); + + // 调用后端接口获取直播类型列表 + ApiClient.getService(getApplicationContext()).getLiveTypes() + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + Log.d(TAG, "loadLiveTypesForDialog() onResponse: code=" + response.code()); + + ApiResponse> body = response.body(); + List types = + response.isSuccessful() && body != null && body.isOk() && body.getData() != null + ? body.getData() + : null; + + runOnUiThread(() -> { + if (types != null && !types.isEmpty()) { + Log.d(TAG, "loadLiveTypesForDialog() 成功获取 " + types.size() + " 个直播类型"); + + // 打印所有类型信息,便于调试 + for (com.example.livestreaming.net.LiveTypeResponse type : types) { + Log.d(TAG, " 类型: " + type.getName() + " (code=" + type.getCode() + ", sort=" + type.getSort() + ")"); + } + + // 提取类型名称 + String[] typeNames = new String[types.size()]; + for (int i = 0; i < types.size(); i++) { + typeNames[i] = types.get(i).getName(); + } + + ArrayAdapter adapter = new ArrayAdapter<>(MainActivity.this, + android.R.layout.simple_dropdown_item_1line, typeNames); + typeSpinner.setAdapter(adapter); + + // 设置默认选中第一项 + if (typeNames.length > 0) { + typeSpinner.setText(typeNames[0], false); + Log.d(TAG, "loadLiveTypesForDialog() 默认选中: " + typeNames[0]); + } + + // 保存类型列表供后续使用 + typeSpinner.setTag(types); + Log.d(TAG, "loadLiveTypesForDialog() 类型列表已保存到Tag中"); + } else { + Log.w(TAG, "loadLiveTypesForDialog() 未获取到类型数据,使用默认类型"); + useDefaultLiveTypes(typeSpinner); + } + }); + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "loadLiveTypesForDialog() onFailure: " + t.getMessage(), t); + // 网络错误,使用默认类型 + runOnUiThread(() -> { + useDefaultLiveTypes(typeSpinner); + }); + } + }); + } + + /** + * 使用默认的直播类型(当后端接口失败时的备用方案) + */ + private void useDefaultLiveTypes(MaterialAutoCompleteTextView typeSpinner) { + String[] defaultTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"}; + ArrayAdapter adapter = new ArrayAdapter<>(MainActivity.this, + android.R.layout.simple_dropdown_item_1line, defaultTypes); + typeSpinner.setAdapter(adapter); + + // 设置默认选中第一项 + if (defaultTypes.length > 0) { + typeSpinner.setText(defaultTypes[0], false); + } + + Log.d(TAG, "useDefaultLiveTypes() 使用默认类型,共 " + defaultTypes.length + " 个"); + } + private void loadCoverAssetsAsync() { // 在后台线程加载资源文件,避免阻塞UI new Thread(() -> { 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 840383ac..a0cdefb8 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 @@ -285,6 +285,14 @@ public interface ApiService { @GET("api/front/category/statistics") Call>> getCategoryStatistics(@Query("type") int type); + // ==================== 直播类型接口 ==================== + + /** + * 获取直播类型列表(游戏、才艺、户外、音乐、美食、聊天等) + */ + @GET("api/front/live/public/types") + Call>> getLiveTypes(); + // ==================== 关注接口 ==================== @GET("api/front/follow/followers") diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/LiveTypeResponse.java b/android-app/app/src/main/java/com/example/livestreaming/net/LiveTypeResponse.java new file mode 100644 index 00000000..0d5b5b0e --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/net/LiveTypeResponse.java @@ -0,0 +1,89 @@ +package com.example.livestreaming.net; + +import com.google.gson.annotations.SerializedName; + +/** + * 直播类型响应对象 + */ +public class LiveTypeResponse { + + @SerializedName("id") + private Integer id; + + @SerializedName("name") + private String name; + + @SerializedName("code") + private String code; + + @SerializedName("icon") + private String icon; + + @SerializedName("description") + private String description; + + @SerializedName("sort") + private Integer sort; + + // Getters and Setters + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + @Override + public String toString() { + return "LiveTypeResponse{" + + "id=" + id + + ", name='" + name + '\'' + + ", code='" + code + '\'' + + ", icon='" + icon + '\'' + + ", description='" + description + '\'' + + ", sort=" + sort + + '}'; + } +}