跳轉到

案例研究:法律合約管理

摘要:某台灣法律科技平台的合約管理模組在客戶量增長後,面臨合約版本混亂、無法自動追蹤變更、訴訟文件編號不規範等問題。導入 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),
    );
}

相關資源

Commercial License

This feature requires a commercial license. Contact our team for pricing and deployment support.

Contact Sales