做电商系统二开的团队,几乎都踩过同一个坑:拿到源码直接改,功能上线时很爽,等官方发新版本要升级时,全是冲突,改到崩溃。
LikeShop 全系列产品(单商户、多商户、垂直行业版)在设计时已经预留了扩展机制,但很多二开团队没有顺着架构来,直接硬改核心文件,最后把可升级的源码改成了「一次性版本」。后续官方修复漏洞、加新功能,都没法平滑同步,技术债越积越重。
这篇文章从目录结构、钩子机制、升级兼容三个维度,整理一套可落地的二开代码规范。无论是 PHP 版还是 Java 版,核心思路都是一致的:最小侵入、物理隔离、扩展优先、兼容升级。
一、目录结构规范:官方源码与二开代码物理隔离
所有规范的第一步,都是先把「官方的」和「自己改的」分开。混在一起写,短期快,长期全是维护成本。
1.1 PHP 版(ThinkPHP 架构)目录约定
LikeShop PHP 版基于 ThinkPHP 框架,标准 MVC 结构。二开最忌讳直接在官方模块里改控制器、改模型、改模板。
禁止直接修改的目录
这些是官方核心代码,升级时会全量覆盖,直接修改会导致升级即丢失:
vendor/:框架与第三方依赖app/admin/、app/api/:官方后台与前台接口模块app/common/:公共模型、服务层public/static/admin/、public/static/api/:官方静态资源
推荐的二开目录结构
在不破坏官方结构的前提下,新增独立目录承载二开代码:
项目根目录
├── app/
│ ├── extra/ # 二开业务模块(新增)
│ │ ├── controller/ # 自定义控制器
│ │ ├── model/ # 自定义模型
│ │ ├── service/ # 自定义业务服务
│ │ └── validate/ # 自定义验证器
│ └── common/
│ └── hook/ # 自定义钩子实现(推荐)
├── addons/ # 插件化二开(推荐,完全隔离)
│ └── your_addon/ # 自定义插件
├── public/
│ └── static/extra/ # 二开静态资源(js/css/images)
├── theme/
│ └── extra/ # 二开自定义模板
└── config/
└── extra.php # 二开自定义配置
正确的扩展姿势:继承优先
需要修改官方逻辑时,优先继承官方类,在子类里重写方法,而不是直接改原文件。
php
<?php
// ✅ 推荐:继承官方服务类,重写指定方法
namespace app\extra\service;
use app\common\service\OrderService as BaseOrderService;
class OrderService extends BaseOrderService
{
// 只重写需要扩展的方法
public function createOrder(array $data)
{
// 前置扩展逻辑
$this->beforeCreate($data);
// 调用父类原生逻辑
$result = parent::createOrder($data);
// 后置扩展逻辑
$this->afterCreate($result);
return $result;
}
}
这样官方升级后,只要父类方法签名不变,二开代码几乎不用改。
1.2 Java 版(SpringBoot 架构)目录约定
Java 版基于 SpringBoot,分层清晰。二开核心原则是:不修改官方包内的类,自定义包独立维护。
推荐包结构
com.likeshop
├── official/ # 官方代码包(升级时全量替换)
│ ├── controller/
│ ├── service/
│ ├── mapper/
│ └── entity/
└── custom/ # 二开自定义包(完全独立)
├── controller/ # 自定义接口
├── service/ # 自定义业务逻辑
├── listener/ # 事件监听器(核心扩展方式)
├── dto/
└── config/ # 自定义配置类
配置文件扩展
不要直接改 application.yml,新增 application-custom.yml,在主配置里引入:
yaml
spring:
profiles:
include: custom
升级时官方配置文件全量替换,自定义配置不受影响。
二、钩子/事件机制:非侵入式扩展的核心
二开最高级的做法,是完全不碰官方业务代码,只通过钩子/事件注入自己的逻辑。LikeShop 在核心交易链路上都预留了事件点,这是官方推荐的扩展方式。
2.1 为什么优先用钩子
- 零侵入:不修改官方源码,升级零冲突;
- 解耦:二开逻辑和主流程分离,互不影响;
- 可插拔:不需要时直接移除钩子,不影响主流程;
- 可维护:所有二开逻辑集中在钩子/监听器里,一目了然。
2.2 PHP 版钩子与行为的正确用法
ThinkPHP 的行为(Hook)机制是 PHP 版扩展的核心。官方在下单、支付、退款、注册等关键节点都预埋了钩子。
常用内置钩子点
order_paid:订单支付成功后order_cancel:订单取消后user_register:用户注册后goods_save:商品保存后refund_success:退款成功后
示例:支付成功后发放积分
不要去改 PayService.php,而是注册一个钩子监听:
php
<?php
// 1. 定义钩子行为
namespace app\common\hook;
class IntegralHook
{
/**
* 订单支付成功钩子
*/
public function orderPaid($params)
{
$order = $params['order'];
$userId = $order['user_id'];
$points = intval($order['pay_amount'] * 1);
// 二开逻辑:发放积分
integral_service()->add($userId, $points, '购物返积分');
}
}
php
<?php
// 2. 注册钩子(tags.php 中配置)
return [
'order_paid' => [
'app\\common\\hook\\IntegralHook',
// 可叠加多个钩子,互不影响
],
];
官方主流程完全不用动,二开逻辑通过钩子注入。后续官方升级支付流程,只要钩子点不变,二开代码就兼容。
2.3 Java 版事件监听扩展方式
Java 版基于 Spring 事件机制,官方核心节点都会发布对应事件,二开通过 @EventListener 监听即可。
示例:支付成功后触发分销结算
java
/**
* 二开监听器:支付成功处理分销佣金
* 完全不侵入官方支付代码
*/
@Component
public class DistributionListener {
@Autowired
private CommissionService commissionService;
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
Order order = event.getOrder();
// 二开业务逻辑
if (order.getDistributorId() != null) {
commissionService.calculateAndSettle(order);
}
}
}
官方的 OrderPaidEvent 是稳定的扩展点,只要事件对象不变,升级就不会断。
2.4 钩子使用的边界规范
- 不要在钩子中修改主流程状态:钩子只做后置扩展,不要回改订单、库存等核心数据,避免破坏主事务;
- 耗时操作异步处理:发短信、生成报表、统计分析等操作,扔到队列里异步执行,不要阻塞主流程;
- 异常隔离:钩子逻辑报错,不要影响主流程正常结束,做好异常捕获和日志;
- 一个钩子只做一件事:不同业务拆不同钩子,不要堆在一个钩子里面。
三、版本升级兼容方案:最小化升级冲突
官方版本迭代是必然的(修复漏洞、性能优化、新增功能),二开项目能不能平滑升级,取决于前期侵入程度。
3.1 四层扩展优先级
从优到劣依次是,越靠前升级成本越低:
- 钩子/事件扩展 → 完全无侵入,升级零成本;
- 继承重写 → 官方方法签名不变就兼容,成本极低;
- 新增文件 → 只加不减,升级不会覆盖,成本低;
- 修改官方源码 → 升级必冲突,成本最高,尽量避免。
原则:能用上一层,就不用下一层。 比如能通过钩子实现的,就不要继承;能通过继承实现的,就不要改官方源码。
3.2 数据库变更的兼容管理
数据库是升级里另一个重灾区。二开加字段、加表,不要直接改官方表结构。
规范做法:
- 二开字段加前缀 :比如
ext_xxx,和官方字段区分开; - 二开表加前缀 :比如
ls_ext_xxx,和官方表区分; - SQL 单独管理 :二开的建表、改表语句,单独放在
sql/extra/目录,和官方升级 SQL 分开; - 禁止删除官方字段:宁可冗余字段,也不要删官方字段,否则升级必报错。
3.3 完整升级步骤
按照这个流程升级,可以把冲突降到最低:
- 全量备份:代码、数据库、静态资源全部备份,留回滚方案;
- 官方代码替换:用新版本官方目录覆盖旧版官方目录,保留二开目录不动;
- 数据库升级:先执行官方升级 SQL,再执行二开增量 SQL;
- 冲突排查:对比变更日志,检查二开用到的钩子、继承的父类是否有变动;
- 回归测试:重点测二开功能和核心交易链路;
- 灰度发布:先在测试环境验证,再生产环境灰度切换。
3.4 冲突处理策略
- 如果是官方修复 bug 导致的方法变动,同步修改二开继承类对应方法;
- 如果是官方新增了同类功能,优先替换为官方能力,逐步下线二开实现;
- 如果是官方调整了钩子参数,同步修改钩子接收参数。
四、二开避坑与最佳实践
4.1 几条绝对红线
- 不要修改框架核心文件:ThinkPHP / SpringBoot 框架源码、官方核心类,动了后续永远没法升级;
- 不要在官方方法里硬编码业务:比如在下单方法里直接加一段分销逻辑,后续升级合并不想死;
- 不要删除官方字段和方法:哪怕暂时用不上,也留着,否则其他依赖的地方会炸;
- 不要改官方表结构的字段类型 :比如把
varchar改text,很容易导致官方 SQL 执行异常。
4.2 团队协作规范
- 二开代码统一命名空间、统一前缀,一眼就能区分官方和自定义;
- 每个二开功能写注释:「为什么改、改了哪、对应需求」,后人接盘不懵;
- 建立二开变更文档:记录每个版本改了哪些文件、加了哪些钩子、对应官方哪个基线版本。
4.3 静态资源与前端二开规范
- 二开前端组件单独目录,不要混入官方组件库;
- 主题样式用覆盖方式,不要直接改官方样式文件;
- 前端二开同样遵循:扩展组件 > 修改组件 > 重写页面。
结语
二开这件事,短期看「直接改最快」,长期看「按规范扩展最省」。
LikeShop 无论是 PHP 版还是 Java 版,在设计时都预留了钩子、继承、插件等扩展机制,本质就是希望二开团队不用硬改源码,能跟着官方版本一起迭代。顺着架构做扩展,而不是逆着架构硬改,前期多花一点时间做结构设计,后续每次升级都能省下几倍的时间。
对团队来说,代码规范从来不是束缚,是长期效率的保障。