diff --git a/frontend/mobile-app/lib/core/constants/api_endpoints.dart b/frontend/mobile-app/lib/core/constants/api_endpoints.dart index 84ba84a2..a108d214 100644 --- a/frontend/mobile-app/lib/core/constants/api_endpoints.dart +++ b/frontend/mobile-app/lib/core/constants/api_endpoints.dart @@ -96,4 +96,14 @@ class ApiEndpoints { // Pending Actions (-> Identity Service) static const String pendingActions = '/user/pending-actions'; static const String pendingActionsComplete = '/user/pending-actions'; // POST /:id/complete + + // [2026-02-17] 预种计划 (-> Planting Service / PrePlantingModule) + // 3171 USDT/份预种,累计 5 份自动合成 1 棵树 + // 所有端点与现有 /planting/* 完全独立 + static const String prePlanting = '/pre-planting'; + static const String prePlantingConfig = '$prePlanting/config'; // 开关配置 + static const String prePlantingEligibility = '$prePlanting/eligibility'; // 购买资格检查 + static const String prePlantingPosition = '$prePlanting/position'; // 持仓信息 + static const String prePlantingOrders = '$prePlanting/orders'; // 订单 CRUD + static const String prePlantingMerges = '$prePlanting/merges'; // 合并记录 } diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index ce58617f..8fff87c9 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -9,6 +9,8 @@ import '../services/authorization_service.dart'; import '../services/deposit_service.dart'; import '../services/wallet_service.dart'; import '../services/planting_service.dart'; +// [2026-02-17] 新增:预种计划服务(3171 USDT/份,独立于现有认种) +import '../services/pre_planting_service.dart'; import '../services/reward_service.dart'; import '../services/notification_service.dart'; import '../services/system_config_service.dart'; @@ -95,6 +97,13 @@ final plantingServiceProvider = Provider((ref) { return PlantingService(apiClient: apiClient); }); +// [2026-02-17] Pre-Planting Service Provider (调用 planting-service / PrePlantingModule) +// 预种计划:3171 USDT/份,累计 5 份合成 1 棵树。与上方 PlantingService 完全独立。 +final prePlantingServiceProvider = Provider((ref) { + final apiClient = ref.watch(apiClientProvider); + return PrePlantingService(apiClient: apiClient); +}); + // Reward Service Provider (直接调用 reward-service) final rewardServiceProvider = Provider((ref) { final apiClient = ref.watch(apiClientProvider); diff --git a/frontend/mobile-app/lib/core/services/pre_planting_service.dart b/frontend/mobile-app/lib/core/services/pre_planting_service.dart new file mode 100644 index 00000000..41e756d1 --- /dev/null +++ b/frontend/mobile-app/lib/core/services/pre_planting_service.dart @@ -0,0 +1,511 @@ +import 'package:flutter/foundation.dart'; +import '../network/api_client.dart'; + +// ============================================ +// [2026-02-17] 预种计划 API 服务 +// ============================================ +// +// 3171 预种计划(拼种/团购计划)的 Flutter 端 API 调用服务。 +// 用户以 3171 USDT/份参与认种,累计 5 份自动合成 1 棵树。 +// +// === API 端点 === +// 所有端点走 planting-service 的 PrePlantingModule: +// - GET /pre-planting/position 获取预种持仓 +// - GET /pre-planting/config 获取预种开关状态 +// - POST /pre-planting/orders 创建预种订单 +// - POST /pre-planting/orders/:no/pay 支付预种订单 +// - GET /pre-planting/orders 获取预种订单列表 +// - GET /pre-planting/merges 获取合并记录列表 +// - GET /pre-planting/merges/:id 获取合并详情 +// - POST /pre-planting/merges/:id/sign 签署合并合同 +// +// === 与现有 PlantingService 的关系 === +// 完全独立。PlantingService 处理整棵树认种(15831 USDT/棵), +// PrePlantingService 处理预种份额(3171 USDT/份)。 +// 两者调用不同的 API 端点,互不影响。 + +/// 预种订单状态 +enum PrePlantingOrderStatus { + created, // 已创建(待支付) + paid, // 已支付(算力已生效) + merged, // 已合并(5 份合成 1 棵树) +} + +/// 预种合同签署状态 +enum PrePlantingContractStatus { + pending, // 待签署 + signed, // 已签署 + expired, // 已过期 +} + +/// 预种开关配置 +class PrePlantingConfig { + final bool isActive; // 预种功能是否开启 + final DateTime? activatedAt; // 开启时间 + + PrePlantingConfig({ + required this.isActive, + this.activatedAt, + }); + + factory PrePlantingConfig.fromJson(Map json) { + return PrePlantingConfig( + isActive: json['isActive'] ?? false, + activatedAt: json['activatedAt'] != null + ? DateTime.parse(json['activatedAt']) + : null, + ); + } +} + +/// 预种持仓信息(每用户一条) +class PrePlantingPosition { + final int totalPortions; // 累计购买份数(含已合并) + final int availablePortions; // 待合并份数(0-4) + final int mergedPortions; // 已合并份数 + final int totalTreesMerged; // 已合成的树数 + final String? provinceCode; // 省代码(首次购买时选择,后续复用) + final String? cityCode; // 市代码 + final String? provinceName; // 省名称 + final String? cityName; // 市名称 + final DateTime? firstPurchaseAt; // 首次购买时间(1 年冻结起点) + + PrePlantingPosition({ + required this.totalPortions, + required this.availablePortions, + required this.mergedPortions, + required this.totalTreesMerged, + this.provinceCode, + this.cityCode, + this.provinceName, + this.cityName, + this.firstPurchaseAt, + }); + + /// 距离下一次合并还需多少份 + int get portionsToNextMerge => availablePortions == 0 ? 5 : 5 - availablePortions; + + /// 合并进度 (0.0 - 1.0) + double get mergeProgress => availablePortions / 5.0; + + /// 是否已选择省市(首次购买后即锁定) + bool get hasProvinceCity => provinceCode != null && cityCode != null; + + factory PrePlantingPosition.fromJson(Map json) { + return PrePlantingPosition( + totalPortions: json['totalPortions'] ?? 0, + availablePortions: json['availablePortions'] ?? 0, + mergedPortions: json['mergedPortions'] ?? 0, + totalTreesMerged: json['totalTreesMerged'] ?? 0, + provinceCode: json['provinceCode'], + cityCode: json['cityCode'], + provinceName: json['provinceName'], + cityName: json['cityName'], + firstPurchaseAt: json['firstPurchaseAt'] != null + ? DateTime.parse(json['firstPurchaseAt']) + : null, + ); + } +} + +/// 预种订单 +class PrePlantingOrder { + final String orderNo; + final int portionCount; // 购买份数(通常为 1) + final double pricePerPortion; // 每份价格(3171 USDT) + final double totalAmount; // 总金额 + final PrePlantingOrderStatus status; + final String? mergedToMergeId; // 合并后指向的 MergeId + final DateTime? paidAt; + final DateTime? mergedAt; + final DateTime createdAt; + + PrePlantingOrder({ + required this.orderNo, + required this.portionCount, + required this.pricePerPortion, + required this.totalAmount, + required this.status, + this.mergedToMergeId, + this.paidAt, + this.mergedAt, + required this.createdAt, + }); + + factory PrePlantingOrder.fromJson(Map json) { + return PrePlantingOrder( + orderNo: json['orderNo'] ?? '', + portionCount: json['portionCount'] ?? 1, + pricePerPortion: (json['pricePerPortion'] ?? 3171).toDouble(), + totalAmount: (json['totalAmount'] ?? 3171).toDouble(), + status: _parseStatus(json['status']), + mergedToMergeId: json['mergedToMergeId']?.toString(), + paidAt: json['paidAt'] != null ? DateTime.parse(json['paidAt']) : null, + mergedAt: json['mergedAt'] != null ? DateTime.parse(json['mergedAt']) : null, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + ); + } + + static PrePlantingOrderStatus _parseStatus(String? status) { + switch (status) { + case 'CREATED': return PrePlantingOrderStatus.created; + case 'PAID': return PrePlantingOrderStatus.paid; + case 'MERGED': return PrePlantingOrderStatus.merged; + default: return PrePlantingOrderStatus.created; + } + } +} + +/// 预种合并记录(5 份 → 1 棵树) +class PrePlantingMerge { + final String mergeNo; + final List sourceOrderNos; // 5 笔来源订单号 + final int treeCount; // 合并产生的树数(固定 1) + final PrePlantingContractStatus contractStatus; + final DateTime? contractSignedAt; + final DateTime? miningEnabledAt; + final String? selectedProvince; + final String? selectedCity; + final DateTime mergedAt; + + PrePlantingMerge({ + required this.mergeNo, + required this.sourceOrderNos, + required this.treeCount, + required this.contractStatus, + this.contractSignedAt, + this.miningEnabledAt, + this.selectedProvince, + this.selectedCity, + required this.mergedAt, + }); + + /// 是否已签约 + bool get isSigned => contractStatus == PrePlantingContractStatus.signed; + + /// 是否挖矿已开启 + bool get isMiningEnabled => miningEnabledAt != null; + + factory PrePlantingMerge.fromJson(Map json) { + return PrePlantingMerge( + mergeNo: json['mergeNo'] ?? '', + sourceOrderNos: (json['sourceOrderNos'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + [], + treeCount: json['treeCount'] ?? 1, + contractStatus: _parseContractStatus(json['contractStatus']), + contractSignedAt: json['contractSignedAt'] != null + ? DateTime.parse(json['contractSignedAt']) + : null, + miningEnabledAt: json['miningEnabledAt'] != null + ? DateTime.parse(json['miningEnabledAt']) + : null, + selectedProvince: json['selectedProvince'], + selectedCity: json['selectedCity'], + mergedAt: json['mergedAt'] != null + ? DateTime.parse(json['mergedAt']) + : DateTime.now(), + ); + } + + static PrePlantingContractStatus _parseContractStatus(String? status) { + switch (status) { + case 'PENDING': return PrePlantingContractStatus.pending; + case 'SIGNED': return PrePlantingContractStatus.signed; + case 'EXPIRED': return PrePlantingContractStatus.expired; + default: return PrePlantingContractStatus.pending; + } + } +} + +/// 创建预种订单响应 +class CreatePrePlantingOrderResponse { + final String orderNo; + final int portionCount; + final double totalAmount; + final double pricePerPortion; + + CreatePrePlantingOrderResponse({ + required this.orderNo, + required this.portionCount, + required this.totalAmount, + required this.pricePerPortion, + }); + + factory CreatePrePlantingOrderResponse.fromJson(Map json) { + return CreatePrePlantingOrderResponse( + orderNo: json['orderNo'] ?? '', + portionCount: json['portionCount'] ?? 1, + totalAmount: (json['totalAmount'] ?? 3171).toDouble(), + pricePerPortion: (json['pricePerPortion'] ?? 3171).toDouble(), + ); + } +} + +/// 预种购买资格检查结果 +class PrePlantingEligibility { + final bool canPurchase; // 是否可以购买 + final int? maxAdditional; // 最多还能买几份(开关关闭时的凑满限制) + final String? message; // 不可购买的原因说明 + + PrePlantingEligibility({ + required this.canPurchase, + this.maxAdditional, + this.message, + }); + + factory PrePlantingEligibility.fromJson(Map json) { + return PrePlantingEligibility( + canPurchase: json['canPurchase'] ?? false, + maxAdditional: json['maxAdditional'], + message: json['message'], + ); + } +} + +// ============================================ +// 预种计划 API 服务 +// ============================================ + +/// 预种计划 API 服务 +/// +/// [2026-02-17] 新增:提供预种计划的所有 API 调用 +/// +/// 所有端点走 planting-service 的 /pre-planting/* 路由, +/// 由 PrePlantingModule 的 PrePlantingController 处理。 +/// 与现有 PlantingService 的 /planting/* 端点完全隔离。 +class PrePlantingService { + final ApiClient _apiClient; + + PrePlantingService({required ApiClient apiClient}) : _apiClient = apiClient; + + // === 配置与资格 === + + /// 获取预种功能配置(开关状态) + /// + /// 调用 admin-service 的预种配置 API + Future getConfig() async { + try { + debugPrint('[PrePlantingService] 获取预种配置'); + final response = await _apiClient.get('/pre-planting/config'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return PrePlantingConfig.fromJson(data); + } + + throw Exception('获取预种配置失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 获取预种配置失败: $e'); + rethrow; + } + } + + /// 检查购买资格 + /// + /// 开关开启时任何人可买;关闭时已有未满份额的用户可继续凑满 + Future checkEligibility() async { + try { + debugPrint('[PrePlantingService] 检查购买资格'); + final response = await _apiClient.get('/pre-planting/eligibility'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return PrePlantingEligibility.fromJson(data); + } + + throw Exception('检查购买资格失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 检查购买资格失败: $e'); + rethrow; + } + } + + // === 持仓查询 === + + /// 获取我的预种持仓信息 + /// + /// 返回累计份数、待合并份数、已合成树数、省市信息等 + Future getMyPosition() async { + try { + debugPrint('[PrePlantingService] 获取预种持仓'); + final response = await _apiClient.get('/pre-planting/position'); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint( + '[PrePlantingService] 持仓: total=${data['totalPortions']}, ' + 'available=${data['availablePortions']}, ' + 'merged=${data['totalTreesMerged']}', + ); + return PrePlantingPosition.fromJson(data); + } + + throw Exception('获取预种持仓失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 获取预种持仓失败: $e'); + rethrow; + } + } + + // === 订单操作 === + + /// 创建预种订单 + /// + /// [portionCount] 购买份数(通常为 1) + /// [provinceCode] 省代码(首次购买时必填,续购自动复用) + /// [provinceName] 省名称 + /// [cityCode] 市代码 + /// [cityName] 市名称 + /// + /// 购买流程:创建订单 → 自动扣款 → 分配权益 → 检查合并 + /// 省市信息首次购买时由用户选择,后续购买自动复用。 + Future createOrder({ + int portionCount = 1, + String? provinceCode, + String? provinceName, + String? cityCode, + String? cityName, + }) async { + try { + debugPrint('[PrePlantingService] 创建预种订单: portionCount=$portionCount'); + + final Map data = { + 'portionCount': portionCount, + }; + // 首次购买需要提供省市信息 + if (provinceCode != null) data['provinceCode'] = provinceCode; + if (provinceName != null) data['provinceName'] = provinceName; + if (cityCode != null) data['cityCode'] = cityCode; + if (cityName != null) data['cityName'] = cityName; + + final response = await _apiClient.post( + '/pre-planting/orders', + data: data, + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + final responseData = response.data as Map; + debugPrint('[PrePlantingService] 订单创建成功: ${responseData['orderNo']}'); + return CreatePrePlantingOrderResponse.fromJson(responseData); + } + + throw Exception('创建预种订单失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 创建预种订单失败: $e'); + rethrow; + } + } + + /// 获取我的预种订单列表 + Future> getMyOrders({ + int page = 1, + int pageSize = 20, + }) async { + try { + debugPrint('[PrePlantingService] 获取预种订单列表'); + final response = await _apiClient.get( + '/pre-planting/orders', + queryParameters: {'page': page, 'pageSize': pageSize}, + ); + + if (response.statusCode == 200) { + List items; + if (response.data is List) { + items = response.data as List; + } else if (response.data is Map) { + items = (response.data as Map)['items'] as List? ?? []; + } else { + items = []; + } + return items + .map((e) => PrePlantingOrder.fromJson(e as Map)) + .toList(); + } + + throw Exception('获取预种订单列表失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 获取预种订单列表失败: $e'); + rethrow; + } + } + + // === 合并记录 === + + /// 获取我的合并记录列表(5 份 → 1 棵树) + Future> getMyMerges({ + int page = 1, + int pageSize = 20, + }) async { + try { + debugPrint('[PrePlantingService] 获取合并记录列表'); + final response = await _apiClient.get( + '/pre-planting/merges', + queryParameters: {'page': page, 'pageSize': pageSize}, + ); + + if (response.statusCode == 200) { + List items; + if (response.data is List) { + items = response.data as List; + } else if (response.data is Map) { + items = (response.data as Map)['items'] as List? ?? []; + } else { + items = []; + } + return items + .map((e) => PrePlantingMerge.fromJson(e as Map)) + .toList(); + } + + throw Exception('获取合并记录失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 获取合并记录失败: $e'); + rethrow; + } + } + + /// 获取合并详情 + Future getMergeDetail(String mergeNo) async { + try { + debugPrint('[PrePlantingService] 获取合并详情: mergeNo=$mergeNo'); + final response = await _apiClient.get('/pre-planting/merges/$mergeNo'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return PrePlantingMerge.fromJson(data); + } + + throw Exception('获取合并详情失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 获取合并详情失败: $e'); + rethrow; + } + } + + /// 签署合并合同 + /// + /// 5 份合并后需要签约,签约后解锁交易/提现/授权限制 + Future signMergeContract(String mergeNo) async { + try { + debugPrint('[PrePlantingService] 签署合并合同: mergeNo=$mergeNo'); + final response = await _apiClient.post( + '/pre-planting/merges/$mergeNo/sign', + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[PrePlantingService] 合同签署成功'); + return PrePlantingMerge.fromJson(data); + } + + throw Exception('签署合同失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PrePlantingService] 签署合同失败: $e'); + rethrow; + } + } +}