From 68fff0f5ecde904564146cec6b17573ad9c6da57 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 29 Apr 2026 16:53:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(feedback):=20=E6=8E=A5=E5=85=A5=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E7=BB=9F=E4=B8=80=E5=90=8E=E7=AB=AF=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除原有的反馈控制器、绑定及表单视图,新增基于 cryptography 的加密 URL 生成模块和 WebView 页面。用户反馈功能现通过加密参数跳转到统一的 App Comment 服务端页面,提升安全性与维护性。 --- .../feedback/bindings/feedback_bindings.dart | 9 - lib/modules/feedback/comment_url.dart | 86 +++++++ .../controllers/feedback_controller.dart | 59 ----- .../feedback/views/comment_webview.dart | 211 ++++++++++++++++++ lib/modules/feedback/views/feedback_view.dart | 173 +------------- lib/routes/app_pages.dart | 2 - pubspec.yaml | 1 + 7 files changed, 304 insertions(+), 237 deletions(-) delete mode 100644 lib/modules/feedback/bindings/feedback_bindings.dart create mode 100644 lib/modules/feedback/comment_url.dart delete mode 100644 lib/modules/feedback/controllers/feedback_controller.dart create mode 100644 lib/modules/feedback/views/comment_webview.dart diff --git a/lib/modules/feedback/bindings/feedback_bindings.dart b/lib/modules/feedback/bindings/feedback_bindings.dart deleted file mode 100644 index 0ffbaccde..000000000 --- a/lib/modules/feedback/bindings/feedback_bindings.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/feedback_controller.dart'; - -class FeedbackBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => FeedbackController()); - } -} diff --git a/lib/modules/feedback/comment_url.dart b/lib/modules/feedback/comment_url.dart new file mode 100644 index 000000000..303468e55 --- /dev/null +++ b/lib/modules/feedback/comment_url.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import '../../data/models/appconfig.dart'; + +/// 生成加密评论页 URL(与 App Comment 服务端 SIGN_SECRET 一致) +/// +/// [userId]、[packageName]、[appName]、[appVersion] 必填;[userName]、[lang] 可选。 +Future buildCommentUrl({ + required String userId, + String? userName, + required String packageName, + required String appName, + required String appVersion, + String lang = 'en', +}) async { + final signSecret = _commentSignSecret(); + var commentServer = _commentServerUrl(); + + if (signSecret.isEmpty || commentServer.isEmpty) { + throw Exception( + 'COMMENT_SIGN_SECRET / COMMENT_SERVER_URL 未配置:' + '请在 getAppConfig 的 env 中下发,或在 .env 中填写(本地调试)', + ); + } + + commentServer = commentServer.trim().replaceAll(RegExp(r'/$'), ''); + + final payload = { + 'userId': userId, + 'userName': userName ?? userId, + 'packageName': packageName, + 'appName': appName, + 'appVersion': appVersion, + 'lang': lang, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'nonce': _randomHex(8), + }; + + final algo = AesGcm.with256bits(); + final key = await algo.newSecretKeyFromBytes(_hexDecode(signSecret)); + final nonce = algo.newNonce(); + final box = await algo.encrypt( + utf8.encode(jsonEncode(payload)), + secretKey: key, + nonce: nonce, + ); + + // 格式: iv(12 bytes) + tag(16 bytes) + ciphertext + final blob = [...nonce, ...box.mac.bytes, ...box.cipherText]; + final token = base64Url.encode(blob).replaceAll('=', ''); + return '$commentServer/?d=$token'; +} + +/// 优先使用服务端 [getAppConfig] 下发的 `env`,否则回退 `.env`(本地调试)。 +String _commentSignSecret() { + if (AppConfig.isInitialized()) { + final v = AppConfig.env('COMMENT_SIGN_SECRET'); + if (v != null && v.isNotEmpty) return v; + } + return dotenv.env['COMMENT_SIGN_SECRET'] ?? ''; +} + +String _commentServerUrl() { + if (AppConfig.isInitialized()) { + final v = AppConfig.env('COMMENT_SERVER_URL'); + if (v != null && v.isNotEmpty) return v; + } + return dotenv.env['COMMENT_SERVER_URL'] ?? ''; +} + +List _hexDecode(String hex) => List.generate( + hex.length ~/ 2, + (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), + ); + +String _randomHex(int byteLen) { + final r = Random.secure(); + return List.generate( + byteLen, + (_) => r.nextInt(256).toRadixString(16).padLeft(2, '0'), + ).join(); +} diff --git a/lib/modules/feedback/controllers/feedback_controller.dart b/lib/modules/feedback/controllers/feedback_controller.dart deleted file mode 100644 index 2cb4eaaf3..000000000 --- a/lib/modules/feedback/controllers/feedback_controller.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../../data/services/network/api.dart'; - -class FeedbackController extends GetxController { - final TextEditingController feedbackController = TextEditingController(); - //final TextEditingController contactController = TextEditingController(); - - final RxBool isSubmitting = false.obs; - final GlobalKey formKey = GlobalKey(); - - @override - void onClose() { - feedbackController.dispose(); - //contactController.dispose(); - super.onClose(); - } - - // 提交反馈 - Future submitFeedback() async { - if (!formKey.currentState!.validate()) { - return; - } - - isSubmitting.value = true; - - try { - final feedbackText = feedbackController.text; - Map data = { - "message": feedbackText, // 反馈内容 - }; - - final response = await Api.feedback(data); - - if (response != null) { - Get.back(); // 返回上一页 - Get.snackbar( - 'success'.tr, // 成功 - 'feedbackSubmitSuccess'.tr, // 反馈提交成功 - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green.withOpacity(0.7), - colorText: Colors.white, - margin: const EdgeInsets.all(10), - ); - } - } catch (e) { - Get.snackbar( - 'error'.tr, // 错误 - 'feedbackSubmitFailed'.tr, // 反馈提交失败 - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red.withOpacity(0.7), - colorText: Colors.white, - margin: const EdgeInsets.all(10), - ); - } finally { - isSubmitting.value = false; - } - } -} diff --git a/lib/modules/feedback/views/comment_webview.dart b/lib/modules/feedback/views/comment_webview.dart new file mode 100644 index 000000000..e71a2bc4c --- /dev/null +++ b/lib/modules/feedback/views/comment_webview.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:uuid/uuid.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../../core/theme/app_colors.dart'; +import '../../../core/utils/logger.dart'; +import '../../../data/models/user_Info.dart'; +import '../comment_url.dart'; + +/// 用户反馈:加载加密后的 App Comment 评论页(每次进入重新生成 URL) +class CommentWebView extends StatefulWidget { + const CommentWebView({super.key}); + + @override + State createState() => _CommentWebViewState(); +} + +class _CommentWebViewState extends State { + WebViewController? _controller; + bool _isLoading = true; + String? _error; + + static const _anonStorageKey = 'comment_feedback_anon_id'; + + @override + void initState() { + super.initState(); + _loadUrl(); + } + + Future _loadUrl() async { + try { + final packageInfo = await PackageInfo.fromPlatform(); + final lang = Get.locale?.languageCode ?? 'en'; + + final String userId; + final String? userName; + if (User.isLoggedIn()) { + userId = User.instance.uid.isNotEmpty + ? User.instance.uid + : _ensureAnonymousUserId(); + userName = User.instance.name.isNotEmpty ? User.instance.name : null; + } else { + userId = _ensureAnonymousUserId(); + userName = null; + } + + final url = await buildCommentUrl( + userId: userId, + userName: userName, + packageName: packageInfo.packageName, + appName: packageInfo.appName, + appVersion: '${packageInfo.version}+${packageInfo.buildNumber}', + lang: lang, + ); + + Logger.debug('评论页 URL 已生成'); + + if (!mounted) return; + + setState(() { + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.white) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (_) { + Logger.debug('WebView 页面加载完成'); + if (mounted) setState(() => _isLoading = false); + }, + onWebResourceError: (error) { + Logger.error( + 'WebView 资源错误: code=${error.errorCode}, ' + 'type=${error.errorType}, desc=${error.description}, ' + 'mainFrame=${error.isForMainFrame}', + ); + if (error.isForMainFrame == true && mounted) { + setState(() { + _isLoading = false; + _error = error.description; + }); + } + }, + onHttpError: (error) { + Logger.error( + 'WebView HTTP 错误: status=${error.response?.statusCode}, ' + 'uri=${error.request?.uri}', + ); + }, + ), + ) + ..setOnConsoleMessage((message) { + Logger.debug( + '[WebView Console] ${message.level.name}: ${message.message}', + ); + }) + ..loadRequest(Uri.parse(url)); + }); + } catch (e, st) { + Logger.error('生成评论页 URL 失败', e, st); + if (mounted) { + setState(() { + _isLoading = false; + _error = e.toString(); + }); + } + } + } + + String _ensureAnonymousUserId() { + final storage = GetStorage(); + var id = storage.read(_anonStorageKey); + if (id == null || id.isEmpty) { + id = const Uuid().v4(); + storage.write(_anonStorageKey, id); + } + return id; + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode + ? AppColors.darkBackground + : Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + elevation: 0, + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode ? AppColors.textPrimary : Colors.black87, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'feedback'.tr, + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? AppColors.textPrimary : Colors.black87, + ), + ), + centerTitle: true, + ), + body: _buildBody(isDarkMode), + ); + } + + Widget _buildBody(bool isDarkMode) { + if (_error != null) { + final msgColor = + isDarkMode ? AppColors.textSecondary : Colors.black54; + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48.sp, + color: msgColor, + ), + SizedBox(height: 16.h), + Text( + 'loadFailed'.tr, + style: TextStyle(fontSize: 16.sp, color: msgColor), + textAlign: TextAlign.center, + ), + SizedBox(height: 8.h), + Text( + _error!, + style: TextStyle(fontSize: 12.sp, color: msgColor), + textAlign: TextAlign.center, + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () { + setState(() { + _error = null; + _isLoading = true; + _controller = null; + }); + _loadUrl(); + }, + child: Text('retry'.tr), + ), + ], + ), + ), + ); + } + + return Stack( + children: [ + if (_controller != null) WebViewWidget(controller: _controller!), + if (_isLoading) const Center(child: CircularProgressIndicator()), + ], + ); + } +} diff --git a/lib/modules/feedback/views/feedback_view.dart b/lib/modules/feedback/views/feedback_view.dart index 7c3bbe09e..885c14fd5 100644 --- a/lib/modules/feedback/views/feedback_view.dart +++ b/lib/modules/feedback/views/feedback_view.dart @@ -1,174 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../controllers/feedback_controller.dart'; -class FeedbackView extends GetView { - const FeedbackView({Key? key}) : super(key: key); +import 'comment_webview.dart'; + +/// 用户反馈:嵌入 App Comment 加密评论页(WebView) +class FeedbackView extends StatelessWidget { + const FeedbackView({super.key}); @override Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[100], - appBar: AppBar( - elevation: 0, - backgroundColor: isDarkMode ? Colors.black : Colors.white, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - size: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - onPressed: () => Get.back(), - ), - title: Text( - 'feedback'.tr, // 帮助与反馈 - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - ), - body: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Form( - key: controller.formKey, - child: ListView( - padding: EdgeInsets.all(16.w), - children: [ - // 顶部提示文字 - Text( - 'feedbackDescription'.tr, // 请描述您遇到的问题或建议 - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - SizedBox(height: 16.h), - - // 反馈内容输入框 - 统一容器样式 - Container( - decoration: BoxDecoration( - // 保持适合各自模式的背景色 - color: isDarkMode ? Colors.grey[800] : Colors.white, - // 统一圆角 - borderRadius: BorderRadius.circular(16.r), - // 统一阴影效果 - 关键改进点 - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.3) - : Colors.black.withOpacity(0.08), - blurRadius: 12, - offset: const Offset(0, 4), - spreadRadius: 0, - ), - ], - // 统一边框样式 - 关键改进点 - border: Border.all( - color: isDarkMode ? Colors.grey[700]! : Colors.grey[200]!, - width: 1.5, - ), - ), - child: Padding( - padding: - EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), - child: TextFormField( - controller: controller.feedbackController, - maxLines: 8, - maxLength: 500, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 14.sp, - height: 1.4, - ), - decoration: InputDecoration( - hintText: 'feedbackHint'.tr, // 请输入您的反馈内容... - hintStyle: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - border: InputBorder.none, - focusedBorder: InputBorder.none, - counterStyle: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontSize: 12.sp, - ), - contentPadding: EdgeInsets.zero, - fillColor: Colors.transparent, - filled: true, - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'feedbackRequired'.tr; // 请输入反馈内容 - } - return null; - }, - ), - ), - ), - SizedBox(height: 40.h), - - // 提交按钮 - 统一容器样式 - Obx(() => Container( - // 统一按钮容器装饰 - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.r), - // 统一阴影效果 - boxShadow: controller.isSubmitting.value - ? [] - : [ - BoxShadow( - color: Colors.blue.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - spreadRadius: 0, - ), - ], - ), - child: ElevatedButton( - onPressed: controller.isSubmitting.value - ? null - : controller.submitFeedback, - style: ElevatedButton.styleFrom( - backgroundColor: controller.isSubmitting.value - ? Colors.grey[400] - : Colors.blue[600], - padding: EdgeInsets.symmetric(vertical: 14.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - elevation: 0, - shadowColor: Colors.transparent, - ), - child: controller.isSubmitting.value - ? SizedBox( - height: 20.h, - width: 20.h, - child: CircularProgressIndicator( - strokeWidth: 2.w, - valueColor: const AlwaysStoppedAnimation( - Colors.white), - ), - ) - : Text( - 'submit'.tr, // 提交 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - )), - ], - ), - ), - ), - ); + return const CommentWebView(); } } diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index d24555e14..a072a94de 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -9,7 +9,6 @@ import '../modules/ble/views/ble_view.dart'; import '../../modules/devices/bindings/devices_binding.dart'; import '../../modules/devices/views/devices_view.dart'; -import '../modules/feedback/bindings/feedback_bindings.dart'; import '../modules/feedback/views/feedback_view.dart'; import '../modules/home/views/home_view.dart'; import '../modules/home/bindings/home_binding.dart'; @@ -269,7 +268,6 @@ abstract class AppPages { GetPage( name: Routes.feedback, page: () => const FeedbackView(), - binding: FeedbackBinding(), transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), diff --git a/pubspec.yaml b/pubspec.yaml index f4de6fa47..78847cec1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: synchronized: ^3.1.0 intl: ^0.20.2 crypto: ^3.0.3 + cryptography: ^2.7.0 flutter_tts: ^4.2.2 logger: ^2.0.2+1 flutter_blue_plus: ^1.35.5