二开第一问:这个需求该进 driver,还是进 modules?
二开最贵的错误往往不是写慢了,而是写错目录:支付 SDK 塞进订单服务、秒杀价写进商品类型、注册送礼焊在登录接口里。短期能跑,升级和协作都会疼。
这篇按开源项目 CRMEB 当前仓库的约定,把「选型」拆成可执行的判定,并标出那些看起来像插件、其实过不了零侵入的坑。路径相对 backend/。
两类目录,两套生命周期
| 落位 | 目录 | 解决什么 | 安装后关键表 |
|---|---|---|---|
| 驱动 | driver/{type}/{Dir}/ | 和第三方说话,业务规则不变 | eb_driver |
| 业务模块 | modules/{领域}/{name}/ | 卖什么、怎么卖、从哪登录、发什么奖 | 安装日志 + 自有表/菜单 |
驱动运行时不扫目录:拷进去但没走后台安装,DriverRegistry::scan($type) 读不到。业务模块靠 plugin.json 被发现,靠 Installer 写菜单和种子,靠 App::register() / boot() 挂注册中心与事件。
强制红线:插件不得改 app/、crmeb/、核心前端、public/install/、composer.json、config/、.env。需要改这些才能完成的需求,先停下来问清楚,不要假装已经零侵入。
判定顺序:五步,不要跳
flowchart TD
A["新需求"] --> B{"只是换第三方通道?"}
B -->|是| C["driver/{type}/"]
B -->|否| D{"改变库存/履约/售后形态?"}
D -->|是| E["modules/product"]
D -->|否| F{"改变活动价/资格/限购且失败要阻断下单?"}
F -->|是| G["modules/trade"]
F -->|否| H{"卸掉后仍能原价成交,只是不再发奖?"}
H -->|是| I["modules/marketing"]
H -->|否| J{"新登录端或渠道 SDK?"}
J -->|是| K["modules/channel"]
J -->|否| L{"推广关系与佣金?"}
L -->|是| M["modules/distribution"]
L -->|否| N["读 domain-selection 或问清楚再动手"]
口诀可以再缩一句:去掉它之后,订单语义还在不在? 还在 → 多半是营销或通知;成交价/资格变了 → 交易模式;履约和售后形态变了 → 产品类型;只换供应商 → 驱动。
驱动十类(目前只有这些)
pay / sms / email / upload / logistics / express / invoice / bot / printer / copyproduct。
新增「第十一类驱动」要动 crmeb/ 与 app/,超出插件范围。接一家新短信商,是在已有 sms 类型下加目录,不是新造类型。
模块领域对照
| 领域 | 现役例子 | 典型扩展点 |
|---|---|---|
| product | normal / virtual / cardkey | ProductTypeRegistry + 订单适配器 + 售后规则 |
| trade | seckill / combination / bargain / integral | TradeModeRegistry,allowsOrderDiscount() 等 |
| marketing | newuser / signin / lottery | event.php 监听字符串事件 |
| channel | h5 / wechat_mini / wechat_mp / app | 渠道认证与登录 |
| distribution | 扁平 modules/distribution/ | 关系快照、佣金、可挂 MCP 工具 |
驱动四个名字必须对齐
目录名 {Dir} 必须等于 Str::studly(driver_key)。例如 ali_pay → AliPay,yihaotong → Yihaotong。不对齐时门面抛 Driver [xxx] not supported.,在 Linux 上还可能直接 PSR-4 加载失败。
| 名字 | 规则 |
|---|---|
目录 {Dir} | Str::studly(driver_key) |
| 命名空间 | driver\{type}\{Dir},类名通常 Driver |
plugin.json 的 path | 与真实目录完全一致 |
顶层 key 与 driver.driver_key | 建议写成同一个值 |
可以用下面确认 studly 结果:
cd backend && php -r 'require "vendor/autoload.php"; echo \think\helper\Str::studly("your_key"), PHP_EOL;'
链路是:安装写 eb_driver → 业务取配置 → 门面按 key 拼类名 → 驱动类。目录存在 ≠ 已安装。
零侵入接得上,和接不上
按当前仓库文档核对过的结论:
相对好接(按 key 动态实例化):短信、邮件、上传存储、商品采集。适合作为第一个驱动二开练手题。
装了驱动也不够(核心有写死点):
| 类型 | 现象 |
|---|---|
| pay | 支付方式列表与端上 channel_key 分支有白名单;只装驱动,后台/端上可能看不到或调不起 |
| logistics | 查询驱动白名单,新 key 可能直接「不支持」 |
| printer | 出票服务按名称放行与拼配置 |
| invoice | 调用方固定一号通 |
| bot | 通知渠道写死钉钉/企微路径 |
| express | 面单类命名空间固定落在 driver\logistics\ |
遇到上表,交付方案里要写明:是改核心(需同意)、是只支持已有通道、还是另做业务适配层。不要在客户现场才发现「驱动装上了但发不了」。
业务模块:选型错了的典型症状
- 秒杀价写在 marketing 监听器里改应付金额 → 禁用模块后用户仍能以活动价下单,或监听顺序导致价算乱。应落 trade,并声明
allowsOrderDiscount()。 - 卡密/网盘履约写在支付成功监听器里,却不注册产品类型 → 订单中台看不到正确的 source_type、售后规则、详情路由。
- 渠道登录逻辑塞进
app/api→ 升级必冲突;应modules/channel/{name}+ 认证器契约。 - 分销工具写进
modules/service/mcp/tools/并use modules\distribution\...→arch:check跨模块互引直接报错。工具应放在分销模块自己的mcp/里,经McpToolsRegister注册。
选型对了,再用工具链
php think make:module <name> --label=中文名 --type=...
php think arch:check
php think plugin:manifest-check
make:module 生成合规模块骨架;arch:check 拦三层依赖与跨模块互引;清单检查拦 path / DIY 声明悬空。事件用字符串 key,模块在自己的 event.php 追加,随安装状态加载。
代价
1. 选型没有编译器。 目录能编译、后台能安装,不代表领域对。验收要加一条:禁用/卸载后,成交语义是否仍符合预期。
2. 驱动「装上」和「业务调用到」是两件事。 白名单类驱动必须在方案里单独验证调用链。
3. 跨模块互引会被架构检查拦住。 共享只能走 crmeb 契约、字符串事件、或把代码放到拥有数据的那个模块里。
排障
- 驱动报不支持:先对 studly 目录名、再查是否已安装写入
eb_driver、最后查业务侧是否白名单。 - 模块菜单没有:是否跑过 Installer,是否加入
model_checkbox,角色是否包含新菜单 ID。 - 事件没触发:模块是否启用、
event.php是否在boot()里loadEvent、事件名是否与触发端字符串一致。 arch:check红:按报错搬use,不要关检查硬合并。