删除dns,弹窗广告广告位调整

This commit is contained in:
谢宇宁
2026-09-16 14:49:57 +07:00
parent 1bbca71b7d
commit d6c95e16eb
15 changed files with 53 additions and 843 deletions
+1 -1
View File
@@ -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) {
+4 -9
View File
@@ -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 = ''; // 脱衣价格
+3 -34
View File
@@ -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';
+39 -4
View File
@@ -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);
+3 -9
View File
@@ -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;
}
+2 -42
View File
@@ -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 '';
}
}
-176
View File
@@ -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);
}
-188
View File
@@ -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;
}
-22
View File
@@ -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)''';
}
-90
View File
@@ -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''';
}
-5
View File
@@ -1,5 +0,0 @@
/// Provider of an easy way of performing DNS lookups.
library;
export '_dnsolve.dart';
export 'exception.dart';
-42
View File
@@ -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;
}
-3
View File
@@ -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}';