分销代理多端触达系统 · 二次开发教程(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。

级别数量含义
P07上线门禁,代码冻结,不可配置绕过
P17应实现,可配置
P21建议项

三、冻结是怎么实现的

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"
}

注意两个细节:

  1. 响应头 x-compliance —— 便于前端和网关识别合规拦截,做专门提示(不是普通报错)
  2. 错误日志脱敏 —— console.error 只打 name 和 message,不打印完整堆栈。生产日志里出现完整堆栈很容易泄露路径、依赖版本、甚至代码片段

四、二开禁区清单

下面这些地方改了会出事,按危险程度排序:

🔴 绝对不要碰

禁区文件 / 位置后果
MAX_DISTRIBUTION_LEVELdomain/compliance-guardrails.js层级突破 2 级,直接踩传销刑事门槛(30 人 + 3 层)
PROHIBITED_COMPENSATION_FACTORS 里加白名单同上人头计酬复活,定性为传销
ALLOWED_COMPENSATION_BASES 加非交易基数同上计酬脱离真实交易,即"拉人头返利"
删掉领域层的 assert 调用commission-engine.js / agent-hierarchy.js护栏形同虚设
把 codeFreeze: true 改成 falseGUARDRAILS 数组让 P0 变成可配置,等于开后门
审计日志可删改handlers/audit.jsG09 失效,出事无法自证清白
敏感词过滤开关handlers/compliance-config.jsG10 失效,绝对化用语直接触广告法

🟡 需要非常谨慎

位置风险建议
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);
  // ...
}

要点:

  1. 复用 ComplianceViolation —— 这样错误会走统一的 422 + x-compliance 响应
  2. 用新的 G 编号(G16 起),不要占用 G01–G15
  3. 在 domain/ 层调用,不是路由层
  4. 补测试,断言它真的会抛

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 怎么部署、数据卷怎么保、上线前必须过一遍的检查清单。