分销代理多端触达系统 · 二次开发教程(00)
📌 转载声明:本文为《分销代理多端触达系统 · 二次开发教程》系列第 00 篇。转载请注明出处;可运行的配套代码与最新版本见官方开源仓库(Apache 2.0):atomgit.com/pangzi_zi/distributor-touch
架构地图与二开路径总纲
项目:Distributor Touch(distributor-touch 开源项目)· 快消 B2B 分销 SaaS 许可证:Apache 2.0 · 后端 Node.js 22+ 零运行时依赖 本文是整套二次开发教程的总纲,先建立全局认知,再按场景跳转对应篇章。
一、先回答一个问题:这套系统解决什么
一句话:品牌商 → 代理商 → 消费者 的一条链路打透,并且从架构上保证不会踩传销红线。
它不是一个单纯的商城,也不是一个单纯的代理管理系统,而是四端一体的组合:
| 端 | 使用者 | 形态 | 职责 |
|---|---|---|---|
| 管理端 | 品牌商运营 | B/S(PHP 门户页) | 管商品、订单、代理商、佣金、店铺、合规 |
| 代理端 | 代理商 | B/S + 小程序 | 看业绩、佣金、素材、培训、海报 |
| C 端商城 | 消费者 | B/S + 小程序 | 逛店、加购、下单、支付 |
| 小程序 | 代理 + 消费者 | UniApp 一套代码 | 编译到 H5 / 微信小程序 / App / 企微侧边栏 |
在线体验:https://www.chaoyi.club/mp-demo.html(演示站 show.chaoyi.club 数据独立,账号 admin / admin123)
二、目录结构:改哪里先看这里
distributor-touch/
├── backend/ # Node.js 后端(零依赖,ESM)
│ └── src/
│ ├── server.js # ★ 单一路由分发入口(1500+ 行,所有 API 都在这)
│ ├── persist.js # ★ 持久化注册中心
│ ├── seed.js # 种子数据
│ ├── config.js # .env / 环境变量
│ ├── handlers/ # ★ 业务模块(30 个,一个领域一个文件)
│ ├── domain/ # ★ 领域规则(合规护栏、佣金引擎、代理层级)
│ ├── auth/ # JWT + API Key + 中间件
│ ├── agent/ # AI 助手(意图解析 / 工具表 / 编排)
│ ├── store/file-store.js # 原子写入的 JSON 存储层
│ ├── utils/id-gen.js # ID 生成器
│ └── tests/ # node:test 单元测试(30 个文件)
│
├── services/
│ ├── php/portal/ # ★ B/S 门户(无框架)
│ │ ├── index.php # 路由表
│ │ ├── config.php # 配置 + 导航项
│ │ ├── controllers/ # PortalController / AgentController / AuthController
│ │ └── views/ # 38 个视图模板
│ ├── docker/ # Docker Compose(nginx + 多服务)
│ │ ├── nginx/nginx.conf
│ │ ├── www/ # 官网静态站
│ │ └── docker-compose.yml
│ ├── go/ java/ # 多语言服务端示例
│
├── frontend/ # 管理端前端资源
├── harmony/ # 鸿蒙端
├── uniapp/ # ★ UniApp 小程序(vite 结构,源码在 src/)
│ └── src/
│ ├── pages.json # 路由 + tabBar
│ ├── manifest.json # 多端编译配置
│ ├── pages/ # 页面(含 shop/ 商城 5 页)
│ ├── utils/ # api / cart / i18n / auth / config / nav
│ └── static/tabbar/ # 图标
│
└── docs/ openapi.yaml # 文档与 API 契约
三、技术栈与三条设计铁律
3.1 后端零运行时依赖
backend/package.json 的 dependencies 是空的。所有能力用 Node 内置模块实现:
- HTTP 服务:
node:http - 路由:手写
if (path === '...' && method === '...')分发 - JWT:
node:crypto手写 HMAC - 持久化:
node:fs原子写 JSON - 测试:
node:test内置
这对你意味着:克隆下来 node src/server.js 就能跑,没有 npm install 的等待,也没有供应链风险。但代价是没有框架帮你做约定 —— 所以下面的规范必须自己遵守。
3.2 三条铁律
| 铁律 | 说明 | 违反后果 |
|---|---|---|
写操作必须 scheduleSave() | 改了内存里的 Map 一定要触发落盘,否则重启丢数据 | 数据丢失 |
| 新增可持久化模块必须注册三处 | handler 导出 _internalState/_restoreState → persist.js MODULES → server.js 路由 | 重启数据不恢复 |
领域规则放 domain/,不放 handler | 合规校验、佣金计算属于领域层 | 护栏被绕过 |
四、二次开发场景速查表
拿到源码后,最常做的 9 件事和对应要改的地方:
| # | 场景 | 主要改动点 | 教程篇章 |
|---|---|---|---|
| 1 | 加一个业务实体(如店铺、仓库、会员等级) | handlers/xxx.js + persist.js + server.js + tests/ | 02 |
| 2 | 加一个管理端页面 | index.php 路由 + PortalController 方法 + views/ + config.php 导航 | 03 |
| 3 | 改商城业务规则(满减、拆单、优惠券) | handlers/orders.js + coupons.js + 门户 cart.js + 小程序 utils/cart.js | 04 |
| 4 | 小程序加页面 / 改 UI | pages.json + pages/xxx.vue + utils/api.js + utils/i18n.js | 05 |
| 5 | 接支付 / 短信 / 物流 | handlers/payment.js + gateway.js + config.js | 07 |
| 6 | 加一种语言 | 门户 i18n/ + 小程序 utils/i18n.js | 05 |
| 7 | 改主题 / 品牌换肤 | handlers/theme.js + frontend/ 样式变量 | 07 |
| 8 | 扩展合规校验 | domain/compliance-guardrails.js | 06 |
| 9 | 加 AI 助手能力 | agent/tools.js + agent/intent-parser.js + agent/orchestrator.js | 02 附 |
五、核心链路:一个请求是怎么走完的
理解这条链路,二开时就知道该在哪一层动手。
5.1 管理端打开「店铺管理」页
浏览器 GET /portal/stores
↓ nginx: location /portal/ { rewrite ^/portal/(.*)$ /$1 break; proxy_pass php-portal }
↓ PHP: index.php 路由表查到 '/stores' => PortalController::adminStores
↓ PortalController::adminStores()
├─ ensureToken() 会话里没 token 就用 admin 账号换一个
├─ apiCall('/api/stores') → 后端
├─ apiCall('/api/products') → 后端(用来统计每店商品数)
└─ render('admin_stores', $data)
↓ 后端 GET /api/stores
├─ authenticate() 中间件:token 有效?角色够吗?
└─ storesMod.listStores() 读内存 Map
↓ 返回 JSON → PHP 组装 → 渲染 views/admin_stores.php
关键点:PHP 门户不直接碰数据库,它只是后端的透传层 + 视图渲染层。所有业务规则都在 Node 后端。
⚠️ 很多二开同学在门户改了半天发现没效果,就是因为改错了层 —— 表单字段要生效,必须同时改 PHP 的转发链和后端的 handler,少一头字段就静默丢失。
5.2 消费者下一笔跨店订单
购物车(前端 localStorage,按 storeId 分组)
↓ 点「结算」→ 逐店调用 POST /api/orders(一店一单)
↓ orders.js: 校验 storeId 存在 → 计算金额 → 落库 → scheduleSave()
↓ 前端拿到 N 个订单号 → 依次调用 POST /api/orders/:id/pay
↓ 成功一个就从购物车移除该店商品;失败的店保留
六、数据是怎么存的
没有数据库,用的是单文件 JSON 快照 db.json。
写操作 → 改内存 Map → scheduleSave()(2 秒去抖)→ saveSnapshot() → db.json
启动 → loadSnapshot() 读 db.json → 各模块 _restoreState() → 跳过 seed
存储层 store/file-store.js 做了三件重要的事:
- 原子写入:先写
db.json.tmp再rename,避免进程被杀时留下半截文件 - Map/Set 序列化:JSON 不认 Map,写入时自动转 Object / Array
- 多机支持:
config.dataDir可指向共享文件系统
快照结构(每个注册的模块占一个 key):
{
"_savedAt": "2026-08-29T04:00:00.000Z",
"agents": [...],
"products": [...],
"orders": [...],
"stores": [...],
"coupons": [...]
}
💡 这个设计的取舍:零依赖、易备份、易调试(db.json直接cat);代价是不适合百万级数据和高并发写。如果你的客户量级上来了,把store/file-store.js换成 MySQL/PostgreSQL 实现即可,上层 handler 代码一行都不用改 —— 这是预留好的替换点。
七、权限模型
三级角色 + 两种认证方式:
| 角色 | 能做什么 |
|---|---|
admin | 全部(品牌商管理员) |
agent | 只看自己的数据(佣金、业绩、素材) |
user | C 端公开数据 |
认证中间件 auth/middleware.js 用两张表控制:
const PUBLIC_ROUTES = [ // 无需登录
{ method: 'GET', path: '/api/health' },
{ method: 'GET', path: '/api/themes' },
// ...
];
const ADMIN_ONLY_ROUTES = [ // 必须 admin 角色
{ method: 'POST', path: '/api/materials' },
{ method: 'POST', path: '/api/orders' },
// ...
];
二开时加接口,第一件事就是想清楚它属于哪一类,然后决定:
- 公开 → 加进
PUBLIC_ROUTES(C 端匿名能访问) - 管理员 → 加进
ADMIN_ONLY_ROUTES,或在 handler 里判断currentUser?.role !== 'admin' - 其他 → 只要求登录态即可
⚠️ 别图省事把接口全设成公开。C 端页面是完全匿名的,公开接口等于对互联网开放。
八、建议的阅读顺序
- 只想加个功能模块 → 01 环境 → 02 新增业务模块 → 03 管理端页面
- 想改商城/交易 → 01 → 04 C 端商城与拆单
- 做小程序 → 01 → 05 UniApp 二开
- 上线前必读 → 06 合规禁区 → 07 测试部署
九、先记住这 5 个坑
这 5 个坑是实战中真踩过的,写在最前面帮你省时间:
- Windows 下
npm test会 MODULE_NOT_FOUND ——node --test src/tests/的尾斜杠在 Windows 解析不了,要用node --test "src/tests/*.test.js"。 createProduct()返回的是{ product, warnings }不是商品本身 —— 直接拿返回值读.id会 undefined。assert.throws()不返回错误对象 —— 想断言err.status得自己包一层expectError(fn, re)。- PHP 门户的 demo 版禁止接口透传 —— 新增的
/api/xxx必须在index.php路由表里显式注册,否则 403。 - 改了 php-portal 的代码必须 rebuild 镜像 —— 它是
COPY进容器的,不是 volume 挂载,改文件不重建不生效。
下一篇
01 环境搭建与本地跑通 —— 三种启动方式、种子数据、健康检查,以及 Windows 下的专属坑。