This commit is contained in:
lz_db
2026-10-25 20:30:33 +08:00
parent 1d9cd1f9c7
commit 074229f417
14 changed files with 222 additions and 19 deletions
+6
View File
@@ -50,6 +50,12 @@
<el-table-column prop="order_no" label="订单号" min-width="180" show-overflow-tooltip /> <el-table-column prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="user_id" label="用户 ID" width="90" /> <el-table-column prop="user_id" label="用户 ID" width="90" />
<el-table-column prop="amount_yuan" label="金额(元)" width="100" /> <el-table-column prop="amount_yuan" label="金额(元)" width="100" />
<el-table-column label="USDT 应付" width="110">
<template #default="{ row }">{{ row.usdt_amount ? `${row.usdt_amount} USDT` : '-' }}</template>
</el-table-column>
<el-table-column label="汇率" width="80">
<template #default="{ row }">{{ row.usdt_rate ?? '-' }}</template>
</el-table-column>
<el-table-column prop="points" label="积分" width="90" /> <el-table-column prop="points" label="积分" width="90" />
<el-table-column label="支付方式" width="100"> <el-table-column label="支付方式" width="100">
<template #default="{ row }">{{ PAY_TEXT[row.pay_method] || '-' }}</template> <template #default="{ row }">{{ PAY_TEXT[row.pay_method] || '-' }}</template>
+13
View File
@@ -37,6 +37,10 @@ final activeFeedStrategyProvider =
/// (否则在"我的"等页面退后台再回来会误恢复声音) /// (否则在"我的"等页面退后台再回来会误恢复声音)
final feedTabVisibleProvider = StateProvider<bool>((_) => true); final feedTabVisibleProvider = StateProvider<bool>((_) => true);
/// 游客今日免费额度已用尽(全局):所有 Feed 策略/搜索播放统一锁死播放,
/// 直到登录成功才解除——避免切换 Tab 或点「再逛逛」后继续免费观看
final guestLockedProvider = StateProvider<bool>((_) => false);
/// Feed 页 UI 事件(Notifier 不持有 BuildContext,通过事件流驱动弹窗/Toast) /// Feed 页 UI 事件(Notifier 不持有 BuildContext,通过事件流驱动弹窗/Toast)
sealed class FeedEvent { sealed class FeedEvent {
const FeedEvent(); const FeedEvent();
@@ -208,6 +212,11 @@ class FeedNotifier extends StateNotifier<FeedState> {
/// 翻页:滑出即暂停、滑入自动播放并上报观看计费(《01-开发文档》5.3) /// 翻页:滑出即暂停、滑入自动播放并上报观看计费(《01-开发文档》5.3)
void onPageChanged(int index) { void onPageChanged(int index) {
if (index < 0 || index >= state.items.length) return; if (index < 0 || index >= state.items.length) return;
if (_ref.read(guestLockedProvider)) {
// 游客已锁:只记位置,不绑定播放
state = state.copyWith(currentIndex: index);
return;
}
final item = state.items[index]; final item = state.items[index];
// 同一视频重新绑定(冷启动跳页)时保留播放位置,否则新页从 0 开始 // 同一视频重新绑定(冷启动跳页)时保留播放位置,否则新页从 0 开始
final sameVideo = pool.currentVideoId == item.videoId; final sameVideo = pool.currentVideoId == item.videoId;
@@ -245,6 +254,8 @@ class FeedNotifier extends StateNotifier<FeedState> {
/// 绑定播放窗口 + 观看计费上报 /// 绑定播放窗口 + 观看计费上报
Future<void> _bindAndReport(int index) async { Future<void> _bindAndReport(int index) async {
if (index < 0 || index >= state.items.length) return; if (index < 0 || index >= state.items.length) return;
// 游客免费额度已用尽:锁死播放(不绑定、不播放),直到登录成功
if (_ref.read(guestLockedProvider)) return;
final items = state.items; final items = state.items;
await pool.bindWindow( await pool.bindWindow(
index, index,
@@ -267,6 +278,8 @@ class FeedNotifier extends StateNotifier<FeedState> {
} on ApiException catch (e) { } on ApiException catch (e) {
pool.pauseCurrent(); pool.pauseCurrent();
if (e.isGuestLimit) { if (e.isGuestLimit) {
// 全局锁:此后任何策略/搜索播放都不再起播,点「再逛逛」也看不了
_ref.read(guestLockedProvider.notifier).state = true;
_eventController.add(const FeedGuestLimitEvent()); _eventController.add(const FeedGuestLimitEvent());
} else if (e.isPointsInsufficient) { } else if (e.isPointsInsufficient) {
final points = (e.data?['points'] as num?)?.toInt() ?? 0; final points = (e.data?['points'] as num?)?.toInt() ?? 0;
+24 -7
View File
@@ -8,6 +8,7 @@ import '../../core/log/action_logger.dart';
import '../../core/storage/feed_progress_store.dart'; import '../../core/storage/feed_progress_store.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../models/video_item.dart'; import '../../models/video_item.dart';
import '../../providers/auth_provider.dart';
import '../../providers/feed_provider.dart'; import '../../providers/feed_provider.dart';
import '../../router.dart'; import '../../router.dart';
import 'widgets/comment_sheet.dart'; import 'widgets/comment_sheet.dart';
@@ -85,6 +86,8 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
void didPopNext() { void didPopNext() {
if (!mounted) return; if (!mounted) return;
if (!ref.read(feedTabVisibleProvider)) return; if (!ref.read(feedTabVisibleProvider)) return;
// 游客额度已用尽:保持锁定,不恢复播放
if (ref.read(guestLockedProvider)) return;
ActionLogger.I.log('feed_did_pop_next', page: '/home', detail: { ActionLogger.I.log('feed_did_pop_next', page: '/home', detail: {
'strategy': _strategy.name, 'strategy': _strategy.name,
}); });
@@ -124,8 +127,8 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
notifier.pool.pauseCurrent(); notifier.pool.pauseCurrent();
} else if (state == AppLifecycleState.resumed) { } else if (state == AppLifecycleState.resumed) {
_appActive = true; _appActive = true;
// 仅在首页 Tab 可见时恢复(在"我的"等页面回前台不播) // 仅在首页 Tab 可见时恢复(在"我的"等页面回前台不播);游客锁定时不恢复
if (ref.read(feedTabVisibleProvider)) { if (ref.read(feedTabVisibleProvider) && !ref.read(guestLockedProvider)) {
unawaited(() async { unawaited(() async {
await notifier.pool.resume(); await notifier.pool.resume();
await notifier.rebindCurrent(); await notifier.rebindCurrent();
@@ -183,11 +186,13 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
} }
// jumpToPage 后 resume + rebindCurrent:suspend 已 stop 槽位,仅 play // jumpToPage 后 resume + rebindCurrent:suspend 已 stop 槽位,仅 play
// 播不起来,必须 rebind 重新 open(有断点记录则 seek 续播) // 播不起来,必须 rebind 重新 open(有断点记录则 seek 续播)
unawaited(() async { if (!ref.read(guestLockedProvider)) {
final n = ref.read(feedProvider(_strategy).notifier); unawaited(() async {
await n.pool.resume(); final n = ref.read(feedProvider(_strategy).notifier);
await n.rebindCurrent(); await n.pool.resume();
}()); await n.rebindCurrent();
}());
}
}); });
} }
@@ -208,6 +213,15 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final state = ref.watch(feedProvider(_strategy)); final state = ref.watch(feedProvider(_strategy));
final notifier = ref.read(feedProvider(_strategy).notifier); final notifier = ref.read(feedProvider(_strategy).notifier);
final guestLocked = ref.watch(guestLockedProvider);
// 登录成功后解除游客锁并恢复当前视频播放(登录态下改为积分/会员计费)
ref.listen<AuthState>(authProvider, (previous, next) {
if (next.isLoggedIn && ref.read(guestLockedProvider)) {
ref.read(guestLockedProvider.notifier).state = false;
unawaited(ref.read(feedProvider(_strategy).notifier).rebindCurrent());
}
});
// 冷启动续播:列表加载完成后把 PageView 定位到恢复的下标(每个策略一次)。 // 冷启动续播:列表加载完成后把 PageView 定位到恢复的下标(每个策略一次)。
// Notifier 会在 refresh 时按上次 videoId 恢复 currentIndex,此处只负责跳页, // Notifier 会在 refresh 时按上次 videoId 恢复 currentIndex,此处只负责跳页,
@@ -304,6 +318,9 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
bottom: 0, bottom: 0,
child: FreeQuotaBanner(remaining: state.freeRemainingToday), child: FreeQuotaBanner(remaining: state.freeRemainingToday),
), ),
// 游客免费额度已用尽:全屏锁(必须登录,点「再逛逛」也无法继续观看)
if (guestLocked) const Positioned.fill(child: GuestLockOverlay()),
], ],
), ),
); );
@@ -59,6 +59,50 @@ void showGuestLimitDialog(BuildContext context) {
); );
} }
/// 游客免费额度用完后的全屏锁:盖住播放区并吞掉手势,
/// 用户点「再逛逛」关闭弹窗后依然无法继续观看(必须登录)
class GuestLockOverlay extends StatelessWidget {
const GuestLockOverlay({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withValues(alpha: 0.92),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock_outline, size: 48, color: AppColors.gold),
const SizedBox(height: 16),
const Text(
'今日免费次数已用完',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white),
),
const SizedBox(height: 8),
const Text(
'注册或登录后继续观看,登录即可解锁全部视频',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13, color: AppColors.textSecondary, height: 1.6),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/login'),
child: const Text('去登录/注册'),
),
],
),
),
),
);
}
}
/// 积分不足:引导充值 / 开通会员(2001) /// 积分不足:引导充值 / 开通会员(2001)
void showPointsInsufficientDialog(BuildContext context, int points) { void showPointsInsufficientDialog(BuildContext context, int points) {
showDialog<void>( showDialog<void>(
+18 -2
View File
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import '../../core/log/action_logger.dart'; import '../../core/log/action_logger.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../models/video_item.dart'; import '../../models/video_item.dart';
import '../../providers/auth_provider.dart';
import '../../providers/feed_provider.dart'; import '../../providers/feed_provider.dart';
import '../../providers/search_provider.dart'; import '../../providers/search_provider.dart';
import '../../router.dart'; import '../../router.dart';
@@ -66,6 +67,8 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
@override @override
void didPopNext() { void didPopNext() {
if (!mounted) return; if (!mounted) return;
// 游客额度已用尽:保持锁定,不恢复播放
if (ref.read(guestLockedProvider)) return;
final notifier = ref.read(feedProvider(_strategy).notifier); final notifier = ref.read(feedProvider(_strategy).notifier);
unawaited(() async { unawaited(() async {
await notifier.pool.resume(); await notifier.pool.resume();
@@ -94,8 +97,9 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
} else if (state == AppLifecycleState.inactive) { } else if (state == AppLifecycleState.inactive) {
notifier.pool.pauseCurrent(); notifier.pool.pauseCurrent();
} else if (state == AppLifecycleState.resumed) { } else if (state == AppLifecycleState.resumed) {
// 本页不可见时(压在登录页/评论页下)不恢复播放 // 本页不可见时(压在登录页/评论页下)不恢复播放;游客锁定时也不恢复
if (ModalRoute.of(context)?.isCurrent ?? false) { if ((ModalRoute.of(context)?.isCurrent ?? false) &&
!ref.read(guestLockedProvider)) {
unawaited(() async { unawaited(() async {
await notifier.pool.resume(); await notifier.pool.resume();
await notifier.rebindCurrent(); await notifier.rebindCurrent();
@@ -134,6 +138,15 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
final state = ref.watch(feedProvider(_strategy)); final state = ref.watch(feedProvider(_strategy));
final notifier = ref.read(feedProvider(_strategy).notifier); final notifier = ref.read(feedProvider(_strategy).notifier);
final keyword = ref.watch(searchProvider).keyword; final keyword = ref.watch(searchProvider).keyword;
final guestLocked = ref.watch(guestLockedProvider);
// 登录成功后解除游客锁并恢复当前视频
ref.listen<AuthState>(authProvider, (previous, next) {
if (next.isLoggedIn && ref.read(guestLockedProvider)) {
ref.read(guestLockedProvider.notifier).state = false;
unawaited(ref.read(feedProvider(_strategy).notifier).rebindCurrent());
}
});
return PopScope( return PopScope(
// 系统返回键/手势也要先停搜索池再放行 pop // 系统返回键/手势也要先停搜索池再放行 pop
@@ -222,6 +235,9 @@ class _SearchFeedScreenState extends ConsumerState<SearchFeedScreen>
bottom: 0, bottom: 0,
child: FreeQuotaBanner(remaining: state.freeRemainingToday), child: FreeQuotaBanner(remaining: state.freeRemainingToday),
), ),
// 游客免费额度已用尽:全屏锁(必须登录,点「再逛逛」也无法继续观看)
if (guestLocked) const Positioned.fill(child: GuestLockOverlay()),
], ],
), ),
), ),
+1 -1
View File
@@ -2,7 +2,7 @@ name: video_app
description: "短视频应用(Flutter,iOS/Android 双端)" description: "短视频应用(Flutter,iOS/Android 双端)"
publish_to: 'none' publish_to: 'none'
version: 1.3.32+48 version: 1.3.33+49
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2
+2 -1
View File
@@ -29,7 +29,8 @@ WECHAT_MCHID=
WECHAT_APP_ID= WECHAT_APP_ID=
WECHAT_API_V3_KEY= WECHAT_API_V3_KEY=
WECHAT_PRIVATE_KEY= WECHAT_PRIVATE_KEY=
TRON_API_KEY= # TronGrid API Key(在 trongrid.io 免费申请,用于提高链上查询配额;不是钱包地址,收款地址在 config.yaml 的 recharge.usdt.address)
TRON_API_KEY=85350f6d-1739-4d13-935e-8803b5ca6fa5
# ===== 短信(短信宝)/ 邮件(未配置时验证码走开发模式)===== # ===== 短信(短信宝)/ 邮件(未配置时验证码走开发模式)=====
SMSBAO_USERNAME=followwin SMSBAO_USERNAME=followwin
Binary file not shown.
+4 -3
View File
@@ -1,9 +1,10 @@
"""USDT (TRC20) 渠道:唯一金额生成。 """USDT (TRC20) 渠道:唯一金额生成。
USDT 无支付平台回调,到账依赖服务端轮询 TronGrid(每 15s): USDT 无支付平台回调,到账由服务端每 15s 轮询 TronGrid 完成
匹配条件 = 收款地址一致 + 金额(含尾差)精确相等 + 转账时间在订单有效期内 + 确认数 >= 19; (settle_service.poll_usdt_deposits,随 main.py lifespan 启动):
匹配条件 = 收款地址一致 + 金额(含尾差)精确相等 + 转账时间在订单有效期内 + 交易已固化(≥19 确认);
匹配成功即入账(tx_hash 写入 third_trade_no,唯一索引保证幂等)。 匹配成功即入账(tx_hash 写入 third_trade_no,唯一索引保证幂等)。
当前以 Celery worker 部署后实现轮询任务;开发期可用后台"人工补单"入账。 后台另有"人工补单"入口兜底(链上查询异常时手动核实入账)。
""" """
import random import random
from decimal import ROUND_HALF_UP, Decimal from decimal import ROUND_HALF_UP, Decimal
+5
View File
@@ -362,6 +362,11 @@ async def login(db: AsyncSession, redis: aioredis.Redis, req, ip: str) -> dict:
user.last_login_ip = ip user.last_login_ip = ip
await db.commit() await db.commit()
# 单点登录:新设备登录即踢掉其它设备。
# 递增令牌纪元使旧 access token 立即失效(deps 每次请求校验 ep),
# 清空 refresh 白名单使旧 refresh 无法刷新;随后签发的新 Token 带新纪元。
await redis.incr(f"auth:epoch:{user.id}")
await redis.delete(f"auth:refresh:{user.id}")
tokens = await _issue_tokens(redis, user.id) tokens = await _issue_tokens(redis, user.id)
return {"user_id": user.id, **tokens, "user": await _user_brief(db, user)} return {"user_id": user.id, **tokens, "user": await _user_brief(db, user)}
+104 -4
View File
@@ -8,10 +8,11 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
import httpx
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import biz_config from app.config import biz_config, settings
from app.database import SessionFactory from app.database import SessionFactory
from app.models.billing import RechargeOrder, WatchRecord from app.models.billing import RechargeOrder, WatchRecord
from app.models.incentive import Referral, VideoEarning from app.models.incentive import Referral, VideoEarning
@@ -27,6 +28,10 @@ BATCH_SIZE = 500
INTERVAL_REFERRAL = 600 # 推荐激活结算 10min INTERVAL_REFERRAL = 600 # 推荐激活结算 10min
INTERVAL_EARNINGS = 300 # 创作收益结算 5min INTERVAL_EARNINGS = 300 # 创作收益结算 5min
INTERVAL_ORDER_CLOSE = 60 # 订单超时关单 1min INTERVAL_ORDER_CLOSE = 60 # 订单超时关单 1min
INTERVAL_USDT_POLL = 15 # USDT 链上流水轮询 15s
# USDT-TRC20 主网合约地址
USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
def _referral_cfg() -> dict: def _referral_cfg() -> dict:
@@ -128,17 +133,111 @@ async def settle_video_earnings(db: AsyncSession) -> dict:
# ---------- 订单超时关单 ---------- # ---------- 订单超时关单 ----------
async def close_expired_orders(db: AsyncSession) -> dict: async def close_expired_orders(db: AsyncSession) -> dict:
"""待支付订单超过 expire_at → 已关闭""" """待支付订单超过 expire_at → 已关闭。
USDT 订单额外留 10 分钟宽限:用户在有效期末尾转账时,区块固化可能晚于
订单过期时间,若立即关单会导致已付款无法自动入账(只能人工补单)。
"""
usdt_cfg = (biz_config.get("recharge") or {}).get("usdt") or {}
grace_min = int(usdt_cfg.get("confirm_grace_min", 10))
now = datetime.now()
rows = (await db.execute( rows = (await db.execute(
select(RechargeOrder) select(RechargeOrder)
.where(RechargeOrder.status == 0, RechargeOrder.expire_at < datetime.now()) .where(RechargeOrder.status == 0, RechargeOrder.expire_at < now)
.order_by(RechargeOrder.id) .order_by(RechargeOrder.id)
.limit(BATCH_SIZE) .limit(BATCH_SIZE)
)).scalars().all() )).scalars().all()
closed = 0
for o in rows: for o in rows:
if o.pay_method == 3 and o.expire_at > now - timedelta(minutes=grace_min):
continue # USDT 待确认宽限期
o.status = 2 o.status = 2
closed += 1
await db.commit() await db.commit()
return {"closed": len(rows)} return {"closed": closed}
# ---------- USDT 链上自动对账 ----------
async def poll_usdt_deposits(db: AsyncSession) -> dict:
"""USDT 自动对账:轮询收款地址的 TRC20 流水,四要素匹配后幂等入账。
匹配条件:to=收款地址 + 金额(含尾差)精确相等 + 转账时间在订单有效期内
+ 交易已固化(only_confirmed,波场固化块即已被 ≥19 个 SR 确认)。
入账走 payment_service.settle_order(tx_hash 写 third_trade_no,唯一索引幂等)。
双 worker 会同时跑本任务:settle_order 的行锁保证只有一方入账成功。
"""
from app.services import payment_service
cfg = (biz_config.get("recharge") or {}).get("usdt") or {}
address = cfg.get("address") or ""
if not address:
return {}
expire_min = int(cfg.get("order_expire_min", 30))
now = datetime.now()
# 有效期内的待支付 USDT 订单
orders = (await db.execute(
select(RechargeOrder).where(
RechargeOrder.pay_method == 3,
RechargeOrder.status == 0,
RechargeOrder.created_at >= now - timedelta(minutes=expire_min),
)
)).scalars().all()
if not orders:
return {}
min_ts = int(min(o.created_at for o in orders).timestamp() * 1000)
headers = {"TRON-PRO-API-KEY": settings.tron_api_key} if settings.tron_api_key else {}
async with httpx.AsyncClient(timeout=20) as client:
resp = await client.get(
f"https://api.trongrid.io/v1/accounts/{address}/transactions/trc20",
params={
"only_to": "true", # 只要转入
"only_confirmed": "true", # 只要已固化(等价确认数≥19)
"contract_address": USDT_CONTRACT,
"min_timestamp": min_ts,
"limit": 200,
},
headers=headers,
)
resp.raise_for_status()
txs = resp.json().get("data") or []
matched = 0
used_txs: set[str] = set() # 同一笔流水只允许命中一个订单(防同金额订单重复入账)
# 先快照订单关键字段:settle_order 失败时 rollback 会让 ORM 对象过期,
# 继续遍历原对象会触发异步懒加载错误
snapshots = [
(o.order_no, o.usdt_amount, o.created_at)
for o in orders if o.usdt_amount is not None
]
for order_no, usdt_amount, created_at in snapshots:
start_ms = created_at.timestamp() * 1000
end_ms = (created_at + timedelta(minutes=expire_min)).timestamp() * 1000
for tx in txs:
tx_hash = tx.get("transaction_id") or ""
if not tx_hash or tx_hash in used_txs:
continue
if tx.get("to") != address:
continue
# USDT 6 位小数:链上整数 → Decimal 精确比对(含随机尾差)
if Decimal(tx.get("value") or "0") / Decimal(1_000_000) != usdt_amount:
continue
ts = int(tx.get("block_timestamp") or 0)
if not (start_ms <= ts <= end_ms):
continue
try:
await payment_service.settle_order(db, order_no, tx_hash)
used_txs.add(tx_hash)
matched += 1
logger.info("USDT 订单 %s 自动到账: tx=%s amount=%s",
order_no, tx_hash, usdt_amount)
except Exception:
await db.rollback()
logger.exception("USDT 订单 %s 入账失败", order_no)
break
return {"usdt_orders": len(snapshots), "matched": matched} if matched else {}
# ---------- 周期协程(lifespan 挂载) ---------- # ---------- 周期协程(lifespan 挂载) ----------
@@ -163,4 +262,5 @@ def start_periodic_tasks() -> list[asyncio.Task]:
asyncio.create_task(_run_loop("推荐激活结算", settle_referral_activations, INTERVAL_REFERRAL)), asyncio.create_task(_run_loop("推荐激活结算", settle_referral_activations, INTERVAL_REFERRAL)),
asyncio.create_task(_run_loop("创作收益结算", settle_video_earnings, INTERVAL_EARNINGS)), asyncio.create_task(_run_loop("创作收益结算", settle_video_earnings, INTERVAL_EARNINGS)),
asyncio.create_task(_run_loop("订单超时关单", close_expired_orders, INTERVAL_ORDER_CLOSE)), asyncio.create_task(_run_loop("订单超时关单", close_expired_orders, INTERVAL_ORDER_CLOSE)),
asyncio.create_task(_run_loop("USDT 链上对账", poll_usdt_deposits, INTERVAL_USDT_POLL)),
] ]
+1 -1
View File
@@ -14,7 +14,7 @@ membership:
year: { days: 365, points: 3650, enabled: true } year: { days: 365, points: 3650, enabled: true }
lifetime: { days: 0, points: 10000, enabled: true } # days=0 表示永久 lifetime: { days: 0, points: 10000, enabled: true } # days=0 表示永久
recharge: recharge:
usdt: { rate: 7.2, network: TRC20, address: "", confirmations: 19, order_expire_min: 30 } usdt: { rate: 7.2, network: TRC20, address: "TYoufZFGwG8JAUuyGG2xtgjRtkczTXycuG", confirmations: 19, order_expire_min: 30, confirm_grace_min: 10 }
referral: referral:
register_reward_points: 10 # 邀请有效新用户奖励积分,不限量 register_reward_points: 10 # 邀请有效新用户奖励积分,不限量
rebate_percent: 5 # 被推荐人充值积分返利比例→资产 rebate_percent: 5 # 被推荐人充值积分返利比例→资产