删除dns,弹窗广告广告位调整
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 = ''; // 脱衣价格
|
||||
|
||||
@@ -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<void> _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<void> _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<void> _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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<void>? appDialogFuture;
|
||||
/// 广告弹窗链:九宫格应用广告 → 图片公告 → 文字公告。
|
||||
/// 每次启动只跑一次;首页自己的弹窗链会 await 它,保证不叠弹
|
||||
static Future<void>? adDialogFuture;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
//跟着启动落地页弹(不管默认落在哪个 tab),不再依赖首页被构建
|
||||
appDialogFuture ??= _showAppDialog();
|
||||
adDialogFuture ??= _showAdDialogs();
|
||||
}
|
||||
|
||||
Future<void> _showAppDialog() async {
|
||||
/// 逐个 await 串行弹,ab 测未命中广告则整条链都不弹
|
||||
Future<void> _showAdDialogs() async {
|
||||
if (!AdManager().showAbTestAd) return;
|
||||
await _showAppDialog(); //九宫格应用广告
|
||||
await _showImageDialog(); //图片公告
|
||||
await _showWordDialog(); //文字公告
|
||||
}
|
||||
|
||||
///九宫格应用广告
|
||||
Future<void> _showAppDialog() async {
|
||||
final appList = AdManager().adsByType(3);
|
||||
if (appList.isEmpty) return;
|
||||
await Get.dialog(AppCenterDialog(appList: appList));
|
||||
}
|
||||
|
||||
///图片公告(上下双排,每次弹两张)
|
||||
Future<void> _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<void> _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);
|
||||
|
||||
@@ -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<String?> _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;
|
||||
}
|
||||
|
||||
@@ -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<String> _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<String> _dnsSolve() async {
|
||||
final response = await DNSolve().lookup(
|
||||
Config.dns,
|
||||
dnsSec: true,
|
||||
type: RecordType.txt,
|
||||
provider: DNSProvider.aliyun,
|
||||
);
|
||||
|
||||
//必须先取出来判空,不能写成 `?? []`:那个空字面量会被推成 List<dynamic>,
|
||||
//整行 LUB 退化后 record 也成了 dynamic,record.data.split().map().toList()
|
||||
//一路 dynamic 下去,运行期是 List<dynamic>,传给 setStringList(List<String>) 直接抛。
|
||||
//全程 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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SRVRecord>? 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<dynamic>? json) {
|
||||
if (json == null) {
|
||||
return const _Answer(null);
|
||||
}
|
||||
|
||||
final records = json
|
||||
.map((answer) => _Record.fromJson(answer as Map<String, dynamic>))
|
||||
.toList();
|
||||
final srvs = <SRVRecord>[];
|
||||
|
||||
{
|
||||
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<String, dynamic> 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<SRVRecord> sort(List<SRVRecord> 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);
|
||||
}
|
||||
@@ -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, String>{
|
||||
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<ResolveResponse> 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 = <String, String>{};
|
||||
queryParams
|
||||
..putIfAbsent('name', () => domain)
|
||||
..putIfAbsent('type', () => _typeToInt(type).toString())
|
||||
..putIfAbsent('dnssec', () => dnsSec.toString());
|
||||
|
||||
final headers = <String, String>{'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<String, dynamic>);
|
||||
}
|
||||
|
||||
/// Performs a reverse DNS lookup for the given IP address.
|
||||
Future<List<_Record>> 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, String>{};
|
||||
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 = <String, String>{'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<String, dynamic>);
|
||||
return response.answer!.records ?? [];
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request to the specified URL with optional query
|
||||
/// parameters and headers.
|
||||
Future<String> _get(
|
||||
String url, {
|
||||
Map<String, String>? queryParameters,
|
||||
Map<String, String>? 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;
|
||||
}
|
||||
@@ -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<String, dynamic>? 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)''';
|
||||
}
|
||||
@@ -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<String, dynamic> 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<dynamic>?),
|
||||
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<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
if (data is Map) return [_Question.fromJson(Map<String, dynamic>.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''';
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/// Provider of an easy way of performing DNS lookups.
|
||||
library;
|
||||
|
||||
export '_dnsolve.dart';
|
||||
export 'exception.dart';
|
||||
@@ -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<String, String> 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;
|
||||
}
|
||||
@@ -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}';
|
||||
|
||||
|
||||
-217
@@ -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 <target> [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_<mode>_<version>_<MMdd-HHmm>.apk`
|
||||
- iOS:`hgdj_<mode>_<exportMethod>_<version>_<MMdd-HHmm>.ipa`,如 `hgdj_release_adhoc_1.0.0_0806-0925.ipa`
|
||||
5. Android 发版前跑 VirusTotal 包毒检测
|
||||
6. 上传到分发平台(落地页 / 蒲公英 / 自有 CDN),Telegram 告警群通知
|
||||
|
||||
真机调试(不出正式包):`flutter build ios --release && flutter install --release -d <deviceId>`,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<int>.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` + `<major>_<minor>_<patch>`(数字转英文),如 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`
|
||||
Reference in New Issue
Block a user