Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
+278
View File
@@ -0,0 +1,278 @@
import 'dart:async';
import 'package:flutter_mm_core/im_core.dart';
import 'package:get/get.dart';
import 'package:jydj/tools_base/debug_log.dart';
import 'package:jydj/tools_base/global_store/store.dart';
import 'package:jydj/tools_base/loading/loading_alert_widget.dart';
import 'package:jydj/tools_base/toast.dart';
import 'api/im_api_service.dart';
import 'chat_detail_page/chat_detail_page.dart';
import 'im_ad/im_ad_manager.dart';
import 'im_notification.dart';
import 'model/im_message_resp.dart';
import 'model/im_user_model.dart';
class ImManager {
// 单例
static final ImManager _ins = ImManager._();
factory ImManager() => _ins;
String socketURL = '';
String dynamicConfigDomain = '';
bool _isConnectSuccess = false; // true 连接成功
bool _isConnectIng = false; //true: 正在连接中
Future<bool>? _connectServerFuture;
Future<bool>? _refreshTokenFuture;
ImManager._();
String get _imToken => globalStore.meInfo?.imToken ?? "";
int? get imUserId => globalStore.meInfo?.imUserId;
bool _initFinish = false;
void initImSDK() {
if (socketURL.isEmpty || dynamicConfigDomain.isEmpty) {
debugLog("IM-initImSDK 参数配置有问题");
return;
}
if (_initFinish) return;
_initFinish = true;
ImCore.initSDK(
appKey: '',
appSecret: '',
socketURL: socketURL,
dynamicConfigDomain: dynamicConfigDomain,
);
ImCore.configureLog(
showGeneralLog: true,
showHttpLog: true,
showWebSocketLog: true,
showHeartbeatLog: false,
showDatabaseLog: true,
showMediaLog: true,
);
debugLog('IM-SDK 初始化完成');
_addChatListener();
}
// token失效或者断开连接,需要重新连接
Future<bool> connectServer() {
if (_isConnectSuccess) return Future.value(true);
if (_refreshTokenFuture != null) return _refreshTokenFuture!;
if (_connectServerFuture != null) return _connectServerFuture!;
_connectServerFuture = _connectServer().whenComplete(() {
_connectServerFuture = null;
});
return _connectServerFuture!;
}
Future<bool> _connectServer() async {
try {
if (_refreshTokenFuture != null) return _refreshTokenFuture!;
if (_isConnectSuccess) return true;
if (_isConnectIng) return _isConnectSuccess;
_isConnectIng = true;
_isConnectSuccess = false;
if (imUserId == null) {
debugLog('imUserId 为空');
ImUserModel? model = await IMApiService.getImUserInfo(uid: globalStore.meInfo?.uid);
globalStore.meInfo?.imUserId = int.tryParse(model?.imUserId ?? "");
}
if (_imToken.isEmpty) {
debugLog('IM- IM-token 获取失败');
} else {
bool ok = await _loginWithToken(_imToken);
if (ok) {
await ImAdManager.pullImAdsOnce();
_onIMConversation();
}
}
} catch (e) {
debugLog('IM-登录失败:$e');
} finally {
_isConnectIng = false;
}
return _isConnectSuccess;
}
Future<bool> _loginWithToken(String token) async {
final ok = await ImCore.linkImRequest(token: token).timeout(const Duration(seconds: 15));
if (!ok) {
debugLog('IM-登录失败');
return false;
}
await ImCore.whenReady.timeout(const Duration(seconds: 15));
_isConnectSuccess = true;
debugLog('IM-登录成功');
return true;
}
Future<bool> _refreshTokenAndReconnect() {
if (_refreshTokenFuture != null) return _refreshTokenFuture!;
_refreshTokenFuture = _refreshTokenAndReconnectInternal().whenComplete(() {
_refreshTokenFuture = null;
});
return _refreshTokenFuture!;
}
Future<bool> _refreshTokenAndReconnectInternal() async {
try {
_isConnectSuccess = false;
debugLog('IM-token 已失效,开始重新获取');
ImTokenInfo? model = await IMApiService.getImToken();
final token = model?.imToken ?? '';
if (token.isEmpty) {
debugLog('IM-token 重新获取失败');
return false;
}
globalStore.meInfo?.imToken = token;
if (model?.imUserId != null) {
globalStore.meInfo?.imUserId = model?.imUserId;
}
final ok = await _loginWithToken(token);
debugLog(ok ? 'IM-token 刷新并重连成功' : 'IM-token 刷新后重连失败');
return ok;
} catch (e) {
debugLog('IM-token 刷新并重连失败:$e');
return false;
}
}
void signOut() async {
try {
_isConnectSuccess = false;
await ImCore.loginOutRequest();
debugLog('IM-退出成功');
} catch (e) {
debugLog('IM-退出失败:$e');
}
}
/// 账号切换/重新登录:先登出旧账号 IM(重置当前用户运行态与本地会话),
/// 再用新账号(globalStore.meInfo 已更新)重新连接,避免会话列表残留旧数据。
Future<void> reloginOnAccountSwitch() async {
try {
_isConnectSuccess = false;
_isConnectIng = false;
_connectServerFuture = null;
_refreshTokenFuture = null;
await ImCore.loginOutRequest();
debugLog('IM-切换账号已登出旧会话');
} catch (e) {
debugLog('IM-切换账号登出失败:$e');
}
await connectServer();
}
void _addChatListener() {
removeAllListener();
ImCore.addRecvNewMessageListener(_onMessageReceived);
ImCore.addSendFailedListener(_onSendFailed);
ImCore.addMessageAckFailedListener(_onMessageAckFailed);
ImCore.addConversationChangedListener(_onIMConversation);
ImCore.addTokenExpiredListener(_onTokenExpired);
ImCore.onTokenExpired = _onTokenExpired;
}
void _onIMConversation() async {
ModelConversationListResult sessionRet = await ImCore.getConversations(includeArchivedCount: false);
ImMsgProvider().setUnReadStatus(sessionRet);
}
void _onTokenExpired() {
unawaited(_refreshTokenAndReconnect());
}
void _onMessageReceived(ModelMessage message) {
debugLog('IM-消息收到:${message.content},来自:${message.senderID}');
if (message.isPassthrough) {
// 全局透传消息(chatMessageType=400)监听:用于接收后端 AD_NOTIFY 等业务信号。
// 透传不入库、不计未读、不进会话;只在线推。
// 解析 / 去重 / 拉广告逻辑统一在 ImAdManager 里。
ImAdManager.onRecv(message);
} else {
imEventBus.emit(IMMsgEvent(message, true));
}
}
void _onSendFailed(ImCoreException exception, ModelMessage? message) {
imEventBus.emit(IMMsgEvent(message, false));
debugLog('IM-消息发送失败:${exception.message}');
if (exception.message.contains("当前 IM 未连接")) {
_isConnectSuccess = false;
connectServer();
}
}
void _onMessageAckFailed(ImCoreException exception, ModelMessage? message) {
debugLog('IM-消息 ACK 失败:${exception.message}');
_isConnectSuccess = false;
connectServer();
}
Future<bool> sendMessage(ModelCustomMessage message) async {
try {
final ok = await ImCore.sendMessage(msg: message);
if (ok) {
debugLog('已请求发送消息');
return true;
} else {
debugLog('发送消息失败');
return false;
}
} catch (e) {
debugLog('发送消息失败:$e');
return false;
}
}
void removeAllListener() {
ImCore.removeRecvNewMessageListener(_onMessageReceived);
ImCore.removeSendFailedListener(_onSendFailed);
ImCore.removeMessageAckFailedListener(_onMessageAckFailed);
ImCore.removeConversationChangedListener(_onIMConversation);
ImCore.removeTokenExpiredListener(_onTokenExpired);
}
}
Future jumpToChatDetail(IMSessionModel model) async {
// 优先用 Session.imUserId(SDK conversation.targetId,已经是 IM userId)
// 没有再回退到 ensureFriend 转换
int peerImUserId = model.imUserId ?? 0;
if (peerImUserId <= 0) {
final peerId = model.peerId ?? 0;
if (peerId <= 0) return;
try {
LoadingAlertWidget.show();
final result = await IMApiService.ensureFriend(peerId);
peerImUserId = int.tryParse(result.peerImUserId ?? '') ?? 0;
LoadingAlertWidget.cancel();
} catch (_) {
LoadingAlertWidget.cancel();
showToast('无法打开聊天,请稍后再试');
return;
}
}
if (peerImUserId <= 0) {
showToast('无法打开聊天,请稍后再试');
return;
}
return Get.to(
ChatDetailPage(
imUserId: peerImUserId,
userName: model.nickName,
uid: model.peerId,
peerAvatar: model.avatar,
),
preventDuplicates: false,
);
}