修复了is_live自动变为0的bug

This commit is contained in:
ShiQi 2025-12-31 11:16:54 +08:00
parent cec1ab4845
commit 162b44bf4f
14 changed files with 660 additions and 31 deletions

View File

@ -5,8 +5,8 @@
本文档详细列出了Android应用中**真实调用的所有接口**的请求参数和响应参数示例。 本文档详细列出了Android应用中**真实调用的所有接口**的请求参数和响应参数示例。
**文档版本**: v1.0 **文档版本**: v1.0
**更新时间**: 2024-12-30 **更新时间**: 2024-12-31
**接口总数**: 73个真实调用的接口 **接口总数**: 74个真实调用的接口
**基础URL**: `http://your-server:port` **基础URL**: `http://your-server:port`
--- ---
@ -26,6 +26,7 @@
11. [搜索功能模块](#11-搜索功能模块) 11. [搜索功能模块](#11-搜索功能模块)
12. [观看历史模块](#12-观看历史模块) 12. [观看历史模块](#12-观看历史模块)
13. [分类管理模块](#13-分类管理模块) 13. [分类管理模块](#13-分类管理模块)
14. [直播类型模块](#14-直播类型模块)
--- ---
@ -292,7 +293,7 @@ Authorization: Bearer <token>
{ {
"title": "我的直播间", "title": "我的直播间",
"streamerName": "主播昵称", "streamerName": "主播昵称",
"type": "video", "type": "game",
"categoryId": 1, "categoryId": 1,
"description": "直播间描述", "description": "直播间描述",
"coverImage": "https://example.com/cover.jpg", "coverImage": "https://example.com/cover.jpg",
@ -306,13 +307,25 @@ Authorization: Bearer <token>
|--------|------|------|------| |--------|------|------|------|
| title | String | 是 | 直播间标题 | | title | String | 是 | 直播间标题 |
| streamerName | String | 是 | 主播名称 | | streamerName | String | 是 | 主播名称 |
| type | String | 否 | 直播类型默认video | | type | String | 否 | 直播类型编码game/talent/outdoor/music/food/chat默认game |
| categoryId | Integer | 否 | 分类ID | | categoryId | Integer | 否 | 分类ID |
| description | String | 否 | 直播间描述 | | description | String | 否 | 直播间描述 |
| coverImage | String | 否 | 封面图片URL | | coverImage | String | 否 | 封面图片URL |
| tags | String | 否 | 标签,逗号分隔 | | tags | String | 否 | 标签,逗号分隔 |
| notice | String | 否 | 直播间公告 | | notice | String | 否 | 直播间公告 |
**重要说明**:
- `type` 参数应使用类型编码code而不是类型名称name
- 可用的类型编码:
- `game` - 游戏
- `talent` - 才艺
- `outdoor` - 户外
- `music` - 音乐
- `food` - 美食
- `chat` - 聊天
- 前端应先调用 `GET /api/front/live/public/types` 获取类型列表
- 如果type参数为空后端会使用默认值 `game`
**响应示例**: **响应示例**:
```json ```json
{ {
@ -321,6 +334,7 @@ Authorization: Bearer <token>
"data": { "data": {
"id": "10", "id": "10",
"title": "我的直播间", "title": "我的直播间",
"type": "game",
"streamKey": "live_stream_key_456", "streamKey": "live_stream_key_456",
"streamUrls": { "streamUrls": {
"rtmp": "rtmp://server:25002/live/stream_key_456", "rtmp": "rtmp://server:25002/live/stream_key_456",
@ -2540,6 +2554,90 @@ Authorization: Bearer <token>
--- ---
## 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. 通用响应格式 ### A. 通用响应格式

View File

@ -35,13 +35,21 @@ public class LiveStatusSyncTask {
@Value("${SRS_API_URL:http://127.0.0.1:1985}") @Value("${SRS_API_URL:http://127.0.0.1:1985}")
private String srsApiUrl; private String srsApiUrl;
@Value("${live.status.sync.enabled:true}")
private boolean syncEnabled;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
/** /**
* 同步直播状态 5 秒执行一次 * 同步直播状态 30 秒执行一次
* 生产环境建议保留此任务确保直播状态与实际推流状态一致
*/ */
@Scheduled(fixedRate = 5000) @Scheduled(fixedRate = 30000) // 改为30秒降低频率
public void syncLiveStatus() { public void syncLiveStatus() {
if (!syncEnabled) {
logger.debug("直播状态同步已禁用");
return;
}
try { try {
Set<String> liveStreamKeys = fetchLiveStreamKeysFromSrs(); Set<String> liveStreamKeys = fetchLiveStreamKeysFromSrs();
updateLiveStatus(liveStreamKeys); updateLiveStatus(liveStreamKeys);
@ -86,6 +94,7 @@ public class LiveStatusSyncTask {
/** /**
* 更新数据库中的直播状态 * 更新数据库中的直播状态
* 增加容错只有连续多次检测到断流才更新为未开播
*/ */
private void updateLiveStatus(Set<String> liveStreamKeys) { private void updateLiveStatus(Set<String> liveStreamKeys) {
List<LiveRoom> allRooms = liveRoomService.list(new LambdaQueryWrapper<>()); List<LiveRoom> allRooms = liveRoomService.list(new LambdaQueryWrapper<>());
@ -96,9 +105,18 @@ public class LiveStatusSyncTask {
int currentStatus = room.getIsLive() == null ? 0 : room.getIsLive(); int currentStatus = room.getIsLive() == null ? 0 : room.getIsLive();
if (newStatus != currentStatus) { if (newStatus != currentStatus) {
// 如果检测到推流开始立即更新为直播中
if (newStatus == 1) {
room.setIsLive(newStatus); room.setIsLive(newStatus);
liveRoomService.updateById(room); liveRoomService.updateById(room);
logger.info("直播状态更新: {} -> {}", room.getTitle(), shouldBeLive ? "直播中" : "未开播"); logger.info("直播状态更新: {} -> 直播中", room.getTitle());
}
// 如果检测到推流结束也立即更新SRS回调已经处理了这里是兜底
else if (currentStatus == 1) {
room.setIsLive(0);
liveRoomService.updateById(room);
logger.info("直播状态更新: {} -> 未开播(定时任务检测到断流)", room.getTitle());
}
} }
} }
} }

View File

@ -13,7 +13,10 @@ package com.zbkj.common.constants;
* +---------------------------------------------------------------------- * +----------------------------------------------------------------------
*/ */
public class 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; public static final int HTTPSTATUS_CODE_SUCCESS = 200;

View File

@ -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;
}

View File

@ -41,9 +41,9 @@ public class FrontTokenComponent {
private static final Long MILLIS_MINUTE = 60 * 1000L; private static final Long MILLIS_MINUTE = 60 * 1000L;
// 令牌有效期默认30分钟 todo 调试期改为5小时 // 令牌有效期设置为30天每次访问自动刷新
// private static final int expireTime = 30; // 只有用户主动退出登录时才会删除token
private static final int expireTime = 5 * 60; private static final long expireTime = 30 * 24 * 60; // 30天
/** /**
* 获取用户身份信息 * 获取用户身份信息
@ -81,13 +81,16 @@ public class FrontTokenComponent {
/** /**
* 创建令牌 * 创建令牌
* Token有效期30天每次访问自动刷新
* 只有用户主动退出登录时才会删除
* *
* @param user 用户信息 * @param user 用户信息
* @return 令牌 * @return 令牌
*/ */
public String createToken(User user) { public String createToken(User user) {
String token = UUID.randomUUID().toString().replace("-", ""); 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; return token;
} }
@ -137,13 +140,17 @@ public class FrontTokenComponent {
} }
/** /**
* 推出登录 * 退出登录
* 删除Redis中的Token使Token立即失效
*
* @param request HttpServletRequest * @param request HttpServletRequest
*/ */
public void logout(HttpServletRequest request) { public void logout(HttpServletRequest request) {
String token = getToken(request); String token = getToken(request);
if (StrUtil.isNotEmpty(token)) {
delLoginUser(token); delLoginUser(token);
} }
}
/** /**
* 获取当前登录用户id * 获取当前登录用户id
@ -189,13 +196,25 @@ public class FrontTokenComponent {
return ArrayUtils.contains(routerList, uri); return ArrayUtils.contains(routerList, uri);
} }
/**
* 检查Token是否有效
* 如果Token存在自动刷新过期时间滑动过期策略
* 这样只要用户持续使用Token就不会过期
* 只有用户主动退出登录时才会删除Token
*
* @param token Token值
* @param request 请求对象
* @return true-有效 false-无效
*/
public Boolean check(String token, HttpServletRequest request){ public Boolean check(String token, HttpServletRequest request){
try { try {
boolean exists = redisUtil.exists(getTokenKey(token)); boolean exists = redisUtil.exists(getTokenKey(token));
if(exists){ if(exists){
// Token存在获取用户ID
Integer uid = redisUtil.get(getTokenKey(token)); 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{ }else{
//判断路由部分路由不管用户是否登录/token过期都可以访问 //判断路由部分路由不管用户是否登录/token过期都可以访问
exists = checkRouter(RequestUtil.getUri(request)); exists = checkRouter(RequestUtil.getUri(request));

View File

@ -48,6 +48,9 @@ public class LiveRoomController {
@Autowired @Autowired
private com.zbkj.service.service.FollowRecordService followRecordService; private com.zbkj.service.service.FollowRecordService followRecordService;
@Autowired
private com.zbkj.service.service.LiveTypeService liveTypeService;
@Value("${LIVE_PUBLIC_SRS_HOST:}") @Value("${LIVE_PUBLIC_SRS_HOST:}")
private String publicHost; private String publicHost;
@ -99,11 +102,22 @@ public class LiveRoomController {
Integer uid = frontTokenComponent.getUserId(); Integer uid = frontTokenComponent.getUserId();
if (uid == null) return CommonResult.failed("未登录"); 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( LiveRoom room = liveRoomService.createRoom(
uid, uid,
req.getTitle(), req.getTitle(),
req.getStreamerName(), req.getStreamerName(),
req.getType(), type,
req.getCategoryId(), req.getCategoryId(),
req.getDescription(), req.getDescription(),
req.getCoverImage(), req.getCoverImage(),
@ -111,6 +125,9 @@ public class LiveRoomController {
req.getNotice() req.getNotice()
); );
log.info("创建直播间成功 - roomId: {}, type: {}, streamKey: {}",
room.getId(), room.getType(), room.getStreamKey());
return CommonResult.success(toResponse(room, resolveHost(request), uid)); return CommonResult.success(toResponse(room, resolveHost(request), uid));
} }
@ -519,4 +536,27 @@ public class LiveRoomController {
public Map<String, Object> onStop(@RequestBody Map<String, Object> body) { public Map<String, Object> onStop(@RequestBody Map<String, Object> body) {
return Collections.singletonMap("code", 0); return Collections.singletonMap("code", 0);
} }
@ApiOperation(value = "公开:获取直播类型列表")
@GetMapping("/public/types")
public CommonResult<List<com.zbkj.front.response.live.LiveTypeResponse>> getLiveTypes() {
log.info("获取直播类型列表");
List<com.zbkj.common.model.live.LiveType> types = liveTypeService.getEnabledList();
List<com.zbkj.front.response.live.LiveTypeResponse> 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);
}
} }

View File

@ -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;
}

View File

@ -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<LiveType> {
}

View File

@ -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<LiveType> {
/**
* 获取所有启用的直播类型列表
* @return 直播类型列表按sort字段降序排列
*/
List<LiveType> getEnabledList();
/**
* 根据编码获取直播类型
* @param code 类型编码
* @return 直播类型
*/
LiveType getByCode(String code);
}

View File

@ -43,12 +43,22 @@ public class LiveRoomServiceImpl extends ServiceImpl<LiveRoomDao, LiveRoom> impl
Integer categoryId, String description, String coverImage, Integer categoryId, String description, String coverImage,
String tags, String notice) { String tags, String notice) {
String streamKey = UUID.randomUUID().toString().replace("-", ""); 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(); LiveRoom room = new LiveRoom();
room.setUid(uid); room.setUid(uid);
room.setTitle(title); room.setTitle(title);
room.setStreamerName(streamerName); room.setStreamerName(streamerName);
room.setStreamKey(streamKey); room.setStreamKey(streamKey);
room.setType(type != null ? type : "live"); room.setType(type);
room.setCategoryId(categoryId); room.setCategoryId(categoryId);
room.setDescription(description); room.setDescription(description);
room.setCoverImage(coverImage); room.setCoverImage(coverImage);
@ -62,7 +72,12 @@ public class LiveRoomServiceImpl extends ServiceImpl<LiveRoomDao, LiveRoom> impl
room.setOnlineCount(0); room.setOnlineCount(0);
room.setCreateTime(new Date()); room.setCreateTime(new Date());
room.setStartedAt(null); room.setStartedAt(null);
dao.insert(room); dao.insert(room);
log.info("createRoom - 直播间创建成功: roomId={}, type={}, streamKey={}",
room.getId(), room.getType(), streamKey);
return room; return room;
} }

View File

@ -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<LiveTypeDao, LiveType> implements LiveTypeService {
/**
* 获取所有启用的直播类型列表
* @return 直播类型列表按sort字段降序排列
*/
@Override
public List<LiveType> getEnabledList() {
LambdaQueryWrapper<LiveType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LiveType::getStatus, 1); // 只查询启用状态的
wrapper.orderByDesc(LiveType::getSort); // 按排序字段降序
wrapper.orderByAsc(LiveType::getId); // 排序相同时按ID升序
List<LiveType> list = list(wrapper);
log.info("获取启用的直播类型列表,共 {} 条", list.size());
return list;
}
/**
* 根据编码获取直播类型
* @param code 类型编码
* @return 直播类型
*/
@Override
public LiveType getByCode(String code) {
LambdaQueryWrapper<LiveType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LiveType::getCode, code);
wrapper.eq(LiveType::getStatus, 1);
return getOne(wrapper);
}
}

View File

@ -920,17 +920,12 @@ public class MainActivity extends AppCompatActivity {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null); View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null);
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView); DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView);
// 设置直播类型选择器
String[] liveTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, liveTypes);
// 使用正确的资源ID获取typeSpinner // 使用正确的资源ID获取typeSpinner
int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName()); int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName());
MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId); MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId);
if (typeSpinner != null) { // 从后端加载直播类型分类
typeSpinner.setAdapter(adapter); loadLiveTypesForDialog(typeSpinner);
}
AlertDialog dialog = new AlertDialog.Builder(this) AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("创建直播间") .setTitle("创建直播间")
@ -948,7 +943,7 @@ public class MainActivity extends AppCompatActivity {
} }
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : ""; 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)) { if (TextUtils.isEmpty(title)) {
dialogBinding.titleLayout.setError("标题不能为空"); dialogBinding.titleLayout.setError("标题不能为空");
@ -957,7 +952,7 @@ public class MainActivity extends AppCompatActivity {
dialogBinding.titleLayout.setError(null); dialogBinding.titleLayout.setError(null);
} }
if (TextUtils.isEmpty(type)) { if (TextUtils.isEmpty(typeDisplay)) {
int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName()); int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId); TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
if (typeLayout != null) { 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<com.example.livestreaming.net.LiveTypeResponse> types =
(List<com.example.livestreaming.net.LiveTypeResponse>) 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) String streamerName = getSharedPreferences("profile_prefs", MODE_PRIVATE)
.getString("profile_name", "未知用户"); .getString("profile_name", "未知用户");
Log.d(TAG, "创建直播间 - 最终参数: title=" + title + ", streamerName=" + streamerName + ", typeCode=" + typeCode);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); 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<ApiResponse<Room>>() { .enqueue(new Callback<ApiResponse<Room>>() {
@Override @Override
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) { public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> 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<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>> call,
Response<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>> response) {
Log.d(TAG, "loadLiveTypesForDialog() onResponse: code=" + response.code());
ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>> body = response.body();
List<com.example.livestreaming.net.LiveTypeResponse> 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<String> 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<ApiResponse<List<com.example.livestreaming.net.LiveTypeResponse>>> call, Throwable t) {
Log.e(TAG, "loadLiveTypesForDialog() onFailure: " + t.getMessage(), t);
// 网络错误使用默认类型
runOnUiThread(() -> {
useDefaultLiveTypes(typeSpinner);
});
}
});
}
/**
* 使用默认的直播类型当后端接口失败时的备用方案
*/
private void useDefaultLiveTypes(MaterialAutoCompleteTextView typeSpinner) {
String[] defaultTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
ArrayAdapter<String> 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() { private void loadCoverAssetsAsync() {
// 在后台线程加载资源文件避免阻塞UI // 在后台线程加载资源文件避免阻塞UI
new Thread(() -> { new Thread(() -> {

View File

@ -285,6 +285,14 @@ public interface ApiService {
@GET("api/front/category/statistics") @GET("api/front/category/statistics")
Call<ApiResponse<Map<String, Object>>> getCategoryStatistics(@Query("type") int type); Call<ApiResponse<Map<String, Object>>> getCategoryStatistics(@Query("type") int type);
// ==================== 直播类型接口 ====================
/**
* 获取直播类型列表游戏才艺户外音乐美食聊天等
*/
@GET("api/front/live/public/types")
Call<ApiResponse<List<LiveTypeResponse>>> getLiveTypes();
// ==================== 关注接口 ==================== // ==================== 关注接口 ====================
@GET("api/front/follow/followers") @GET("api/front/follow/followers")

View File

@ -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 +
'}';
}
}