rwadurian/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts

336 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string>('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<StreamableFile> {
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;
}
}
}