diff --git a/backend/services/referral-service/src/infrastructure/kafka/event-ack.publisher.ts b/backend/services/referral-service/src/infrastructure/kafka/event-ack.publisher.ts index 378697e4..d9962bd9 100644 --- a/backend/services/referral-service/src/infrastructure/kafka/event-ack.publisher.ts +++ b/backend/services/referral-service/src/infrastructure/kafka/event-ack.publisher.ts @@ -1,5 +1,5 @@ -import { Injectable, Logger, Inject } from '@nestjs/common'; -import { ClientKafka } from '@nestjs/microservices'; +import { Injectable, Logger } from '@nestjs/common'; +import { KafkaService } from '../messaging/kafka.service'; /** * 事件确认消息结构 @@ -30,10 +30,7 @@ export class EventAckPublisher { private readonly logger = new Logger(EventAckPublisher.name); private readonly serviceName = 'referral-service'; - constructor( - @Inject('KAFKA_SERVICE') - private readonly kafkaClient: ClientKafka, - ) {} + constructor(private readonly kafkaService: KafkaService) {} /** * 发送处理成功确认 @@ -48,9 +45,10 @@ export class EventAckPublisher { }; try { - this.kafkaClient.emit('planting.events.ack', { + await this.kafkaService.publish({ + topic: 'planting.events.ack', key: eventId, - value: JSON.stringify(ackMessage), + value: ackMessage, }); this.logger.log(`[ACK] ✓ Sent success confirmation for event ${eventId} (${eventType})`); @@ -73,9 +71,10 @@ export class EventAckPublisher { }; try { - this.kafkaClient.emit('planting.events.ack', { + await this.kafkaService.publish({ + topic: 'planting.events.ack', key: eventId, - value: JSON.stringify(ackMessage), + value: ackMessage, }); this.logger.warn(`[ACK] ✗ Sent failure confirmation for event ${eventId}: ${errorMessage}`); diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index 65e98e90..9b996f1b 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -7,6 +7,7 @@ import '../services/referral_service.dart'; import '../services/authorization_service.dart'; import '../services/deposit_service.dart'; import '../services/wallet_service.dart'; +import '../services/planting_service.dart'; // Storage Providers final secureStorageProvider = Provider((ref) { @@ -58,6 +59,12 @@ final walletServiceProvider = Provider((ref) { return WalletService(apiClient: apiClient); }); +// Planting Service Provider +final plantingServiceProvider = Provider((ref) { + final apiClient = ref.watch(apiClientProvider); + return PlantingService(apiClient: apiClient); +}); + // Override provider with initialized instance ProviderContainer createProviderContainer(LocalStorage localStorage) { return ProviderContainer( diff --git a/frontend/mobile-app/lib/core/services/planting_service.dart b/frontend/mobile-app/lib/core/services/planting_service.dart new file mode 100644 index 00000000..f277f7c4 --- /dev/null +++ b/frontend/mobile-app/lib/core/services/planting_service.dart @@ -0,0 +1,299 @@ +import 'package:flutter/foundation.dart'; +import '../network/api_client.dart'; + +/// 认种订单状态 +enum PlantingOrderStatus { + created, // 已创建 + provinceCitySelected, // 已选择省市 + provinceCityConfirmed, // 已确认省市 + paid, // 已支付 + fundAllocated, // 资金已分配 + cancelled, // 已取消 +} + +/// 认种订单响应 +class PlantingOrder { + final String orderNo; + final int treeCount; + final double totalAmount; + final PlantingOrderStatus status; + final String? selectedProvince; + final String? selectedCity; + final DateTime? provinceCitySelectedAt; + final DateTime? provinceCityConfirmedAt; + final DateTime? paidAt; + final bool isMiningEnabled; + final DateTime createdAt; + + PlantingOrder({ + required this.orderNo, + required this.treeCount, + required this.totalAmount, + required this.status, + this.selectedProvince, + this.selectedCity, + this.provinceCitySelectedAt, + this.provinceCityConfirmedAt, + this.paidAt, + required this.isMiningEnabled, + required this.createdAt, + }); + + factory PlantingOrder.fromJson(Map json) { + return PlantingOrder( + orderNo: json['orderNo'] ?? '', + treeCount: json['treeCount'] ?? 0, + totalAmount: (json['totalAmount'] ?? 0).toDouble(), + status: _parseStatus(json['status']), + selectedProvince: json['selectedProvince'], + selectedCity: json['selectedCity'], + provinceCitySelectedAt: json['provinceCitySelectedAt'] != null + ? DateTime.parse(json['provinceCitySelectedAt']) + : null, + provinceCityConfirmedAt: json['provinceCityConfirmedAt'] != null + ? DateTime.parse(json['provinceCityConfirmedAt']) + : null, + paidAt: json['paidAt'] != null ? DateTime.parse(json['paidAt']) : null, + isMiningEnabled: json['isMiningEnabled'] ?? false, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + ); + } + + static PlantingOrderStatus _parseStatus(String? status) { + switch (status) { + case 'CREATED': + return PlantingOrderStatus.created; + case 'PROVINCE_CITY_SELECTED': + return PlantingOrderStatus.provinceCitySelected; + case 'PROVINCE_CITY_CONFIRMED': + return PlantingOrderStatus.provinceCityConfirmed; + case 'PAID': + return PlantingOrderStatus.paid; + case 'FUND_ALLOCATED': + return PlantingOrderStatus.fundAllocated; + case 'CANCELLED': + return PlantingOrderStatus.cancelled; + default: + return PlantingOrderStatus.created; + } + } +} + +/// 创建订单响应 +class CreateOrderResponse { + final String orderNo; + final int treeCount; + final double totalAmount; + final double pricePerTree; + + CreateOrderResponse({ + required this.orderNo, + required this.treeCount, + required this.totalAmount, + required this.pricePerTree, + }); + + factory CreateOrderResponse.fromJson(Map json) { + return CreateOrderResponse( + orderNo: json['orderNo'] ?? '', + treeCount: json['treeCount'] ?? 0, + totalAmount: (json['totalAmount'] ?? 0).toDouble(), + pricePerTree: (json['pricePerTree'] ?? 2199).toDouble(), + ); + } +} + +/// 认种服务 +/// +/// 提供认种订单创建、省市选择、支付等功能 +class PlantingService { + final ApiClient _apiClient; + + PlantingService({required ApiClient apiClient}) : _apiClient = apiClient; + + /// 创建认种订单 + /// + /// [treeCount] 认种数量 + /// 返回订单号和总金额 + Future createOrder(int treeCount) async { + try { + debugPrint('[PlantingService] 创建认种订单: treeCount=$treeCount'); + + final response = await _apiClient.post( + '/planting/orders', + data: {'treeCount': treeCount}, + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[PlantingService] 订单创建成功: ${data['orderNo']}'); + return CreateOrderResponse.fromJson(data); + } + + throw Exception('创建订单失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 创建订单失败: $e'); + rethrow; + } + } + + /// 选择省市 + /// + /// [orderNo] 订单号 + /// [provinceCode] 省份代码 + /// [cityCode] 城市代码 + Future selectProvinceCity( + String orderNo, + String provinceCode, + String cityCode, + ) async { + try { + debugPrint('[PlantingService] 选择省市: orderNo=$orderNo, province=$provinceCode, city=$cityCode'); + + final response = await _apiClient.post( + '/planting/orders/$orderNo/select-province-city', + data: { + 'provinceCode': provinceCode, + 'cityCode': cityCode, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[PlantingService] 省市选择成功'); + return PlantingOrder.fromJson(data); + } + + throw Exception('选择省市失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 选择省市失败: $e'); + rethrow; + } + } + + /// 确认省市选择 + /// + /// [orderNo] 订单号 + /// 确认后不可修改省市 + Future confirmProvinceCity(String orderNo) async { + try { + debugPrint('[PlantingService] 确认省市选择: orderNo=$orderNo'); + + final response = await _apiClient.post( + '/planting/orders/$orderNo/confirm-province-city', + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[PlantingService] 省市确认成功'); + return PlantingOrder.fromJson(data); + } + + throw Exception('确认省市失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 确认省市失败: $e'); + rethrow; + } + } + + /// 支付订单 + /// + /// [orderNo] 订单号 + /// 支付成功后开始挖矿 + Future payOrder(String orderNo) async { + try { + debugPrint('[PlantingService] 支付订单: orderNo=$orderNo'); + + final response = await _apiClient.post( + '/planting/orders/$orderNo/pay', + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[PlantingService] 支付成功'); + return PlantingOrder.fromJson(data); + } + + throw Exception('支付失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 支付失败: $e'); + rethrow; + } + } + + /// 获取订单详情 + /// + /// [orderNo] 订单号 + Future getOrder(String orderNo) async { + try { + debugPrint('[PlantingService] 获取订单详情: orderNo=$orderNo'); + + final response = await _apiClient.get('/planting/orders/$orderNo'); + + if (response.statusCode == 200) { + final data = response.data as Map; + return PlantingOrder.fromJson(data); + } + + throw Exception('获取订单失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 获取订单失败: $e'); + rethrow; + } + } + + /// 获取我的订单列表 + /// + /// [page] 页码 + /// [pageSize] 每页数量 + Future> getMyOrders({int page = 1, int pageSize = 20}) async { + try { + debugPrint('[PlantingService] 获取我的订单列表: page=$page, pageSize=$pageSize'); + + final response = await _apiClient.get( + '/planting/orders', + queryParameters: { + 'page': page, + 'pageSize': pageSize, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + final items = data['items'] as List? ?? []; + return items.map((e) => PlantingOrder.fromJson(e)).toList(); + } + + throw Exception('获取订单列表失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 获取订单列表失败: $e'); + rethrow; + } + } + + /// 取消订单 + /// + /// [orderNo] 订单号 + /// 只有未支付的订单可以取消 + Future cancelOrder(String orderNo) async { + try { + debugPrint('[PlantingService] 取消订单: orderNo=$orderNo'); + + final response = await _apiClient.post( + '/planting/orders/$orderNo/cancel', + ); + + if (response.statusCode == 200) { + debugPrint('[PlantingService] 订单取消成功'); + return; + } + + throw Exception('取消订单失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 取消订单失败: $e'); + rethrow; + } + } +} diff --git a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart index ccd5da13..af095daa 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart @@ -3,15 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:city_pickers/city_pickers.dart'; import '../widgets/planting_confirm_dialog.dart'; +import '../../../../core/di/injection_container.dart'; /// 认种省市选择页面参数 class PlantingLocationParams { final int quantity; final double totalPrice; + final String orderNo; // 订单号 PlantingLocationParams({ required this.quantity, required this.totalPrice, + required this.orderNo, }); } @@ -20,11 +23,13 @@ class PlantingLocationParams { class PlantingLocationPage extends ConsumerStatefulWidget { final int quantity; final double totalPrice; + final String orderNo; const PlantingLocationPage({ super.key, required this.quantity, required this.totalPrice, + required this.orderNo, }); @override @@ -33,11 +38,17 @@ class PlantingLocationPage extends ConsumerStatefulWidget { } class _PlantingLocationPageState extends ConsumerState { - /// 选中的省份 - String? _selectedProvince; + /// 选中的省份名称 + String? _selectedProvinceName; - /// 选中的城市 - String? _selectedCity; + /// 选中的城市名称 + String? _selectedCityName; + + /// 选中的省份代码 + String? _selectedProvinceCode; + + /// 选中的城市代码 + String? _selectedCityCode; /// 是否正在提交 bool _isSubmitting = false; @@ -66,8 +77,10 @@ class _PlantingLocationPageState extends ConsumerState { if (result != null) { setState(() { - _selectedProvince = result.provinceName; - _selectedCity = result.cityName; + _selectedProvinceName = result.provinceName; + _selectedCityName = result.cityName; + _selectedProvinceCode = result.provinceId; + _selectedCityCode = result.cityId; }); } } @@ -84,7 +97,7 @@ class _PlantingLocationPageState extends ConsumerState { /// 确认选择 void _confirmSelection() async { - if (_selectedProvince == null || _selectedCity == null) { + if (_selectedProvinceName == null || _selectedCityName == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('请选择省份和城市'), @@ -97,8 +110,8 @@ class _PlantingLocationPageState extends ConsumerState { // 显示确认弹窗(带5秒倒计时) await PlantingConfirmDialog.show( context: context, - province: _selectedProvince!, - city: _selectedCity!, + province: _selectedProvinceName!, + city: _selectedCityName!, onConfirm: _submitPlanting, ); } @@ -108,8 +121,20 @@ class _PlantingLocationPageState extends ConsumerState { setState(() => _isSubmitting = true); try { - // TODO: 调用 API 提交认种请求 - await Future.delayed(const Duration(seconds: 1)); + final plantingService = ref.read(plantingServiceProvider); + + // 1. 选择省市 + await plantingService.selectProvinceCity( + widget.orderNo, + _selectedProvinceCode!, + _selectedCityCode!, + ); + + // 2. 确认省市选择 + await plantingService.confirmProvinceCity(widget.orderNo); + + // 3. 支付订单 + await plantingService.payOrder(widget.orderNo); if (mounted) { // 显示成功提示 @@ -142,7 +167,7 @@ class _PlantingLocationPageState extends ConsumerState { /// 是否可以提交 bool get _canSubmit => - _selectedProvince != null && _selectedCity != null && !_isSubmitting; + _selectedProvinceName != null && _selectedCityName != null && !_isSubmitting; @override Widget build(BuildContext context) { @@ -305,12 +330,12 @@ class _PlantingLocationPageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - _selectedProvince ?? '选择省份', + _selectedProvinceName ?? '选择省份', style: TextStyle( fontSize: 16, fontFamily: 'Inter', height: 1.5, - color: _selectedProvince != null + color: _selectedProvinceName != null ? const Color(0xFF5D4037) : const Color(0xFF5D4037).withValues(alpha: 0.5), ), @@ -363,12 +388,12 @@ class _PlantingLocationPageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - _selectedCity ?? '选择市级', + _selectedCityName ?? '选择市级', style: TextStyle( fontSize: 16, fontFamily: 'Inter', height: 1.5, - color: _selectedCity != null + color: _selectedCityName != null ? const Color(0xFF5D4037) : const Color(0xFF5D4037).withValues(alpha: 0.5), ), diff --git a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_quantity_page.dart b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_quantity_page.dart index 206d1468..8b36998a 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_quantity_page.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_quantity_page.dart @@ -28,6 +28,9 @@ class _PlantingQuantityPageState extends ConsumerState { /// 是否正在加载 bool _isLoading = false; + /// 是否正在创建订单 + bool _isCreatingOrder = false; + /// 加载错误信息 String? _errorMessage; @@ -108,16 +111,44 @@ class _PlantingQuantityPageState extends ConsumerState { context.pop(); } - /// 下一步:选择省市 - void _goToNextStep() { - if (_quantity > 0 && _quantity <= _maxQuantity) { - context.push( - RoutePaths.plantingLocation, - extra: PlantingLocationParams( - quantity: _quantity, - totalPrice: _quantity * _pricePerTree, - ), - ); + /// 下一步:创建订单并选择省市 + Future _goToNextStep() async { + if (_quantity <= 0 || _quantity > _maxQuantity || _isCreatingOrder) { + return; + } + + setState(() => _isCreatingOrder = true); + + try { + // 1. 调用 API 创建订单 + final plantingService = ref.read(plantingServiceProvider); + final response = await plantingService.createOrder(_quantity); + + if (mounted) { + // 2. 跳转到省市选择页面,传递订单号 + context.push( + RoutePaths.plantingLocation, + extra: PlantingLocationParams( + quantity: _quantity, + totalPrice: _quantity * _pricePerTree, + orderNo: response.orderNo, + ), + ); + } + } catch (e) { + debugPrint('创建订单失败: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('创建订单失败: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isCreatingOrder = false); + } } } @@ -518,7 +549,7 @@ class _PlantingQuantityPageState extends ConsumerState { /// 构建底部按钮 Widget _buildBottomButton() { - final bool canProceed = _quantity > 0 && _quantity <= _maxQuantity && !_isLoading; + final bool canProceed = _quantity > 0 && _quantity <= _maxQuantity && !_isLoading && !_isCreatingOrder; return Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), @@ -547,17 +578,26 @@ class _PlantingQuantityPageState extends ConsumerState { ] : null, ), - child: const Center( - child: Text( - '下一步:选择省市', - style: TextStyle( - fontSize: 18, - fontFamily: 'Inter', - fontWeight: FontWeight.w500, - height: 1.56, - color: Colors.white, - ), - ), + child: Center( + child: _isCreatingOrder + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + '下一步:选择省市', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.56, + color: Colors.white, + ), + ), ), ), ),