1
This commit is contained in:
26
lib/request/api/room_api.dart
Normal file
26
lib/request/api/room_api.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:app/request/dto/room/rtc_token_dto.dart';
|
||||
import 'package:app/request/network/request.dart';
|
||||
|
||||
import '../dto/room/room_info_dto.dart';
|
||||
|
||||
/// 获取房间列表
|
||||
Future<List<RoomInfoDto>> getRoomListApi() async {
|
||||
var res = await Request().get('/study_room/get_study_room_list');
|
||||
return List<RoomInfoDto>.from(res.map((x) => RoomInfoDto.fromJson(x)));
|
||||
}
|
||||
|
||||
///获取自习室的websocket令牌
|
||||
Future<String> getWsTokenApi(int roomId) async {
|
||||
var res = await Request().get('/study_room/get_ws_token', {
|
||||
"study_room_id": roomId,
|
||||
});
|
||||
return res['token'];
|
||||
}
|
||||
|
||||
///获取自习室的RTC令牌
|
||||
Future<RtcTokenDto> getRtcTokenApi(int roomId) async {
|
||||
var res = await Request().get('/study_room/get_rtc_token', {
|
||||
"study_room_id": roomId,
|
||||
});
|
||||
return RtcTokenDto.fromJson(res);
|
||||
}
|
||||
29
lib/request/api/user_api.dart
Normal file
29
lib/request/api/user_api.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:app/request/dto/user/user_info_dto.dart';
|
||||
import 'package:app/request/network/request.dart';
|
||||
|
||||
import '../dto/user/login_dto.dart';
|
||||
|
||||
///发送验证码
|
||||
Future<void> sendCodeApi(String tel) async {
|
||||
await Request().get("/send_sms_code", {"tel": tel});
|
||||
}
|
||||
|
||||
///登录
|
||||
Future<LoginDto> loginApi(String tel, String code) async {
|
||||
var res = await Request().post("/login", {
|
||||
"tel": tel,
|
||||
"sms_code": code,
|
||||
});
|
||||
return LoginDto.fromJson(res);
|
||||
}
|
||||
|
||||
/// 获取用户信息
|
||||
Future<UserInfoDto> getUserInfoApi() async {
|
||||
var response = await Request().get("/get_my_info");
|
||||
return UserInfoDto.fromJson(response);
|
||||
}
|
||||
|
||||
///退出登录
|
||||
Future<void> logoutApi() async {
|
||||
await Request().get("/logout");
|
||||
}
|
||||
24
lib/request/dto/room/room_file_dto.dart
Normal file
24
lib/request/dto/room/room_file_dto.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
class RoomFileDto {
|
||||
RoomFileDto({
|
||||
this.fileName = "",
|
||||
this.fileSize = 0,
|
||||
this.fileUrl = "",
|
||||
});
|
||||
|
||||
RoomFileDto.fromJson(Map<String, dynamic> json)
|
||||
: fileName = json['file_name'] ?? "",
|
||||
fileSize = json['file_size'] ?? 0,
|
||||
fileUrl = json['file_url'] ?? "";
|
||||
|
||||
String fileName;
|
||||
int fileSize;
|
||||
String fileUrl;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['file_name'] = fileName;
|
||||
map['file_size'] = fileSize;
|
||||
map['file_url'] = fileUrl;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
39
lib/request/dto/room/room_info_dto.dart
Normal file
39
lib/request/dto/room/room_info_dto.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
class RoomInfoDto {
|
||||
|
||||
|
||||
RoomInfoDto({
|
||||
required this.teacherBackground,
|
||||
required this.roomName,
|
||||
required this.startTime,
|
||||
required this.teacherName,
|
||||
required this.endTime,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
String teacherBackground;
|
||||
String roomName;
|
||||
String startTime;
|
||||
String teacherName;
|
||||
String endTime;
|
||||
int id;
|
||||
|
||||
factory RoomInfoDto.fromJson(Map<dynamic, dynamic> json) =>
|
||||
RoomInfoDto(
|
||||
teacherBackground: json["teacher_background"],
|
||||
roomName: json["room_name"],
|
||||
startTime: json["start_time"],
|
||||
teacherName: json["teacher_name"],
|
||||
endTime: json["end_time"],
|
||||
id: json["id"],
|
||||
);
|
||||
|
||||
Map<dynamic, dynamic> toJson() =>
|
||||
{
|
||||
"teacher_background": teacherBackground,
|
||||
"room_name": roomName,
|
||||
"start_time": startTime,
|
||||
"teacher_name": teacherName,
|
||||
"end_time": endTime,
|
||||
"id": id,
|
||||
};
|
||||
}
|
||||
39
lib/request/dto/room/room_type_dto.dart
Normal file
39
lib/request/dto/room/room_type_dto.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
class RoomTypeDto {
|
||||
final int studyRoomId;
|
||||
final int teacherId;
|
||||
final String teacherRtcUid;
|
||||
final String teacherWsClientId;
|
||||
final int roomStatus;
|
||||
final String dataType;
|
||||
|
||||
RoomTypeDto({
|
||||
required this.studyRoomId,
|
||||
required this.teacherId,
|
||||
required this.teacherRtcUid,
|
||||
required this.teacherWsClientId,
|
||||
required this.roomStatus,
|
||||
required this.dataType,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map["study_room_id"] = studyRoomId;
|
||||
map["teacher_id"] = teacherId;
|
||||
map["teacher_rtc_uid"] = teacherRtcUid;
|
||||
map["teacher_ws_client_id"] = teacherWsClientId;
|
||||
map["room_status"] = roomStatus;
|
||||
map["data_type"] = dataType;
|
||||
return map;
|
||||
}
|
||||
|
||||
factory RoomTypeDto.fromJson(Map<String, dynamic> json) {
|
||||
return RoomTypeDto(
|
||||
studyRoomId: json["study_room_id"] ?? 0,
|
||||
teacherId: json["teacher_id"] ?? 0,
|
||||
teacherRtcUid: json["teacher_rtc_uid"] ?? "",
|
||||
teacherWsClientId: json["teacher_ws_client_id"] ?? "",
|
||||
roomStatus: json["room_status"] ?? 0,
|
||||
dataType: json["data_type"] ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/request/dto/room/room_user_dto.dart
Normal file
67
lib/request/dto/room/room_user_dto.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
class RoomUserDto {
|
||||
final int userId;
|
||||
final String rtcUid;
|
||||
final int microphoneStatus;
|
||||
final int cameraStatus;
|
||||
final int speekerStatus;
|
||||
final String wsClientId;
|
||||
final String userName;
|
||||
final String avatar;
|
||||
final int userType;
|
||||
final List<String> filesList;
|
||||
final String dataType;
|
||||
final int handup;
|
||||
final int online; //0离线,1在线
|
||||
|
||||
const RoomUserDto({
|
||||
required this.userId,
|
||||
required this.rtcUid,
|
||||
required this.microphoneStatus,
|
||||
required this.cameraStatus,
|
||||
required this.speekerStatus,
|
||||
required this.wsClientId,
|
||||
required this.userName,
|
||||
required this.avatar,
|
||||
required this.userType,
|
||||
required this.filesList,
|
||||
required this.dataType,
|
||||
required this.handup,
|
||||
required this.online,
|
||||
});
|
||||
|
||||
factory RoomUserDto.fromJson(Map<String, dynamic> json) {
|
||||
return RoomUserDto(
|
||||
userId: json["user_id"] ?? 0,
|
||||
rtcUid: json["rtc_uid"] ?? "",
|
||||
microphoneStatus: json["microphone_status"] ?? 0,
|
||||
cameraStatus: json["camera_status"] ?? 0,
|
||||
speekerStatus: json["speeker_status"] ?? 0,
|
||||
wsClientId: json["ws_client_id"] ?? "",
|
||||
userName: json["user_name"] ?? "",
|
||||
avatar: json["avatar"] ?? "",
|
||||
userType: json["user_type"] ?? 0,
|
||||
filesList: json["files"] != null ? List<String>.from(json["files"]) : <String>[],
|
||||
dataType: json["data_type"] ?? "",
|
||||
handup: json["handup"] ?? 0,
|
||||
online: json["online"] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"user_id": userId,
|
||||
"rtc_uid": rtcUid,
|
||||
"microphone_status": microphoneStatus,
|
||||
"camera_status": cameraStatus,
|
||||
"speeker_status": speekerStatus,
|
||||
"ws_client_id": wsClientId,
|
||||
"user_name": userName,
|
||||
"avatar": avatar,
|
||||
"user_type": userType,
|
||||
"files": filesList,
|
||||
"data_type": dataType,
|
||||
"handup": handup,
|
||||
"online": online,
|
||||
};
|
||||
}
|
||||
}
|
||||
27
lib/request/dto/room/rtc_token_dto.dart
Normal file
27
lib/request/dto/room/rtc_token_dto.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
class RtcTokenDto {
|
||||
RtcTokenDto({
|
||||
required this.uid,
|
||||
required this.expiresAt,
|
||||
required this.channel,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
String uid;
|
||||
DateTime expiresAt;
|
||||
String channel;
|
||||
String token;
|
||||
|
||||
factory RtcTokenDto.fromJson(Map<dynamic, dynamic> json) => RtcTokenDto(
|
||||
uid: json["uid"],
|
||||
expiresAt: DateTime.parse(json["expires_at"]),
|
||||
channel: json["channel"],
|
||||
token: json["token"],
|
||||
);
|
||||
|
||||
Map<dynamic, dynamic> toJson() => {
|
||||
"uid": uid,
|
||||
"expires_at": expiresAt.toIso8601String(),
|
||||
"channel": channel,
|
||||
"token": token,
|
||||
};
|
||||
}
|
||||
14
lib/request/dto/user/login_dto.dart
Normal file
14
lib/request/dto/user/login_dto.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
class LoginDto {
|
||||
String accessToken;
|
||||
|
||||
LoginDto({required this.accessToken});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map["accessToken"] = accessToken;
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
LoginDto.fromJson(dynamic json) : accessToken = json["accessToken"] ?? "";
|
||||
}
|
||||
60
lib/request/dto/user/user_info_dto.dart
Normal file
60
lib/request/dto/user/user_info_dto.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
class UserInfoDto {
|
||||
UserInfoDto({
|
||||
required this.accountType,
|
||||
required this.extraInfo,
|
||||
required this.name,
|
||||
required this.tel,
|
||||
required this.id,
|
||||
required this.avatar,
|
||||
});
|
||||
|
||||
/// 1学生 2老师
|
||||
int accountType;
|
||||
ExtraInfo extraInfo;
|
||||
String name;
|
||||
String tel;
|
||||
int id;
|
||||
String avatar;
|
||||
|
||||
factory UserInfoDto.fromJson(Map<dynamic, dynamic> json) => UserInfoDto(
|
||||
accountType: json["account_type"],
|
||||
extraInfo: ExtraInfo.fromJson(json["extra_info"]),
|
||||
name: json["name"],
|
||||
tel: json["tel"],
|
||||
id: json["id"],
|
||||
avatar: json["avatar"],
|
||||
);
|
||||
|
||||
Map<dynamic, dynamic> toJson() => {
|
||||
"account_type": accountType,
|
||||
"extra_info": extraInfo.toJson(),
|
||||
"name": name,
|
||||
"tel": tel,
|
||||
"id": id,
|
||||
"avatar": avatar,
|
||||
};
|
||||
}
|
||||
|
||||
class ExtraInfo {
|
||||
ExtraInfo({
|
||||
required this.vipEndTime,
|
||||
required this.vipStartTime,
|
||||
required this.vipStatus,
|
||||
});
|
||||
|
||||
String vipEndTime;
|
||||
String vipStartTime;
|
||||
int vipStatus; // 0:普通用户 1:VIP
|
||||
|
||||
factory ExtraInfo.fromJson(Map<dynamic, dynamic> json) => ExtraInfo(
|
||||
vipEndTime: json["vip_end_time"],
|
||||
vipStartTime: json["vip_start_time"],
|
||||
vipStatus: json["vip_status"],
|
||||
);
|
||||
|
||||
Map<dynamic, dynamic> toJson() => {
|
||||
"vip_end_time": vipEndTime,
|
||||
"vip_start_time": vipStartTime,
|
||||
"vip_status": vipStatus,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
|
||||
import '../dto/base_dto.dart';
|
||||
|
||||
|
||||
///请求拦截器
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) {
|
||||
) async {
|
||||
String token = await UserStore.getToken();
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
@@ -18,6 +21,7 @@ void onResponse(
|
||||
) {
|
||||
var apiResponse = ApiDto.fromJson(response.data);
|
||||
if (apiResponse.code == 1) {
|
||||
response.data = apiResponse.data;
|
||||
handler.next(response);
|
||||
} else {
|
||||
handler.reject(
|
||||
@@ -35,17 +39,25 @@ void onError(
|
||||
DioException e,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
var title = "";
|
||||
if (e.type == DioExceptionType.connectionTimeout) {
|
||||
print("请求超时");
|
||||
title = "请求超时";
|
||||
} else if (e.type == DioExceptionType.badResponse) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
print("接口404不存在");
|
||||
title = "接口404不存在";
|
||||
} else {
|
||||
print("500");
|
||||
title = "500";
|
||||
}
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
print("网络连接失败");
|
||||
title = "网络连接失败";
|
||||
} else {
|
||||
print("接口请求异常报错");
|
||||
title = "异常其他错误";
|
||||
}
|
||||
showError(title);
|
||||
handler.next(e);
|
||||
}
|
||||
|
||||
///显示错误信息
|
||||
void showError(String message) {
|
||||
EasyLoading.showError(message);
|
||||
}
|
||||
|
||||
103
lib/request/websocket/room_protocol.dart
Normal file
103
lib/request/websocket/room_protocol.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
enum RoomCommand {
|
||||
///ping服务器,用于心跳
|
||||
ping("ping"),
|
||||
|
||||
///加入房间
|
||||
joinRoom("into_room"),
|
||||
|
||||
///获取房间信息(没啥用)
|
||||
getRoomInfo("room_data"),
|
||||
|
||||
///学生开关扬声器、摄像头、麦克风
|
||||
switchCamera("mute_self"),
|
||||
|
||||
///学生上传文件
|
||||
uploadFile("upload_file"),
|
||||
|
||||
///学生举手
|
||||
handUp("handup"),
|
||||
|
||||
///老师开启自习室
|
||||
openRoom("start_study_room"),
|
||||
|
||||
///老师关闭自习室
|
||||
closeRoom("close_study_room"),
|
||||
|
||||
///老师开关学生的扬声器、关闭摄像头、关闭麦克风
|
||||
switchStudentCamera("mute_user"),
|
||||
|
||||
///老师清除学生的举手
|
||||
clearHandUp("clear_handup"),
|
||||
|
||||
///邀请学生进入白板
|
||||
inviteStudent("invite_whiteboard");
|
||||
|
||||
final String value;
|
||||
|
||||
const RoomCommand(this.value);
|
||||
}
|
||||
|
||||
enum RoomEvent {
|
||||
///人员变化事件
|
||||
changeUser("sys_room_user_changed"),
|
||||
|
||||
///学生端开启扬声器
|
||||
openSpeaker("user_unmute_self_speeker"),
|
||||
|
||||
///学生端关闭扬声器
|
||||
closeSpeaker("user_mute_self_speeker"),
|
||||
|
||||
///学生开启麦克风
|
||||
openMic("user_unmute_self_microphone"),
|
||||
|
||||
///学生关闭麦克风
|
||||
closeMic("user_mute_self_microphone"),
|
||||
|
||||
///学生开启摄像头
|
||||
openCamera("user_unmute_self_camera"),
|
||||
|
||||
///学生关闭摄像头
|
||||
closeCamera("user_mute_self_camera"),
|
||||
|
||||
///学生文件上传完毕
|
||||
fileUploadComplete("sys_user_file_uploaded"),
|
||||
|
||||
///学生举手事件
|
||||
handUp("sys_user_handup"),
|
||||
|
||||
///自习室以开启,进入自习室(学生用)
|
||||
openRoom("sys_start_study_room"),
|
||||
|
||||
///自习室以关闭,退出自习室(学生用)
|
||||
closeRoom("sys_close_study_room"),
|
||||
|
||||
///老师关闭学生的扬声器
|
||||
closeStudentSpeaker("sys_control_mute_speeker"),
|
||||
|
||||
///老师打开学生的扬声器
|
||||
openStudentSpeaker("sys_control_unmute_speeker"),
|
||||
|
||||
///老师关闭学生的麦克风
|
||||
closeStudentMic("sys_control_mute_microphone"),
|
||||
|
||||
///老师关闭学生的摄像头
|
||||
closeStudentCamera("sys_control_mute_camera"),
|
||||
|
||||
///老师清除学生的举手(学生用)
|
||||
clearHandUp("sys_clear_handup"),
|
||||
|
||||
///学生收到白板邀请(学生用)
|
||||
inviteWhiteboard("sys_invite_whiteboard");
|
||||
|
||||
final String value;
|
||||
|
||||
const RoomEvent(this.value);
|
||||
|
||||
/// 根据 值获取枚举
|
||||
static RoomEvent fromStr(String value) {
|
||||
return RoomEvent.values.firstWhere(
|
||||
(e) => e.value == value,
|
||||
orElse: () => throw ArgumentError('Invalid weather type value: $value'),
|
||||
);
|
||||
}
|
||||
}
|
||||
122
lib/request/websocket/room_websocket.dart
Normal file
122
lib/request/websocket/room_websocket.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/request/api/room_api.dart';
|
||||
import 'package:app/request/websocket/room_protocol.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../dto/room/rtc_token_dto.dart';
|
||||
|
||||
Logger logger = Logger();
|
||||
|
||||
class RoomWebSocket {
|
||||
///单例设计模式
|
||||
RoomWebSocket._();
|
||||
|
||||
static final RoomWebSocket _instance = RoomWebSocket._();
|
||||
|
||||
factory RoomWebSocket() => _instance;
|
||||
|
||||
/// WebSocket和心跳定时器
|
||||
String url = "";
|
||||
WebSocket? _socket;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _reconnectTimer; //错误重连的定时器
|
||||
|
||||
///令牌
|
||||
String wsToken = ""; //自习室的websocket令牌
|
||||
int roomId = 0; //房间号
|
||||
RtcTokenDto? rtcToken; // rtc的令牌
|
||||
|
||||
///用 StreamController 分化消息给订阅者
|
||||
final StreamController<RoomMessage> _msgController = StreamController.broadcast();
|
||||
|
||||
Stream<RoomMessage> get stream => _msgController.stream;
|
||||
|
||||
///初始化令牌
|
||||
/// -[id] 房间id
|
||||
Future<void> initToken(int id) async {
|
||||
roomId = id;
|
||||
final rtcFuture = getRtcTokenApi(id);
|
||||
final wsFuture = getWsTokenApi(id);
|
||||
|
||||
rtcToken = await rtcFuture;
|
||||
wsToken = await wsFuture;
|
||||
}
|
||||
|
||||
///开始连接
|
||||
Future<void> connect() async {
|
||||
try {
|
||||
_socket = await WebSocket.connect(
|
||||
"${Config.wsUrl()}?token=$wsToken&study_room_id=$roomId",
|
||||
);
|
||||
logger.i("连接成功");
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
|
||||
//监听消息
|
||||
_socket!.listen(
|
||||
(data) {
|
||||
//监听事件
|
||||
final jsonMap = jsonDecode(data);
|
||||
RoomMessage msg = RoomMessage(RoomEvent.fromStr(jsonMap['action']), jsonMap['data']);
|
||||
_msgController.add(msg);
|
||||
},
|
||||
onDone: () {},
|
||||
onError: (_) {
|
||||
logger.e("连接异常断开");
|
||||
},
|
||||
);
|
||||
//心跳
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(Duration(seconds: 15), (_) {
|
||||
logger.i("发送心跳");
|
||||
send(RoomCommand.ping);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.e("连接失败");
|
||||
_reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
///发送指令
|
||||
void send(RoomCommand action, [Map<String, dynamic>? params]) {
|
||||
final msg = {
|
||||
"action": action.value,
|
||||
"data": params,
|
||||
};
|
||||
_socket!.add(jsonEncode(msg));
|
||||
}
|
||||
|
||||
///连接错误事件
|
||||
void _reconnect() {
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer.periodic(Duration(seconds: 3), (timer) {
|
||||
logger.e("正在重连");
|
||||
connect();
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
//心跳取消
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
//socket取消
|
||||
_socket?.close();
|
||||
// 销毁事件流
|
||||
_msgController.close();
|
||||
// 错误重连取消
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
logger.i("websocket销毁成功");
|
||||
}
|
||||
}
|
||||
|
||||
///websocket服务器发过来的事件和数据
|
||||
class RoomMessage {
|
||||
final RoomEvent event; //事件名称
|
||||
final dynamic data; //事件数据
|
||||
|
||||
RoomMessage(this.event, this.data);
|
||||
}
|
||||
Reference in New Issue
Block a user