案例研究:法律合約管理¶
摘要:某台灣法律科技平台的合約管理模組在客戶量增長後,面臨合約版本混亂、無法自動追蹤變更、訴訟文件編號不規範等問題。導入 NextPDF Pro 的 DiffEngine 與 Bates 編號功能後,律師審查合約修訂版本的時間縮短 73%,訴訟文件的可追溯性達到法院電子訴訟系統的要求。
挑戰¶
現況痛點¶
「法思(LegalThink)」是一家提供合約管理 SaaS 的台灣法律科技新創,2024 年底客戶數突破 500 家,但隨之而來的是技術債問題:
- 版本追蹤缺失:合約在 Google Docs → Word → PDF 多次轉換後,修訂記錄遺失。律師必須眼睛逐行比對新舊版本,每份合約耗時 45–90 分鐘
- 無標準化編號:訴訟案件提交法院的文件使用各自為政的頁碼,法官難以在庭審中快速定位引用內容
- 範本品質不一致:50 份合約範本由 10 位律師分別維護,格式差異導致客戶對平台專業度產生質疑
- 沒有變更審計日誌:無法回答「這份合約的第 7 條何時被修改、由誰修改」這類合規問題
技術約束¶
- 系統以 PHP 8.4 + Symfony 7 構建(需升級至 PHP 8.5)
- 合約 PDF 由多個來源上傳(客戶上傳、律師生成、對方發送),格式參差不齊
- 部分舊合約為掃描 PDF,需要與新版本進行視覺差異比較
- 法院電子訴訟系統要求文件帶有 Bates 編號且格式符合規範
解決方案¶
graph TD
Upload["合約 PDF 上傳\n(任意來源)"] --> Normalizer["ContractNormalizer\n格式標準化"]
Normalizer --> VersionStore["版本儲存\n(不可變快照)"]
VersionStore --> DiffEngine["DiffEngine\n(Structural + Visual)"]
DiffEngine --> DiffReport["差異報告\n(JSON + HTML + PDF)"]
VersionStore --> Template["TemplateEngine\n合約範本生成"]
Template --> BatesNumberer["BatesNumberer\n訴訟文件編號"]
BatesNumberer --> Court["法院電子訴訟系統"]
DiffReport --> AuditLog["不可否認審計日誌\n(附 PAdES B-B 簽章)"] 合約版本比較核心實作¶
use NextPDF\Pro\Diff\DiffEngine;
use NextPDF\Pro\Diff\DiffMode;
use NextPDF\Pro\Diff\DiffOptions;
use NextPDF\Pro\Diff\DiffReport;
final class ContractVersionComparer
{
private readonly DiffEngine $diffEngine;
public function __construct()
{
$this->diffEngine = new DiffEngine(
DiffOptions::create(mode: DiffMode::Composite)
->withIgnoreWhitespace(true)
->withIgnoreFontChanges(false) // 字體變更在合約中有意義(如刪除線)
->withTextMatchThreshold(0.85)
->withOutputAnnotatedPdf(true)
->withPageRange(startPage: 1, endPage: PHP_INT_MAX)
);
}
/**
* @return ComparisonResult 含差異報告與標記版本 PDF
*/
public function compare(
ContractVersion $original,
ContractVersion $revised,
): ComparisonResult {
$report = $this->diffEngine->compare(
original: $original->getPdfBytes(),
revised: $revised->getPdfBytes(),
);
return new ComparisonResult(
diffReport: $report,
annotatedPdf: $report->getAnnotatedPdf(),
changeCount: $report->getTotalChangeCount(),
changedClauses: $this->extractChangedClauses($report),
);
}
/**
* @return list<ChangedClause> 解析出的變更條款清單
*/
private function extractChangedClauses(DiffReport $report): array
{
$clauses = [];
foreach ($report->getChanges() as $change) {
// 使用正規表達式識別條款編號(如「第七條」、「7.1」)
if (preg_match('/^(第[一二三四五六七八九十百]+條|[\d]+\.[\d]+)/', $change->getOriginalText() ?? '', $match)) {
$clauses[] = new ChangedClause(
clauseNumber: $match[1],
changeType: $change->getType(),
originalText: $change->getOriginalText(),
revisedText: $change->getRevisedText(),
pageNumber: $change->getPageNumber(),
boundingBox: $change->getBoundingBox(),
);
}
}
return $clauses;
}
}
Bates 編號實作¶
use NextPDF\Pro\Bates\BatesNumberer;
use NextPDF\Pro\Bates\BatesOptions;
use NextPDF\Pro\Bates\BatesPosition;
final class LitigationDocumentBundler
{
public function createBundle(
string $caseNumber,
/** @var list<LitigationDocument> $documents */
array $documents,
): string {
// 收集所有文件的 PDF 位元組
$pdfs = array_map(
fn(LitigationDocument $doc) => $doc->getPdfBytes(),
$documents,
);
// Bates 編號:格式為「案號-頁次」,如 LT-2026-001-000001
$batesPrefix = sprintf('LT-%s-', $caseNumber);
$numberer = new BatesNumberer(
BatesOptions::create(
prefix: $batesPrefix,
startNumber: 1,
padLength: 6,
position: BatesPosition::BottomRight,
fontSize: 8.0,
fontColor: '#374151',
fontFamily: 'Helvetica',
margin: 15.0,
// 在頁面頂部也加入案號(法院要求)
headerText: sprintf('案號:%s', $caseNumber),
headerPosition: BatesPosition::TopCenter,
)
);
// 批次編號(跨文件連續)
$numberedPdfs = $numberer->numberBatch($pdfs);
// 合併為單一訴訟文件包
$merger = new PdfMerger();
return $merger->merge($numberedPdfs);
}
}
合約範本引擎¶
use NextPDF\Pro\Template\ContractTemplateEngine;
use NextPDF\Pro\Template\TemplateOptions;
final class ContractTemplateRenderer
{
public function render(
ContractTemplate $template,
ContractParties $parties,
ContractTerms $terms,
): string {
$engine = new ContractTemplateEngine(
TemplateOptions::create()
->withAutoToc(true)
->withBatesPlaceholder(false) // Bates 由後續流程加入
->withPageNumbering(true)
->withWatermark(text: 'DRAFT', opacity: 0.15) // 草稿浮水印
);
return $engine->render(
template: $template->getPath(),
variables: [
'party_a_name' => $parties->getPartyA()->getLegalName(),
'party_a_id' => $parties->getPartyA()->getUniformNumber(),
'party_b_name' => $parties->getPartyB()->getLegalName(),
'party_b_id' => $parties->getPartyB()->getUniformNumber(),
'contract_date' => $terms->getEffectiveDate()->format('Y 年 m 月 d 日'),
'contract_value' => number_format($terms->getContractValue()),
'payment_terms' => $terms->getPaymentTerms(),
'governing_law' => '中華民國法律',
'jurisdiction' => '台灣台北地方法院',
],
);
}
}
成果¶
| 指標 | 導入前 | 導入後 | 改善幅度 |
|---|---|---|---|
| 合約審查時間(每份) | 45–90 分鐘(人工逐行比對) | 12–15 分鐘(看差異報告) | 73% |
| Bates 編號錯誤率 | 8.3%(人工標記) | 0% | 100% |
| 範本版本衝突 | 平均每月 3 次 | 0 次 | 100% |
| 法院文件退件次數 | 1.8 次/季 | 0 次 | 100% |
| 合約搜尋效率 | 不支援全文搜尋 | 秒級搜尋(文字萃取後索引) | 質的提升 |
| 審計日誌完整性 | 無 | 100%(含 PAdES 簽章) | 達標 |
客戶回饋¶
「以前我的助理要花一個下午比較兩版合約,現在點一下按鈕就能看到清晰的差異報告,連條款編號都自動識別出來了。」— 某律師事務所合夥人
技術亮點¶
不可變版本快照¶
每次上傳新版合約,系統儲存完整 PDF 位元組快照並附加 PAdES B-B 簽章,確保版本不可篡改:
use NextPDF\Pro\Signatures\PAdES\PadesSignatureAppender;
use NextPDF\Pro\Signatures\PAdES\PadesSignatureLevel;
$appender = new PadesSignatureAppender(
PadesSignatureOptions::create(
level: PadesSignatureLevel::BB, // B-B 足以確保版本完整性
certificate: $systemCertificate,
reason: sprintf('System snapshot — version %s', $versionId),
)
);
$signedSnapshot = $appender->sign($contractPdf);
// 儲存至不可覆寫的 S3 物件(配合 Object Lock 政策)
差異報告自動通知¶
// 當差異包含關鍵條款變更時,自動通知負責律師
$result = $comparer->compare($originalVersion, $revisedVersion);
$criticalClauses = ['第一條', '第七條', '第十二條', '第十五條'];
$criticalChanges = array_filter(
$result->getChangedClauses(),
fn(ChangedClause $c) => in_array($c->getClauseNumber(), $criticalClauses, true),
);
if (count($criticalChanges) > 0) {
$notifier->notifyLawyer(
lawyer: $contract->getResponsibleLawyer(),
message: sprintf(
'合約 %s 的關鍵條款已修改:%s',
$contract->getContractNumber(),
implode('、', array_map(fn($c) => $c->getClauseNumber(), $criticalChanges)),
),
diffReportUrl: $this->generateDiffReportUrl($result),
);
}
相關資源¶
- PDF 差異比較引擎
- 進階表單處理(含 Bates 編號)
- PAdES B-LTA 數位簽章
- 文字萃取 — 合約全文索引
Commercial License
This feature requires a commercial license. Contact our team for pricing and deployment support.
Contact Sales