分销代理多端触达系统 · 二次开发教程(06)
📌 转载声明:本文为《分销代理多端触达系统 · 二次开发教程》系列第 06 篇。转载请注明出处;可运行的配套代码与最新版本见官方开源仓库(Apache 2.0):atomgit.com/pangzi_zi/distributor-touch
合规护栏与二开禁区
上一篇:05 UniApp 小程序二开 本篇目标:知道哪些地方能改、哪些碰不得,以及怎么在不破坏基线的前提下扩展自己的校验。 这是整套教程里最该先读的一篇 —— 分销系统的合规风险是真金白银的罚款和刑责,不是纸面文章。
一、为什么要有「代码冻结」这种设计
先看法律依据(项目里锚定的 CB-LEGAL-2026-DIST-001):
- 《禁止传销条例》:传销的刑事门槛是「30 人以上且 3 层以上」
- 2026 年 5 月修订征求意见稿:组织策划罚款上限 500 万元,主管人员 3 年行业禁入
- 二级分销(层级 ≤ 2)明确属于合法经营
结论:只要死死卡住「层级 ≤ 2」和「不许按人头计酬」,就落在合法安全港内。
问题在于:如果这条限制只是配置项,那它是可以被改的。
- 客户说"我就要三级,出事我负责" → 销售为了成单给改了
- 运营误操作 → 三级跑起来了
- 二开同学不知道这条规则 → 顺手加了个"团队人数奖励"
所以项目把 P0 级护栏做成 codeFreeze: true —— 代码冻结:运行时无法通过普通配置绕过,任何越界调用在领域层直接抛异常。
📌 这是这套系统最有价值的设计思想:合规不是配置,是架构。
二、15 项护栏清单
backend/src/domain/compliance-guardrails.js:
export const GUARDRAILS = [
{ id: 'G01', name: '分润层级出厂默认≤2级硬编码', level: 'P0', codeFreeze: true, ref: '护栏①' },
{ id: 'G02', name: '人头/邀请数计酬入口全链路屏蔽', level: 'P0', codeFreeze: true, ref: '护栏②' },
{ id: 'G03', name: '单级返利异常预警', level: 'P1', codeFreeze: false, ref: '护栏③' },
{ id: 'G04', name: '税务代扣/委托代征对接', level: 'P1', codeFreeze: false, ref: '护栏④' },
{ id: 'G05', name: '累计120万市场主体登记提示', level: 'P2', codeFreeze: false, ref: '护栏⑤' },
{ id: 'G06', name: '分级告知同意(注册/绑定/触达/画像)', level: 'P0', codeFreeze: true, ref: '护栏⑥' },
{ id: 'G07', name: '算法推荐开关可关', level: 'P1', codeFreeze: false, ref: '护栏⑦' },
{ id: 'G08', name: '三级数据权限矩阵', level: 'P1', codeFreeze: false, ref: '护栏⑧' },
{ id: 'G09', name: '合规审计日志不可篡改', level: 'P0', codeFreeze: true, ref: '护栏⑨' },
{ id: 'G10', name: '品牌素材敏感词/绝对化用语过滤', level: 'P0', codeFreeze: true, ref: '护栏⑩' },
{ id: 'G11', name: '品类级限制与资质核验', level: 'P1', codeFreeze: false, ref: '护栏⑪' },
{ id: 'G12', name: '违规举报+品牌商封禁', level: 'P1', codeFreeze: false, ref: '护栏⑫' },
{ id: 'G13', name: '反传销合规承诺函上传', level: 'P0', codeFreeze: true, ref: '护栏⑬' },
{ id: 'G14', name: '数据出境安全评估(涉跨境触发)', level: 'P0', codeFreeze: true, ref: '护栏⑭' },
{ id: 'G15', name: '招募文案合规校验', level: 'P1', codeFreeze: false, ref: '护栏⑮' },
];
export const P0_GUARDRAIDS = GUARDRAILS.filter(g => g.level === 'P0').map(g => g.id);
export const CODE_FREEZE_GUARDRAIDS = GUARDRAILS.filter(g => g.codeFreeze).map(g => g.id);
7 项 P0,全部代码冻结:G01 / G02 / G06 / G09 / G10 / G13 / G14。
| 级别 | 数量 | 含义 |
|---|---|---|
| P0 | 7 | 上线门禁,代码冻结,不可配置绕过 |
| P1 | 7 | 应实现,可配置 |
| P2 | 1 | 建议项 |
三、冻结是怎么实现的
3.1 常量 + 断言函数
// 护栏①:分润层级上限,出厂常量锁定
export const MAX_DISTRIBUTION_LEVEL = 2;
// 护栏②:计酬基数仅限真实交易;人头因子禁止出现在任何配置
export const ALLOWED_COMPENSATION_BASES = ['real_order_amount', 'paid_amount'];
export const PROHIBITED_COMPENSATION_FACTORS = [
'headcount', 'invite_count', 'team_size', 'recruit_count',
];
export class ComplianceViolation extends Error {
constructor(guardrailId, message) {
super(`[合规拦截 ${guardrailId}] ${message}`);
this.name = 'ComplianceViolation';
this.guardrailId = guardrailId;
this.guardrail = guardrailId; // 统一字段名,兼容两种访问方式
this.status = 422;
}
}
export function assertWithinDistributionLevel(level) {
if (level > MAX_DISTRIBUTION_LEVEL) {
throw new ComplianceViolation('G01',
`分销层级 ${level} 超过出厂上限 ${MAX_DISTRIBUTION_LEVEL},已硬编码阻断`);
}
}
export function assertCompensationBase(base) {
if (!ALLOWED_COMPENSATION_BASES.includes(base)) {
throw new ComplianceViolation('G02',
`计酬基数「${base}」不在允许范围(仅真实交易),已屏蔽`);
}
}
export function assertNoProhibitedFactor(factors = []) {
for (const f of factors) {
if (PROHIBITED_COMPENSATION_FACTORS.includes(f)) {
throw new ComplianceViolation('G02',
`计酬因子「${f}」为人头类因子,已全链路屏蔽`);
}
}
}
3.2 在领域层调用(不是路由层)
护栏的调用点在 domain/,不在 handler、更不在路由:
// domain/commission-engine.js —— 创建分润规则时
assertCompensationBase(rule.base);
assertNoProhibitedFactor(rule.factors || []);
if (rule.levels > MAX_DISTRIBUTION_LEVEL) {
throw new ComplianceViolation('G01',
`分润规则配置 ${rule.levels} 级,超过出厂上限 ${MAX_DISTRIBUTION_LEVEL}`);
}
// domain/agent-hierarchy.js —— 绑定代理上下级时
if (childId === parentId) {
throw new ComplianceViolation('G01', '不能将代理绑定到自己');
}
if (hasParent(childId, relations)) {
throw new ComplianceViolation('G01',
`代理 ${childId} 已存在上级关系,禁止改绑以规避层级限制`);
}
if (wouldBe > MAX_DISTRIBUTION_LEVEL) {
throw new ComplianceViolation('G01',
`绑定将导致层级达到 ${wouldBe} 级,超过出厂上限 ${MAX_DISTRIBUTION_LEVEL}`);
}
assertWithinDistributionLevel(depthOf(child, relations));
📌 这条设计原则值得抄进你自己的项目:把不可协商的规则放在最内层(领域层),外层无论是 HTTP API、小程序、定时任务还是后台脚本,都绕不过去。 如果校验写在路由层,别人加一个新路由就绕过了;写在 handler 层,别人直接调领域函数就绕过了。
3.3 错误怎么返回
server.js 的统一错误处理:
function sendCompliance(res, status, guardrail, message, note) {
const token = note || GUARD_ASCII[guardrail] || 'compliance-blocked';
send(res, status, { error: message, guardrail }, { 'x-compliance': `${guardrail}:${token}` });
}
function handleError(res, err) {
if (err?.status) {
const g = err.guardrail || err.guardrailId || 'Gxx';
return sendCompliance(res, err.status, g, err.message);
}
// 脱敏:仅记录错误类型和消息,不打印完整堆栈/对象
console.error(`[server] 内部错误: ${err?.name || 'Error'}: ${err?.message || 'unknown'}`);
return sendCompliance(res, 500, 'Gxx', '服务器内部错误');
}
响应:
HTTP/1.1 422 Unprocessable Entity
x-compliance: G01:compliance-blocked
Content-Type: application/json
{
"error": "[合规拦截 G01] 分销层级 3 超过出厂上限 2,已硬编码阻断",
"guardrail": "G01"
}
注意两个细节:
- 响应头
x-compliance—— 便于前端和网关识别合规拦截,做专门提示(不是普通报错) - 错误日志脱敏 ——
console.error只打name和message,不打印完整堆栈。生产日志里出现完整堆栈很容易泄露路径、依赖版本、甚至代码片段
四、二开禁区清单
下面这些地方改了会出事,按危险程度排序:
🔴 绝对不要碰
| 禁区 | 文件 / 位置 | 后果 |
|---|---|---|
MAX_DISTRIBUTION_LEVEL | domain/compliance-guardrails.js | 层级突破 2 级,直接踩传销刑事门槛(30 人 + 3 层) |
PROHIBITED_COMPENSATION_FACTORS 里加白名单 | 同上 | 人头计酬复活,定性为传销 |
ALLOWED_COMPENSATION_BASES 加非交易基数 | 同上 | 计酬脱离真实交易,即"拉人头返利" |
| 删掉领域层的 assert 调用 | commission-engine.js / agent-hierarchy.js | 护栏形同虚设 |
把 codeFreeze: true 改成 false | GUARDRAILS 数组 | 让 P0 变成可配置,等于开后门 |
| 审计日志可删改 | handlers/audit.js | G09 失效,出事无法自证清白 |
| 敏感词过滤开关 | handlers/compliance-config.js | G10 失效,绝对化用语直接触广告法 |
🟡 需要非常谨慎
| 位置 | 风险 | 建议 |
|---|---|---|
auth/middleware.js 的 PUBLIC_ROUTES | 加太多公开接口 = 数据裸奔 | 每个公开接口都要过一遍"这个字段能公开吗" |
PortalController::ensureToken() 的硬编码 admin | 门户权限 = 管理员权限 | 上线前必改,并考虑用只读专用账号 |
| 佣金计算逻辑 | 算错直接是钱的问题 | 改完必须跑 commission-engine.test.js 和相关用例 |
| 订单状态流转 | 状态错乱导致重复支付/漏发 | 状态变更统一走既有函数,不要直接改字段 |
跨域配置 CORS_ORIGIN | 默认 * | 生产收敛到具体域名 |
🟢 安全区(随便改)
- 新增非分润类的业务模块(店铺、仓库、物流、品类……)
- UI / 主题 / 文案 / 多语言
- 新增报表、统计维度
- 新增支付方式(走
payment.js抽象) - 新增通知渠道(短信、企微、邮件)
- 新增小程序页面
五、正确的扩展姿势
5.1 加一条自己的合规校验
不要改 GUARDRAILS 数组(那是基线),在 domain/ 下新建文件,按同样的模式写:
// domain/my-guardrails.js
import { ComplianceViolation } from './compliance-guardrails.js';
/** G16:单笔订单金额上限(品牌商自定义风控,非法定护栏) */
export const MAX_ORDER_AMOUNT = 50000;
export function assertOrderAmount(amount) {
if (amount > MAX_ORDER_AMOUNT) {
throw new ComplianceViolation('G16',
`单笔订单金额 ${amount} 超过风控上限 ${MAX_ORDER_AMOUNT}`);
}
}
然后在被调用方领域层调用:
// domain/commission-engine.js 或你的业务模块
import { assertOrderAmount } from './my-guardrails.js';
export function calculateCommission(order) {
assertOrderAmount(order.amount);
// ...
}
要点:
- 复用
ComplianceViolation—— 这样错误会走统一的 422 +x-compliance响应 - 用新的 G 编号(G16 起),不要占用 G01–G15
- 在
domain/层调用,不是路由层 - 补测试,断言它真的会抛
5.2 需要"可配置"的合规项怎么办
有些客户确实需要不同的阈值(比如订单金额上限)。正确做法是配置项 + 上下界硬编码:
import { config } from '../config.js';
// 配置可改,但硬编码上下界,防止配出离谱的值
const MIN_LIMIT = 1000;
const MAX_LIMIT = 200000;
export function getOrderAmountLimit() {
const v = parseInt(process.env.ORDER_AMOUNT_LIMIT ?? '', 10);
if (Number.isNaN(v)) return 50000; // 默认
return Math.min(MAX_LIMIT, Math.max(MIN_LIMIT, v)); // 夹紧
}
核心思想:允许调参,但不允许突破边界。级别数是"能不能"的问题,不是"多少"的问题 —— 前者冻结,后者可配。
5.3 加审计
所有写操作都要留痕:
import { appendAudit } from './audit.js';
export function createXxx(body) {
// ...
appendAudit({
action: 'xxx.created',
detail: { id: item.id, name: item.name },
});
return item;
}
命名约定:{模块}.{动作},如 store.created / order.paid / agent.bound。
六、怎么验证护栏还在工作
项目里有专门的测试文件 src/tests/compliance-guardrails.test.js:
node --test src/tests/compliance-guardrails.test.js
改完任何涉及层级、计酬、权限的代码,第一件事就是跑它。
上线前跑全量:
cd backend
node --test "src/tests/*.test.js"
当前基线:581 个用例,全绿。如果你改完数量变少或有 fail,说明动了不该动的地方。
也可以手工打一发验证:
# 尝试创建 3 级分润规则(应被拒)
curl -X POST http://localhost:3000/api/commission/rules \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"三级分润","rate":0.1,"base":"real_order_amount","levels":3,"factors":[]}'
# 期望:422 + x-compliance: G01:...
七、写给二开同学的一句话
这套系统的合规设计看起来"碍手碍脚" —— 你想加个"邀请 5 人奖励"被拦,想做三级分销被拦。
但正是这些拦截,让使用它的人不会坐牢。
如果你的客户真的要求突破这些限制,正确的回答不是"改代码",而是"这个需求有法律风险,我们换个方案实现同样的业务目标"。
下一篇
07 测试、部署与生产上线 —— 测试怎么写、Docker 怎么部署、数据卷怎么保、上线前必须过一遍的检查清单。