From 1c36c849e2e68d2905637c2a9188366a139cff05 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 4 Mar 2026 01:42:47 -0800 Subject: [PATCH] =?UTF-8?q?docs(genex-mobile):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E9=82=80=E8=AF=B7=E5=88=86=E4=BA=AB=E6=A8=A1=E5=9D=97=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=B8=8E=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SharePage: - 文件头注释:完整功能概述、支持的分享场景、数据来源、URL格式、依赖包说明 - 类注释:生命周期描述、Widget 树结构图(ASCII) - 状态变量:详细说明 _info/_loading/_error/_baseInviteUrl - _inviteLink:注释已加载/未加载两种输出示例 - _loadInfo:成功/失败两条路径说明 - _showCopied:SnackBar 样式描述 - _copyCode/_copyLink:示例复制内容 - _shareNative:iOS/Android 行为说明 + 文案模板示例 - _buildHeroCard:视觉层次注释 + QR 参数说明 - _buildStatsCard:布局描述 + 数据来源注释 - _buildShareActions:三项操作的点击行为说明 ReferralService: - 文件头:完整端点一览、推荐码格式、推荐链规则 - ReferralInfo:字段含义 + 后端响应 JSON 示例 - getMyInfo:登录要求说明 - validateCode:用途说明 + 返回值降级策略 - getDirectReferrals:分页参数范围 + 响应 JSON 示例 Co-Authored-By: Claude Sonnet 4.6 --- .../lib/core/services/referral_service.dart | 105 +++++++++- .../presentation/pages/share_page.dart | 188 +++++++++++++++--- 2 files changed, 261 insertions(+), 32 deletions(-) diff --git a/frontend/genex-mobile/lib/core/services/referral_service.dart b/frontend/genex-mobile/lib/core/services/referral_service.dart index 822c0d1..3e89f93 100644 --- a/frontend/genex-mobile/lib/core/services/referral_service.dart +++ b/frontend/genex-mobile/lib/core/services/referral_service.dart @@ -1,14 +1,54 @@ import '../network/api_client.dart'; -/// 推荐关系信息 +// ============================================================ +// ReferralService — 推荐/邀请系统客户端 +// +// 对接后端 referral-service(端口 3013,经 Kong 网关 :8080 转发) +// +// 端点一览: +// GET /api/v1/referral/my 获取当前用户的推荐信息(需 JWT) +// GET /api/v1/referral/validate 验证推荐码是否有效(公开) +// GET /api/v1/referral/direct 获取直接推荐列表(需 JWT) +// +// 推荐码格式:GNX + 5 位无歧义字符(排除 0/O/I/1),示例:GNXAB2C3 +// 推荐链:每个用户拥有唯一推荐码,可追溯至多 50 层祖先节点 +// ============================================================ + +/// 当前用户的推荐关系数据 +/// +/// 对应后端 [GET /api/v1/referral/my] 返回的 `data` 字段。 +/// +/// 后端响应示例: +/// ```json +/// { +/// "code": 0, +/// "data": { +/// "userId": "uuid-...", +/// "referralCode": "GNXAB2C3", +/// "referrerId": "uuid-... | null", +/// "usedCode": "GNXPARENT | null", +/// "directReferralCount": 5, +/// "totalTeamCount": 23 +/// } +/// } +/// ``` class ReferralInfo { + /// 本用户的专属推荐码(格式:GNXxxxxx,固定 8 位大写) final String referralCode; + + /// 直接推荐人数(通过本人推荐码注册的用户数量) final int directReferralCount; + + /// 团队总人数(整条推荐链下的所有层级用户总数) final int totalTeamCount; + + /// 推荐人 userId(若无人推荐则为 null) final String? referrerId; + + /// 注册时填写的推荐码(即 referrerId 对应的 referralCode,冗余存储方便展示) final String? usedCode; - ReferralInfo({ + const ReferralInfo({ required this.referralCode, required this.directReferralCount, required this.totalTeamCount, @@ -25,9 +65,22 @@ class ReferralInfo { usedCode: json['usedCode'] as String?, ); } + + @override + String toString() => + 'ReferralInfo(code=$referralCode, direct=$directReferralCount, team=$totalTeamCount)'; } -/// Referral Service — 对接 referral-service API +/// 推荐服务 — 单例 +/// +/// 用法: +/// ```dart +/// final info = await ReferralService.instance.getMyInfo(); +/// print(info.referralCode); // e.g. GNXAB2C3 +/// ``` +/// +/// 注意:所有方法均需要 ApiClient 携带有效 JWT; +/// 若未登录或 token 过期,将抛出 DioException(401)。 class ReferralService { static final ReferralService _instance = ReferralService._(); static ReferralService get instance => _instance; @@ -35,14 +88,56 @@ class ReferralService { final _api = ApiClient.instance; - /// 获取当前用户的推荐信息(需登录) + // ── 查询 ───────────────────────────────────────────────────────────────── + + /// 获取当前用户的推荐信息 + /// + /// 包含:专属推荐码、直接推荐人数、团队总人数、是否被推荐等。 + /// + /// - 需要登录(请求头携带 Bearer Token) + /// - 首次登录即自动在后端创建推荐档案(由 Kafka 事件触发) + /// + /// Throws [DioException] on network error or 401/403. Future getMyInfo() async { final resp = await _api.get('/api/v1/referral/my'); final data = resp.data['data'] as Map; return ReferralInfo.fromJson(data); } - /// 获取直接推荐列表 + /// 验证推荐码是否有效(公开接口,无需登录) + /// + /// 用于注册页实时校验邀请码。 + /// + /// 后端响应:`{ "code": 0, "data": { "valid": true/false } }` + /// + /// Returns `false` on any error (avoid blocking registration flow). + Future validateCode(String code) async { + try { + final resp = await _api.get( + '/api/v1/referral/validate', + queryParameters: {'code': code.toUpperCase()}, + ); + final data = resp.data['data'] as Map; + return data['valid'] == true; + } catch (_) { + return false; + } + } + + /// 获取直接推荐列表(分页) + /// + /// 每条记录包含被推荐用户的 userId、注册时间、推荐码等基本信息。 + /// + /// - [offset] 分页偏移,从 0 开始 + /// - [limit] 每页条数,最大 50 + /// + /// 后端响应 `data` 字段示例: + /// ```json + /// { + /// "items": [ { "userId": "...", "referralCode": "...", "createdAt": "..." } ], + /// "total": 5 + /// } + /// ``` Future>> getDirectReferrals({ int offset = 0, int limit = 20, diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/share_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/share_page.dart index 202e0ef..fd45ad0 100644 --- a/frontend/genex-mobile/lib/features/profile/presentation/pages/share_page.dart +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/share_page.dart @@ -1,3 +1,35 @@ +// ============================================================ +// SharePage — 邀请好友 / 推广分享页 +// +// 路由:/share(从 ProfilePage「邀请好友」横幅入口跳转) +// +// 功能概述: +// 1. Hero Card — 紫色渐变卡片,包含: +// · 专属二维码(qr_flutter 生成,编码邀请落地页 URL) +// · 推荐码胶囊(可点击一键复制) +// 2. 邀请进度 — 显示直接推荐人数 + 团队总人数 +// 3. 分享操作列表 — 三种快捷方式:复制推荐码 / 复制链接 / 原生分享 +// 4. 主操作按钮 — 「分享给好友」触发系统原生分享弹层 +// +// 支持的分享场景(取决于用户设备安装的 App): +// 微信 / QQ / WhatsApp / Telegram / Line / Twitter/X +// 短信 / 邮件 / AirDrop / 复制到剪贴板 / 保存至文件…… +// +// 数据来源: +// GET /api/v1/referral/my → ReferralService.getMyInfo() +// 推荐码通过 Kafka 事件在用户注册时自动生成,首次打开此页若 +// 数据尚未就绪,将显示 Loading → 加载失败 → 重试 流程。 +// +// 邀请链接格式: +// https://app.gogenex.com/download?ref={referralCode} +// 落地页检测设备后分别跳转 App Store / Google Play / 浏览器下载。 +// ref 参数由注册流程自动读取并预填推荐码输入框。 +// +// 依赖包: +// qr_flutter ^4.1.0 — 本地生成二维码,无网络依赖 +// share_plus ^10.0.2 — 调用系统原生分享弹层(iOS Share Sheet / Android Sharesheet) +// ============================================================ + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -11,11 +43,18 @@ import '../../../../app/i18n/app_localizations.dart'; /// A9. 邀请好友 / 推广分享页 /// -/// 功能: -/// - 专属推荐码二维码(扫码跳转到 App 下载页并自动填入推荐码) -/// - 一键复制推荐码 / 复制链接 -/// - 系统原生分享(微信、WhatsApp、Telegram、短信、邮件……) -/// - 邀请进度(直接推荐人数 + 团队总人数) +/// 页面生命周期: +/// initState → _loadInfo() → [Loading] → [Content | Error] +/// +/// Widget 树概览: +/// ``` +/// Scaffold +/// └── SingleChildScrollView +/// ├── _buildHeroCard() 渐变卡片:QR 码 + 推荐码 +/// ├── _buildStatsCard() 邀请进度:直接推荐 | 团队人数 +/// ├── _buildShareActions() 操作列表:复制码 / 复制链 / 系统分享 +/// └── GenexButton 主按钮:「分享给好友」 +/// ``` class SharePage extends StatefulWidget { const SharePage({super.key}); @@ -24,23 +63,52 @@ class SharePage extends StatefulWidget { } class _SharePageState extends State { + // ── 状态 ──────────────────────────────────────────────────────────────── + + /// 从 referral-service 加载的推荐信息;加载完成前为 null ReferralInfo? _info; + + /// 正在请求后端数据 bool _loading = true; + + /// 请求失败时的错误信息(用于展示重试按钮) String? _error; - /// App 下载落地页(带推荐码参数) + // ── 常量 ──────────────────────────────────────────────────────────────── + + /// App 下载落地页基础地址 + /// + /// 落地页职责: + /// 1. 检测 UA(iOS / Android / 其他) + /// 2. iOS → 跳转 App Store + /// 3. Android → 跳转 Google Play 或直接下载 APK + /// 4. 读取 `ref` 参数并存入 localStorage,注册页自动填充推荐码 static const String _baseInviteUrl = 'https://app.gogenex.com/download'; + // ── 计算属性 ───────────────────────────────────────────────────────────── + + /// 完整邀请链接(二维码内容 + 分享文案使用) + /// + /// 已加载:https://app.gogenex.com/download?ref=GNXAB2C3 + /// 未加载:https://app.gogenex.com/download String get _inviteLink => _info != null ? '$_baseInviteUrl?ref=${_info!.referralCode}' : _baseInviteUrl; + // ── 生命周期 ───────────────────────────────────────────────────────────── + @override void initState() { super.initState(); _loadInfo(); } + // ── 数据加载 ───────────────────────────────────────────────────────────── + + /// 从 referral-service 拉取当前用户的推荐信息 + /// + /// 成功:更新 [_info],隐藏 loading + /// 失败:记录 [_error],显示重试界面 Future _loadInfo() async { setState(() { _loading = true; @@ -54,6 +122,9 @@ class _SharePageState extends State { } } + // ── 用户操作 ───────────────────────────────────────────────────────────── + + /// 复制成功后展示浮动 SnackBar(紫色背景 + 勾号图标) void _showCopied(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -74,17 +145,33 @@ class _SharePageState extends State { ); } + /// 复制推荐码到剪贴板 + /// + /// 示例复制内容:GNXAB2C3 Future _copyCode() async { if (_info == null) return; await Clipboard.setData(ClipboardData(text: _info!.referralCode)); if (mounted) _showCopied(context.t('share.codeCopied')); } + /// 复制完整邀请链接到剪贴板 + /// + /// 示例:https://app.gogenex.com/download?ref=GNXAB2C3 Future _copyLink() async { await Clipboard.setData(ClipboardData(text: _inviteLink)); if (mounted) _showCopied(context.t('share.linkCopied')); } + /// 调用系统原生分享弹层 + /// + /// 分享内容:多行文案(包含推荐码 + 邀请链接) + /// + /// iOS → Share Sheet(AirDrop、微信、邮件、短信……) + /// Android → Sharesheet(微信、WhatsApp、Telegram……) + /// + /// 分享文案模板(zh-CN): + /// 我在用 Genex 玩数字券金融!使用我的推荐码 {code} 注册,立享专属福利。 + /// 下载链接:https://app.gogenex.com/download?ref={code} Future _shareNative() async { final code = _info?.referralCode ?? ''; final text = context @@ -94,6 +181,8 @@ class _SharePageState extends State { await Share.share(text, subject: context.t('share.nativeShareTitle')); } + // ── Build ──────────────────────────────────────────────────────────────── + @override Widget build(BuildContext context) { return Scaffold( @@ -116,6 +205,8 @@ class _SharePageState extends State { ); } + // ── 加载态 ─────────────────────────────────────────────────────────────── + Widget _buildLoading() { return Center( child: Column( @@ -132,6 +223,9 @@ class _SharePageState extends State { ); } + // ── 错误态 ─────────────────────────────────────────────────────────────── + + /// 加载失败界面:图标 + 描述文本 + 重试按钮 Widget _buildError() { return Center( child: Padding( @@ -159,19 +253,21 @@ class _SharePageState extends State { ); } + // ── 主内容 ─────────────────────────────────────────────────────────────── + Widget _buildContent() { return SingleChildScrollView( padding: AppSpacing.pagePadding, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildHeroCard(), + _buildHeroCard(), // QR 码 + 推荐码 const SizedBox(height: 16), - _buildStatsCard(), + _buildStatsCard(), // 邀请进度统计 const SizedBox(height: 16), - _buildShareActions(), + _buildShareActions(), // 快捷操作列表 const SizedBox(height: 24), - GenexButton( + GenexButton( // 主操作按钮 label: context.t('share.shareToFriend'), onPressed: _shareNative, ), @@ -181,7 +277,18 @@ class _SharePageState extends State { ); } - // ── Hero Card: QR 码 + 推荐码 ───────────────────────────────────────────── + // ── Hero Card:二维码 + 推荐码 ──────────────────────────────────────────── + // + // 视觉层次(由外到内): + // Container[渐变背景 + 圆角20 + 阴影] + // └── Column + // ├── 标题行(星形图标 + "扫码下载 Genex") + // ├── "Genex" 大字(字间距 3,FontWeight.w800) + // ├── Container[白色背景] → QrImageView(180×180,紫色点阵) + // └── 推荐码胶囊(半透明白底 + 复制按钮) + // + // QR 码内容:_inviteLink(如 https://app.gogenex.com/download?ref=GNXAB2C3) + // QR 码颜色:eye=深紫 #4834D4,module=主紫 #6C5CE7,背景=白色 Widget _buildHeroCard() { return Container( @@ -200,7 +307,7 @@ class _SharePageState extends State { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), child: Column( children: [ - // 顶部标题 + // ── 顶部标题 ──────────────────────────────────────────────────── Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -223,7 +330,8 @@ class _SharePageState extends State { ), const SizedBox(height: 24), - // QR 码卡片 + // ── 二维码 ────────────────────────────────────────────────────── + // 白色衬底卡片包裹 QrImageView,确保二维码在任何背景下均可扫描 Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( @@ -238,14 +346,16 @@ class _SharePageState extends State { ], ), child: QrImageView( - data: _inviteLink, - version: QrVersions.auto, + data: _inviteLink, // 编码完整邀请 URL + version: QrVersions.auto, // 自动选择最小版本 size: 180, backgroundColor: Colors.white, + // 定位图案(三个角的方框)用深紫色 eyeStyle: const QrEyeStyle( eyeShape: QrEyeShape.square, color: Color(0xFF4834D4), ), + // 数据模块用主紫色 dataModuleStyle: const QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: Color(0xFF6C5CE7), @@ -254,7 +364,8 @@ class _SharePageState extends State { ), const SizedBox(height: 24), - // 推荐码 + // ── 推荐码胶囊 ───────────────────────────────────────────────── + // 点击整个胶囊即可复制,内嵌「复制推荐码」小按钮提示可交互 Text( context.t('share.myReferralCode'), style: AppTypography.caption.copyWith(color: Colors.white60), @@ -265,13 +376,14 @@ class _SharePageState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), + color: Colors.white.withOpacity(0.15), // 半透明白底 borderRadius: BorderRadius.circular(40), border: Border.all(color: Colors.white30), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ + // 推荐码文字:等宽字体 + 字间距 4,视觉更清晰 Text( _info?.referralCode ?? '------', style: AppTypography.h2.copyWith( @@ -282,6 +394,7 @@ class _SharePageState extends State { ), ), const SizedBox(width: 12), + // 内嵌复制提示按钮 Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( @@ -313,7 +426,14 @@ class _SharePageState extends State { ); } - // ── 邀请进度 ────────────────────────────────────────────────────────────── + // ── 邀请进度统计卡片 ───────────────────────────────────────────────────── + // + // 布局:白色圆角卡片,水平等分两格,中间细竖线分隔 + // 左:直接推荐人数(Icons.people) + // 右:团队总人数(Icons.groups) + // + // 数据来源:ReferralInfo.directReferralCount / totalTeamCount + // 加载前显示 0,刷新后实时更新 Widget _buildStatsCard() { return Container( @@ -333,6 +453,7 @@ class _SharePageState extends State { label: context.t('share.directReferrals'), ), ), + // 分隔线 Container(width: 1, height: 44, color: AppColors.borderLight), Expanded( child: _buildStatItem( @@ -346,6 +467,7 @@ class _SharePageState extends State { ); } + /// 单个统计格:图标 + 数值 + 标签 Widget _buildStatItem({ required IconData icon, required String value, @@ -358,22 +480,27 @@ class _SharePageState extends State { children: [ Icon(icon, size: 16, color: AppColors.primary), const SizedBox(width: 4), - Text( - value, - style: AppTypography.h2.copyWith(color: AppColors.primary), - ), + Text(value, style: AppTypography.h2.copyWith(color: AppColors.primary)), ], ), const SizedBox(height: 2), - Text( - label, - style: AppTypography.caption.copyWith(color: AppColors.textSecondary), - ), + Text(label, style: AppTypography.caption.copyWith(color: AppColors.textSecondary)), ], ); } - // ── 分享操作列表 ─────────────────────────────────────────────────────────── + // ── 分享操作列表 ───────────────────────────────────────────────────────── + // + // 白色圆角卡片,ListTile 列表(Divider 分隔): + // + // [紫色图标] 复制推荐码 副文本:GNXAB2C3 > + // [蓝色图标] 复制链接 副文本:https://... > + // [绿色图标] 分享给好友 副文本:通过微信/WhatsApp > + // + // 每项点击行为: + // 复制推荐码 → _copyCode() → 剪贴板 + SnackBar + // 复制链接 → _copyLink() → 剪贴板 + SnackBar + // 分享给好友 → _shareNative() → 系统原生分享弹层 Widget _buildShareActions() { return Column( @@ -395,6 +522,7 @@ class _SharePageState extends State { ), child: Column( children: [ + // 复制推荐码 _buildActionTile( icon: Icons.qr_code_rounded, iconBg: AppColors.primaryContainer, @@ -404,6 +532,7 @@ class _SharePageState extends State { onTap: _copyCode, ), const Divider(indent: 60, height: 1), + // 复制链接 _buildActionTile( icon: Icons.link_rounded, iconBg: AppColors.infoLight, @@ -413,6 +542,7 @@ class _SharePageState extends State { onTap: _copyLink, ), const Divider(indent: 60, height: 1), + // 系统原生分享 _buildActionTile( icon: Icons.share_rounded, iconBg: AppColors.successLight, @@ -428,6 +558,10 @@ class _SharePageState extends State { ); } + /// 操作列表项 + /// + /// - [iconBg] 图标容器背景色(使用语义色的浅色版本) + /// - [subtitle] 单行截断,防止链接/代码过长破坏布局 Widget _buildActionTile({ required IconData icon, required Color iconBg,