all in
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* DOCX 文档解析工具
|
||||
* 使用 docxtemplater 从 docx 文件中提取占位符
|
||||
*/
|
||||
|
||||
import PizZip from 'pizzip';
|
||||
import type { PlaceholderField, PlaceholderSchema } from '~/types/contract-draft';
|
||||
import { DOCUMENT_URL } from '../axios-client';
|
||||
|
||||
/**
|
||||
* 从 docx 文件中提取占位符
|
||||
* @param filePath MinIO 文件路径(相对路径,如 contract-template/买卖/买卖合同范本.docx)
|
||||
* @returns 占位符列表
|
||||
*/
|
||||
export async function extractPlaceholdersFromDocx(
|
||||
filePath: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
// 构建完整的 MinIO URL
|
||||
const fileUrl = `${DOCUMENT_URL}${filePath}`;
|
||||
|
||||
// 从 MinIO 下载文件
|
||||
const response = await fetch(fileUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载文件失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// 获取文件内容(ArrayBuffer)
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
// 转换为 Buffer(PizZip 需要)
|
||||
const content = Buffer.from(arrayBuffer);
|
||||
|
||||
// 使用 PizZip 解压
|
||||
const zip = new PizZip(content);
|
||||
|
||||
// 读取 document.xml 文件内容(不使用 docxtemplater,避免格式化文本的标签分割问题)
|
||||
const documentXml = zip.file('word/document.xml');
|
||||
if (!documentXml) {
|
||||
throw new Error('无法找到 word/document.xml 文件');
|
||||
}
|
||||
|
||||
// 获取 XML 文本内容
|
||||
const xmlContent = documentXml.asText();
|
||||
// console.log('[DOCX Parser] 文档 XML 长度:', xmlContent.length);
|
||||
|
||||
// 移除所有 XML 标签,只保留纯文本
|
||||
const fullText = xmlContent.replace(/<[^>]+>/g, '');
|
||||
// console.log('[DOCX Parser] 文档文本长度:', fullText.length);
|
||||
|
||||
// 使用正则表达式提取所有 {{...}} 占位符
|
||||
const placeholderRegex = /\{\{([^}]+)\}\}/g;
|
||||
const matches = fullText.matchAll(placeholderRegex);
|
||||
|
||||
// 去重并返回
|
||||
const placeholders = new Set<string>();
|
||||
for (const match of matches) {
|
||||
const placeholder = match[1].trim();
|
||||
if (placeholder) {
|
||||
placeholders.add(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderList = Array.from(placeholders);
|
||||
// console.log('[DOCX Parser] 提取到的占位符:', placeholderList);
|
||||
|
||||
return placeholderList;
|
||||
} catch (error) {
|
||||
console.error('[DOCX Parser] 解析文档失败:', error);
|
||||
throw new Error(`解析文档失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从占位符列表生成默认的 PlaceholderSchema
|
||||
* @param placeholders 占位符列表
|
||||
* @returns PlaceholderSchema
|
||||
*/
|
||||
export function generateDefaultSchema(
|
||||
placeholders: string[]
|
||||
): PlaceholderSchema {
|
||||
// 按名称自动分组
|
||||
const fields: PlaceholderField[] = placeholders.map(placeholder => {
|
||||
// 根据占位符名称推测分组
|
||||
let group = '基本信息';
|
||||
if (placeholder.includes('甲方') || placeholder.includes('partyA')) {
|
||||
group = '甲方信息';
|
||||
} else if (placeholder.includes('乙方') || placeholder.includes('partyB')) {
|
||||
group = '乙方信息';
|
||||
} else if (
|
||||
placeholder.includes('金额') ||
|
||||
placeholder.includes('价格') ||
|
||||
placeholder.includes('数量') ||
|
||||
placeholder.includes('amount')
|
||||
) {
|
||||
group = '合同条款';
|
||||
} else if (
|
||||
placeholder.includes('日期') ||
|
||||
placeholder.includes('时间') ||
|
||||
placeholder.includes('date')
|
||||
) {
|
||||
group = '日期信息';
|
||||
}
|
||||
|
||||
// 根据名称推测字段类型
|
||||
let type: 'text' | 'number' | 'date' | 'textarea' = 'text';
|
||||
if (
|
||||
placeholder.includes('金额') ||
|
||||
placeholder.includes('数量') ||
|
||||
placeholder.includes('价格') ||
|
||||
placeholder.includes('amount') ||
|
||||
placeholder.includes('price') ||
|
||||
placeholder.includes('quantity')
|
||||
) {
|
||||
type = 'number';
|
||||
} else if (
|
||||
placeholder.includes('日期') ||
|
||||
placeholder.includes('时间') ||
|
||||
placeholder.includes('date') ||
|
||||
placeholder.includes('time')
|
||||
) {
|
||||
type = 'date';
|
||||
} else if (
|
||||
placeholder.includes('地址') ||
|
||||
placeholder.includes('说明') ||
|
||||
placeholder.includes('备注') ||
|
||||
placeholder.includes('address') ||
|
||||
placeholder.includes('description') ||
|
||||
placeholder.includes('remark')
|
||||
) {
|
||||
type = 'textarea';
|
||||
}
|
||||
|
||||
// 根据名称推测是否必填
|
||||
const required = !placeholder.includes('可选') && !placeholder.includes('optional');
|
||||
|
||||
return {
|
||||
key: placeholder,
|
||||
label: placeholder, // 使用占位符本身作为标签
|
||||
type,
|
||||
required,
|
||||
group
|
||||
};
|
||||
});
|
||||
|
||||
return { fields };
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 合同起草服务
|
||||
* 处理草稿创建、更新等业务逻辑
|
||||
* 包含文件复制功能(预留 MinIO 实现)
|
||||
*/
|
||||
|
||||
import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete } from '~/api/postgrest-client';
|
||||
import type { DraftedContract, CreateDraftRequest } from '~/types/contract-draft';
|
||||
|
||||
/**
|
||||
* 生成草稿文件路径
|
||||
* @param templateFilePath 模板文件路径
|
||||
* @param userId 用户ID
|
||||
* @param templateId 模板ID
|
||||
* @returns 草稿文件路径
|
||||
*/
|
||||
export function generateDraftFilePath(
|
||||
templateFilePath: string,
|
||||
userId: number,
|
||||
templateId: number
|
||||
): string {
|
||||
const timestamp = Date.now();
|
||||
const fileExtension = templateFilePath.split('.').pop() || 'docx';
|
||||
const newFileName = `contract_${templateId}_${userId}_${timestamp}.${fileExtension}`;
|
||||
return `drafts/${newFileName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制 MinIO 文件(预留实现)
|
||||
* @param sourceFilePath 源文件路径
|
||||
* @param targetFilePath 目标文件路径
|
||||
* @param bucket Bucket 名称
|
||||
* @returns 是否成功
|
||||
*
|
||||
* TODO: 实现 MinIO 文件复制逻辑
|
||||
* 1. 安装 minio SDK: npm install minio
|
||||
* 2. 创建 MinIO 客户端实例
|
||||
* 3. 调用 copyObject 方法复制文件
|
||||
*
|
||||
* 参考实现:
|
||||
* ```typescript
|
||||
* import { Client } from 'minio';
|
||||
*
|
||||
* const minioClient = new Client({
|
||||
* endPoint: process.env.MINIO_ENDPOINT || 'localhost',
|
||||
* port: parseInt(process.env.MINIO_PORT || '9000'),
|
||||
* useSSL: false,
|
||||
* accessKey: process.env.MINIO_ACCESS_KEY || '',
|
||||
* secretKey: process.env.MINIO_SECRET_KEY || ''
|
||||
* });
|
||||
*
|
||||
* await minioClient.copyObject(
|
||||
* bucket,
|
||||
* targetFilePath,
|
||||
* `/${bucket}/${sourceFilePath}`,
|
||||
* null
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export async function copyMinioFile(
|
||||
sourceFilePath: string,
|
||||
targetFilePath: string,
|
||||
bucket: string = 'docauditai'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
console.log('[Draft Service] 复制文件(预留实现):', {
|
||||
sourceFilePath,
|
||||
targetFilePath,
|
||||
bucket
|
||||
});
|
||||
|
||||
// TODO: 实现 MinIO 文件复制
|
||||
// 当前为临时实现,直接返回成功
|
||||
console.warn('[Draft Service] ⚠️ 文件复制功能尚未实现,请实施 MinIO 集成');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 文件复制失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建起草合同记录
|
||||
* @param request 创建请求
|
||||
* @param userId 用户ID
|
||||
* @param draftFilePath 可选:草稿文件路径(如果不提供,使用模板路径)
|
||||
* @returns 创建的记录
|
||||
*
|
||||
* 使用场景:
|
||||
* 1. 不传 draftFilePath:直接使用模板文件路径,在原模板上编辑
|
||||
* 2. 传 draftFilePath:使用复制后的文件路径(由文件复制接口提供)
|
||||
*/
|
||||
export async function createDraftContract(
|
||||
request: CreateDraftRequest,
|
||||
userId: number,
|
||||
draftFilePath?: string,
|
||||
jwt?: string
|
||||
): Promise<DraftedContract> {
|
||||
try {
|
||||
// 1. 查询模板信息
|
||||
const templateResponse = await postgrestGet('contract_templates', {
|
||||
select: 'id,file_path',
|
||||
filter: { id: `eq.${request.templateId}` },
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (!templateResponse.data || (Array.isArray(templateResponse.data) && templateResponse.data.length === 0)) {
|
||||
throw new Error('模板不存在');
|
||||
}
|
||||
|
||||
const template = Array.isArray(templateResponse.data) ? templateResponse.data[0] : templateResponse.data;
|
||||
|
||||
// 2. 确定使用的文件路径
|
||||
// 如果没有提供草稿路径,直接使用模板路径(适合直接编辑模板的场景)
|
||||
// 如果提供了草稿路径,使用复制后的文件路径
|
||||
const finalFilePath = draftFilePath || template.file_path;
|
||||
|
||||
console.log('[Draft Service] 创建草稿:', {
|
||||
templateId: request.templateId,
|
||||
templatePath: template.file_path,
|
||||
draftPath: draftFilePath,
|
||||
finalPath: finalFilePath,
|
||||
mode: draftFilePath ? '使用复制文件' : '直接使用模板文件'
|
||||
});
|
||||
|
||||
// 3. 创建草稿记录
|
||||
const insertResponse = await postgrestPost('drafted_contracts', {
|
||||
body: {
|
||||
template_id: request.templateId,
|
||||
file_path: finalFilePath,
|
||||
title: request.title,
|
||||
placeholder_values: {},
|
||||
status: 'draft',
|
||||
created_by: userId
|
||||
},
|
||||
select: '*',
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (!insertResponse.data) {
|
||||
throw new Error('创建草稿记录失败');
|
||||
}
|
||||
|
||||
const draft = Array.isArray(insertResponse.data) ? insertResponse.data[0] : insertResponse.data;
|
||||
return draft as DraftedContract;
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 创建草稿失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除草稿记录
|
||||
* @param draftId 草稿ID
|
||||
* @param userId 用户ID
|
||||
* @param jwt JWT token
|
||||
*/
|
||||
export async function deleteDraft(
|
||||
draftId: number,
|
||||
userId: number,
|
||||
jwt?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await postgrestDelete('drafted_contracts', {
|
||||
filter: {
|
||||
id: `eq.${draftId}`,
|
||||
created_by: `eq.${userId}` // 确保只能删除自己的草稿
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
|
||||
console.log('[Draft Service] 草稿已删除:', draftId);
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 删除草稿失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取草稿详情
|
||||
* @param draftId 草稿ID
|
||||
* @param userId 用户ID
|
||||
* @returns 草稿记录
|
||||
*/
|
||||
export async function getDraftById(
|
||||
draftId: number,
|
||||
userId: number,
|
||||
jwt?: string
|
||||
): Promise<DraftedContract | null> {
|
||||
try {
|
||||
const response = await postgrestGet('drafted_contracts', {
|
||||
select: '*',
|
||||
filter: {
|
||||
id: `eq.${draftId}`,
|
||||
created_by: `eq.${userId}`
|
||||
},
|
||||
token: jwt
|
||||
});
|
||||
|
||||
if (!response.data || (Array.isArray(response.data) && response.data.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const draft = Array.isArray(response.data) ? response.data[0] : response.data;
|
||||
return draft as DraftedContract;
|
||||
} catch (error) {
|
||||
console.error('[Draft Service] 获取草稿失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user