Merge remote-tracking branch 'origin/main' into feat/pay-v2-integration
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Lint — shellcheck (push) Successful in 29s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 22s
ci-pangolin / OpenAPI Sync Check (push) Successful in 40s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 19s
ci-pangolin / Flutter — analyze + test (push) Failing after 4m59s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 1m51s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 1m33s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m59s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 4s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Lint — shellcheck (push) Successful in 29s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 22s
ci-pangolin / OpenAPI Sync Check (push) Successful in 40s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 19s
ci-pangolin / Flutter — analyze + test (push) Failing after 4m59s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 1m51s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 1m33s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m59s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 4s
# Conflicts: # docs/index.html # server/cmd/server/main.go
This commit is contained in:
@@ -99,7 +99,23 @@ class AccountApi {
|
||||
Future<RedeemResult> redeem(String code) async =>
|
||||
RedeemResult.fromJson(await _c.postJson('/v1/redeem', {'code': code}));
|
||||
|
||||
/// POST /v1/ads/unlock — 看广告解锁今日免费额度。
|
||||
Future<void> adUnlock({required String deviceId, required String adToken}) =>
|
||||
_c.postJson('/v1/ads/unlock', {'device_id': deviceId, 'ad_token': adToken});
|
||||
/// POST /v1/ads/unlock — 看广告加时(累加式)。返回本次加时分钟与最新剩余分钟。
|
||||
Future<AdUnlockResult> adUnlock({required String deviceId, required String adToken}) async {
|
||||
final body = await _c.postJson('/v1/ads/unlock', {'device_id': deviceId, 'ad_token': adToken});
|
||||
return AdUnlockResult(
|
||||
grantedMinutes: (body['granted_minutes'] as num?)?.toInt() ?? 0,
|
||||
minutesRemaining: (body['minutes_remaining'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 看广告加时结果(POST /v1/ads/unlock 响应)。
|
||||
class AdUnlockResult {
|
||||
const AdUnlockResult({required this.grantedMinutes, required this.minutesRemaining});
|
||||
|
||||
/// 本次广告实际加时分钟(已达每日封顶时为 0)。
|
||||
final int grantedMinutes;
|
||||
|
||||
/// 加时后账户当日剩余分钟(全账户共享)。
|
||||
final int minutesRemaining;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
//
|
||||
// 历史上各 service/provider 各自重复声明 _kApiUrl;统一收敛到这里,
|
||||
// 由 --dart-define=PANGOLIN_API_URL 注入。
|
||||
// TODO(联调临时): 默认值改成测试节点,避免 release 构建漏传 dart-define;发版前改回 localhost 或正式控制面域名。
|
||||
// 控制面 API 基址(单源,全端 providers 共用)。默认走 CF Tunnel 的 https 域名;
|
||||
// 本地联调可 --dart-define=PANGOLIN_API_URL=http://127.0.0.1:8080 覆盖。
|
||||
const String kApiBaseUrl = String.fromEnvironment(
|
||||
'PANGOLIN_API_URL',
|
||||
defaultValue: 'http://103.119.13.48:8080',
|
||||
defaultValue: 'https://api.yanmeiai.com',
|
||||
);
|
||||
|
||||
/// 用户中心(网页)基址(单源)。已从独立子域 app.yanmeiai.com 迁到主站子路径
|
||||
/// pangolin.yanmeiai.com/user/(旧子域已停用)。末尾不带斜杠;调用方自行拼 path。
|
||||
/// 本地联调可 --dart-define=PANGOLIN_USERCENTER_URL=http://127.0.0.1:3000 覆盖。
|
||||
const String kWebUserCenterBaseUrl = String.fromEnvironment(
|
||||
'PANGOLIN_USERCENTER_URL',
|
||||
defaultValue: 'https://pangolin.yanmeiai.com/user',
|
||||
);
|
||||
|
||||
@@ -121,5 +121,27 @@ class ConnectApi {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
/// 通知控制面吊销本设备在 [nodeId] 上的数据面凭证(F4)。
|
||||
///
|
||||
/// best-effort:断开的本地拆隧道不依赖它,任何失败(网络/401/超时)都吞掉——
|
||||
/// 凭证最迟到 TTL 也会过期,这里只是让「断开」在服务端即刻生效。
|
||||
Future<void> disconnect({
|
||||
required String nodeId,
|
||||
required String deviceId,
|
||||
}) async {
|
||||
try {
|
||||
await _client
|
||||
.post(
|
||||
Uri.parse('$baseUrl/v1/nodes/$nodeId/disconnect'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $authToken',
|
||||
},
|
||||
body: jsonEncode({'device_id': deviceId}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
} catch (_) {/* best-effort */}
|
||||
}
|
||||
|
||||
void dispose() => _client.close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// web_launch.dart — App→Web 单点登录跳转(SSO 换票)。
|
||||
//
|
||||
// 「用户中心(网页)」入口:先向控制面签一张短时单次票据
|
||||
// (POST /v1/auth/web-ticket,需登录),再打开 用户中心网页版 的
|
||||
// /sso?t=<票>&redirect=<路径> 落地页兑票登录——避免用户在网页端重新输入密码。
|
||||
// 签票失败(未登录/网络异常)时降级为直接打开目标页(未登录态浏览)。
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../state/account_providers.dart';
|
||||
import 'api_config.dart';
|
||||
|
||||
/// 打开用户中心网页版 [path](默认首页),尝试免登录(SSO 换票)。
|
||||
Future<void> openWebUserCenter(WidgetRef ref, {String path = '/'}) async {
|
||||
Uri target = Uri.parse('$kWebUserCenterBaseUrl$path');
|
||||
try {
|
||||
final resp = await ref.read(apiClientProvider).postJson('/v1/auth/web-ticket');
|
||||
final ticket = resp['ticket'] as String?;
|
||||
if (ticket != null && ticket.isNotEmpty) {
|
||||
target = Uri.parse(
|
||||
'$kWebUserCenterBaseUrl/sso?t=$ticket&redirect=${Uri.encodeComponent(path)}');
|
||||
}
|
||||
} catch (_) {
|
||||
// 签票失败(未登录/网络异常)降级:直接打开目标页(未登录态浏览)。
|
||||
}
|
||||
await launchUrl(target, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
Reference in New Issue
Block a user