feat(pre-planting): Mobile App 预种计划 Service 层

[2026-02-17] 新增预种计划的 Flutter 端 API 服务层:

1. pre_planting_service.dart(新增)
   - PrePlantingService:预种 API 调用(配置/资格/持仓/订单/合并/签约)
   - 数据模型:PrePlantingPosition、PrePlantingOrder、PrePlantingMerge 等
   - 与现有 PlantingService 完全独立

2. api_endpoints.dart(+10 行)
   - 添加 /pre-planting/* 端点常量

3. injection_container.dart(+9 行)
   - 注册 prePlantingServiceProvider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-18 05:31:39 -08:00
parent e1cd8ed7f2
commit 27751731e8
3 changed files with 530 additions and 0 deletions

View File

@ -96,4 +96,14 @@ class ApiEndpoints {
// Pending Actions (-> Identity Service) // Pending Actions (-> Identity Service)
static const String pendingActions = '/user/pending-actions'; static const String pendingActions = '/user/pending-actions';
static const String pendingActionsComplete = '/user/pending-actions'; // POST /:id/complete 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'; //
} }

View File

@ -9,6 +9,8 @@ import '../services/authorization_service.dart';
import '../services/deposit_service.dart'; import '../services/deposit_service.dart';
import '../services/wallet_service.dart'; import '../services/wallet_service.dart';
import '../services/planting_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/reward_service.dart';
import '../services/notification_service.dart'; import '../services/notification_service.dart';
import '../services/system_config_service.dart'; import '../services/system_config_service.dart';
@ -95,6 +97,13 @@ final plantingServiceProvider = Provider<PlantingService>((ref) {
return PlantingService(apiClient: apiClient); return PlantingService(apiClient: apiClient);
}); });
// [2026-02-17] Pre-Planting Service Provider ( planting-service / PrePlantingModule)
// 3171 USDT/ 5 1 PlantingService
final prePlantingServiceProvider = Provider<PrePlantingService>((ref) {
final apiClient = ref.watch(apiClientProvider);
return PrePlantingService(apiClient: apiClient);
});
// Reward Service Provider ( reward-service) // Reward Service Provider ( reward-service)
final rewardServiceProvider = Provider<RewardService>((ref) { final rewardServiceProvider = Provider<RewardService>((ref) {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);

View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String> 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<String, dynamic> json) {
return PrePlantingMerge(
mergeNo: json['mergeNo'] ?? '',
sourceOrderNos: (json['sourceOrderNos'] as List<dynamic>?)
?.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<String, dynamic> 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<String, dynamic> 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<PrePlantingConfig> getConfig() async {
try {
debugPrint('[PrePlantingService] 获取预种配置');
final response = await _apiClient.get('/pre-planting/config');
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
return PrePlantingConfig.fromJson(data);
}
throw Exception('获取预种配置失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 获取预种配置失败: $e');
rethrow;
}
}
///
///
///
Future<PrePlantingEligibility> checkEligibility() async {
try {
debugPrint('[PrePlantingService] 检查购买资格');
final response = await _apiClient.get('/pre-planting/eligibility');
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
return PrePlantingEligibility.fromJson(data);
}
throw Exception('检查购买资格失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 检查购买资格失败: $e');
rethrow;
}
}
// === ===
///
///
///
Future<PrePlantingPosition> getMyPosition() async {
try {
debugPrint('[PrePlantingService] 获取预种持仓');
final response = await _apiClient.get('/pre-planting/position');
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
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<CreatePrePlantingOrderResponse> createOrder({
int portionCount = 1,
String? provinceCode,
String? provinceName,
String? cityCode,
String? cityName,
}) async {
try {
debugPrint('[PrePlantingService] 创建预种订单: portionCount=$portionCount');
final Map<String, dynamic> 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<String, dynamic>;
debugPrint('[PrePlantingService] 订单创建成功: ${responseData['orderNo']}');
return CreatePrePlantingOrderResponse.fromJson(responseData);
}
throw Exception('创建预种订单失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 创建预种订单失败: $e');
rethrow;
}
}
///
Future<List<PrePlantingOrder>> 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<dynamic> items;
if (response.data is List) {
items = response.data as List<dynamic>;
} else if (response.data is Map<String, dynamic>) {
items = (response.data as Map<String, dynamic>)['items'] as List<dynamic>? ?? [];
} else {
items = [];
}
return items
.map((e) => PrePlantingOrder.fromJson(e as Map<String, dynamic>))
.toList();
}
throw Exception('获取预种订单列表失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 获取预种订单列表失败: $e');
rethrow;
}
}
// === ===
/// 5 1
Future<List<PrePlantingMerge>> 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<dynamic> items;
if (response.data is List) {
items = response.data as List<dynamic>;
} else if (response.data is Map<String, dynamic>) {
items = (response.data as Map<String, dynamic>)['items'] as List<dynamic>? ?? [];
} else {
items = [];
}
return items
.map((e) => PrePlantingMerge.fromJson(e as Map<String, dynamic>))
.toList();
}
throw Exception('获取合并记录失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 获取合并记录失败: $e');
rethrow;
}
}
///
Future<PrePlantingMerge> 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<String, dynamic>;
return PrePlantingMerge.fromJson(data);
}
throw Exception('获取合并详情失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 获取合并详情失败: $e');
rethrow;
}
}
///
///
/// 5 //
Future<PrePlantingMerge> 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<String, dynamic>;
debugPrint('[PrePlantingService] 合同签署成功');
return PrePlantingMerge.fromJson(data);
}
throw Exception('签署合同失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PrePlantingService] 签署合同失败: $e');
rethrow;
}
}
}