From d6c95e16ebf39562d6a8f190814fa7ccc27ed010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E5=AE=87=E5=AE=81?= Date: Wed, 16 Sep 2026 14:49:57 +0700 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4dns=EF=BC=8C=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=B9=BF=E5=91=8A=E5=B9=BF=E5=91=8A=E4=BD=8D=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/alert/splash/line_error_dialog.dart | 2 +- lib/config/config.dart | 13 +- lib/hj_page/home/home_main_logic.dart | 37 +-- .../special_topics_detail_logic.dart | 2 +- lib/hj_page/main_page/main_logic.dart | 43 +++- lib/hj_page/splash/splash_logic.dart | 12 +- lib/hj_utils/detect_line_manager.dart | 44 +--- lib/hj_utils/dns_solve/_answer.dart | 176 -------------- lib/hj_utils/dns_solve/_dnsolve.dart | 188 --------------- lib/hj_utils/dns_solve/_question.dart | 22 -- lib/hj_utils/dns_solve/_response.dart | 90 -------- lib/hj_utils/dns_solve/dnsolve.dart | 5 - lib/hj_utils/dns_solve/exception.dart | 42 ---- lib/hj_utils/store_keys.dart | 3 - project.md | 217 ------------------ 15 files changed, 53 insertions(+), 843 deletions(-) delete mode 100644 lib/hj_utils/dns_solve/_answer.dart delete mode 100644 lib/hj_utils/dns_solve/_dnsolve.dart delete mode 100644 lib/hj_utils/dns_solve/_question.dart delete mode 100644 lib/hj_utils/dns_solve/_response.dart delete mode 100644 lib/hj_utils/dns_solve/dnsolve.dart delete mode 100644 lib/hj_utils/dns_solve/exception.dart delete mode 100644 project.md diff --git a/lib/alert/splash/line_error_dialog.dart b/lib/alert/splash/line_error_dialog.dart index 0dd0f44..255de29 100644 --- a/lib/alert/splash/line_error_dialog.dart +++ b/lib/alert/splash/line_error_dialog.dart @@ -20,7 +20,7 @@ class LineErrorDialog extends StatelessWidget { const LineErrorDialog({super.key, this.callback}); - String get url => '91porn01.cc'; + String get url => 'hgaidj.app'; @override Widget build(BuildContext context) { diff --git a/lib/config/config.dart b/lib/config/config.dart index 06a2370..b05b640 100644 --- a/lib/config/config.dart +++ b/lib/config/config.dart @@ -20,17 +20,12 @@ class Config { static const signKey = 'kaFtkDJRcchRMTI9'; // ===== 域名 / 线路 ===== - static String dns = '91porn.zmnchs.com'; // 运维提供,返回配置域名 /// 本地兜底线路(无本地缓存线路时用) - static const lineList = isDebug - ? [ - 'https://91pornht.remoces.com', - ] - : [ - 'https://d1s84171319hi1.cloudfront.net', - 'https://d2j97wqe01j5pa.cloudfront.net', - ]; + static const lineList = [ + 'https://d1s84171319hi1.cloudfront.net', + 'https://d2j97wqe01j5pa.cloudfront.net', + ]; // ===== AI 功能价格 ===== static String aiUndressPrice = ''; // 脱衣价格 diff --git a/lib/hj_page/home/home_main_logic.dart b/lib/hj_page/home/home_main_logic.dart index 3562b6f..dd3644c 100644 --- a/lib/hj_page/home/home_main_logic.dart +++ b/lib/hj_page/home/home_main_logic.dart @@ -1,14 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:hgdj/hj_model/splash/ads_model.dart'; import 'package:hgdj/hj_page/main_page/provider/msg_provider.dart'; import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart'; import 'package:hgdj/alert/vip_guide/guide_manager.dart'; import 'package:hgdj/alert/vip_guide/timed_popup_manager.dart'; -import 'package:hgdj/tools_base/ad_manager.dart'; -import '../../alert/splash/notice_image_dialog.dart'; -import '../../alert/splash/notice_text_dialog.dart'; import '../../alert/vip_guide/guide_bottom_sheet.dart'; import '../../alert/vip_guide/guide_home_dialog.dart'; import '../../config/config.dart'; @@ -121,17 +117,13 @@ class HomeMainLogic extends GetxController { update(); } - /// 首页开屏弹窗链:图片公告 → 文字公告 → 预售 → 支付分层 → VIP 引导。 + /// 首页弹窗链:预售 → 支付分层 → VIP 引导。 /// 逐个 await 串行弹,每次启动只跑一次。 - /// 九宫格应用广告不在这里:它跟着启动落地页弹,见 MainPageLogic.onReady + /// 广告类弹窗(九宫格/图片公告/文字公告)不在这里:它们跟着启动落地页弹,见 MainPageLogic.onReady Future _showAdChain() async { if (_adShown) return; _adShown = true; - await MainPageLogic.appDialogFuture; //等落地页的九宫格弹窗关掉再接着弹,避免叠在一起 - if (AdManager().showAbTestAd) { - await _showImageDialog(); //图片公告 - await _showWordDialog(); //文字公告 - } + await MainPageLogic.adDialogFuture; //等落地页的广告弹窗链走完再接着弹,避免叠在一起 await PreSaleProvider().showAlert(); //预售弹窗 await _showPayDialog(); //支付分层首页弹窗 @@ -177,27 +169,4 @@ class HomeMainLogic extends GetxController { ), ); } - - ///显示图片公告(上下双排,每次弹两张) - Future _showImageDialog() async { - final adsList = AdManager().adsByType(46); - for (int i = 0; i < adsList.length; i += 2) { - await Get.dialog( - SystemImagesDialog(ads: adsList.skip(i).take(2).toList())); - } - } - - //文字公告 - Future _showWordDialog() async { - for (final wordBean in AdManager().announceList) { - await Get.dialog( - wordBean.type == 0 - ? NoticeTextDialog( - content: wordBean.content.toString(), wordBean: wordBean) - : NoticeImageDialog( - model: - AdsInfoModel(cover: wordBean.cover, href: wordBean.href)), - ); - } - } } diff --git a/lib/hj_page/home/special_topic_detail/special_topics_detail_logic.dart b/lib/hj_page/home/special_topic_detail/special_topics_detail_logic.dart index 044835c..aae78fb 100644 --- a/lib/hj_page/home/special_topic_detail/special_topics_detail_logic.dart +++ b/lib/hj_page/home/special_topic_detail/special_topics_detail_logic.dart @@ -4,8 +4,8 @@ import 'package:hgdj/hj_model/splash/ads_model.dart'; import 'package:hgdj/hj_model/video_model.dart'; import 'package:hgdj/hj_utils/api_service/vid_service.dart'; import 'package:hgdj/hj_utils/const.dart'; -import 'package:hgdj/tools_base/base_list_controller.dart'; import 'package:hgdj/tools_base/ad_manager.dart'; +import 'package:hgdj/tools_base/base_list_controller.dart'; import '../home_cell_style/home_section_cell.dart'; diff --git a/lib/hj_page/main_page/main_logic.dart b/lib/hj_page/main_page/main_logic.dart index 172a9f5..f6583f4 100644 --- a/lib/hj_page/main_page/main_logic.dart +++ b/lib/hj_page/main_page/main_logic.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import 'package:hgdj/assets_tool/images.dart'; import 'package:hgdj/config/config.dart'; import 'package:hgdj/hj_model/home/plate_model.dart'; +import 'package:hgdj/hj_model/splash/ads_model.dart'; import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart'; import 'package:hgdj/hj_utils/const.dart'; import 'package:hgdj/hj_utils/free_play_manager.dart'; @@ -11,6 +12,8 @@ import 'package:hgdj/tools_base/ad_manager.dart'; import 'package:hgdj/tools_base/global_store/store.dart'; import '../../alert/splash/app_center_dialog.dart'; +import '../../alert/splash/notice_image_dialog.dart'; +import '../../alert/splash/notice_text_dialog.dart'; import '../../tools_base/event_bus/event_bus_util.dart'; import '../../tools_base/event_bus/events.dart'; import '../../tools_base/toast.dart'; @@ -145,23 +148,55 @@ class MainPageLogic extends GetxController { }); } - /// 九宫格应用广告弹窗:每次启动只弹一次;首页弹窗链会 await 它,保证不叠弹 - static Future? appDialogFuture; + /// 广告弹窗链:九宫格应用广告 → 图片公告 → 文字公告。 + /// 每次启动只跑一次;首页自己的弹窗链会 await 它,保证不叠弹 + static Future? adDialogFuture; @override void onReady() { super.onReady(); //跟着启动落地页弹(不管默认落在哪个 tab),不再依赖首页被构建 - appDialogFuture ??= _showAppDialog(); + adDialogFuture ??= _showAdDialogs(); } - Future _showAppDialog() async { + /// 逐个 await 串行弹,ab 测未命中广告则整条链都不弹 + Future _showAdDialogs() async { if (!AdManager().showAbTestAd) return; + await _showAppDialog(); //九宫格应用广告 + await _showImageDialog(); //图片公告 + await _showWordDialog(); //文字公告 + } + + ///九宫格应用广告 + Future _showAppDialog() async { final appList = AdManager().adsByType(3); if (appList.isEmpty) return; await Get.dialog(AppCenterDialog(appList: appList)); } + ///图片公告(上下双排,每次弹两张) + Future _showImageDialog() async { + final adsList = AdManager().adsByType(2); + for (int i = 0; i < adsList.length; i += 2) { + await Get.dialog( + SystemImagesDialog(ads: adsList.skip(i).take(2).toList())); + } + } + + ///文字公告 + Future _showWordDialog() async { + for (final wordBean in AdManager().announceList) { + await Get.dialog( + wordBean.type == 0 + ? NoticeTextDialog( + content: wordBean.content.toString(), wordBean: wordBean) + : NoticeImageDialog( + model: + AdsInfoModel(cover: wordBean.cover, href: wordBean.href)), + ); + } + } + void _onReLogin(ReLoginEvent event) { _backToRoot(); jumpToPage(0); diff --git a/lib/hj_page/splash/splash_logic.dart b/lib/hj_page/splash/splash_logic.dart index 18306d4..6d6048c 100644 --- a/lib/hj_page/splash/splash_logic.dart +++ b/lib/hj_page/splash/splash_logic.dart @@ -28,7 +28,6 @@ import 'package:hgdj/tools_base/net/net_manager.dart'; import 'package:hgdj/tools_base/widget/common_alert.dart'; import 'package:path/path.dart' as path; -import '../../alert/splash/line_error_dialog.dart'; import '../../alert/splash/update_dialog.dart'; import '../../hj_utils/free_play_manager.dart'; import '../../tools_base/loading/loading_alert_widget.dart'; @@ -58,8 +57,7 @@ class SplashLogic extends GetxController { await AdManager().preload(); //预热广告内存缓存,后续统一用 AdManager().adsByType _loadAds(isLast: true); //获取上次启动的广告 final line = await _detectLine(); //选线 - //拿不到线路就到此为止:要么用户放弃了重试,要么弹框回调里重试成功、 - //已经自己走了一遍 _initByLine。这里再调一次会重复初始化 + //拿不到线路就到此为止(线路异常弹窗已去掉,失败暂不提示),启动流程停在启动页 if (TextUtil.isEmpty(line)) return; _initByLine(line!); //初始化流程 } @@ -95,7 +93,8 @@ class SplashLogic extends GetxController { update(); } - ///一直检查网络直到成功 + ///选线:拿到线路才继续启动流程 + //选线失败暂时不做处理(线路异常弹窗已去掉),启动流程停在启动页 Future _detectLine() async { LoadingAlertWidget.show(title: "选线中..."); @@ -106,11 +105,6 @@ class SplashLogic extends GetxController { LoadingAlertWidget.cancel(); if (TextUtil.isNotEmpty(line)) return line; } - await LineErrorDialog.show(callback: () async { - final line = await DetectLineManager().detectLineOnce(); - LoadingAlertWidget.cancel(); - if (TextUtil.isNotEmpty(line)) _initByLine(line); //初始化流程 - }); LoadingAlertWidget.cancel(); return null; } diff --git a/lib/hj_utils/detect_line_manager.dart b/lib/hj_utils/detect_line_manager.dart index 05d8d2b..f111a12 100644 --- a/lib/hj_utils/detect_line_manager.dart +++ b/lib/hj_utils/detect_line_manager.dart @@ -4,9 +4,6 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:hgdj/config/config.dart'; -import 'package:hgdj/hj_utils/dns_solve/dnsolve.dart'; -import 'package:hgdj/hj_utils/light_model.dart'; -import 'package:hgdj/hj_utils/store_keys.dart'; import 'package:hgdj/hj_utils/text_util.dart'; import 'package:hgdj/tools_base/debug_log.dart'; import 'package:hgdj/tools_base/net/http_manager.dart'; @@ -39,14 +36,8 @@ class DetectLineManager { /// 一次完整的批量选线过程 Future _detectLineOnce() async { - final saved = await lightKV.getStringList(StoreKeys.DETECT_LINE); - final lines = saved?.isNotEmpty == true ? saved! : Config.lineList; - var successLine = await _pingCheckBatch(lines); - // 本地线路全部失败,回退 DNS 解析 - if (TextUtil.isEmpty(successLine)) { - successLine = await _dnsSolve(); - } - return successLine; + //线路只来自 Config.lineList:DNS 解析兜底和本地线路缓存都已去掉 + return await _pingCheckBatch(Config.lineList); } ///检查一批线路:并发 ping,谁先成功用谁;全失败返回 "" @@ -103,35 +94,4 @@ class DetectLineManager { } return resp.isSuccess; } - - //本地域名不通的情况下,使用 dns 解析 - Future _dnsSolve() async { - final response = await DNSolve().lookup( - Config.dns, - dnsSec: true, - type: RecordType.txt, - provider: DNSProvider.aliyun, - ); - - //必须先取出来判空,不能写成 `?? []`:那个空字面量会被推成 List, - //整行 LUB 退化后 record 也成了 dynamic,record.data.split().map().toList() - //一路 dynamic 下去,运行期是 List,传给 setStringList(List) 直接抛。 - //全程 dynamic,analyze 一个警告都不会给 - final records = response.answer?.records; - if (records == null) return ''; - for (final record in records) { - if (record.data.isEmpty) continue; - debugLog("selectLine", "dnsSolve ==== ${record.data}"); - //DNS TXT 值带引号,去掉后按 _ 拆成多条线路 - final lines = record.data - .split('_') - .map((e) => e.replaceAll("\"", "")) - .where(TextUtil.isNotEmpty) - .toList(); - if (lines.isEmpty) continue; // 这条记录没解析出线路,接着看下一条,别直接放弃 - lightKV.setStringList(StoreKeys.DETECT_LINE, lines); - return await _pingCheckBatch(lines); - } - return ''; - } } diff --git a/lib/hj_utils/dns_solve/_answer.dart b/lib/hj_utils/dns_solve/_answer.dart deleted file mode 100644 index ef1cab3..0000000 --- a/lib/hj_utils/dns_solve/_answer.dart +++ /dev/null @@ -1,176 +0,0 @@ -part of '_dnsolve.dart'; - -/// Represents an answer containing a list of generic records and a list of -/// Service (SRV) records parsed from JSON data. -class _Answer { - const _Answer(this.records, [this.srvs]); - - /// List of generic records. - final List<_Record>? records; - - /// List of Service (SRV) records. - final List? srvs; - - /// Constructs an [_Answer] instance from JSON data. - /// - /// The [json] parameter should be a list of dynamic objects representing - /// DNS records. Returns an [_Answer] instance containing parsed records - /// and Service (SRV) records. - factory _Answer.fromJson(List? json) { - if (json == null) { - return const _Answer(null); - } - - final records = json - .map((answer) => _Record.fromJson(answer as Map)) - .toList(); - final srvs = []; - - { - final RegExp regExp = RegExp(r'(\d+)\s+(\d+)\s+(\d+)\s+([\w\.\-]+)'); - for (final record in records) { - if (record.rType == RecordType.srv) { - final match = regExp.firstMatch(record.data); - - if (match != null) { - final priority = int.parse(match.group(1)!); - final weight = int.parse(match.group(2)!); - final port = int.parse(match.group(3)!); - final target = match.group(4)!; - - srvs.add( - SRVRecord( - priority: priority, - weight: weight, - port: port, - target: target, - fqdn: record.name, - ), - ); - } else { - throw const SRVRecordFormatException( - 'Failed to parse or process the Service (SRV) record', - ); - } - } - } - } - - return _Answer(records, srvs); - } - - @override - String toString() => '''$records'''; -} - -class _Record { - const _Record({ - required this.name, - required this.rType, - required this.ttl, - required this.data, - }); - - final String name; - final RecordType rType; - final int ttl; - final String data; - - factory _Record.fromJson(Map json) => _Record( - name: json['name'] as String, - rType: DNSolve.intToRecord(json['type'] as int), - ttl: json['TTL'] as int, - data: json['data'] as String, - ); - - @override - String toString() => - '''(name: $name, type: $rType, ttl: $ttl, data: $data)'''; - - String get toBind { - final buffer = StringBuffer(); - buffer.write(name); - if (buffer.length < 8) { - buffer.write('\t'); - } - if (buffer.length > 10) { - buffer.write('\t'); - } - buffer.writeAll( - [ttl, '\tIN\t', rType.name.toUpperCase(), '\t', '"', data, '"'], - ); - - return buffer.toString(); - } -} - -/// Returns a Service (SRV) record containing information about a server or -/// service in the domain name system (DNS). -class SRVRecord { - /// Constructs an [SRVRecord] with the specified parameters. - const SRVRecord({ - required this.priority, - required this.weight, - required this.port, - this.target, - required this.fqdn, - }); - - /// The priority of this SRV record. - final int priority; - - /// The weight of this SRV record. - final int weight; - - /// The port on which the service is available. - final int port; - - /// The target domain name of the server. - final String? target; - - /// Fully Qualified Domain Name. - final String fqdn; - - /// Sorts a list of [SRVRecord] instances based on their priority and weight. - static List sort(List records) { - records.sort(_srvRecordSortComparator); - - return records; - } - - /// Comparator function for sorting [SRVRecord] instances. - static int _srvRecordSortComparator(SRVRecord a, SRVRecord b) { - if (a.priority < b.priority) { - return -1; - } else { - if (a.priority > b.priority) { - return 1; - } - - if (a.weight < b.weight) { - return -1; - } else if (a.weight > b.weight) { - return 1; - } else { - return 0; - } - } - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is SRVRecord && - other.runtimeType == runtimeType && - other.priority == priority && - other.weight == weight && - other.port == port && - other.target == other.target; - } - - @override - int get hashCode => Object.hash(priority, weight, port, target); -} diff --git a/lib/hj_utils/dns_solve/_dnsolve.dart b/lib/hj_utils/dns_solve/_dnsolve.dart deleted file mode 100644 index 46d6174..0000000 --- a/lib/hj_utils/dns_solve/_dnsolve.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; -import 'exception.dart'; - -part '_answer.dart'; -part '_question.dart'; -part '_response.dart'; - -/// An enumeration that represents various DNS record types. -enum RecordType { - A, - aaaa, - any, - caa, - cds, - cert, - cname, - dname, - dnskey, - ds, - hinfo, - ipseckey, - nsec, - nsec3PARAM, - naptr, - ptr, - rp, - rrsig, - soa, - spf, - srv, - sshfp, - tlsa, - wks, - txt, - ns, - mx, -} - -/// An enumeration that represents different DNS service providers. -enum DNSProvider { google, cloudflare, aliyun } - -class DNSolve { - DNSolve() { - _client = http.Client(); - } - - late final http.Client _client; - - /// A map that associates [DNSProvider] enum values with their respective DNS - /// provider URLs. - static const _dnsProviders = { - DNSProvider.google: 'https://dns.google.com/resolve', - DNSProvider.cloudflare: 'https://cloudflare-dns.com/dns-query', - DNSProvider.aliyun: 'https://dns.alidns.com/resolve' - }; - - /// Performs a DNS lookup for the given domain. - Future lookup( - /// The domain to lookup. - String domain, { - /// Whether to enable DNSSEC (Domain Name System Security Extensions). - bool dnsSec = false, - - /// The DNS record type to look up (defaults to A). - RecordType type = RecordType.A, - - /// The DNS provider to use (defaults to Google). - DNSProvider provider = DNSProvider.google, - }) async { - assert(domain.isNotEmpty, 'domain should not be empty'); - - final queryParams = {}; - queryParams - ..putIfAbsent('name', () => domain) - ..putIfAbsent('type', () => _typeToInt(type).toString()) - ..putIfAbsent('dnssec', () => dnsSec.toString()); - - final headers = {'Accept': 'application/dns-json'}; - final url = _dnsProviders[provider] ?? 'https://dns.google.com/resolve'; - - final body = - await _get(url, queryParameters: queryParams, headers: headers); - - return ResolveResponse.fromJson(json.decode(body) as Map); - } - - /// Performs a reverse DNS lookup for the given IP address. - Future> reverseLookup( - /// The IP address to perform a reverse lookup for. - String ip, { - /// THE DNS provider to use (defaults to Google). - DNSProvider provider = DNSProvider.google, - }) async { - final queryParams = {}; - String? reverse() { - if (ip.contains('.')) { - return '${ip.split('.').reversed.join('.')}.in-addr.arpa'; - } else if (ip.contains(':')) { - return '${ip.split(':').join().split('').reversed.join('.')}.ip6.arpa'; - } else { - return null; - } - } - - final reversed = reverse(); - if (reversed == null) { - return []; - } - - queryParams - ..putIfAbsent('name', () => reversed) - ..putIfAbsent('type', () => _records[RecordType.ptr]!.toString()); - - final headers = {'Accept': 'application/dns-json'}; - final url = _dnsProviders[provider] ?? 'https://dns.google.com/resolve'; - - final body = - await _get(url, queryParameters: queryParams, headers: headers); - final response = - ResolveResponse.fromJson(json.decode(body) as Map); - return response.answer!.records ?? []; - } - - /// Sends an HTTP GET request to the specified URL with optional query - /// parameters and headers. - Future _get( - String url, { - Map? queryParameters, - Map? headers, - }) async { - late Uri uri; - { - if (queryParameters == null || queryParameters.isEmpty) { - uri = Uri.parse(url); - } else { - uri = Uri.parse(url).replace(queryParameters: queryParameters); - } - } - - final response = await _client.get(uri, headers: headers); - return _handleResponse(response); - } - - /// A map that associates RecordType enum values with their corresponding DNS - /// record types (integer values). - static const _records = { - RecordType.A: 1, - RecordType.aaaa: 28, - RecordType.any: 255, - RecordType.caa: 257, - RecordType.cds: 59, - RecordType.cert: 37, - RecordType.cname: 5, - RecordType.dname: 39, - RecordType.dnskey: 48, - RecordType.ds: 43, - RecordType.hinfo: 13, - RecordType.ipseckey: 45, - RecordType.mx: 15, - RecordType.naptr: 35, - RecordType.ns: 2, - RecordType.nsec: 47, - RecordType.nsec3PARAM: 51, - RecordType.ptr: 12, - RecordType.rp: 17, - RecordType.rrsig: 46, - RecordType.soa: 6, - RecordType.spf: 99, - RecordType.srv: 33, - RecordType.sshfp: 44, - RecordType.tlsa: 52, - RecordType.txt: 16, - RecordType.wks: 11, - }; - - /// Converts an integer DNS record type to a [RecordType] enum value. - static RecordType intToRecord(int type) { - final records = _records.map((key, value) => MapEntry(value, key)); - - return records[type] ?? RecordType.A; - } - - /// Converts a [RecordType] enum value to its corresponding integer DNS record - /// type. - static int _typeToInt(RecordType type) => _records[type] ?? 1; -} diff --git a/lib/hj_utils/dns_solve/_question.dart b/lib/hj_utils/dns_solve/_question.dart deleted file mode 100644 index ccb69e1..0000000 --- a/lib/hj_utils/dns_solve/_question.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of '_dnsolve.dart'; - -class _Question { - const _Question({required this.name, required this.rType}); - - final String? name; - final RecordType? rType; - - factory _Question.fromJson(Map? json) { - if (json == null) { - return const _Question(name: null, rType: null); - } - - return _Question( - name: json['name'] as String, - rType: DNSolve.intToRecord(json['type'] as int), - ); - } - - @override - String toString() => '''(name: $name, rType: $rType)'''; -} diff --git a/lib/hj_utils/dns_solve/_response.dart b/lib/hj_utils/dns_solve/_response.dart deleted file mode 100644 index c0f6446..0000000 --- a/lib/hj_utils/dns_solve/_response.dart +++ /dev/null @@ -1,90 +0,0 @@ -part of '_dnsolve.dart'; - -String _handleResponse(http.Response response) { - if (response.statusCode >= 200 && response.statusCode <= 209) { - return response.body; - } else { - throw ResponseException( - body: response.body, - headers: response.headers, - statusCode: response.statusCode, - ); - } -} - -/// Represents a response from a DNS resolution operation. -/// -/// This class includes information about the resolution status, flags, -/// comments, resolved answer, and the list of questions queried. -class ResolveResponse { - const ResolveResponse({ - required this.status, - required this.tc, - required this.rd, - required this.ra, - required this.ad, - required this.cd, - required this.comment, - required this.answer, - required this.questions, - }); - - /// The status code indicating the result of the DNS resolution. - final int? status; - - /// Indicates if the response was truncated. - final bool? tc; - - /// Indicates if recursion was desired in the request. - final bool? rd; - - /// Indicates if recursion is available in the response. - final bool? ra; - - /// Indicates if the data in the response is authenticated. - final bool? ad; - - /// Indicates if checking is disabled in the response. - final bool? cd; - - /// Additional comments or information related to the resolution response. - final String? comment; - - /// The resolved answer containing DNS records. - final _Answer? answer; - - /// List of questions queried in the resolution request. - final List<_Question>? questions; - - /// Constructs a [ResolveResponse] instance from JSON data. - /// - /// The [json] parameter should be a map containing the fields of a DNS - /// resolution response. Returns a [ResolveResponse] instance with parsed - /// data. - factory ResolveResponse.fromJson(Map json) => ResolveResponse( - status: json['Status'] as int?, - tc: json['TC'] as bool?, - rd: json['RD'] as bool?, - ra: json['RA'] as bool?, - ad: json['AD'] as bool?, - cd: json['CD'] as bool?, - comment: json['comment'] as String?, - answer: _Answer.fromJson(json['Answer'] as List?), - questions: () { - final data = json['Question']; - if (data == null) return null; - if (data is List) { - return (data as List) - .map( - (question) => _Question.fromJson(question as Map), - ) - .toList(); - } - if (data is Map) return [_Question.fromJson(Map.from(data))]; - }(), - ); - - @override - String toString() => - '''status: $status, truncation: $tc, recursion desired(rd): $rd, recursion available(ra): $ra, authenticated data(ad): $ad, checking disabled(cd): $cd, comment: $comment, answer: $answer, questions: $questions'''; -} diff --git a/lib/hj_utils/dns_solve/dnsolve.dart b/lib/hj_utils/dns_solve/dnsolve.dart deleted file mode 100644 index d9767a1..0000000 --- a/lib/hj_utils/dns_solve/dnsolve.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// Provider of an easy way of performing DNS lookups. -library; - -export '_dnsolve.dart'; -export 'exception.dart'; diff --git a/lib/hj_utils/dns_solve/exception.dart b/lib/hj_utils/dns_solve/exception.dart deleted file mode 100644 index fc225d3..0000000 --- a/lib/hj_utils/dns_solve/exception.dart +++ /dev/null @@ -1,42 +0,0 @@ -/// An abstract class representing an exception related to DNS solving. -/// -/// This serves as a base class for exceptions that may occur during DNS -/// resolution or parsing operations. -abstract class DNSolveException implements Exception { - const DNSolveException(); -} - -/// Represents an [Exception] that occured while processing an DNS request. -/// -/// It contains information about the status code, headers, and body of the -/// response. -class ResponseException extends DNSolveException { - const ResponseException({ - required this.statusCode, - required this.headers, - required this.body, - }) : super(); - - /// The status code of the response. - final int statusCode; - - /// The headers of the response. - final Map headers; - - /// The body of the response. - final String body; - - @override - String toString() => - '''Exception(Status Code: $statusCode, Response Headers: $headers, Response Body: $body)'''; -} - -/// An exception indicating that an error occurred while parsing or processing a -/// Service (SRV) record. -/// -/// This is a specific type of [DNSolveException]. -class SRVRecordFormatException extends DNSolveException { - const SRVRecordFormatException(this.message); - - final String message; -} diff --git a/lib/hj_utils/store_keys.dart b/lib/hj_utils/store_keys.dart index 462d8c4..7bcee40 100644 --- a/lib/hj_utils/store_keys.dart +++ b/lib/hj_utils/store_keys.dart @@ -35,9 +35,6 @@ class StoreKeys { static const String DEVICE_ID = 'device_id'; // ===== 网络 / 资源 ===== - /// 本地 ping 通的线路 - static const String DETECT_LINE = 'detectLineKey'; - /// 本地广告列表(按 DEBUG 标志区分) static const String ADS_LIST = '_key_ads_list${Config.isDebug}'; diff --git a/project.md b/project.md deleted file mode 100644 index d3234b2..0000000 --- a/project.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -项目名称: 91porn #如果在dmp则使用dmp的名称 如果不在则使用自定义名称 -一级部门: 北斗 #请填写一级部门名称 北斗 -二级部门: 瑞升 #请填写二级部门名称,无则留空, 如无极 - -DMP编码: #无则留空 - -项目类型: APP - # 可选值: - # H5 / APP / PC / 服务 / 组件 / 工具 / AI / 中间件 - -开发语言: Dart - # 可选值: - # GO / Java / PHP / Node - -项目负责人: DN5368 # 员工编号, 多个用逗号隔开,这里的负责是这套代码的负责人 - -运维负责人: DN9460,DN9651,DN4792 # 负责部署、监控、K8S、发布、证书等 - -产品负责人: YY1145,YY1208 # 项目产品经理 - - -项目成员: DN5368,DN1180 - -项目状态: 开发中 - # 可选值: - # 开发中 / 维护中 / 已停止 / 已归档 - -项目级别: 高 - # 可选值: - # 高 / 中 / 低 , 北斗体系 s和a为 高 b为中 c d为低 - -核心分支: main - # 即部署时候使用的分支,可选值: - # master / main / dev / release - -代码仓库: - - ssh://git@bd-git-sh.jsyyds.com:8082/ruisheng/jha-204-91pron/frontend/91porn-android-client.git - -最后审计人: DN5368 - -最后审计时间: 2026-08-06 - -告警群: tg://blibli-alert-group - -备注: Flutter 客户端(Android + iOS 双端),当前版本 1.0.0,Android 包名 com.noporn.newchatone_twelve_one(版本号变更需同步替换) - ---- - -# 1. 项目说明 - -91porn 移动客户端(Android + iOS 同一套代码),基于 Flutter 3.38.3 + Dart 3.10.0,面向 C 端用户的视频/社区/AI/暗网综合内容 APP。 - -核心模块: - -- **首页 (`hj_page/home/`)**:视频/短视频/漫画/动漫等多 Tab 内容流,含搜索、片库、排行 -- **发现 (`hj_page/find/`)**:榜单、分类、片单 -- **AI (`hj_page/mine/ai/`)**:AI 换脸、AI 脱衣、文生图、图生视频、文生小说等付费功能 -- **会员/支付 (`hj_page/mine/mine_vip/`、`hj_page/pre_sale/`)**:VIP 购买、金币充值、代理充值、预售 -- **个人中心 (`hj_page/user_center_page/`、`hj_page/mine/`)**:用户主页、客服、QA、设置 -- **社区 (`hj_page/community/`)**:帖子、评论、互动 -- **IM (`im_manager/`)**:自研私信/会话,无三方 IM SDK(会话列表、聊天详情、消息通知、聊天页广告位) -- **暗网 (`hj_page/darkweb/`)**:分类专区,独立入口与 VIP 体系 -- **直播 (`hj_page/live/`)**:直播入口 -- **在线客服 (`hj_page/zaixiankefu/`)**:H5 客服对接 -- **播放器**:基于 video_player + ffmpeg_kit_flutter_new_https + 自研 m3u8_downloader -- **视频下载**:Android 走 `m3u8_downloader` 原生 isolate;iOS 走 `IOSVideoDownloader`(用 ffmpeg 拉远程 m3u8 转 mp4 自动入相册,命令 `-c copy -bsf:a aac_adtstoasc -movflags +faststart`,不重编码) - -技术栈: - -- 状态管理:GetX (`GetxController` + `GetBuilder`) + Provider(全局状态) -- 网络:Dio + 自研 `httpManager` + RSA 请求加密 -- 屏幕适配:flutter_screenutil(设计稿 428×927) -- 路由:GetX 路由 -- 本地存储:MMKV + shared_preferences -- 视频下载:`m3u8_downloader-master`(本仓库内) -- 调试工具:ume_core + ume_kit_dio / ume_kit_perf / ume_kit_ui(均取自 pub,不再本地维护) - -项目目录结构: - -``` -hgdj/ -├── lib/ -│ ├── alert/ # 全局弹窗 -│ ├── assets_tool/ # 颜色、图片、资源路径 -│ ├── config/ # 配置常量(域名、版本、AB 等) -│ ├── extension/ # Dart 扩展方法 -│ ├── hj_model/ # 数据模型(手写 fromJson) -│ ├── hj_page/ # 业务页面(按模块分目录) -│ ├── hj_utils/ # 工具方法 + API service -│ ├── im_manager/ # 自研 IM(会话/聊天页/通知) -│ ├── routers/ # 路由定义与跳转 -│ ├── tools_base/ # 基础工具(网络/事件/loading/toast/refresh/缓存) -│ ├── track_event_manager/ # 设备信息 / 请求头 sid 会话 -│ └── main.dart # 入口 -├── android/ # Android 工程 -├── ios/ # iOS 工程 -├── assets/ # 静态资源(图片/flare/字体) -├── m3u8_downloader-master/ # 自研 m3u8 下载库 -├── third_party/ # 三方原生库 -├── dist/ # 打包产物输出目录 -├── build_apk.sh # 双端打包脚本(apk / ipa) -└── pubspec.yaml -``` - - -# 2. 基础设施与中间件 - -- 后端服务:91porn-server(Go,独立仓库)— 提供 APP API、Web 管理、Skd 任务 -- CDN:视频/图片/动漫资源分发(Cloudfront + 自有域名多线路) -- 推送:Telegram Bot 客服群对接 -- 客户端依赖: - - Dio:HTTP 网络层 - - cached_network_image:图片缓存 - - mmkv / shared_preferences:本地 KV - - ffmpeg_kit_flutter_new_https:视频处理(社区维护 fork,自托管二进制带 SSL,原 minimal 包二进制已下架) - - flutter_inappwebview:H5 嵌入(充值/客服/活动页) - - permission_handler:权限申请 - - device_info_plus / package_info_plus:设备信息 - - flutter_native_image / image_pickers:图片选择压缩 - - -# 3. 运维部署 - -客户端通过 `build_apk.sh` 本地打包后分发,**不走 K8S/CI 流水线**。 - -发布流程: - -1. 切到 `main` 分支,更新 `pubspec.yaml` 版本号 + `Config.innerVersion` + `local.properties` + applicationId(按版本号转换规则) -2. 确认 `Config.isDebug = false`(开发分支常年为 `true`,最容易漏) -3. 打包(脚本自动检测 FVM PATH,产物统一复制至 `dist/`): - - ``` - sh build_apk.sh [mode] [export_method] [export_options_plist] [sign_mode] - target a(android) | i(ios) | all - mode release(默认) | debug | profile - export_method (iOS) app-store(默认) | ad-hoc | development | enterprise - export_options_plist ExportOptions.plist 路径,留空用默认 - sign_mode (iOS) codesign(默认) | nocodesign - ``` - - 常用:`sh build_apk.sh a`(Android release,arm64 单 ABI)、`sh build_apk.sh i release ad-hoc`(iOS 内测包)、`sh build_apk.sh all` -4. 产物命名(`APP_NAME` 取自 `pubspec.yaml` 的 `name`,即 `hgdj`): - - Android:`hgdj___.apk` - - iOS:`hgdj____.ipa`,如 `hgdj_release_adhoc_1.0.0_0806-0925.ipa` -5. Android 发版前跑 VirusTotal 包毒检测 -6. 上传到分发平台(落地页 / 蒲公英 / 自有 CDN),Telegram 告警群通知 - -真机调试(不出正式包):`flutter build ios --release && flutter install --release -d `,Xcode 自动签名,Team `NWP977LR25`。 - -回滚方案: - -- 仅客户端无服务端回滚概念 -- 旧版本安装包保留在分发平台,用户可重新下载 -- 服务端通过 `Config.dns` 接口下发动态域名实现服务端切换 - - -# 4. 风险说明 - -- **包名风险**:`com.noporn.newchatone_twelve_one` 与版本号强绑定,每次升版需替换 applicationId(数字转英文单词规则),遗漏会导致 push/支付失败 -- **加密密钥风险**:`Config.encryptKey` / `ANTI_REPLAY_ATTACK_KEY` / `Config.appId = JHA-204` 泄漏会被刷接口 -- **包毒检测**:APK 容易被部分杀毒软件误报,需走 VirusTotal 检测后再分发 -- **包体积**:当前接入 ffmpeg_kit_flutter_new_https(带 SSL,比 minimal 大 +5~10MB)/ video_player / 多个三方 SDK,需定期 `analyze-size` 控制体积 -- **iOS 最低系统版本**:Podfile + Runner.xcodeproj 锁定 15.0(Xcode 16+ 要求 + ffmpeg 7.x 要求),低于此版本无法安装 -- **Android SDK 版本**:minSdk 24 / targetSdk 36,targetSdk 升级需重新验证权限与后台限制 -- **三方依赖**:image_pickers / m3u8_downloader 为内部维护,Flutter 升级时需同步适配 -- **原生插件升级**:`flutter pub get` + `flutter analyze` 全过也不代表能用,含原生代码的插件必须真机 build 验证(audioplayers 升 v6 即栽在这里) -- **release-only 缺陷**:混淆 + strip + 独立 manifest 让 release 有一整类 debug 不复现的问题(详见第 5 节),每次发版必须真机跑 release 包,不能只测 debug -- **域名兜底**:`Config.dns = 91porn.zmnchs.com` 走运维下发,运维接口挂掉会导致 APP 启动失败 -- **Flutter 版本锁定**:3.38.3 强制 fvm,避免系统 SDK 切换触发 flutter_ume / leak_tracker 冲突 -- **暗网模块**:`Config.darkWebEnable` 通过运维下发开关,下发异常会暴露入口 -- **DEBUG 标志**:`Config.isDebug` 切换会影响 `lineList` 兜底线路与日志输出,正式包必须 `false`(开发分支当前为 `true`) - - -# 5. 历史事故(可选) - -## 5.1 release 包专有(debug 一律不复现,最贵的一类) - -debug 与 release 在**混淆、符号 strip、manifest 合并**三处行为不同,以下问题只能靠真机 release 包发现: - -| 现象 | 根因 | 修复 | -|---|---|---| -| iOS 启动卡死 / 本地存储静默读不到 | FFI(MMKV)符号被 strip 掉,运行时找不到 | `STRIP_INSTALLED_PRODUCT = NO`(只设 `STRIP_STYLE = non-global` 不够) | -| Android 安装后直接闪退(SIGSEGV) | 含 native 方法的插件(ffmpeg-kit)被 R8 混淆,`RegisterNatives` 失败 | proguard 补 `-keep`;**新增带 JNI 的插件都要补** | -| 启动弹「线路检测失败」,host lookup errno=7 | `INTERNET` 权限只声明在 debug/profile 的 manifest,`src/main` 漏了 | 权限写进 `src/main/AndroidManifest.xml`,别指望三方插件替你声明 | -| 页面灰屏 / 列表静默空白 | 用 `T.toString() == '类名'` 做分支,混淆后类名变了必然失配 | 一律改 `T == 类型`(比较 Type 对象,不碰名字字符串) | - -## 5.2 其他 - -- **视频下载进度不刷新**(退出重进才更新):本仓库内 `m3u8_downloader` 的 `callbackDispatcher` 缺 `@pragma('vm:entry-point')`,后台 isolate 起不来。教训:本地插件不是黑盒,出问题要读它的源码 -- **刷新控制器双重释放崩溃**:`refreshCtr` 归 `CustomRefreshView` 释放,logic / 子 widget 又 dispose 一次 → `headerMode!` 空断言崩在 unmount 阶段 → 中断整棵树的卸载并连带泄漏。规则:只释放自己 new 的 -- **dio 5.9 偶发 `DioException[unknown] Null check operator`**:仅 CDN 接口偶发,根因不可复现(已证伪 adapter / FusedTransformer 两个方向),用幂等 GET 的 `RetryInterceptor` 自愈 -- **截图保存「提示成功但相册没有」**:`RepaintBoundary` 套在 `SingleChildScrollView` 内被裁剪,截出空图。改用 `Center + FittedBox` 完整渲染,且调用处必须 `await` 判断结果 -- **设置锁屏密码当次不生效、重启后才生效**:把内部可变 list 直接交给未 `await` 的异步回调,回调在 `await` 之后读到的已是被 clear 的空 list(内存态空、持久化态有值)。传副本 `List.of(x)` 修复 -- **audioplayers 无法升 v6**:`pub get` / `analyze` 全过但 Android 侧 build 挂(5.2.1 与 platform_interface 7.1.1 错配 + Kotlin 2.1),已停在 v5.2.1 -- **`pull_to_refresh` 升级后回弹手感夸张**(Flutter 3.16.8 → 3.38.3):改 `pull_refresh.dart` 参数解决;**不迁移 EasyRefresh**——logic 层有 305 处依赖 SmartRefresher 专有 API -- **华为多任务卡片隐私遮盖失败**:系统截快照早于 Flutter 生命周期回调,任何遮盖层(View / 独立 Window)都盖不住,也无法把黑屏换成自定义图。结论:只能用 `FLAG_SECURE` - - -# 6. 交接说明(可选) - -- **Flutter 版本**:必须使用 `fvm flutter`,版本锁定 3.38.3(见 `.fvmrc`),`build_apk.sh` 已内置 FVM PATH 检测 -- **打包**:`build_apk.sh a` 打 Android,`build_apk.sh i` 打 iOS,`build_apk.sh all` 双端;完整参数见第 3 节 -- **applicationId 规则**:`com.noporn.newchat` + `__`(数字转英文),如 1.0.0 → `com.noporn.newchatone_twelve_one` -- **版本号同步点**:`pubspec.yaml` / `Config.innerVersion` / `android/local.properties` / `android/app/build.gradle` 四处必须一致 -- **加密密钥**:RSA 公私钥位于 Android 端 native 层 + Dart 层,调整时需双端同步 -- **AB 配置**:`Config` 类中集中维护(`aiSwitchConf` / `showAbTestAd` 等),新增 AB 需走运维下发 -- **客服地址**:`/customer/url` 接口动态下发完整 URL,前端不再拼 `baseHost` -- **provider 注入**:`main.dart` 中 `MultiProvider` 注入若干 `ChangeNotifier`,新增全局状态走该处 -- **图片资源**:`assets_tool/` 统一管理路径扩展(`.commonImgPath` / `.homePath` / `.darkWebPath` 等) -- **域名错误兜底**:DNS 接口失败时降级到 `Config.LINE_LIST` 本地兜底 -- **网络层异常约定**:dio 异常已被网络层吞成 `BaseRespBean`(业务层不要写 `DioException` catch),但**请求前的签名加密会抛**,需 try 兜底;拦截器只在「有响应但失败」时弹 toast,超时/断网(code 2001-2003)不弹,要各自兜底 -- **列表分页**:统一继承 `tools_base/base_list_controller.dart` 的 `ListBaseLogic`(配 `pullYsRefresh`),别自己重写分页 -- **支付下单**:走 `PayManager().buy / buyVip`(含 loading、余额不足跳充值、回调、source 透传),不要直接调 `BuyService` -- **弹窗**:提示类统一用 `tools_base/widget/common_alert.dart`;「停留 N 分钟后满足条件才弹」走 `TimedPopupManager`(`onClose` 必须 `cancel`,否则泄漏) -- **包毒检测**:发版前必须用 `/check-apk-virus` 跑 VirusTotal -- **包体积分析**:使用 `apk-analyzer` skill 跑 `flutter build apk --analyze-size`