《独立开发者的商业指南·合同、应收与风险》11.1 DPA、SCC 与跨境传输

DPA 不是附件,而是五项会持续产生工时的义务。本节把处理范围、子处理者通知、跨境机制、审计权和删除时限拆成可计算成本,用 LedgerLite 的 16 小时法务与 8 小时审计准备算出年维护 $3,600、首年 $5,400,再对比没有 DPA 丢 5 单的 $4,620 年化收入损失。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

DPA(数据处理协议)常被当成主合同的附件:主合同签了,附件跟着签,事情就结束了。真正决定成本的不是签字那一刻,而是签字之后每年要维护的义务。处理范围会变,子处理者会新增,跨境机制要复核,客户可能行使审计权,合同终止后还要证明数据已经删除或返还。这些都不是法律意见问题,而是工时问题。

LedgerLite 的企业客户会在订阅协议后面附一份 DPA。对一人公司,这份附件最容易被低估:它不像功能开发那样有明确的完成线,也不像发票那样有确定的金额,但它每年都会回来一次。这一节把 DPA 拆成五项可以计数的义务,再把它们换算成年度维护成本和丢单损失。它不判断哪套条款适用于哪种数据;具体适用需要专业意见。

DPA 的五项可计算义务

一份 DPA 的正文可以很长,但落到运营上,通常只有五件事会反复发生。把这五件事写成可计数的口径,DPA 就从一份法律文件变成一张年度工作清单。

义务条款里常见写法可计算口径LedgerLite 的教学假设
处理范围与目的数据类型、处理目的、保留期限数据类别数 × 目的数 × 年复核次数4 类数据、3 个目的、保留 365 天
子处理者通知新增子处理者前 N 天通知通知天数 × 年新增子处理者数提前 30 天,年新增 2 家
跨境传输机制SCC、BCR 或境内处理机制套数 × 文档页数1 套 SCC
审计权客户每年可审计 N 次审计次数 × 每次准备工时1 次/年
删除或返还终止后 N 天内删除并证明天数 × 验证与留证工时30 天

处理范围决定你要维护多少份数据清单。四类数据、三个目的听起来不多,但每一类都要能回答「存在哪里、谁能访问、保留多久、依据是什么」。这些答案在客户问卷里会再出现一次,所以 DPA 的清单不是写给法务看的,是写给下一次安全审查用的。

子处理者通知决定你有没有「变更管理」。只要新增一家云服务、邮件服务或支持外包,就可能触发通知义务。通知天数越短,留给你的操作空间越小;30 天意味着你必须在采购前就完成评估,而不是上线后才补文档。

跨境传输机制决定文档层数。数据从客户所在地区流到你的服务器、再流到子处理者,路径越长,需要说明的环节越多。审计权决定客户能不能走进你的流程;删除或返还时限决定合同结束后你还要做多少验证工作。五项义务里,最容易被忽略的是删除:合同终止不等于数据消失,证明数据消失才是最后一项工作。

SCC:跨境传输多出来的一层成本

SCC 常被理解成「跨境传输的许可证」。在商业层面更准确的说法是:它是一套客户愿意接受的合同模板,把跨境这件事写进一份可以审查的文档。它不消除跨境,只是让跨境变得可描述、可追责、可复用。

机制额外文档客户接受度首年设置工时年度复核工时
无无只适用于境内处理00
SCC一套条款加子处理者清单多数跨境企业客户会要求124
BCR集团内部规则加外部批准材料常见于大型集团40 以上12

这张表里的工时是教学假设,不是报价。它想说明的是结构:SCC 的成本不在签字,而在三件事。第一,首年要把条款、数据处理说明和子处理者清单对齐,假设 12 小时。第二,每年要复核一次条款、清单和实际数据流是否仍然一致,假设 4 小时。第三,每新增一个子处理者,都要把同样的义务向下传递,这部分工时会计入日常采购。

对 LedgerLite,SCC 的首年设置成本是 12 小时 × $150 = $1,800;年度复核的 4 小时已经包含在 16 小时法务工时里。SCC 不是一次性的开关,而是一份需要版本管理的文档。客户换模板、子处理者换地区、产品新增数据字段,都会让这份文档过期。

这也解释了为什么「我们也有 DPA」不是一句可以结束对话的话。客户真正会问的是:DPA 覆盖哪些数据、子处理者是谁、跨境用什么机制、上一次复核是什么时候、删除怎么证明。五个问题里有四个需要文档,而不是口头承诺。

一份 DPA 的年度维护成本

把五项义务换算成工时,LedgerLite 的年度维护成本如下(费率 $150/小时为教学假设):

项目工时金额频率
法务条款复核(含 SCC 年度复核 4 小时)16 小时$2,400每年
审计准备(1 次审计权)8 小时$1,200每年
年度运行成本24 小时$3,600每年
SCC 首次设置12 小时$1,800首年一次
首年总成本36 小时$5,400首年

这张表最重要的不是 $3,600,而是它和已有预算的关系。案例事实表里 LedgerLite 的法律与保险合计 $6,000/年;DPA 维护 $3,600 就吃掉 60%,首年 $5,400 吃掉 90%。这不是说 DPA 不该做,而是说它必须和其他法务支出一起排优先级:买了保险、签了合同,不等于还有余量去维护一套跨境文档。

摊到 260 家客户,$3,600/年等于每客户 $13.85/年;但真正要求 DPA 的只是其中一部分企业客户,成本要摊到那部分客户上,单客成本会明显高于 $13.85。这就是 DPA 的第一个商业特征:它是一项固定成本,客户越集中、要求越定制,单客成本越高。

审计准备是第二个容易漏掉的成本。条款写「客户每年可审计一次」,听起来只是一次访问;实际要准备证据、安排访谈、整理访问日志、准备整改说明。一次 8 小时是保守假设,如果客户要求现场审计或第三方审计,工时会更高。审计权越宽,年度维护成本越高,这一点在谈条款时可以直接换成钱。

没有 DPA 就丢单的收入损失

DPA 的成本可以算,不签 DPA 的损失也可以算。LedgerLite 的 ARPA 是 $77/月,折合年付 $924/客户;新签 120 家/年来自 CAC 的推导。假设其中 10% 的客户会要求 DPA,即 12 家;如果没有 DPA,会丢掉 5 家。这个 10% 和 5 家是教学假设,用来展示算法,不是预测。

口径计算金额
年化收入损失5 家 × $924$4,620
生命周期价值损失5 家 × $4,320$21,600
年度维护成本24 小时 × $150$3,600
首年成本(含 SCC 设置)$3,600 + $1,800$5,400
年化净得$4,620 − $3,600+$1,020
首年净得(年化口径)$4,620 − $5,400−$780
首年净得(LTV 口径)$21,600 − $5,400+$16,200

这张表说明,DPA 的账不能只看一年。按年化收入算,首年含 SCC 设置是净亏 $780,第二年起才净得 $1,020/年;按客户生命周期价值算,首年就是净得 $16,200。两个口径都成立,取决于你把 DPA 当成年度费用还是获客投资。

更重要的结论是:DPA 不是创收项,而是入场券。没有它,你失去的不是 $4,620 的收入,而是参与那 12 家企业客户投标的资格。对一人公司,这种资格损失不会立刻出现在利润表上,但会出现在第二年、第三年的客户结构里:客户越集中在小客户,ARPA 越难提高;越进不了企业单,越没有预算维护 DPA,形成循环。

因此 DPA 的决策顺序应该是:先确认目标客户是否要求 DPA,再决定维护到什么程度。只服务境内小客户,可以把 DPA 做到最简;要拿企业单,就要把五项义务和 SCC 文档当成固定成本,提前放进年度预算。条款的边界可以谈,义务的工时不会消失。

代码:DPA 义务与维护成本

下面的 privacy.ts 把这一节的口径固化成四个函数。summarizeDpa 把条款归一成通知、删除、审计和跨境四项义务;annualDpaCost 用「法务工时 + 审计准备 × 审计次数 + 跨境首年设置」算成本;dpaRevenueAtRisk 用「丢单数 × 年付」算收入损失;dpaNetBenefit 把两者相减。金额用整数美分,工时可以是小数。

// file: src/contract/privacy.ts
// tier: L1
// verify: node --test test/contract/privacy.test.ts

import { type Cents } from "../core/money.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type CrossBorderMechanism = "none" | "scc" | "bcr";

export type DpaTerms = {
  subprocessorNoticeDays: number;
  deletionDeadlineDays: number;
  auditRightsPerYear: number;
  crossBorder: CrossBorderMechanism;
};

export type DpaObligation = {
  subprocessorNoticeRequired: boolean;
  deletionDeadlineDays: number;
  auditRightsPerYear: number;
  crossBorderPaperwork: boolean;
};

export type DpaCostInput = {
  terms: DpaTerms;
  legalHoursPerYear: number;
  auditPrepHoursPerAudit: number;
  crossBorderSetupHours: number;
  hourlyRateCents: Cents;
};

export type DpaCost = {
  legalCostCents: Cents;
  auditCostCents: Cents;
  crossBorderSetupCents: Cents;
  annualRunCostCents: Cents;
  firstYearCostCents: Cents;
  laterYearCostCents: Cents;
};

export type DpaDealInput = {
  dealsLostWithoutDpa: number;
  annualValueCents: Cents;
};

function validHours(value: number): boolean {
  return Number.isFinite(value) && value >= 0;
}

function validCount(value: number): boolean {
  return Number.isInteger(value) && value >= 0;
}

function validAmount(value: Cents): boolean {
  return Number.isInteger(value) && value >= 0;
}

/**
 * A DPA is not one signature but five recurring obligations. This function
 * normalises the terms into the parts that create work: notice duties, a
 * deletion clock, audit rights and a cross-border paper trail.
 */
export function summarizeDpa(terms: DpaTerms): Result<DpaObligation> {
  if (!validCount(terms.subprocessorNoticeDays)) {
    return fail("subprocessor notice must be a non-negative integer of days");
  }
  if (!validCount(terms.deletionDeadlineDays)) {
    return fail("deletion deadline must be a non-negative integer of days");
  }
  if (!validCount(terms.auditRightsPerYear)) {
    return fail("audit rights must be a non-negative integer per year");
  }

  return ok({
    subprocessorNoticeRequired: terms.subprocessorNoticeDays > 0,
    deletionDeadlineDays: terms.deletionDeadlineDays,
    auditRightsPerYear: terms.auditRightsPerYear,
    crossBorderPaperwork: terms.crossBorder !== "none",
  });
}

/**
 * Maintenance cost is legal hours plus audit preparation. Cross-border setup
 * is a first-year cost: the mechanism adds documents, not a permanent fee.
 * Audit preparation is multiplied by the audit rights in the terms, because
 * two audit rights per year mean two preparation cycles, not one.
 */
export function annualDpaCost(input: DpaCostInput): Result<DpaCost> {
  if (
    !validHours(input.legalHoursPerYear) ||
    !validHours(input.auditPrepHoursPerAudit) ||
    !validHours(input.crossBorderSetupHours)
  ) {
    return fail("hours must be non-negative");
  }
  if (!validAmount(input.hourlyRateCents)) {
    return fail("hourly rate must be a non-negative integer amount");
  }
  const obligation = summarizeDpa(input.terms);
  if (!obligation.ok) {
    return fail(obligation.error);
  }

  const legalCostCents = Math.round(
    input.legalHoursPerYear * input.hourlyRateCents,
  );
  const auditCostCents = Math.round(
    input.auditPrepHoursPerAudit *
      obligation.value.auditRightsPerYear *
      input.hourlyRateCents,
  );
  const crossBorderSetupCents = obligation.value.crossBorderPaperwork
    ? Math.round(input.crossBorderSetupHours * input.hourlyRateCents)
    : 0;
  const annualRunCostCents = legalCostCents + auditCostCents;

  return ok({
    legalCostCents,
    auditCostCents,
    crossBorderSetupCents,
    annualRunCostCents,
    firstYearCostCents: annualRunCostCents + crossBorderSetupCents,
    laterYearCostCents: annualRunCostCents,
  });
}

/**
 * The other side of the DPA is revenue that never arrives. Lost deals times
 * annual contract value is the annual revenue at risk; net benefit subtracts
 * the maintenance cost from that number.
 */
export function dpaRevenueAtRisk(input: DpaDealInput): Result<Cents> {
  if (!validCount(input.dealsLostWithoutDpa)) {
    return fail("lost deals must be a non-negative integer");
  }
  if (!validAmount(input.annualValueCents)) {
    return fail("annual value must be a non-negative integer amount");
  }
  return ok(input.dealsLostWithoutDpa * input.annualValueCents);
}

export function dpaNetBenefit(
  revenueAtRiskCents: Cents,
  costCents: Cents,
): Cents {
  return revenueAtRiskCents - costCents;
}

代码里有四个口径值得注意。第一,crossBorderSetupCents 只在 crossBorder !== "none" 时计入,且只进 firstYearCostCents,不进 laterYearCostCents;SCC 是首年设置成本,不是永久年费。第二,auditCostCents 乘以 auditRightsPerYear,因为两次审计权意味着两轮准备,不是一轮。第三,annualRunCostCents 只含法务和审计,不含首年设置,便于和年化收入损失直接比较。第四,dpaNetBenefit 不判断正负号的含义,正数是净得,负数是净亏,解释权留给使用场景。

代码不处理「DPA 是否合法」或「SCC 是否适用」,这些不是算术问题。它只回答一个财务问题:把义务拆成工时之后,这份 DPA 一年要花多少钱,不签会丢多少钱。

小结

  • DPA 可以拆成五项可计数义务:处理范围、子处理者通知、跨境机制、审计权、删除或返还时限。
  • SCC 的商业含义是多一层合同与文档成本:首年设置 12 小时,年度复核 4 小时,子处理者每新增一家都要向下传递义务。
  • LedgerLite 的 DPA 年维护成本是 16 小时法务 + 8 小时审计准备 = $3,600;首年加 SCC 设置 $1,800,合计 $5,400。
  • $3,600 占法律与保险预算 $6,000 的 60%,首年 $5,400 占 90%;DPA 不是免费附件。
  • 没有 DPA 会丢掉要求 DPA 的客户:5 家 × $924 = $4,620/年,按 LTV 算是 $21,600。
  • 年化口径下首年净亏 $780,第二年起净得 $1,020/年;LTV 口径下首年净得 $16,200。
  • DPA 是入场券,不是创收项;先确认目标客户是否要求,再决定维护深度。

阅读导航:上一节:10.3 备选方案与迁移成本 · 下一节:11.2 数据泄露的责任与通知 。

常见错误与边界

  1. 把 DPA 当成签字就结束的附件。 处理范围、子处理者、跨境机制、审计权和删除时限每年都会回来一次;签字只是开始。
  2. 把 SCC 当成免费开关。 SCC 首年要 12 小时设置,之后每年要复核;子处理者每新增一家,义务就要向下传递一次。
  3. 只算法务工时,不算审计准备。 一次审计权对应一轮证据、访谈和整改说明;两次审计权要把准备成本乘以二。
  4. 把跨境设置成本摊进每年。 SCC 设置是首年一次性成本,年度复核才是经常性成本;混在一起会高估或低估某一年。
  5. 用名义年费比较 DPA 成本。 应把年化收入损失和 LTV 损失分开算;首年按年化口径可能净亏,按 LTV 口径可能净得。
  6. 假设所有客户都要求 DPA。 只有一部分客户会要求;先确认比例,再决定维护深度,否则会把固定成本摊错对象。
  7. 忽略删除与返还的验证成本。 合同终止不等于数据消失;证明删除需要日志、清单和留证工时。
  8. 把 DPA 当成创收项。 DPA 不直接带来收入,它决定你能否进入要求 DPA 的投标;用丢单损失而不是收入增量来评估。
  9. 只维护一套模板,不维护子处理者清单。 模板相同、数据流不同,文档就会过期;清单和条款要一起复核。
  10. 把本节数字当成报价。 工时、费率和丢单数都是教学假设;真实成本要按自己的数据流、客户要求和外部报价重算。

DPA 的五项义务里,处理范围和删除时限最容易被忽略,子处理者通知和跨境机制最容易被低估,审计权最容易被当成一次性事件。下一节把这些义务放进一次事故:数据真的泄露时,谁通知、谁付钱、保险赔多少、责任上限挡多少。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查