Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit bbb642ff
authored
2026-06-03 14:59:34 +0800
by
tanghuan
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
iot更新
1 parent
a2c3bf0e
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
326 additions
and
2 deletions
android/app/src/main/AndroidManifest.xml
lib/bloc/setting/setting_cubit.dart
lib/bloc/web_cubit.dart
lib/config/constant.dart
lib/config/locator.dart
lib/data/repositories/message/open_setting_handler.dart
lib/data/repositories/version_repository.dart
lib/main.dart
lib/services/app_upgrade_service.dart
pubspec.yaml
android/app/src/main/AndroidManifest.xml
View file @
bbb642f
...
...
@@ -18,6 +18,9 @@
<uses-permission
android:name=
"android.permission.READ_MEDIA_IMAGES"
/>
<uses-permission
android:name=
"android.permission.READ_MEDIA_VIDEO"
/>
<!-- APP 自动更新:安装未知来源应用 -->
<uses-permission
android:name=
"android.permission.REQUEST_INSTALL_PACKAGES"
/>
<application
android:label=
"班小二"
...
...
lib/bloc/setting/setting_cubit.dart
View file @
bbb642f
...
...
@@ -3,6 +3,7 @@ import 'dart:io';
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/services/app_upgrade_service.dart'
;
import
'package:equatable/equatable.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:fluwx/fluwx.dart'
;
...
...
@@ -80,6 +81,9 @@ class SettingCubit extends Cubit<SettingState> {
// IM 登出
// await getIt.get<ImService>().logout();
// 退出登录后,停止APP自动更新轮询
getIt
.
get
<
AppUpgradeService
>().
stop
();
router
.
go
(
'/loginMain'
);
}
...
...
lib/bloc/web_cubit.dart
View file @
bbb642f
...
...
@@ -8,6 +8,7 @@ import 'package:appframe/config/locator.dart';
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/data/models/message/h5_message.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
import
'package:appframe/services/app_upgrade_service.dart'
;
import
'package:appframe/services/im_service.dart'
;
import
'package:appframe/services/local_server_service.dart'
;
import
'package:appframe/services/player_service.dart'
;
...
...
@@ -264,6 +265,9 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 登录IM
_loginIM
();
// 登录成功后启动 APP 自动更新轮询(仅 Android 内部生效)
getIt
.
get
<
AppUpgradeService
>().
start
();
// 注册 WebCubit 实例,供其它页面使用
WebCubitHolder
.
register
(
this
);
}
...
...
lib/config/constant.dart
View file @
bbb642f
...
...
@@ -49,8 +49,12 @@ class Constant {
/// 版本相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// app 版本号规则
static
const
String
appVersion
=
EnvConfig
.
version
;
/// app 版本号
///
/// 实际取值来源于 pubspec.yaml 的 version 字段,
/// 在 main.dart 中通过 package_info_plus 读取后赋值;
/// EnvConfig.version 仅作为读取失败时的兜底默认值。
static
String
appVersion
=
EnvConfig
.
version
;
/// H5的起始终最低版本号规则
static
String
h5Version
=
'0.2.6'
;
...
...
@@ -68,6 +72,13 @@ class Constant {
// ? 'http://192.168.2.177/xeapp_conf_dev.json'
:
'https://bxe-obs.banxiaoer.com/conf/xeapp_conf_pro.json'
;
/// Android APP 最新版本配置地址(用于 APP 自动更新)
/// APP 自动更新轮询间隔
static
const
Duration
appUpgradeCheckInterval
=
Duration
(
minutes:
5
);
/// APP 自动更新时,下载到本地的 APK 缓存子目录
static
const
String
appUpgradeApkDir
=
'app_upgrade'
;
/// 内部 H5 dist 目录
static
const
String
h5DistDir
=
'http_dist_assets'
;
...
...
lib/config/locator.dart
View file @
bbb642f
...
...
@@ -42,8 +42,10 @@ import 'package:appframe/data/repositories/message/window_info_handler.dart';
import
'package:appframe/data/repositories/phone_auth_repository.dart'
;
import
'package:appframe/data/repositories/user_auth_repository.dart'
;
import
'package:appframe/data/repositories/subs_repository.dart'
;
import
'package:appframe/data/repositories/version_repository.dart'
;
import
'package:appframe/data/repositories/wechat_auth_repository.dart'
;
import
'package:appframe/services/api_service.dart'
;
import
'package:appframe/services/app_upgrade_service.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
import
'package:appframe/services/im_service.dart'
;
import
'package:appframe/services/local_server_service.dart'
;
...
...
@@ -232,6 +234,9 @@ Future<void> setupLocator() async {
/// imService
getIt
.
registerSingleton
<
ImService
>(
ImService
());
/// APP 自动更新服务(仅 Android 生效)
getIt
.
registerLazySingleton
<
AppUpgradeService
>(()
=>
AppUpgradeService
());
/// 播放
getIt
.
registerSingleton
<
PlayerService
>(
PlayerService
());
...
...
@@ -243,5 +248,6 @@ Future<void> setupLocator() async {
getIt
.
registerLazySingleton
<
PhoneAuthRepository
>(()
=>
PhoneAuthRepository
());
getIt
.
registerLazySingleton
<
UserAuthRepository
>(()
=>
UserAuthRepository
());
getIt
.
registerLazySingleton
<
SubsRepository
>(()
=>
SubsRepository
());
getIt
.
registerLazySingleton
<
VersionRepository
>(()
=>
VersionRepository
());
}
lib/data/repositories/message/open_setting_handler.dart
View file @
bbb642f
...
...
@@ -4,6 +4,7 @@ import 'package:app_settings/app_settings.dart';
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/services/app_upgrade_service.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
import
'package:fluwx/fluwx.dart'
;
import
'package:path_provider/path_provider.dart'
;
...
...
@@ -174,6 +175,9 @@ class OpenSettingHandler extends MessageHandler {
// IM 登出
// await getIt.get<ImService>().logout();
// 退出登录后,停止APP自动更新轮询
getIt
.
get
<
AppUpgradeService
>().
stop
();
router
.
go
(
'/loginMain'
);
}
}
lib/data/repositories/version_repository.dart
0 → 100644
View file @
bbb642f
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/services/api_service.dart'
;
import
'package:dio/dio.dart'
;
class
VersionRepository
{
late
final
ApiService
_appService
;
VersionRepository
()
{
_appService
=
getIt
<
ApiService
>(
instanceName:
'appApiService'
);
}
///
/// 参数
/// {
/// "userid":"xxxxxxx",
/// "ver":"1.0.9",
/// "sence":"xxj"
/// }
/// 返回
/// {"code":0,"data":{"force":0,"lastVersion":"","url":""},"message":"查询成功"}
///
///
Future
<
dynamic
>
globalVersion
(
String
userid
,
String
ver
,
String
sence
)
async
{
Response
resp
=
await
_appService
.
get
(
'/api/v1/comm/golbal/version'
,
queryParameters:
{
"userid"
:
userid
,
"ver"
:
ver
,
"sence"
:
sence
,
},
);
return
resp
.
data
;
}
}
lib/main.dart
View file @
bbb642f
...
...
@@ -8,6 +8,7 @@ import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import
'package:archive/archive.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:package_info_plus/package_info_plus.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
import
'app.dart'
;
...
...
@@ -16,12 +17,28 @@ void main() async {
WidgetsFlutterBinding
.
ensureInitialized
();
await
setupLocator
();
await
_initAppVersion
();
await
_initH5Version
();
await
_initIOSGesture
();
runApp
(
const
App
());
}
/// 从 pubspec.yaml 中读取并初始化 APP 版本号
///
/// pubspec.yaml 中的 version 字段作为全局唯一数据源,
/// 避免与构建参数、硬编码出现不一致。
Future
<
void
>
_initAppVersion
()
async
{
try
{
final
info
=
await
PackageInfo
.
fromPlatform
();
if
(
info
.
version
.
isNotEmpty
)
{
Constant
.
appVersion
=
info
.
version
;
}
}
catch
(
e
)
{
debugPrint
(
'读取 APP 版本号失败,使用默认值:
$e
'
);
}
}
Future
<
void
>
_initH5Version
()
async
{
// 1 读取保存的H5版本
var
h5Version
=
getIt
.
get
<
SharedPreferences
>().
getString
(
Constant
.
h5VersionKey
);
...
...
lib/services/app_upgrade_service.dart
0 → 100644
View file @
bbb642f
import
'dart:async'
;
import
'dart:io'
;
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/data/repositories/version_repository.dart'
;
import
'package:dio/dio.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:open_file/open_file.dart'
;
import
'package:path_provider/path_provider.dart'
;
import
'package:permission_handler/permission_handler.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
/// APP 自动更新服务(仅 Android)
///
/// 功能:
/// 1. 登录成功后每 [Constant.appUpgradeCheckInterval] 轮询服务端最新版本配置;
/// 2. 与当前 APP 版本([Constant.appVersion],来源于 pubspec.yaml)比较,判断是否需要更新;
/// 3. 需要更新时下载 APK 并调用系统安装界面进行覆盖安装。
class
AppUpgradeService
{
late
final
VersionRepository
_versionRepository
;
AppUpgradeService
()
{
_versionRepository
=
getIt
.
get
<
VersionRepository
>();
}
Timer
?
_timer
;
/// 是否正在执行一次完整的检测/下载/安装流程,避免并发触发
bool
_busy
=
false
;
/// 已成功触发安装的版本,避免重复下载与安装
String
?
_installedVersion
;
/// 启动定时轮询。仅 Android 平台生效,仅在登录成功后调用。
void
start
()
{
if
(!
Platform
.
isAndroid
)
{
return
;
}
if
(
_timer
!=
null
)
{
return
;
}
// 立即执行一次,再按周期循环
_check
();
_timer
=
Timer
.
periodic
(
Constant
.
appUpgradeCheckInterval
,
(
_
)
=>
_check
());
}
/// 停止轮询
void
stop
()
{
_timer
?.
cancel
();
_timer
=
null
;
}
Future
<
void
>
_check
()
async
{
if
(
_busy
)
{
return
;
}
_busy
=
true
;
try
{
final
remote
=
await
_fetchLatestVersion
();
if
(
remote
==
null
)
{
return
;
}
final
remoteVersion
=
remote
[
'version'
];
final
remoteUrl
=
remote
[
'url'
];
if
(
remoteVersion
==
null
||
remoteVersion
.
isEmpty
||
remoteUrl
==
null
||
remoteUrl
.
isEmpty
)
{
return
;
}
// 已安装过同一版本,跳过(避免重复弹安装框)
if
(
_installedVersion
==
remoteVersion
)
{
return
;
}
// 版本一致或本地版本更高则不升级
if
(!
_isNewerVersion
(
remoteVersion
,
Constant
.
appVersion
))
{
return
;
}
final
apkPath
=
await
_downloadApk
(
remoteVersion
,
remoteUrl
);
if
(
apkPath
==
null
)
{
return
;
}
final
ok
=
await
_ensureInstallPermission
();
if
(!
ok
)
{
debugPrint
(
'[AppUpgrade] 用户未授予未知应用安装权限'
);
return
;
}
final
r
=
await
OpenFile
.
open
(
apkPath
,
type:
'application/vnd.android.package-archive'
);
debugPrint
(
'[AppUpgrade] 触发安装: type=
${r.type}
message=
${r.message}
'
);
if
(
r
.
type
==
ResultType
.
done
)
{
_installedVersion
=
remoteVersion
;
}
}
catch
(
e
,
s
)
{
debugPrint
(
'[AppUpgrade] 检测/升级失败:
$e
\n
$s
'
);
}
finally
{
_busy
=
false
;
}
}
/// 从服务端拉取最新版本信息
Future
<
Map
<
String
,
String
>?>
_fetchLatestVersion
()
async
{
try
{
var
sharedPreferences
=
getIt
.
get
<
SharedPreferences
>();
var
userCode
=
sharedPreferences
.
getString
(
'auth_userCode'
)
??
''
;
if
(
userCode
.
isEmpty
)
{
return
null
;
}
final
result
=
await
_versionRepository
.
globalVersion
(
userCode
,
Constant
.
appVersion
,
'xxj'
,
);
if
(
result
==
null
||
result
is
!
Map
)
{
return
null
;
}
final
code
=
result
[
'code'
];
if
(
code
!=
0
)
{
return
null
;
}
final
data
=
result
[
'data'
];
if
(
data
==
null
||
data
is
!
Map
)
{
return
null
;
}
return
{
'version'
:
(
data
[
'lastVersion'
]
??
''
).
toString
(),
'url'
:
(
data
[
'url'
]
??
''
).
toString
(),
};
// return {
// 'version': '1.0.9',
// 'url': 'https://bxe-files.obs.cn-north-4.myhuaweicloud.com/pubs/apks/test/bxeapp-dev-1.0.2606011.apk',
// };
}
catch
(
e
)
{
debugPrint
(
'[AppUpgrade] 拉取版本配置失败:
$e
'
);
return
null
;
}
}
/// 下载 APK 到本地缓存目录,返回本地路径。已存在则直接复用。
Future
<
String
?>
_downloadApk
(
String
version
,
String
url
)
async
{
final
baseDir
=
await
getExternalStorageDirectory
()
??
await
getApplicationSupportDirectory
();
final
dirPath
=
'
${baseDir.path}
/
${Constant.appUpgradeApkDir}
'
;
final
dir
=
Directory
(
dirPath
);
if
(!
await
dir
.
exists
())
{
await
dir
.
create
(
recursive:
true
);
}
final
apkPath
=
'
$dirPath
/app_
$version
.apk'
;
final
apkFile
=
File
(
apkPath
);
if
(
await
apkFile
.
exists
()
&&
await
apkFile
.
length
()
>
0
)
{
return
apkPath
;
}
// 下载到 .tmp,下载完成后再重命名,避免半成品文件被识别为有效 APK
final
tmpPath
=
'
$apkPath
.tmp'
;
final
tmpFile
=
File
(
tmpPath
);
if
(
await
tmpFile
.
exists
())
{
await
tmpFile
.
delete
();
}
final
dio
=
Dio
();
try
{
final
resp
=
await
dio
.
download
(
url
,
tmpPath
,
options:
Options
(
receiveTimeout:
const
Duration
(
minutes:
10
)),
onReceiveProgress:
(
received
,
total
)
{
if
(
total
>
0
)
{
final
percent
=
(
received
/
total
*
100
).
toStringAsFixed
(
0
);
debugPrint
(
'[AppUpgrade] 下载
$version
:
$percent
%'
);
}
},
);
if
(
resp
.
statusCode
!=
200
)
{
debugPrint
(
'[AppUpgrade] 下载失败: statusCode=
${resp.statusCode}
url=
$url
'
);
if
(
await
tmpFile
.
exists
())
{
await
tmpFile
.
delete
();
}
return
null
;
}
await
tmpFile
.
rename
(
apkPath
);
return
apkPath
;
}
catch
(
e
)
{
debugPrint
(
'[AppUpgrade] 下载 APK 失败:
$e
'
);
if
(
await
tmpFile
.
exists
())
{
try
{
await
tmpFile
.
delete
();
}
catch
(
_
)
{}
}
return
null
;
}
finally
{
dio
.
close
(
force:
true
);
}
}
/// 确保 Android 8.0+ 下具有"未知应用安装"权限
Future
<
bool
>
_ensureInstallPermission
()
async
{
if
(!
Platform
.
isAndroid
)
{
return
true
;
}
final
status
=
await
Permission
.
requestInstallPackages
.
status
;
if
(
status
.
isGranted
)
{
return
true
;
}
final
result
=
await
Permission
.
requestInstallPackages
.
request
();
return
result
.
isGranted
;
}
/// 判断 [remote] 是否比 [current] 更新
/// 版本格式:x.y.z(不足位补 0)
bool
_isNewerVersion
(
String
remote
,
String
current
)
{
final
r
=
_parseVersion
(
remote
);
final
c
=
_parseVersion
(
current
);
final
len
=
r
.
length
>
c
.
length
?
r
.
length
:
c
.
length
;
for
(
var
i
=
0
;
i
<
len
;
i
++)
{
final
a
=
i
<
r
.
length
?
r
[
i
]
:
0
;
final
b
=
i
<
c
.
length
?
c
[
i
]
:
0
;
if
(
a
>
b
)
{
return
true
;
}
if
(
a
<
b
)
{
return
false
;
}
}
return
false
;
}
List
<
int
>
_parseVersion
(
String
v
)
{
return
v
.
split
(
'.'
)
.
map
((
e
)
=>
int
.
tryParse
(
e
.
replaceAll
(
RegExp
(
r'[^0-9]'
),
''
))
??
0
)
.
toList
();
}
}
pubspec.yaml
View file @
bbb642f
...
...
@@ -18,6 +18,7 @@ dependencies:
archive
:
^4.0.7
connectivity_plus
:
^7.0.0
device_info_plus
:
^11.5.0
package_info_plus
:
^8.3.1
dio
:
^5.9.0
equatable
:
^2.0.7
get_it
:
^8.2.0
...
...
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment