「商业化这条线基本通了」
卷三读到最后一节,团队通常会给出一个模糊结论:「商业化这条线基本通了,可以看平台化了。」
这句话在评审会上很难被反驳,因为没有人能立刻说出「基本」缺什么。等到下个版本出事,复盘才会发现:账号合并没做冲突检查、退款没有回收资产、埋点口径改过两次、隐私政策没跟上 SDK 清单。
本节不接受「基本通了」。它只认三种状态,并且把没有证据的「通过」直接降级。
九组检查项
卷三的范围对应九组:
| 组 | 核心问题 | 对应章节 |
|---|---|---|
| 收入与口径 | 赚了多少、口径是否一致 | 第 1、5 章 |
| 账号与登录 | 用户是谁、能不能找回 | 第 2 章 |
| 渠道与包体 | 包从哪来、是否可复现 | 第 3 章 |
| 支付与商品 | 钱能不能收到、商品是否正确 | 第 4 章 |
| 订单与对账 | 收到之后能不能对上 | 第 5 章 |
| 广告与归因 | 流量怎么变现、新增算给谁 | 第 6、7 章 |
| 风控与数据 | 作弊能不能挡住、数据是否可信 | 第 8、10 章 |
| 运营与触达 | 活动与消息是否可控 | 第 9、11 章 |
| 合规与发行 | 材料与红线是否齐备 | 第 12 章 |
九组不是并列清单,而是商业化判断链的九个关节。任何一组全空,都说明那条链断了一节。
把门禁写成代码
评审器的规则只有三条:
pass必须有非空证据,否则降级为partial;partial与fail都是阻塞项;- 空评审直接判失败,而不是「默认通过」。
// file: src/lab/review-gate-biz.ts
// tier: L1
// verify: node --test test/lab/review-gate-biz.test.ts
export type ReviewStatus = "pass" | "partial" | "fail";
export interface ReviewItem {
readonly id: string;
readonly group: string;
readonly status: ReviewStatus;
readonly evidence: string;
}
export interface GroupSummary {
readonly group: string;
readonly pass: number;
readonly partial: number;
readonly fail: number;
}
export interface GateResult {
readonly passed: boolean;
readonly total: number;
readonly counts: Readonly<Record<ReviewStatus, number>>;
readonly groups: readonly GroupSummary[];
readonly blockers: readonly ReviewItem[];
readonly openGroups: readonly string[];
readonly reasons: readonly string[];
}
export const REVIEW_GROUPS = [
"收入与口径",
"账号与登录",
"渠道与包体",
"支付与商品",
"订单与对账",
"广告与归因",
"风控与数据",
"运营与触达",
"合规与发行",
] as const;
export function evaluateGate(items: readonly ReviewItem[]): GateResult {
const reasons: string[] = [];
const normalized: ReviewItem[] = [];
for (const item of items) {
if (item.status === "pass" && item.evidence.trim() === "") {
reasons.push(`${item.id}: pass without evidence counts as partial`);
normalized.push({ ...item, status: "partial" });
continue;
}
normalized.push(item);
}
const counts: Record<ReviewStatus, number> = { pass: 0, partial: 0, fail: 0 };
for (const item of normalized) {
counts[item.status] += 1;
}
const groups = REVIEW_GROUPS.map((group) => {
const scoped = normalized.filter((item) => item.group === group);
return {
group,
pass: scoped.filter((item) => item.status === "pass").length,
partial: scoped.filter((item) => item.status === "partial").length,
fail: scoped.filter((item) => item.status === "fail").length,
};
});
const blockers = normalized.filter((item) => item.status !== "pass");
const openGroups = groups.filter((group) => group.pass === 0).map((group) => group.group);
if (normalized.length === 0) {
reasons.push("no review items");
}
if (counts.partial > 0) {
reasons.push(`${counts.partial} partial item(s) block the gate`);
}
if (counts.fail > 0) {
reasons.push(`${counts.fail} failed item(s) block the gate`);
}
return {
passed: normalized.length > 0 && blockers.length === 0,
total: normalized.length,
counts,
groups,
blockers,
openGroups,
reasons,
};
}
export function failingGroups(result: GateResult): readonly string[] {
return result.groups.filter((group) => group.fail > 0).map((group) => group.group);
}
export function formatGate(result: GateResult): string {
const header = "group\tpass\tpartial\tfail";
const rows = result.groups.map(
(group) => `${group.group}\t${group.pass}\t${group.partial}\t${group.fail}`,
);
return [header, ...rows].join("\n");
}
export const DEFAULT_REVIEW: readonly ReviewItem[] = [
{ id: "revenue", group: "收入与口径", status: "pass", evidence: "src/biz/revenue-mix.ts" },
{ id: "unit-economics", group: "收入与口径", status: "pass", evidence: "src/biz/unit-economics.ts" },
{ id: "identity", group: "账号与登录", status: "pass", evidence: "src/biz/identity.ts" },
{ id: "session", group: "账号与登录", status: "pass", evidence: "src/biz/login-session.ts" },
{ id: "channel", group: "渠道与包体", status: "pass", evidence: "src/biz/channel-build.ts" },
{ id: "catalog", group: "支付与商品", status: "pass", evidence: "src/biz/catalog.ts" },
{ id: "receipt", group: "支付与商品", status: "pass", evidence: "src/biz/receipt-verify.ts" },
{ id: "order", group: "订单与对账", status: "pass", evidence: "src/biz/order-fsm.ts" },
{ id: "attribution", group: "广告与归因", status: "pass", evidence: "src/biz/attribution.ts" },
{ id: "risk", group: "风控与数据", status: "pass", evidence: "src/biz/risk-engine.ts" },
{ id: "campaign", group: "运营与触达", status: "pass", evidence: "src/biz/campaign.ts" },
{ id: "privacy", group: "合规与发行", status: "pass", evidence: "src/biz/privacy-manifest.ts" },
];
evaluateGate 返回四类信息:是否通过、状态计数、按组汇总、阻塞项列表。另外两个派生视图很有用:
openGroups:一项都没通过的组,用来识别「完全没开始」;failingGroups:存在fail的组,用来识别「做了但坏了」。
两个口径不能混。一个组可能全是 partial(做了没证据),也可能有 pass 有 fail(部分可用部分坏)。前者是没验证,后者是验证出问题,处理方式完全不同。
一次方舟的评审记录
假设方舟在赛季前跑一次评审,12 项里有 2 项没过:
| 项 | 组 | 状态 | 证据 | 处理 |
|---|---|---|---|---|
| receipt | 支付与商品 | partial | 只测了沙盒凭证 | 补生产凭证校验与失败用例 |
| refund | 订单与对账 | fail | 退款后资产未回收 | 阻塞版本,先补回收链路 |
| 其余 10 项 | — | pass | 文件路径与测试输出 | 保持 |
结果:passed: false,blockers 是这两项;openGroups 为空(每组至少一项通过),failingGroups 是「订单与对账」。
这个区分决定处理顺序:订单与对账组有 fail,说明能力存在但坏了,优先级最高;支付与商品组只有 partial,说明验证不完整,可以排在其后。先修坏的,再补没验的。
与卷二评审器的关系
卷二的评审器与本节结构相似,但分组与证据类型不同:
| 维度 | 卷二 | 卷三 |
|---|---|---|
| 分组 | 协议、连接、离线、同步、战报、流量、资源、热更、下载 | 收入、账号、渠道、支付、订单、广告、风控、运营、合规 |
| 检查对象 | 客户端与外部系统的技术契约 | 客户端、渠道与中台之间的商业契约 |
| 典型证据 | 契约测试、补丁测试、演练记录 | 订单测试、对账报表、渠道材料 |
| 空白组含义 | 跨边界技术能力缺失 | 商业链路能力缺失 |
两卷的门禁都要求「证据可复现」,但卷三的证据更多来自报表与渠道后台,而不是单测。这不是标准降低了,而是检查对象变了:退款是否回收资产,无法只靠一个纯函数证明。
双案例的收口表
把九组门禁套到两款产品上,得到两张可复核的表:
| 组 | 消消 | 方舟 |
|---|---|---|
| 收入与口径 | pass | pass |
| 账号与登录 | pass | pass |
| 渠道与包体 | pass | pass |
| 支付与商品 | pass | partial(生产凭证待补) |
| 订单与对账 | pass | fail(退款回收缺失) |
| 广告与归因 | pass | partial(归因回传延迟) |
| 风控与数据 | pass | pass |
| 运营与触达 | pass | pass |
| 合规与发行 | pass | pass |
「不适用」不是「通过」。消消的强联网战斗不适用不代表其同步能力通过;评审时应记录为范围外,并在汇总里单列。把不适用算成 pass,会让通过率虚高,也会掩盖真正缺失的组。
进入卷四之前
卷四讨论平台化与长线治理。进入之前,有四个问题必须能回答:
- 两款产品的复用边界在哪? 如果卷三还没区分「中台共用」与「产品专属」,卷四的组件库与配置平台就无从谈起。
- 配置归属谁? 商品目录、活动配置、渠道参数分别由哪个平台维护?归属不清,长线运营一定出现双份配置。
- 指标口径归口哪里? 商业化、增长、财务三套报表如果各算各的,卷四的指标治理会从负数开始。
- 事故责任怎么划? 支付掉单、活动超发、渠道包错误,分别由哪条线应急?没有责任人,平台化只会把问题集中得更快。
四个问题里,前两个直接依赖卷三;后两个是卷三向卷四的接口。答不上来,说明卷三的账号、渠道与数据章节还没落地。
带病进入卷四
如果只能回答其中两三个问题,也可以进入卷四,但必须把未答项登记为「带病进入」:
| 未答项 | 影响 | 修复版本 |
|---|---|---|
| 配置归属 | 活动与商品出现双份配置 | 卷四第 4 章前 |
| 指标归口 | 三套报表口径不一致 | 卷四第 6 章前 |
登记的意义不是免责,而是排期。允许带病进入,不允许假装没病——这句话在卷三同样适用。
与相邻章节的关系
本节依赖 13.1 的指标矩阵与 13.2 的发行门禁:前者提供数值证据,后者提供发布证据。附录 C 是本节检查项的来源,本节把清单变成可运行门禁。
读到这里,卷三的判断链已经闭合:从收入结构出发,经账号、渠道、支付、订单、广告、归因、风控、运营、埋点、推送与合规,最后收口为指标矩阵、发行门禁与评审器。卷四会在这条链上接入平台复用与长线治理。
小结
- 卷三评审有九组:收入、账号、渠道、支付、订单、广告、风控、运营、合规。
- 门禁只认 pass/partial/fail,
pass无证据自动降级,空评审判失败。 openGroups识别完全没开始的组,failingGroups识别做了但坏了的组。- 先修
fail,再补partial;两类问题的处理顺序不能反。 - 「不适用」要单列,不能算成通过,否则通过率虚高。
- 进入卷四前回答四个问题:复用边界、配置归属、指标归口、事故责任。
- 允许带病进入,但未答项必须登记并排期。
阅读导航:上一节:13.2 发行清单与上线门禁 · 下一节:目录与学习路径 。
常见错误与边界
- 把评审做成打分。 平均分掩盖空白组;门禁只认通过与否,不做加权。
- 允许「部分通过」。 设计有、证据无,等于没验证;按规则直接降级。
- 证据写成「已确认」。 证据必须是路径、命令输出或线上查询,能被人复现。
- 把不适用算成通过。 消消没有强联网战斗,不代表同步能力通过;应单列为范围外。
- 只看总分不看分组。 九组里一组全空,总分仍可能很高;
openGroups专门抓这个。 - 混淆空白组与失败组。 前者没开始,后者做坏了;处理优先级不同。
- 评审只做一次。 每个大版本重跑,保留历史,趋势比单次结论更有信息量。
- 把评审器当自动化测试。 它校验记录自洽,不替代订单测试、对账与渠道演练。
- 带病进入变成常态。 每个版本都带病,说明门禁没有约束力。
- 删除旧评审记录。 没有历史,就无法区分「一直如此」与「本版本退步」。
- 忽略渠道证据。 渠道后台截图与账单是证据,口头确认不是。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。