feat(mobile,deploy): 接入安卓极光与华为通道并准备公网部署配置
客户端默认指向 jxd.jinniu.ink 的 HTTPS/WSS;登录后把 OpenIM userID 设为极光 alias。 华为 agconnect-services.json 不伪造、不入库。Master Secret 只从服务器文件注入。 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+37
-14
@@ -1,14 +1,24 @@
|
||||
/// 服务器地址集中配置。
|
||||
///
|
||||
/// 服务器换了地址只需要改这一个文件:
|
||||
/// 1. 改下面的 [serverHost] 为新的服务器内网 IP(或域名);
|
||||
/// 2. 如果端口也被重新映射,改对应端口常量即可。
|
||||
/// 默认指向公网测试域名 `jxd.jinniu.ink`(HTTPS / WSS)。
|
||||
/// 内网联调不要改本文件,打包时覆盖:
|
||||
/// `--dart-define=SERVER_HOST=192.168.200.11 --dart-define=USE_TLS=false`
|
||||
/// 仅覆盖登录服务:`--dart-define=AUTH_HOST=127.0.0.1`
|
||||
library;
|
||||
|
||||
/// 服务器内网地址(不带协议、不带端口)
|
||||
const String serverHost = '192.168.200.11';
|
||||
/// 公网测试域名(不带协议、不带端口)
|
||||
const String defaultPublicHost = 'jxd.jinniu.ink';
|
||||
|
||||
/// OpenIM API 端口(服务端 docker 映射,默认 10002)
|
||||
/// 服务器地址(不带协议、不带端口)
|
||||
const String serverHost = String.fromEnvironment(
|
||||
'SERVER_HOST',
|
||||
defaultValue: defaultPublicHost,
|
||||
);
|
||||
|
||||
/// 是否走 HTTPS/WSS(公网默认 true;内网联调传 false)
|
||||
const bool useTls = bool.fromEnvironment('USE_TLS', defaultValue: true);
|
||||
|
||||
/// OpenIM API 端口(服务端 docker 映射,默认 10002;TLS 时由反向代理走 443)
|
||||
const int apiPort = 10002;
|
||||
|
||||
/// OpenIM WebSocket 端口(默认 10001)
|
||||
@@ -20,17 +30,30 @@ const int livekitPort = 17880;
|
||||
/// 公司账号登录后端端口(B-60 account-server,docker 默认映射 10010)
|
||||
const int authApiPort = 10010;
|
||||
|
||||
/// OpenIM API 地址,例:http://192.168.200.11:10002
|
||||
const String apiAddr = 'http://$serverHost:$apiPort';
|
||||
const String _authHostOverride = String.fromEnvironment('AUTH_HOST', defaultValue: '');
|
||||
|
||||
/// OpenIM 消息长连接地址,例:ws://192.168.200.11:10001
|
||||
const String wsAddr = 'ws://$serverHost:$wsPort';
|
||||
/// OpenIM API 地址
|
||||
String get apiAddr => useTls ? 'https://$serverHost' : 'http://$serverHost:$apiPort';
|
||||
|
||||
/// LiveKit 连接地址,例:ws://192.168.200.11:17880
|
||||
const String livekitUrl = 'ws://$serverHost:$livekitPort';
|
||||
/// OpenIM 消息长连接地址
|
||||
String get wsAddr => useTls ? 'wss://$serverHost/msg_gateway' : 'ws://$serverHost:$wsPort';
|
||||
|
||||
/// 公司账号登录后端地址,例:http://192.168.200.11:10010
|
||||
/// LiveKit 连接地址
|
||||
String get livekitUrl => useTls ? 'wss://$serverHost/livekit' : 'ws://$serverHost:$livekitPort';
|
||||
|
||||
/// 公司账号登录后端地址
|
||||
///
|
||||
/// 仅本地联调用:`--dart-define=AUTH_HOST=127.0.0.1` 可覆盖登录服务地址(OpenIM/LiveKit
|
||||
/// 仍走 [serverHost]),正式打包不带该参数则与 [serverHost] 一致。
|
||||
const String authApiBase = 'http://${String.fromEnvironment('AUTH_HOST', defaultValue: serverHost)}:$authApiPort';
|
||||
String get authApiBase {
|
||||
if (_authHostOverride.isNotEmpty) {
|
||||
return 'http://$_authHostOverride:$authApiPort';
|
||||
}
|
||||
return useTls ? 'https://$serverHost/account' : 'http://$serverHost:$authApiPort';
|
||||
}
|
||||
|
||||
/// 极光 AppKey(可进客户端;Master Secret 禁止出现在客户端或仓库)
|
||||
const String jpushAppKey = '95fc2352648a7d4568ac8d72';
|
||||
|
||||
/// 登录页自检:探测消息长连接入口(TLS 走反向代理路径,明文走宿主端口)
|
||||
String get wsProbeUrl => useTls ? 'https://$serverHost/msg_gateway' : 'http://$serverHost:$wsPort/';
|
||||
|
||||
@@ -7,10 +7,12 @@ import 'screens/home_screen.dart';
|
||||
import 'screens/login_screen.dart';
|
||||
import 'services/call_service.dart';
|
||||
import 'services/im_service.dart';
|
||||
import 'services/push_service.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
PushService.instance.init();
|
||||
runApp(const ChanglianApp());
|
||||
}
|
||||
|
||||
@@ -27,6 +29,7 @@ class _ChanglianAppState extends State<ChanglianApp> {
|
||||
super.initState();
|
||||
// 被踢下线 / token 失效:清凭证、回登录页
|
||||
IMService.instance.onForceLogout = () async {
|
||||
await PushService.instance.unbindUser();
|
||||
await LoginScreen.clearCredential();
|
||||
navigatorKey.currentState?.pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
@@ -90,6 +93,7 @@ class _SplashGateState extends State<SplashGate> {
|
||||
await im.init().timeout(const Duration(seconds: 20));
|
||||
await im.login(userID: credential.$1, token: credential.$2).timeout(const Duration(seconds: 20));
|
||||
CallService.instance.start();
|
||||
await PushService.instance.bindUser(credential.$1);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../config.dart';
|
||||
import '../services/auth_api.dart';
|
||||
import '../services/call_service.dart';
|
||||
import '../services/im_service.dart';
|
||||
import '../services/push_service.dart';
|
||||
import '../theme.dart';
|
||||
import 'home_screen.dart';
|
||||
|
||||
@@ -92,7 +93,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
);
|
||||
// 3. 启动通话信令监听
|
||||
CallService.instance.start();
|
||||
// 4. 存凭证用于下次自动登录
|
||||
// 4. 极光 alias = OpenIM userID,供服务端离线推送
|
||||
await PushService.instance.bindUser(result.userID);
|
||||
// 5. 存凭证用于下次自动登录
|
||||
await LoginScreen.saveCredential(result.userID, result.token, result.nickname);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
@@ -150,7 +153,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final results = await Future.wait([
|
||||
probe('账号服务', '$authApiBase/api/health'),
|
||||
probe('消息接口', '$apiAddr/'),
|
||||
probe('消息长连接', 'http://$serverHost:$wsPort/'),
|
||||
probe('消息长连接', wsProbeUrl),
|
||||
]);
|
||||
return '自检:${results.join(',')}';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
|
||||
import '../services/im_service.dart';
|
||||
import '../services/push_service.dart';
|
||||
import '../theme.dart';
|
||||
import '../widgets/avatar.dart';
|
||||
import 'login_screen.dart';
|
||||
@@ -166,6 +167,7 @@ class _MineScreenState extends State<MineScreen> {
|
||||
onPressed: () async {
|
||||
Navigator.of(ctx).pop();
|
||||
await LoginScreen.clearCredential();
|
||||
await PushService.instance.unbindUser();
|
||||
await IMService.instance.logout();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
|
||||
@@ -50,7 +50,12 @@ class MessageSendService {
|
||||
message: message,
|
||||
userID: isGroup ? null : peer,
|
||||
groupID: isGroup ? groupID : null,
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
offlinePushInfo: OfflinePushInfo(
|
||||
title: '畅联',
|
||||
desc: '你收到一条新消息',
|
||||
iOSBadgeCount: true,
|
||||
iOSPushSound: '+1',
|
||||
),
|
||||
);
|
||||
return sent;
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jpush_flutter/jpush_flutter.dart';
|
||||
import 'package:jpush_flutter/jpush_interface.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../config.dart';
|
||||
import '../nav.dart';
|
||||
import '../screens/home_screen.dart';
|
||||
import 'im_service.dart';
|
||||
|
||||
/// 极光推送封装:初始化、通知权限、Registration ID、把 OpenIM userID 设为 alias。
|
||||
///
|
||||
/// OpenIM 服务端 JPUSH 离线推送按 alias=userID 下发,因此登录后必须 setAlias。
|
||||
/// 本阶段只接安卓;iOS 不调用 setup。
|
||||
class PushService {
|
||||
PushService._();
|
||||
|
||||
static final PushService instance = PushService._();
|
||||
|
||||
final JPushFlutterInterface _jpush = JPush.newJPush();
|
||||
bool _ready = false;
|
||||
String? _boundAlias;
|
||||
|
||||
Future<void> init() async {
|
||||
if (_ready) return;
|
||||
if (kIsWeb || !Platform.isAndroid) return;
|
||||
try {
|
||||
_jpush.addEventHandler(
|
||||
onReceiveNotification: (message) async {},
|
||||
onOpenNotification: (message) async {
|
||||
_onNotificationClick();
|
||||
},
|
||||
onReceiveMessage: (message) async {},
|
||||
onReceiveNotificationAuthorization: (message) async {},
|
||||
onConnected: (message) async {},
|
||||
onCommandResult: (message) async {},
|
||||
);
|
||||
_jpush.setAuth(enable: true);
|
||||
_jpush.setup(
|
||||
appKey: jpushAppKey,
|
||||
channel: 'developer-default',
|
||||
production: true,
|
||||
debug: false,
|
||||
);
|
||||
_jpush.applyPushAuthority(
|
||||
const NotificationSettingsIOS(sound: true, alert: true, badge: true),
|
||||
);
|
||||
await _requestAndroidNotificationPermission();
|
||||
_ready = true;
|
||||
await _jpush.getRegistrationID();
|
||||
} catch (_) {
|
||||
// 推送失败不能挡住登录和聊天
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> bindUser(String userID) async {
|
||||
if (userID.isEmpty) return;
|
||||
await init();
|
||||
if (!_ready) return;
|
||||
try {
|
||||
await _jpush.setAlias(userID);
|
||||
_boundAlias = userID;
|
||||
} catch (_) {
|
||||
// alias 失败时下次登录再试
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unbindUser() async {
|
||||
if (!_ready) return;
|
||||
try {
|
||||
await _jpush.deleteAlias();
|
||||
} catch (_) {
|
||||
// 本地照常清理
|
||||
}
|
||||
_boundAlias = null;
|
||||
}
|
||||
|
||||
String? get boundAlias => _boundAlias;
|
||||
|
||||
Future<void> _requestAndroidNotificationPermission() async {
|
||||
try {
|
||||
final status = await Permission.notification.status;
|
||||
if (!status.isGranted) {
|
||||
await Permission.notification.request();
|
||||
}
|
||||
} catch (_) {
|
||||
// 权限申请失败不阻断
|
||||
}
|
||||
}
|
||||
|
||||
void _onNotificationClick() {
|
||||
final nav = navigatorKey.currentState;
|
||||
if (nav == null) return;
|
||||
if (!IMService.instance.loggedIn) return;
|
||||
nav.pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const HomeScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user