以下情境为教学假设,人物与数字不代表经核实的真实案例。
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 常被理解成「跨境传输的许可证」。在商业层面更准确的说法是:它是一套客户愿意接受的合同模板,把跨境这件事写进一份可以审查的文档。它不消除跨境,只是让跨境变得可描述、可追责、可复用。
| 机制 | 额外文档 | 客户接受度 | 首年设置工时 | 年度复核工时 |
|---|---|---|---|---|
| 无 | 无 | 只适用于境内处理 | 0 | 0 |
| SCC | 一套条款加子处理者清单 | 多数跨境企业客户会要求 | 12 | 4 |
| 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 数据泄露的责任与通知 。
常见错误与边界
- 把 DPA 当成签字就结束的附件。 处理范围、子处理者、跨境机制、审计权和删除时限每年都会回来一次;签字只是开始。
- 把 SCC 当成免费开关。 SCC 首年要 12 小时设置,之后每年要复核;子处理者每新增一家,义务就要向下传递一次。
- 只算法务工时,不算审计准备。 一次审计权对应一轮证据、访谈和整改说明;两次审计权要把准备成本乘以二。
- 把跨境设置成本摊进每年。 SCC 设置是首年一次性成本,年度复核才是经常性成本;混在一起会高估或低估某一年。
- 用名义年费比较 DPA 成本。 应把年化收入损失和 LTV 损失分开算;首年按年化口径可能净亏,按 LTV 口径可能净得。
- 假设所有客户都要求 DPA。 只有一部分客户会要求;先确认比例,再决定维护深度,否则会把固定成本摊错对象。
- 忽略删除与返还的验证成本。 合同终止不等于数据消失;证明删除需要日志、清单和留证工时。
- 把 DPA 当成创收项。 DPA 不直接带来收入,它决定你能否进入要求 DPA 的投标;用丢单损失而不是收入增量来评估。
- 只维护一套模板,不维护子处理者清单。 模板相同、数据流不同,文档就会过期;清单和条款要一起复核。
- 把本节数字当成报价。 工时、费率和丢单数都是教学假设;真实成本要按自己的数据流、客户要求和外部报价重算。
DPA 的五项义务里,处理范围和删除时限最容易被忽略,子处理者通知和跨境机制最容易被低估,审计权最容易被当成一次性事件。下一节把这些义务放进一次事故:数据真的泄露时,谁通知、谁付钱、保险赔多少、责任上限挡多少。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。