feat: 会员 provider(状态/套餐/下单/轮询/领奖)

This commit is contained in:
2026-07-31 13:50:02 +08:00
parent e20839b695
commit 38cf41d44d
+109 -1
View File
@@ -1,4 +1,10 @@
/// 会员 providerTask 1 仅含权益文案纯函数(Task 2 将补全状态/套餐/下单/轮询/领奖)
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_provider.dart';
/// 会员 provider:会员状态/套餐/下单/轮询/领奖
class MemberInfo {
final bool isVip;
final String expireAt;
@@ -11,6 +17,13 @@ class MemberInfo {
required this.planName,
required this.benefits,
});
factory MemberInfo.fromJson(Map<String, dynamic> e) => MemberInfo(
isVip: e['is_vip'] as bool? ?? false,
expireAt: e['expire_at'] as String? ?? '',
planName: e['plan_name'] as String? ?? '',
benefits: (e['benefits'] as List<dynamic>? ?? []).cast<String>(),
);
}
/// 权益 key → 文案
@@ -23,3 +36,98 @@ const benefitLabels = {
List<String> benefitTexts(MemberInfo m) =>
m.benefits.map((b) => benefitLabels[b] ?? b).toList();
class MemberNotifier extends AsyncNotifier<MemberInfo> {
@override
Future<MemberInfo> build() async {
final api = ref.read(apiClientProvider);
final data = await api.get<Map<String, dynamic>>('/member/status');
return MemberInfo.fromJson(data);
}
Future<void> refresh() async {
state = await AsyncValue.guard(build);
}
/// 领取广告激励(服务端限频);adType: effect_extra | vip_trial
/// 返回当日剩余次数;超出限频抛 ApiException
Future<int> claimReward(String adType) async {
final api = ref.read(apiClientProvider);
final data =
await api.post<Map<String, dynamic>>('/ad/reward/claim', {'ad_type': adType});
await refresh(); // vip_trial 可能开通体验会员
return (data['reward']?['remaining_today'] as num?)?.toInt() ?? 0;
}
}
final memberProvider =
AsyncNotifierProvider<MemberNotifier, MemberInfo>(MemberNotifier.new);
class MemberPlan {
final int id;
final String name;
final int priceFen;
final int durationDays;
final List<String> features;
const MemberPlan({
required this.id,
required this.name,
required this.priceFen,
required this.durationDays,
required this.features,
});
factory MemberPlan.fromJson(Map<String, dynamic> e) => MemberPlan(
id: (e['id'] as num).toInt(),
name: e['name'] as String? ?? '',
priceFen: (e['price_fen'] as num?)?.toInt() ?? 0,
durationDays: (e['duration_days'] as num?)?.toInt() ?? 30,
features: _parseFeatures(e['features'] as String? ?? ''),
);
static List<String> _parseFeatures(String s) {
try {
return (jsonDecode(s) as List<dynamic>).cast<String>();
} catch (_) {
return const [];
}
}
String get priceText =>
'¥${(priceFen / 100).toStringAsFixed(priceFen % 100 == 0 ? 0 : 1)}';
}
final memberPlanProvider = FutureProvider<List<MemberPlan>>((ref) async {
final api = ref.read(apiClientProvider);
final list = await api.get<List<dynamic>>('/member/plan/list');
return list
.map((e) => MemberPlan.fromJson(e as Map<String, dynamic>))
.toList();
});
class OrderResult {
final String orderNo;
final String payUrl;
const OrderResult({required this.orderNo, required this.payUrl});
}
/// 创建支付订单(后端调虎皮棋下单,返回收银台 URL)
Future<OrderResult> createMemberOrder(WidgetRef ref, int planId) async {
final api = ref.read(apiClientProvider);
final data =
await api.post<Map<String, dynamic>>('/member/order/create', {'plan_id': planId});
return OrderResult(
orderNo: data['order_no'] as String? ?? '',
payUrl: data['pay_url'] as String? ?? '',
);
}
/// 订单状态(支付页 2s 轮询):pending | paid | closed
Future<String> fetchOrderStatus(WidgetRef ref, String orderNo) async {
final api = ref.read(apiClientProvider);
final data = await api.get<Map<String, dynamic>>('/member/order/status',
query: {'order_no': orderNo});
return data['status'] as String? ?? '';
}