336 lines
8.5 KiB
TypeScript
336 lines
8.5 KiB
TypeScript
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;
|
||
}
|
||
}
|
||
}
|