fix(pre-planting): 签署合同前检查实名认证 + 修正合同金额

- getMergeContractPdf: KYC 为 null 时返回 400,不允许查看合同
- getMergeContractPdf: 从源订单汇总实际绿积分金额,CNY = 绿积分 × 1.1
- Flutter: KYC 错误时显示专用提示 + "去完成实名认证" 按钮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-28 10:59:26 -08:00
parent b1e5e6b29f
commit cd73b2dec4
2 changed files with 53 additions and 22 deletions

View File

@ -406,10 +406,19 @@ export class PrePlantingApplicationService {
return m; return m;
}); });
// 获取用户 KYC 信息 // 获取用户 KYC 信息 - 未完成实名认证则不允许查看合同
const kycInfo = await this.identityServiceClient.getUserKycInfo( const kycInfo = await this.identityServiceClient.getUserKycInfo(merge.accountSequence);
merge.accountSequence, if (!kycInfo || !kycInfo.realName) {
); throw new BadRequestException('请先完成实名认证方可查看和签署合同');
}
// 从源订单汇总实际支付的绿积分金额
const sourceOrders = await this.prisma.prePlantingOrder.findMany({
where: { orderNo: { in: merge.sourceOrderNos } },
select: { totalAmount: true },
});
const greenPointsAmount = sourceOrders.reduce((sum, o) => sum + o.totalAmount, 0);
const totalAmount = Math.round(greenPointsAmount * 1.1 * 100) / 100; // 含税 CNY
// 生成北京时间日期 // 生成北京时间日期
const now = new Date(Date.now() + 8 * 60 * 60 * 1000); const now = new Date(Date.now() + 8 * 60 * 60 * 1000);
@ -417,10 +426,12 @@ export class PrePlantingApplicationService {
return this.pdfGeneratorService.generateContractPdf({ return this.pdfGeneratorService.generateContractPdf({
contractNo: mergeNo, contractNo: mergeNo,
userRealName: kycInfo?.realName || '未认证', userRealName: kycInfo.realName,
userIdCard: kycInfo?.idCardNumber || '', userIdCard: kycInfo.idCardNumber || '',
userPhone: kycInfo?.phoneNumber || '', userPhone: kycInfo.phoneNumber || '',
treeCount: merge.treeCount, treeCount: merge.treeCount,
totalAmount,
greenPointsAmount,
signingDate, signingDate,
}); });
} }

View File

@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../../../../core/di/injection_container.dart'; import '../../../../core/di/injection_container.dart';
import '../../../../core/services/pre_planting_service.dart'; import '../../../../core/services/pre_planting_service.dart';
import '../../../../routes/route_paths.dart';
import '../../../contract_signing/presentation/widgets/signature_pad.dart'; import '../../../contract_signing/presentation/widgets/signature_pad.dart';
/// ///
@ -38,6 +39,7 @@ class _PrePlantingMergeSigningPageState
bool _isSubmitting = false; bool _isSubmitting = false;
bool _showSignaturePad = false; bool _showSignaturePad = false;
String? _errorMessage; String? _errorMessage;
bool _isKycRequired = false;
int _downloadProgress = 0; int _downloadProgress = 0;
int _downloadRetryCount = 0; int _downloadRetryCount = 0;
@ -90,10 +92,13 @@ class _PrePlantingMergeSigningPageState
} catch (e) { } catch (e) {
debugPrint('[MergeSigningPage] PDF加载失败: $e'); debugPrint('[MergeSigningPage] PDF加载失败: $e');
if (mounted) { if (mounted) {
final msg = e.toString();
final isKyc = msg.contains('实名认证') || msg.contains('400');
setState(() { setState(() {
_isPdfLoading = false; _isPdfLoading = false;
_downloadRetryCount++; _isKycRequired = isKyc;
_errorMessage = '加载合同失败,请点击重试'; _errorMessage = isKyc ? '请先完成实名认证方可查看和签署合同' : '加载合同失败,请点击重试';
if (!isKyc) _downloadRetryCount++;
}); });
} }
} }
@ -306,8 +311,11 @@ class _PrePlantingMergeSigningPageState
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.cloud_download_outlined, Icon(
color: Color(0xFFD4AF37), size: 56), _isKycRequired ? Icons.person_off_outlined : Icons.cloud_download_outlined,
color: _isKycRequired ? Colors.red : const Color(0xFFD4AF37),
size: 56,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_errorMessage!, _errorMessage!,
@ -315,17 +323,29 @@ class _PrePlantingMergeSigningPageState
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( if (_isKycRequired)
onPressed: _loadPdf, ElevatedButton.icon(
style: ElevatedButton.styleFrom( onPressed: () => context.push(RoutePaths.kycEntry),
backgroundColor: const Color(0xFFD4AF37), style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric( backgroundColor: Colors.red,
horizontal: 32, vertical: 12)), padding: const EdgeInsets.symmetric(
icon: const Icon(Icons.refresh, color: Colors.white), horizontal: 32, vertical: 12)),
label: const Text('重试', icon: const Icon(Icons.verified_user, color: Colors.white),
style: TextStyle(color: Colors.white, fontSize: 16)), label: const Text('去完成实名认证',
), style: TextStyle(color: Colors.white, fontSize: 16)),
if (_downloadRetryCount > 0) )
else
ElevatedButton.icon(
onPressed: _loadPdf,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD4AF37),
padding: const EdgeInsets.symmetric(
horizontal: 32, vertical: 12)),
icon: const Icon(Icons.refresh, color: Colors.white),
label: const Text('重试',
style: TextStyle(color: Colors.white, fontSize: 16)),
),
if (!_isKycRequired && _downloadRetryCount > 0)
Padding( Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: Text( child: Text(