Commit 02ea609e by ethanlamzs

Merge branch 'feature-2607-pay' into feature-2604-menu-iosvip

2 parents 24c81ee0 333e28ac
......@@ -102,7 +102,7 @@ dependencies {
// XiaoMi
//implementation("com.tencent.timpush:xiaomi:8.7.7201")
// OPPO
implementation("com.tencent.timpush:oppo:8.8.7357")
//implementation("com.tencent.timpush:oppo:8.8.7357")
// vivo
//implementation("com.tencent.timpush:vivo:8.8.7357")
// Honor
......
#################################################
# 脚本代码中不能含有中文,否则默认PowerShell环境可能会报错
#################################################
# 1. 读取脚本参数
#-------------------------------------------
param(
# app打包环境:dev, prod
[Parameter(Mandatory = $true)]
[ValidateSet("dev", "pro")]
[string]$appenv,
# App版本,例如:1.0.2512181
[Parameter(Mandatory = $true)]
[string]$version,
# 品牌, 例如: vivo, oppo, xiaomi, honor
[Parameter(Mandatory = $true)]
[string]$brand,
# 下载地址
[string]$url = "https://bxe.obs.cn-north-4.myhuaweicloud.com/fronts/material/xehybrid/assets/basepkg/base-$appenv.zip",
# 下载保存文件的路径
[string]$downloadPath = "C:\Users\tang-\StudioProjects\appframe\assets\base-$appenv.zip",
# App路径
[string]$appPath = "C:\Users\tang-\StudioProjects\appframe"
)
# 2. 验证目录存在
#-------------------------------------------
if (-not (Test-Path $appPath))
{
Write-Error "appPath not exists: $appPath"
exit 1
}
# 确保下载目录存在
$downloadDir = Split-Path $downloadPath -Parent
if (-not (Test-Path $downloadDir))
{
Write-Error "the path for save file does not exists: $appPath"
exit 1
}
# 3. 下载文件(带错误处理)
#-------------------------------------------
try
{
Write-Host "downloading from : $url" -ForegroundColor Green
Write-Host "save to : $downloadPath" -ForegroundColor Green
Invoke-WebRequest -Uri $url -OutFile $downloadPath -ErrorAction Stop
if (Test-Path $downloadPath)
{
Write-Host "download success!" -ForegroundColor Green
}
else
{
Write-Error "download failed!"
exit 1
}
}
catch
{
Write-Error "downloading failed: $_"
exit 1
}
# 4. 执行 flutter build
#-------------------------------------------
try
{
Set-Location -Path $appPath -ErrorAction Stop
Write-Host "flutter building ..." -ForegroundColor Green
Write-Host "appenv: $appenv, version: $version, brand: $brand" -ForegroundColor Cyan
flutter build apk `
--release `
--split-per-abi `
--target-platform android-arm64 `
--dart-define=env=$appenv `
--dart-define=version=$version `
--dart-define=brand=$brand
if ($LASTEXITCODE -eq 0)
{
Write-Host "build success!" -ForegroundColor Green
}
else
{
Write-Error "build failed!"
exit $LASTEXITCODE
}
}
catch
{
Write-Error "building failed: $_"
exit 1
}
# 5. 复制生成新命名的APK文件
# "build\app\outputs\flutter-apk\app-arm64-v8a-release.apk"
#-------------------------------------------
try
{
$apkOutputDir = Join-Path $appPath "build\app\outputs\flutter-apk"
$sourceApk = Join-Path $apkOutputDir "app-arm64-v8a-release.apk"
$newApkName = "bxeapp-$appenv-$version-$brand.apk"
$destApkPath = Join-Path $apkOutputDir $newApkName
if (Test-Path $sourceApk)
{
Copy-Item -Path $sourceApk -Destination $destApkPath -Force
Write-Host "Copied and renamed: $newApkName" -ForegroundColor Yellow
}
else
{
Write-Warning "Source APK not found: $sourceApk"
}
}
catch
{
Write-Error "Failed to copy and rename APK: $_"
}
......@@ -54,3 +54,6 @@ flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 0000
#flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008030-001C75810E42402E
flutter build ios --build-number=$_ver
......@@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class LoginPhoneState extends Equatable {
final bool agreed;
final bool showAgreed;
final bool showGuide;
final bool allowSend;
final int seconds;
......@@ -20,6 +21,7 @@ class LoginPhoneState extends Equatable {
const LoginPhoneState({
this.agreed = false,
this.showAgreed = false,
this.showGuide = false,
this.allowSend = true,
this.seconds = 0,
});
......@@ -27,12 +29,14 @@ class LoginPhoneState extends Equatable {
LoginPhoneState copyWith({
bool? agreed,
bool? showAgreed,
bool? showGuide,
bool? allowSend,
int? seconds,
}) {
return LoginPhoneState(
agreed: agreed ?? this.agreed,
showAgreed: showAgreed ?? this.showAgreed,
showGuide: showGuide ?? this.showGuide,
allowSend: allowSend ?? this.allowSend,
seconds: seconds ?? this.seconds,
);
......@@ -42,6 +46,7 @@ class LoginPhoneState extends Equatable {
List<Object?> get props => [
agreed,
showAgreed,
showGuide,
allowSend,
seconds,
];
......@@ -103,7 +108,16 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
return;
}
if (result['code'] != 0) {
Fluttertoast.showToast(msg: result['error'], gravity: ToastGravity.TOP, backgroundColor: Colors.red);
final err = result['error'] ?? '';
// 手机号未注册时,打开指引界面
if(err.contains('手机号尚未注册用户')) {
if(!state.showGuide) {
emit(state.copyWith(showGuide: true));
}
return;
}
Fluttertoast.showToast(msg: err, gravity: ToastGravity.TOP, backgroundColor: Colors.red);
return;
}
......@@ -168,6 +182,10 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
emit(state.copyWith(showAgreed: false));
}
void cancelShowGuide() {
emit(state.copyWith(showGuide: false));
}
void goLoginMain() {
router.go('/loginMain');
}
......
......@@ -22,6 +22,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
import 'package:volume_controller/volume_controller.dart';
import 'package:webview_flutter/webview_flutter.dart';
......@@ -34,6 +35,7 @@ class WebState extends Equatable {
final bool loaded;
final bool isUpgrading;
final bool suggestUpgrade;
final bool suggestAppUpgrade;
final String title;
final int titleColor;
......@@ -82,6 +84,7 @@ class WebState extends Equatable {
this.loaded = false,
this.isUpgrading = false,
this.suggestUpgrade = false,
this.suggestAppUpgrade = false,
this.title = '',
this.titleColor = 0xFFFFFFFF,
this.bgColor = 0xFF7691FA,
......@@ -113,6 +116,7 @@ class WebState extends Equatable {
bool? loaded,
bool? isUpgrading,
bool? suggestUpgrade,
bool? suggestAppUpgrade,
String? title,
int? titleColor,
int? bgColor,
......@@ -143,6 +147,7 @@ class WebState extends Equatable {
loaded: loaded ?? this.loaded,
isUpgrading: isUpgrading ?? this.isUpgrading,
suggestUpgrade: suggestUpgrade ?? this.suggestUpgrade,
suggestAppUpgrade: suggestAppUpgrade ?? this.suggestAppUpgrade,
title: title ?? this.title,
titleColor: titleColor ?? this.titleColor,
bgColor: bgColor ?? this.bgColor,
......@@ -176,6 +181,7 @@ class WebState extends Equatable {
loaded,
isUpgrading,
suggestUpgrade,
suggestAppUpgrade,
title,
titleColor,
bgColor,
......@@ -244,6 +250,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 遮罩界面
emit(state.copyWith(isUpgrading: true));
await _downloadH5Zip(configVersion, downloadUrl);
// 此处提前设置了新的H5业务包版本号,接下来启动本地服务的时候,解压对应版本的zip包
_setH5Version(configVersion);
// 下载完成后取消遮罩,继续初始化其它数据
emit(state.copyWith(isUpgrading: false));
......@@ -251,6 +258,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 后台下载,完成后提示用户
_downloadH5Zip(configVersion, downloadUrl).then(
(value) {
// 此处提前设置了新的H5业务包版本号,下次启动本地服务的时候,解压对应版本的zip包
_setH5Version(configVersion);
emit(state.copyWith(suggestUpgrade: true));
},
......@@ -300,10 +308,18 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
String version = response.data['version'] as String;
String force = response.data['force'] as String;
String zip = response.data['zip'] as String;
String appversionCheck = response.data['appversionCheck'] as String? ?? '0';
String appversionAndroid = response.data['appversionAndroid'] as String? ?? '';
String appversionIos = response.data['appversionIos'] as String? ?? '';
return {
'version': version,
'force': force,
'zip': '$zip$version.zip',
'appversionCheck': appversionCheck,
'appversionAndroid': appversionAndroid,
'appversionIos': appversionIos,
};
} finally {
dio.close(force: true);
......@@ -704,34 +720,198 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
///
/// 升级提示
///
void suggestUpgrade(BuildContext ctx) {
showDialog(
context: ctx,
Future<void> suggestUpgrade(BuildContext context) async {
// showDialog(
// context: ctx,
// barrierDismissible: false,
// builder: (BuildContext context) {
// return AlertDialog(
// title: Text('温馨提示'),
// content: Text('资源已更新为最新版本,是否现在进行加载?'),
// actions: <Widget>[
// TextButton(
// child: Text('取消'),
// onPressed: () {
// Navigator.of(context).pop();
// emit(state.copyWith(suggestUpgrade: false));
// },
// ),
// TextButton(
// child: Text('确定'),
// onPressed: () {
// Navigator.of(context).pop();
// emit(state.copyWith(suggestUpgrade: false));
// router.go('/reload');
// },
// ),
// ],
// );
// },
// );
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('温馨提示'),
content: Text('资源已更新为最新版本,是否现在进行加载?'),
actions: <Widget>[
TextButton(
child: Text('取消'),
onPressed: () {
Navigator.of(context).pop();
emit(state.copyWith(suggestUpgrade: false));
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
TextButton(
child: Text('确定'),
onPressed: () {
Navigator.of(context).pop();
emit(state.copyWith(suggestUpgrade: false));
router.go('/reload');
},
),
title: Text(
'资源包更新',
style: TextStyle(
fontSize: 17,
color: Color(0xFF000000),
// fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text: '资源更新包已准备就绪,是否立即加载?',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
],
),
),
actions: [
Table(
children: [
TableRow(
children: [
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF666666),
textStyle: TextStyle(fontSize: 17),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('稍后再说'),
),
),
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop('OK');
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF7691FA),
textStyle: TextStyle(fontSize: 17),
minimumSize: Size.fromHeight(40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('立即加载'),
),
),
],
),
],
),
],
);
},
);
emit(state.copyWith(suggestUpgrade: false));
if (result != null) {
router.go('/reload');
}
}
Future<void> suggestAppUpdate(BuildContext context) async {
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
title: Text(
'发现新版本',
style: TextStyle(
fontSize: 17,
color: Color(0xFF000000),
// fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text: '已有新版 APP 可供下载,建议您立即下载更新以获得更好体验。',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
],
),
),
actions: [
Table(
children: [
TableRow(
children: [
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF666666),
textStyle: TextStyle(fontSize: 17),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('稍后再说'),
),
),
TableCell(
child: TextButton(
onPressed: () {
Navigator.of(context).pop('OK');
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF7691FA),
textStyle: TextStyle(fontSize: 17),
minimumSize: Size.fromHeight(40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('打开下载页面'),
),
),
],
),
],
),
],
);
},
);
emit(state.copyWith(suggestAppUpgrade: false));
if (result != null) {
final uri = Uri.parse('https://www.banxe.cn/apps/pro.html?t=${DateTime.now().millisecondsSinceEpoch}');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
///
......@@ -1242,6 +1422,74 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
///
bool _hasBeenResumed = false;
/// 版本检测并发控制标志
bool _isCheckingVersion = false;
/// H5资源已下载标志(本次生命周期内)
// bool _hasDownloadedH5 = false;
/// APP版本检测上次执行时间(2小时内只执行一次)
DateTime? _lastAppVersionCheckTime;
///
/// 从后台恢复时进行版本检测
/// 参照 _init() 方法中的版本检测逻辑
/// 检测或下载中时不重复处理
///
Future<void> _checkVersionUpgrade() async {
if (_isCheckingVersion /*|| _hasDownloadedH5*/) {
return;
}
_isCheckingVersion = true;
try {
var curVersion = getIt.get<SharedPreferences>().getString(Constant.h5VersionKey) ?? Constant.h5Version;
var versionConfig = await _getVersionConfig();
var configVersion = versionConfig['version'] as String;
var downloadUrl = versionConfig['zip'] as String;
var appversionCheck = versionConfig['appversionCheck'] as String;
var appversionAndroid = versionConfig['appversionAndroid'] as String;
var appversionIos = versionConfig['appversionIos'] as String;
if (appversionCheck == '1') {
final now = DateTime.now();
if (_lastAppVersionCheckTime == null || now.difference(_lastAppVersionCheckTime!).inHours >= 2) {
if ((Platform.isAndroid && appversionAndroid != '' && appversionAndroid != Constant.appVersion) ||
(Platform.isIOS && appversionIos != '' && appversionIos != Constant.appVersion)) {
emit(state.copyWith(suggestAppUpgrade: true));
}
_lastAppVersionCheckTime = now;
// 重置标志
_isCheckingVersion = false;
// 提示了APP升级后,不再进行H5版本检测
return;
}
}
if (Constant.needUpgrade && curVersion != configVersion) {
// 闭包中提前捕获变量,确保回调执行时读取的是当前值
final capturedConfigVersion = configVersion;
_downloadH5Zip(capturedConfigVersion, downloadUrl).then((_) {
_setH5Version(capturedConfigVersion);
// _hasDownloadedH5 = true;
emit(state.copyWith(suggestUpgrade: true));
}).catchError((e) {
debugPrint('后台H5资源下载失败 $e');
}).whenComplete(() {
_isCheckingVersion = false;
});
} else {
// 不需要下载时,立即重置标志
_isCheckingVersion = false;
}
} catch (e) {
debugPrint('后台H5版本检测失败 $e');
}
}
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState lifecycleState) async {
// 只针对iOS进行处理
......@@ -1264,27 +1512,30 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 从后台恢复时,调用 appOnShow 指令
var resp = {'unique': '', 'cmd': 'appOnShow', 'data': '', 'errMsg': ''};
_sendResponse(resp);
// 代表从后台恢复,进行版本检测
_checkVersionUpgrade();
}
// switch (state) {
// case AppLifecycleState.resumed:
// // 应用从后台回到前台时触发
// print('resumed--------------');
// debugPrint('======> lifecycleState resumed--------------');
// break;
// case AppLifecycleState.paused:
// // 应用进入后台时触发
// print('paused--------------');
// debugPrint('======> lifecycleState paused--------------');
// break;
// case AppLifecycleState.inactive:
// // 应用处于非活跃状态时触发
// print('inactive--------------');
// debugPrint('======> lifecycleState inactive--------------');
// break;
// case AppLifecycleState.detached:
// // 应用即将退出时触发
// print('detached--------------');
// debugPrint('======> lifecycleState detached--------------');
// break;
// case AppLifecycleState.hidden:
// print('hidden--------------');
// debugPrint('======> lifecycleState hidden--------------');
// break;
// }
}
......
......@@ -50,12 +50,18 @@ class Constant {
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// app 版本号规则
static const String appVersion = EnvConfig.version;
///
/// 实际取值来源于 pubspec.yaml 的 version 字段,
/// 在 main.dart 中通过 package_info_plus 读取后赋值;
/// EnvConfig.version 仅作为读取失败时的兜底默认值。
static String appVersion = EnvConfig.version;
/// H5的起始终最低版本号规则
static String h5Version = '0.2.6';
/// 当前的H5业务包的版本
/// 从 key=h5VersionKey 的缓存中获取,获取不到时值时,说明是第一次打开APP,则从基础业务包中读取的。
static String h5Version = '0.0.0';
/// H5的版本号存储的key
/// 方便升级判断处理,存储H5的版本号。下载安装包之后,会提前设置版本号,启动本地服务时,解压对应版本的zip包。
static const String h5VersionKey = 'h5_version';
/// 用于显示的H5版本号存储的key
......@@ -87,6 +93,9 @@ class Constant {
static const String wxAppId = 'wx8c32ea248f0c7765';
static const String universalLink = 'https://dev.banxiaoer.net/path/to/wechat/';
/// 微信支付商户号
static const String wxPartnerId = '1514530791';
/// IM 相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
......
......@@ -41,6 +41,7 @@ import 'package:appframe/data/repositories/message/wechat_qr_bind_handler.dart';
import 'package:appframe/data/repositories/message/video_info_handler.dart';
import 'package:appframe/data/repositories/message/wifi_info_handler.dart';
import 'package:appframe/data/repositories/message/window_info_handler.dart';
import 'package:appframe/data/repositories/message/xe_create_order_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';
......@@ -222,6 +223,9 @@ Future<void> setupLocator() async {
/// 设置屏幕模式
getIt.registerLazySingleton<MessageHandler>(() => ScreenHandler(), instanceName: 'setScreen');
/// 创建订单指令
getIt.registerLazySingleton<MessageHandler>(() => XeCreateOrderHandler(), instanceName: 'xeCreateOrder');
/// service
///
/// local server
......
import 'package:appframe/config/constant.dart';
import 'package:appframe/services/dispatcher.dart';
class AppInfoHandler extends MessageHandler {
@override
Future<Map<String, dynamic>> handleMessage(params) async {
return {"version": "0.1", "theme": "light"};
return {
"version": Constant.appVersion,
"theme": "light",
"appshop": '',
};
}
}
import 'dart:async';
import 'dart:convert';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter/foundation.dart';
import 'package:fluwx/fluwx.dart';
class XeCreateOrderHandler extends MessageHandler {
late final Fluwx _fluwx;
late final UserAuthRepository _userAuthRepository;
late FluwxCancelable _fluwxCancelable;
// 支付结果 Completer,用于将异步回调转为 await
Completer<WeChatPaymentResponse>? _payCompleter;
// 超时定时器
Timer? _timeoutTimer;
XeCreateOrderHandler() {
_fluwx = getIt.get<Fluwx>();
_userAuthRepository = getIt.get<UserAuthRepository>();
}
@override
Future<dynamic> handleMessage(params) async {
// 从 params 中提取支付参数
final String chargeCode = params['chargeCode'] as String? ?? '';
final int durationType = params['durationType'] as int? ?? 0;
final int duration = params['duration'] as int? ?? 0;
final int totalFee = params['totalFee'] as int? ?? 0;
final bool renew = params['renew'] as bool? ?? false;
final String phone = params['phone'] as String? ?? '';
final String userCode = params['userCode'] as String? ?? '';
final String tbxStuId = params['tbxStuId'] as String? ?? '';
final String stuId = params['stuId'] as String? ?? '';
// ext 为 JSON 格式字符串
final String ext = params['ext'] as String? ?? '{}';
// 解析 ext
final Map<String, dynamic> extMap = jsonDecode(ext);
final useCoin = extMap['useCoin'] as bool? ?? false;
debugPrint('PayHandler params: chargeCode=$chargeCode, durationType=$durationType, '
'duration=$duration, totalFee=$totalFee, renew=$renew, '
'phone=$phone, userCode=$userCode, tbxStuId=$tbxStuId, stuId=$stuId, '
'ext=$ext');
// 1. 检查微信是否安装
if (!await _fluwx.isWeChatInstalled) {
throw Exception('设备上未安装微信App,不支持微信支付');
}
// 2. 请求后端获取预支付订单信息
final deviceType = 'android';
var result = await _userAuthRepository.createOrder(userCode, tbxStuId, chargeCode, duration, durationType, totalFee,
renew, phone, stuId, ext, useCoin ? 1 : 0, deviceType);
if (result == null) {
throw Exception('获取支付信息失败');
}
var data = result['data'] as Map<String, dynamic>?;
if (data == null) {
throw Exception('支付参数获取失败');
}
// 3. 从后端响应中提取预支付ID
final prepayId = data['prepayId'] as String?;
if (prepayId == null || prepayId.isEmpty) {
throw Exception('预支付订单ID获取失败');
}
// 4. 从后端响应中提取支付参数(nonceStr、timestamp、paySign 由后端生成)
final appId = Constant.wxAppId;
final partnerId = Constant.wxPartnerId;
final packageValue = 'Sign=WXPay';
final nonceStr = data['nonceStr'] as String;
final timestamp = data['timeStamp'] as int;
final paySign = data['paySign'] as String;
final wxTradeNo = data['wxTradeNo'] as String;
debugPrint('prepayId: $prepayId');
debugPrint('nonceStr: $nonceStr');
debugPrint('timestamp: $timestamp');
debugPrint('paySign: $paySign');
debugPrint('appId: $appId');
debugPrint('partnerId: $partnerId');
debugPrint('packageValue: $packageValue');
debugPrint('wxTradeNo: $wxTradeNo');
if (nonceStr.isEmpty || paySign.isEmpty || wxTradeNo.isEmpty) {
throw Exception('支付参数获取失败');
}
// 5. 注册支付结果监听器
_payCompleter = Completer<WeChatPaymentResponse>();
_fluwxCancelable = _fluwx.addSubscriber(_responseListener);
// 6. 启动超时兜底(60秒 * 10)
_timeoutTimer = Timer(const Duration(seconds: 600), () {
if (_payCompleter != null && !_payCompleter!.isCompleted) {
_payCompleter!.completeError(TimeoutException('支付超时'));
}
});
// 7. 调用 fluwx.pay() 拉起微信支付
final payResult = await _fluwx.pay(
which: Payment(
// 微信开放平台 AppID
appId: appId,
// 商户号(固定)
partnerId: partnerId,
// 预支付ID(后端返回)
prepayId: prepayId,
// 固定值 Sign=WXPay
packageValue: packageValue,
// 随机字符串(后端返回)
nonceStr: nonceStr,
// 时间戳(后端返回,秒级)
timestamp: timestamp,
// 签名(后端生成)
sign: paySign,
),
);
if (!payResult) {
_cleanup();
throw Exception('拉起微信支付失败');
}
// 8. 等待支付结果回调
try {
final response = await _payCompleter!.future;
if (response.isSuccessful) {
debugPrint('wechat pay success: ${response.errStr}');
return {"orderUnique": wxTradeNo, "orderState": 1};
} else if (response.errCode == -2) {
debugPrint('wechat pay cancel: ${response.errStr}');
// 用户取消支付
// throw Exception('已取消支付');
return {"orderUnique": wxTradeNo, "orderState": 0};
} else {
debugPrint('wechat pay failed: ${response.errStr}');
// 支付失败
throw Exception('支付失败');
}
} catch (e) {
debugPrint('wechat pay error: $e');
throw Exception('支付异常:$e');
} finally {
_cleanup();
}
}
/// Fluwx 响应监听器
void _responseListener(WeChatResponse response) {
if (response is WeChatPaymentResponse) {
_timeoutTimer?.cancel();
if (_payCompleter != null && !_payCompleter!.isCompleted) {
_payCompleter!.complete(response);
}
}
}
/// 清理资源
void _cleanup() {
_fluwxCancelable.cancel();
_timeoutTimer?.cancel();
_timeoutTimer = null;
_payCompleter = null;
}
}
......@@ -129,4 +129,37 @@ class UserAuthRepository {
}
}
///
/// chargeCodes : bxe_vip
/// duration: durationType=1-天数|=2-月数|=3-yyyyMMdd|=4-学期数
/// durationType : 1.天|2.月|3.特定过期日期|4.学期|5.永久
/// totalFee: 经过计算后的实收价格,单位分
/// useCoin: 1.使用,0.不使用
///
Future<dynamic> createOrder(String userId, String tbxStuId, String chargeCodes, int duration, int durationType, int totalFee,
bool renew, String phone, String stuId, String ext, int useCoin, String deviceType) async {
try {
Response resp = await _appService.post(
'/api/v1/comm/user/buyvip',
{
"userId": userId,
"tbxStuId": tbxStuId,
"chargeCodes": chargeCodes,
"duration": duration,
"durationType": durationType,
"totalFee": totalFee,
"renew": renew,
"phone": phone,
"stuId": stuId,
"ext": ext,
"useCoin": useCoin,
"deviceType": deviceType
},
);
return resp.statusCode == HttpStatus.ok ? resp.data : null;
} on DioException {
return null;
}
}
}
......@@ -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);
......@@ -45,13 +62,17 @@ Future<void> _initH5Version() async {
var h5Version = json['version'] ?? Constant.h5Version;
Constant.h5Version = h5Version;
getIt.get<SharedPreferences>().setString(Constant.h5ShowVersionKey, h5Version);
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.setString(Constant.h5VersionKey, h5Version);
sharedPreferences.setString(Constant.h5ShowVersionKey, h5Version);
return;
}
}
} catch (e) {
Constant.h5Version = '0.0.0';
getIt.get<SharedPreferences>().setString(Constant.h5ShowVersionKey, '0.0.0');
var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.setString(Constant.h5VersionKey, '0.0.0');
sharedPreferences.setString(Constant.h5ShowVersionKey, '0.0.0');
debugPrint(e.toString());
}
}
......
......@@ -123,6 +123,8 @@ class LoginPhonePage extends StatelessWidget {
listener: (context, state) {
if (state.showAgreed) {
_showAgreementDialog(context, context.read<LoginPhoneCubit>());
} else if (state.showGuide) {
_showGuideDialog(context, context.read<LoginPhoneCubit>());
}
},
),
......@@ -438,4 +440,73 @@ class LoginPhonePage extends StatelessWidget {
loginPhoneCubit.cancelAgreed();
}
}
Future<void> _showGuideDialog(BuildContext context, LoginPhoneCubit loginPhoneCubit) async {
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
title: Text(
'手机号登录指引',
style: TextStyle(
fontSize: 17,
color: Color(0xFF000000),
// fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
// text: '感谢使用班小二APP,为保护您的个人权益,请仔细阅读并充分理解',
text: '输入的手机号尚未绑定用户,需先通过微信方式登录,并成功绑定手机号之后,才可使用手机号进行登录。点击查看',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
TextSpan(
text: '《手机号登录指引》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
router.push(
'/link',
extra: {'url': 'https://bxr.banxiaoer.net/apps/phoneGuide.html', 'title': '手机号登录指引'},
);
},
),
TextSpan(
text: ',了解绑定步骤。',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop('OK');
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF7691FA),
textStyle: TextStyle(fontSize: 17),
minimumSize: Size.fromHeight(40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('关闭'),
),
],
);
},
);
loginPhoneCubit.cancelShowGuide();
}
}
......@@ -6,6 +6,7 @@ import 'package:appframe/config/env_config.dart';
import 'package:appframe/config/routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingPage extends StatelessWidget {
const SettingPage({super.key});
......@@ -95,7 +96,10 @@ class SettingPage extends StatelessWidget {
onTap: () {
router.push(
'/link',
extra: {'url': 'https://bxr.banxiaoer.net/apps/versions.html', 'title': '版本记录'},
extra: {
'url': 'https://bxr.banxiaoer.net/apps/versions.html?t=${DateTime.now().millisecondsSinceEpoch}',
'title': '版本记录'
},
);
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
......@@ -111,7 +115,7 @@ class SettingPage extends StatelessWidget {
router.push(
'/link',
extra: {
'url': 'https://bxr.banxiaoer.net/apps/useragreement.html',
'url': 'https://bxr.banxiaoer.net/apps/useragreement.html?t=${DateTime.now().millisecondsSinceEpoch}',
'title': '用户协议'
},
);
......@@ -129,7 +133,7 @@ class SettingPage extends StatelessWidget {
router.push(
'/link',
extra: {
'url': 'https://bxr.banxiaoer.net/apps/privacysettings.html',
'url': 'https://bxr.banxiaoer.net/apps/privacysettings.html?t=${DateTime.now().millisecondsSinceEpoch}',
'title': '隐私设置'
},
);
......@@ -146,7 +150,10 @@ class SettingPage extends StatelessWidget {
onTap: () {
router.push(
'/link',
extra: {'url': 'https://bxr.banxiaoer.net/apps/produce.html', 'title': '关于'},
extra: {
'url': 'https://bxr.banxiaoer.net/apps/produce.html?t=${DateTime.now().millisecondsSinceEpoch}',
'title': '关于'
},
);
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
......@@ -158,6 +165,31 @@ class SettingPage extends StatelessWidget {
),
),
const SizedBox(height: 8),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.download, size: 20),
title: const Text('下载与更新', style: TextStyle(fontSize: 14)),
onTap: () async {
final uri = Uri.parse('https://www.banxe.cn/apps/pro.html?t=${DateTime.now().millisecondsSinceEpoch}');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
},
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
dense: true,
visualDensity: VisualDensity.compact,
),
],
),
),
const SizedBox(height: 8),
...customerService,
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
......@@ -235,7 +267,7 @@ class SettingPage extends StatelessWidget {
const SizedBox(height: 4),
],
const Text(
'V1.0',
'V1.0.10',
style: TextStyle(
color: Colors.grey,
fontSize: 12,
......
......@@ -154,6 +154,8 @@ class WebPage extends StatelessWidget {
context.read<WebCubit>().chooseVideo(context);
} else if (state.wechatQrBindCmdFlag) {
context.read<WebCubit>().showWechatQrBindDialog(context);
} else if (state.suggestAppUpgrade) {
context.read<WebCubit>().suggestAppUpdate(context);
}
},
),
......
......@@ -9,7 +9,8 @@ class VideoUtil {
///
/// 将视频格式转换为mp4
/// 转码的同时,根据文件大小自动选择压缩参数
/// iOS: h264_videotoolbox 硬件加速 + 按文件大小自动码率
/// iOS 18.6.x: libx264 + 按文件大小自动CRF(h264_videotoolbox在iOS 18.6.x存在稳定性问题)
/// 其它iOS版本: h264_videotoolbox 硬件加速 + 按文件大小自动码率
/// Android: libx264 + 按文件大小自动CRF
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
///
......@@ -23,14 +24,24 @@ class VideoUtil {
final fileSize = await File(inputPath).length();
String cmd;
if (Platform.isIOS) {
if (Platform.isIOS /*&& !_isIos18_6()*/) {
final bitrate = _getBitrateByFileSize(fileSize);
// cmd = '-i "$inputPath" '
// '-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
// '-b:v ${bitrate}k ' // 根据文件大小自动计算码率
// '-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
// '-c:a aac ' // 音频转为 AAC (兼容性最好)
// '-b:a 128k ' // 音频码率
// '"$outputPath"';
cmd = '-i "$inputPath" '
'-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
'-b:v ${bitrate}k ' // 根据文件大小自动计算码率
'-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
'-c:a aac ' // 音频转为 AAC (兼容性最好)
'-b:a 128k ' // 音频码率
'-c:v h264_videotoolbox '
'-b:v ${bitrate}k '
'-profile:v high ' // 确保使用高压缩率 Profile
'-vf "scale=trunc(oh*a/2)*2:720,format=yuv420p" ' // 强制 720p 且确保像素格式为 yuv420p
'-c:a aac '
'-b:a 128k '
'-movflags +faststart ' // 关键:优化 MP4 结构,支持边下边播
'-y ' // 自动覆盖已存在文件
'"$outputPath"';
} else {
final crf = _getCrfByFileSize(fileSize);
......@@ -71,7 +82,10 @@ class VideoUtil {
///
/// 通过 ffmpeg 压缩视频
/// [quality] 压缩质量,可选值: 'low' | 'middle' | 'high'
/// 未指定时根据文件大小自动计算CRF:文件越大压缩率越高,文件越小清晰度越高
/// 未指定时根据文件大小自动计算压缩参数:文件越大压缩率越高,文件越小清晰度越高
/// iOS 18.6.x: libx264 + CRF(h264_videotoolbox在iOS 18.6.x存在稳定性问题)
/// 其它iOS版本: h264_videotoolbox 硬件加速 + 码率控制
/// Android: libx264 + CRF
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
///
static Future<bool> compressVideo(
......@@ -82,40 +96,82 @@ class VideoUtil {
}) async {
final duration = await _getVideoDuration(inputPath);
// 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20
// 中等质量: CRF 23-26
// 低质量: CRF 28-32
int crf;
if (quality != null && quality.isNotEmpty) {
switch (quality) {
case 'low':
crf = 32;
break;
case 'middle':
crf = 26;
break;
case 'high':
crf = 20;
break;
default:
throw Exception('参数错误');
String cmd;
if (Platform.isIOS /*&& !_isIos18_6()*/) {
// iOS(非18.6+): 使用 h264_videotoolbox 硬件加速
// h264_videotoolbox 不支持 CRF,使用码率控制质量
int bitrate;
if (quality != null && quality.isNotEmpty) {
switch (quality) {
case 'low':
bitrate = 1500;
break;
case 'middle':
bitrate = 2500;
break;
case 'high':
bitrate = 4000;
break;
default:
throw Exception('参数错误');
}
} else {
final fileSize = await File(inputPath).length();
bitrate = _getBitrateByFileSize(fileSize);
}
// cmd = '-i "$inputPath" ' // 输入文件
// '-c:v h264_videotoolbox ' // iOS硬件加速视频编码器
// '-b:v ${bitrate}k ' // 根据质量/文件大小自动计算码率
// '-c:a aac ' // 音频编码器
// '-b:a 128k ' // 音频码率
// '-movflags faststart ' // 优化MP4文件结构
// '"$outputPath"'; // 输出文件
cmd = '-i "$inputPath" '
'-c:v h264_videotoolbox '
'-b:v ${bitrate}k '
'-profile:v high ' // 使用高配置以获得更好画质
'-pix_fmt yuv420p ' // 确保像素格式兼容性
'-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ' // 强制偶数分辨率,防止报错
'-c:a aac '
'-b:a 128k '
'-movflags +faststart ' // 允许边下边播
'"$outputPath"';
} else {
// 根据文件大小自动计算CRF
final fileSize = await File(inputPath).length();
crf = _getCrfByFileSize(fileSize);
// iOS 18.6+ / Android: 使用 libx264 软件编码
// 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20
// 中等质量: CRF 23-26
// 低质量: CRF 28-32
int crf;
if (quality != null && quality.isNotEmpty) {
switch (quality) {
case 'low':
crf = 32;
break;
case 'middle':
crf = 26;
break;
case 'high':
crf = 20;
break;
default:
throw Exception('参数错误');
}
} else {
final fileSize = await File(inputPath).length();
crf = _getCrfByFileSize(fileSize);
}
cmd = '-i "$inputPath" ' // 输入文件
'-c:v libx264 ' // 视频编码器
'-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率
// '-preset medium ' // 编码预设
'-preset fast ' // 编码预设,编码速度显著提升,体积损失很小
'-threads 0 ' // 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件
}
String cmd = '-i "$inputPath" ' // 输入文件
'-c:v libx264 ' // 视频编码器
'-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率
// '-preset medium ' // 编码预设
'-preset fast ' // 编码预设,编码速度显著提升,体积损失很小
'-threads 0 ' // 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件
final completer = Completer<bool>();
......@@ -138,6 +194,23 @@ class VideoUtil {
return completer.future;
}
/// 判断当前iOS系统版本是否为18.6.x
/// iOS 18.6.x中h264_videotoolbox硬件编码器存在稳定性问题,需改用libx264
static bool _isIos18_6() {
if (!Platform.isIOS) return false;
try {
final versionStr = Platform.operatingSystemVersion;
final regex = RegExp(r'(\d+)\.(\d+)');
final match = regex.firstMatch(versionStr);
if (match != null) {
final major = int.parse(match.group(1)!);
final minor = int.parse(match.group(2)!);
return major == 18 && minor == 6;
}
} catch (_) {}
return false;
}
/// 根据文件大小自动计算视频码率(kbps)
/// 用于iOS h264_videotoolbox硬件加速模式(不支持CRF,需用码率控制质量)
/// 文件越大码率越低(压缩率越高,清晰度越低)
......
name: appframe
description: "app frame project."
publish_to: 'none'
version: 1.0.9+5
version: 1.0.10+6
environment:
sdk: ">=3.5.0 <4.0.0"
......@@ -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
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!