2 Commits

Author SHA1 Message Date
Jolie dde5bc76db feat(call): 实现通话类型图标区分和RTC频道管理功能 2 months ago
Jolie 832dc1a21d feat(call): 添加RTC通话消费响应处理和玫瑰余额显示 2 months ago
8 changed files with 156 additions and 34 deletions
Unified View
  1. 17
      lib/controller/discover/room_controller.dart
  2. 32
      lib/controller/message/call_controller.dart
  3. 42
      lib/model/rtc/consume_rtc_channel_response.dart
  4. 3
      lib/network/rtc_api.dart
  5. 35
      lib/network/rtc_api.g.dart
  6. 46
      lib/pages/message/video_call_page.dart
  7. 8
      lib/widget/message/call_item.dart
  8. 7
      lib/widget/message/call_type_selection_dialog.dart

17
lib/controller/discover/room_controller.dart

@ -375,7 +375,6 @@ class RoomController extends GetxController with WidgetsBindingObserver {
} }
Future<void> leaveChannel() async { Future<void> leaveChannel() async {
// RTC
if (currentRole == CurrentRole.broadcaster) { if (currentRole == CurrentRole.broadcaster) {
try { try {
// RTC API // RTC API
@ -393,6 +392,22 @@ class RoomController extends GetxController with WidgetsBindingObserver {
} catch (e) { } catch (e) {
print('❌ 销毁 RTC 频道异常: $e'); print('❌ 销毁 RTC 频道异常: $e');
} }
} else if (currentRole == CurrentRole.maleAudience || currentRole == CurrentRole.femaleAudience) {
try {
// RTC
final channelId = RTCManager.instance.currentChannelId;
if (channelId != null && channelId.isNotEmpty) {
final data = {'channelId': channelId};
final response = await _networkService.rtcApi.disconnectRtcChannel(data);
if (response.data.isSuccess) {
print('✅ [RoomController] 嘉宾已断开 RTC 频道连接,channelId: $channelId');
} else {
print('⚠️ [RoomController] 断开 RTC 频道连接失败: ${response.data.message}');
}
}
} catch (e) {
print('❌ [RoomController] 断开 RTC 频道连接异常: $e');
}
} }
isLive.value = false; isLive.value = false;

32
lib/controller/message/call_controller.dart

@ -3,6 +3,7 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart'; import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart';
import 'package:dating_touchme_app/model/rtc/consume_rtc_channel_response.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/network/network_service.dart'; import 'package:dating_touchme_app/network/network_service.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart';
@ -117,6 +118,9 @@ class CallController extends GetxController {
String? _callChannelId; String? _callChannelId;
int? _callUid; int? _callUid;
//
final Rxn<ConsumeRtcChannelResponse> consumeResponse = Rxn<ConsumeRtcChannelResponse>();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -450,9 +454,6 @@ class CallController extends GetxController {
// //
// _callChannelId // _callChannelId
if (_callChannelId != null && _callChannelId!.isNotEmpty) { if (_callChannelId != null && _callChannelId!.isNotEmpty) {
Future .delayed(Duration(seconds: 1), () async {
await _consumeOneOnOneRtcChannel();
});
_startConsumeTimer(); _startConsumeTimer();
print('✅ [CallController] 接收方接听后已启动消费定时器'); print('✅ [CallController] 接收方接听后已启动消费定时器');
} }
@ -591,6 +592,8 @@ class CallController extends GetxController {
void _stopConsumeTimer() { void _stopConsumeTimer() {
_consumeTimer?.cancel(); _consumeTimer?.cancel();
_consumeTimer = null; _consumeTimer = null;
//
consumeResponse.value = null;
} }
/// RTC频道接口 /// RTC频道接口
@ -606,7 +609,28 @@ class CallController extends GetxController {
'channelId': consumeChannelId, 'channelId': consumeChannelId,
}); });
if (response.data.isSuccess) { if (response.data.isSuccess) {
print('✅ [CallController] 已调用消费一对一RTC频道接口,channelId: $consumeChannelId');
final consumeData = response.data.data;
if (consumeData != null) {
//
consumeResponse.value = consumeData;
print('✅ [CallController] 已调用消费一对一RTC频道接口,channelId: $consumeChannelId, isFree: ${consumeData.isFree}, status: ${consumeData.status}, availableBalance: ${consumeData.availableBalance}, unitSellingBalance: ${consumeData.unitSellingBalance}');
// status == 3
if (consumeData.status == 3) {
print('⚠️ [CallController] 检测到 status=3,自动挂断通话');
await hangUpCall();
return;
}
//
if (consumeData.isFree) {
_stopConsumeTimer();
print('✅ [CallController] 检测到免费通话,已停止消费定时器');
}
} else {
print('✅ [CallController] 已调用消费一对一RTC频道接口,channelId: $consumeChannelId');
}
} else { } else {
print('⚠️ [CallController] 消费一对一RTC频道接口失败: ${response.data.message}'); print('⚠️ [CallController] 消费一对一RTC频道接口失败: ${response.data.message}');
} }

42
lib/model/rtc/consume_rtc_channel_response.dart

@ -0,0 +1,42 @@
/// RTC频道响应模型
class ConsumeRtcChannelResponse {
final bool isFree;
final int status;
final int availableBalance;
final int unitSellingBalance;
final String? code;
ConsumeRtcChannelResponse({
required this.isFree,
required this.status,
required this.availableBalance,
required this.unitSellingBalance,
this.code,
});
factory ConsumeRtcChannelResponse.fromJson(Map<String, dynamic> json) {
return ConsumeRtcChannelResponse(
isFree: json['isFree'] as bool? ?? false,
status: json['status'] as int? ?? 0,
availableBalance: json['availableBalance'] as int? ?? 0,
unitSellingBalance: json['unitSellingBalance'] as int? ?? 0,
code: json['code'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'isFree': isFree,
'status': status,
'availableBalance': availableBalance,
'unitSellingBalance': unitSellingBalance,
'code': code,
};
}
@override
String toString() {
return 'ConsumeRtcChannelResponse(isFree: $isFree, status: $status, availableBalance: $availableBalance, unitSellingBalance: $unitSellingBalance, code: $code)';
}
}

3
lib/network/rtc_api.dart

@ -1,6 +1,7 @@
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart'; import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart';
import 'package:dating_touchme_app/model/live/gift_product_model.dart'; import 'package:dating_touchme_app/model/live/gift_product_model.dart';
import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart'; import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.dart';
import 'package:dating_touchme_app/model/rtc/consume_rtc_channel_response.dart';
import 'package:dating_touchme_app/model/rtc/link_mic_card_model.dart'; import 'package:dating_touchme_app/model/rtc/link_mic_card_model.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart';
@ -133,7 +134,7 @@ abstract class RtcApi {
/// RTC频道 /// RTC频道
@POST(ApiUrls.consumeOneOnOneRtcChannel) @POST(ApiUrls.consumeOneOnOneRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> consumeOneOnOneRtcChannel(
Future<HttpResponse<BaseResponse<ConsumeRtcChannelResponse>>> consumeOneOnOneRtcChannel(
@Body() Map<String, dynamic> data, @Body() Map<String, dynamic> data,
); );
} }

35
lib/network/rtc_api.g.dart

@ -750,30 +750,33 @@ class _RtcApi implements RtcApi {
} }
@override @override
Future<HttpResponse<BaseResponse<dynamic>>> consumeOneOnOneRtcChannel(
Map<String, dynamic> data,
) async {
Future<HttpResponse<BaseResponse<ConsumeRtcChannelResponse>>>
consumeOneOnOneRtcChannel(Map<String, dynamic> data) async {
final _extra = <String, dynamic>{}; final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{}; final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{}; final _headers = <String, dynamic>{};
final _data = <String, dynamic>{}; final _data = <String, dynamic>{};
_data.addAll(data); _data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/consume/one-on-one/rtc-channel',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _options =
_setStreamType<HttpResponse<BaseResponse<ConsumeRtcChannelResponse>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/consume/one-on-one/rtc-channel',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options); final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
late BaseResponse<ConsumeRtcChannelResponse> _value;
try { try {
_value = BaseResponse<dynamic>.fromJson(
_value = BaseResponse<ConsumeRtcChannelResponse>.fromJson(
_result.data!, _result.data!,
(json) => json as dynamic,
(json) =>
ConsumeRtcChannelResponse.fromJson(json as Map<String, dynamic>),
); );
} on Object catch (e, s) { } on Object catch (e, s) {
errorLogger?.logError(e, s, _options); errorLogger?.logError(e, s, _options);

46
lib/pages/message/video_call_page.dart

@ -561,15 +561,45 @@ class _VideoCallPageState extends State<VideoCallPage> {
bottom: MediaQuery.of(context).size.height * 0.25, bottom: MediaQuery.of(context).size.height * 0.25,
left: 0, left: 0,
right: 0, right: 0,
child: Center(
child: Text(
isCallConnected ? _formatDuration(duration) : '正在呼叫中',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
child: Column(
children: [
Center(
child: Text(
isCallConnected ? _formatDuration(duration) : '正在呼叫中',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
), ),
),
Obx(() {
final callSession = _callController.currentCall.value;
final consumeData = _callController.consumeResponse.value;
// waitCalling
final isCalling = callSession?.status == CallStatus.waitCalling;
final isFree = consumeData?.isFree == true;
if (isCalling || isFree) {
return const SizedBox.shrink();
}
final availableBalance = consumeData?.availableBalance ?? 44;
final unitSellingBalance = consumeData?.unitSellingBalance ?? 35;
return Center(
child: Text(
'玫瑰剩余$availableBalance支($unitSellingBalance玫瑰/分钟)',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
);
})
],
), ),
); );
}); });

8
lib/widget/message/call_item.dart

@ -136,7 +136,12 @@ class CallItem extends StatelessWidget {
if (callStatus == CallStatus.rejected) { if (callStatus == CallStatus.rejected) {
return Assets.imagesRejectCall; return Assets.imagesRejectCall;
} else { } else {
return Assets.imagesAcceptCall;
//
if (callType == CallType.video) {
return Assets.imagesSendVideoCall;
} else {
return Assets.imagesSendCall;
}
} }
} }
} }
@ -225,6 +230,7 @@ class CallItem extends StatelessWidget {
width: 24.w, width: 24.w,
height: 24.w, height: 24.w,
fit: BoxFit.contain, fit: BoxFit.contain,
color: isSentByMe ? Colors.white : Colors.orange,
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
// //

7
lib/widget/message/call_type_selection_dialog.dart

@ -49,7 +49,8 @@ class CallTypeSelectionDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String price = _formatPrice(voiceProduct);
String voicePrice = _formatPrice(voiceProduct);
String videoPrice = _formatPrice(videoProduct);
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -72,7 +73,7 @@ class CallTypeSelectionDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 16.w), padding: EdgeInsets.symmetric(vertical: 16.w),
child: Center( child: Center(
child: Text( child: Text(
price.isNotEmpty ? '语音通话 ($price)' : '语音通话',
voicePrice.isNotEmpty ? '语音通话 ($voicePrice)' : '语音通话',
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: const Color.fromRGBO(51, 51, 51, 1), color: const Color.fromRGBO(51, 51, 51, 1),
@ -98,7 +99,7 @@ class CallTypeSelectionDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 16.w), padding: EdgeInsets.symmetric(vertical: 16.w),
child: Center( child: Center(
child: Text( child: Text(
price.isNotEmpty ? '视频通话 ($price)' : '视频通话',
videoPrice.isNotEmpty ? '视频通话 ($videoPrice)' : '视频通话',
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: const Color.fromRGBO(51, 51, 51, 1), color: const Color.fromRGBO(51, 51, 51, 1),

Loading…
Cancel
Save