import { Controller, Get, Post, Param, Body, UseGuards, Request, HttpCode, HttpStatus, Logger, Res, StreamableFile, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { ContractSigningService } from '../../application/services/contract-signing.service'; import { PdfGeneratorService } from '../../infrastructure/pdf/pdf-generator.service'; /** * 签署合同请求DTO */ interface SignContractDto { signatureBase64: string; // Base64编码的签名图片 signatureHash: string; // SHA256哈希 deviceInfo: { deviceId?: string; deviceModel?: string; osVersion?: string; appVersion?: string; }; location?: { latitude?: number; longitude?: number; }; } /** * 合同签署配置控制器(公开接口,不需要认证) */ @Controller('planting/contract-signing') export class ContractSigningConfigController { private readonly contractSigningEnabled: boolean; constructor(private readonly configService: ConfigService) { // 默认启用合同签署功能 this.contractSigningEnabled = this.configService.get('CONTRACT_SIGNING_ENABLED', 'true') === 'true'; } /** * 获取合同签署配置(公开接口) * 用于前端判断是否需要在认种前检查实名认证 */ @Get('config') getConfig() { return { success: true, data: { contractSigningEnabled: this.contractSigningEnabled, }, }; } } /** * 合同签署控制器 * * 独立模块,提供合同签署相关的API */ @Controller('planting/contract-signing') @UseGuards(JwtAuthGuard) export class ContractSigningController { private readonly logger = new Logger(ContractSigningController.name); constructor( private readonly contractSigningService: ContractSigningService, private readonly pdfGeneratorService: PdfGeneratorService, ) {} /** * 获取用户待签署的合同任务列表 */ @Get('pending') async getPendingTasks(@Request() req: { user: { id: string } }) { const userId = BigInt(req.user.id); const tasks = await this.contractSigningService.getPendingTasks(userId); return { success: true, data: tasks, }; } /** * 获取用户所有未签署的合同任务(包括超时的) * 用于App启动时检查 */ @Get('unsigned') async getUnsignedTasks(@Request() req: { user: { id: string } }) { const userId = BigInt(req.user.id); const tasks = await this.contractSigningService.getUnsignedTasks(userId); return { success: true, data: tasks, }; } /** * 获取签署任务详情 */ @Get('tasks/:orderNo') async getTask( @Param('orderNo') orderNo: string, @Request() req: { user: { id: string } }, ) { const userId = BigInt(req.user.id); const task = await this.contractSigningService.getTask(orderNo, userId); if (!task) { return { success: false, message: '签署任务不存在', }; } return { success: true, data: task, }; } /** * 记录用户已滚动到底部 */ @Post('tasks/:orderNo/scroll-complete') @HttpCode(HttpStatus.OK) async markScrollComplete( @Param('orderNo') orderNo: string, @Request() req: { user: { id: string } }, ) { const userId = BigInt(req.user.id); try { await this.contractSigningService.markScrollComplete(orderNo, userId); return { success: true, message: '已记录滚动到底部', }; } catch (error) { this.logger.error(`Failed to mark scroll complete: ${error.message}`); return { success: false, message: error.message, }; } } /** * 记录用户确认法律效力 */ @Post('tasks/:orderNo/acknowledge') @HttpCode(HttpStatus.OK) async acknowledgeContract( @Param('orderNo') orderNo: string, @Request() req: { user: { id: string } }, ) { const userId = BigInt(req.user.id); try { await this.contractSigningService.acknowledgeContract(orderNo, userId); return { success: true, message: '已确认法律效力', }; } catch (error) { this.logger.error(`Failed to acknowledge contract: ${error.message}`); return { success: false, message: error.message, }; } } /** * 上传签名并完成签署 */ @Post('tasks/:orderNo/sign') @HttpCode(HttpStatus.OK) async signContract( @Param('orderNo') orderNo: string, @Body() dto: SignContractDto, @Request() req: { user: { id: string }; ip: string; headers: { 'user-agent'?: string } }, ) { const userId = BigInt(req.user.id); const ipAddress = req.ip || 'unknown'; const userAgent = req.headers['user-agent'] || 'unknown'; try { // TODO: 上传签名图片到云存储,获取URL // 目前暂时使用base64数据作为URL占位 const signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`; await this.contractSigningService.signContract(orderNo, userId, { signatureCloudUrl, signatureHash: dto.signatureHash, ipAddress, deviceInfo: dto.deviceInfo, userAgent, location: dto.location, }); return { success: true, message: '合同签署成功', }; } catch (error) { this.logger.error(`Failed to sign contract: ${error.message}`); return { success: false, message: error.message, }; } } /** * 补签合同(超时后用户仍可补签) */ @Post('tasks/:orderNo/late-sign') @HttpCode(HttpStatus.OK) async lateSignContract( @Param('orderNo') orderNo: string, @Body() dto: SignContractDto, @Request() req: { user: { id: string }; ip: string; headers: { 'user-agent'?: string } }, ) { const userId = BigInt(req.user.id); const ipAddress = req.ip || 'unknown'; const userAgent = req.headers['user-agent'] || 'unknown'; try { // TODO: 上传签名图片到云存储,获取URL const signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`; await this.contractSigningService.lateSignContract(orderNo, userId, { signatureCloudUrl, signatureHash: dto.signatureHash, ipAddress, deviceInfo: dto.deviceInfo, userAgent, location: dto.location, }); return { success: true, message: '合同补签成功', }; } catch (error) { this.logger.error(`Failed to late-sign contract: ${error.message}`); return { success: false, message: error.message, }; } } /** * 获取当前有效的合同模板(供前端预览) */ @Get('template') async getActiveTemplate() { const template = await this.contractSigningService.getActiveTemplate(); if (!template) { return { success: false, message: '没有可用的合同模板', }; } return { success: true, data: { version: template.version, title: template.title, content: template.content, }, }; } /** * 获取合同 PDF 文件 * 用于前端展示和签署 */ @Get('tasks/:orderNo/pdf') async getContractPdf( @Param('orderNo') orderNo: string, @Request() req: { user: { id: string } }, @Res({ passthrough: true }) res: Response, ): Promise { const userId = BigInt(req.user.id); try { const task = await this.contractSigningService.getTask(orderNo, userId); if (!task) { throw new Error('签署任务不存在'); } // 格式化签署日期 const signingDate = new Date().toISOString().split('T')[0]; // 生成 PDF(使用 pdf-lib 直接操作 PDF 模板) const pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ orderNo: task.orderNo, userRealName: task.userRealName || '未认证', userIdCard: task.userIdCardNumber || '', userPhone: task.userPhoneNumber || '', treeCount: task.treeCount, signingDate, }); // 设置响应头 res.set({ 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="contract-${orderNo}.pdf"`, 'Content-Length': pdfBuffer.length, }); return new StreamableFile(pdfBuffer); } catch (error) { this.logger.error(`Failed to generate PDF for order ${orderNo}:`, error); throw error; } } }