摘要
CRMEB Pro 二开新增后台功能时,很多人第一反应是"加一个 Controller 方法"。但后台接口不是只要能访问就算完成,它还牵扯路由、权限名称、菜单权限、Controller、参数校验、Services、Dao、Model、缓存清理、统一返回格式。
漏掉任何一环,都会出现一些很烦的问题:
text
接口能访问,但后台按钮没权限
菜单里看不到新增接口
参数没校验,脏数据进库
列表能查,但弹窗选择器查不到
新增成功,但缓存不更新
返回格式不一致,前端不好处理
今天底层专题第三篇,就用 CRMEB Pro 真实模块把新增后台接口的完整链路讲清楚。示例主要参考商品热词、商品单位和权限菜单,因为这些模块代码短、链路完整,适合二开照着落地。
本文基于项目真实代码:
text
crmeb_pro/route/admin.php
crmeb_pro/app/controller/admin/AuthController.php
crmeb_pro/app/controller/admin/v1/product/StoreProductWords.php
crmeb_pro/app/controller/admin/v1/product/StoreProductUnit.php
crmeb_pro/app/controller/admin/v1/system/SystemMenus.php
crmeb_pro/app/services/product/product/StoreProductWordsServices.php
crmeb_pro/app/services/product/product/StoreProductUnitServices.php
crmeb_pro/app/dao/product/product/StoreProductWordsDao.php
crmeb_pro/app/model/product/product/StoreProductWords.php
crmeb_pro/app/validate/admin/product/StoreProductWordsValidate.php
crmeb_pro/app/validate/admin/product/StoreProductUnitValidate.php
1. 新增后台接口不是"写个方法"就完事
一个完整后台接口至少要经过这几层:
text
route/admin.php
Controller
Validate
Services
Dao
Model
权限菜单
前端 API
前端页面
后端最小闭环是:
text
1. 路由能找到接口
2. 权限系统能识别接口
3. Controller 能接收参数
4. Validate 能拦住非法输入
5. Services 能处理业务规则
6. Dao 能封装查询或写入
7. Model 能映射表和搜索器
8. 返回格式符合 success/fail
所以不要只盯 Controller。
2. 第一步:先看 route/admin.php 怎么写
商品热词路由在:
php
// crmeb_pro/route/admin.php
Route::group('product', function () {
// 热词列表
Route::get('words', 'v1.product.StoreProductWords/index')
->option(['real_name' => '热词列表']);
// 所有热词
Route::get('words/get_all', 'v1.product.StoreProductWords/getAllWords')
->option(['real_name' => '所有热词']);
// 热词详情
Route::get('words/:id', 'v1.product.StoreProductWords/info')
->option(['real_name' => '商品热词编辑']);
// 热词添加、编辑
Route::post('words/:id', 'v1.product.StoreProductWords/save')
->option(['real_name' => '商品热词编辑']);
// 删除商品热词
Route::delete('words/:id', 'v1.product.StoreProductWords/delete')
->option(['real_name' => '删除商品热词']);
// 商品热词修改状态
Route::put('words/set_show/:id/:is_show', 'v1.product.StoreProductWords/set_show')
->option(['real_name' => '商品热词修改状态']);
});
这段代码里有几个关键信息:
text
URL:words、words/:id、words/set_show/:id/:is_show
HTTP 方法:GET、POST、DELETE、PUT
Controller:v1.product.StoreProductWords
方法:index、getAllWords、info、save、delete、set_show
权限名称:real_name
后台权限菜单里很多接口展示和识别,都依赖 real_name。所以新增接口时不要只写路由地址,要补清楚 option(['real_name' => 'xxx'])。
3. Route::resource 适合标准 CRUD
商品单位用的是资源路由:
php
Route::get('get_all_unit', 'v1.product.StoreProductUnit/getAllUnit')
->option(['real_name' => '获取所有商品单位']);
Route::resource('unit', 'v1.product.StoreProductUnit')->option(['real_name' => [
'index' => '获取商品单位列表',
'read' => '获取商品单位详情',
'create' => '获取创建商品单位表单',
'save' => '保存商品单位',
'edit' => '获取修改商品单位表单',
'update' => '修改商品单位',
'delete' => '删除商品单位'
]]);
资源路由适合这类标准接口:
text
index:列表
create:创建表单
save:保存
read:详情
edit:编辑表单
update:更新
delete:删除
如果你的新功能也是标准 CRUD,用 Route::resource() 会更清晰;如果是批量操作、状态切换、导出、同步、审核、特殊动作,就单独写 Route::get/post/put/delete。
4. 权限菜单不是手填猜的,SystemMenus 能扫描路由
权限菜单模块在:
text
crmeb_pro/app/controller/admin/v1/system/SystemMenus.php
它有一个 ruleList() 方法,会读取路由文件并找出未添加的权限规则:
php
public function ruleList($type = 1)
{
$this->app = app();
$rule = $type == 1 ? 'adminapi/' : 'storeapi/';
$this->app->route->setTestMode(true);
$this->app->route->clear();
$path = $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR;
$files = is_dir($path) ? scandir($path) : [];
foreach ($files as $file) {
if (strpos($file, '.php')) {
include $path . $file;
}
}
$ruleList = $this->app->route->getRuleList();
$ruleNewList = [];
foreach ($ruleList as $item) {
if (Str::contains($item['rule'], $rule)) {
$ruleNewList[] = $item;
}
}
}
后面还会排除已添加到菜单权限里的接口:
php
$menuApiList = $this->services->getColumn([
'auth_type' => 2,
'is_del' => 0,
'type' => $type
], "concat(`api_url`,'_',lower(`methods`)) as rule");
这说明新增接口后,后台权限菜单不是靠人脑记忆。路由写对、real_name 补对,权限规则列表才容易识别出来。
5. Controller:只做参数、权限入口和统一返回
商品热词 Controller:
php
// crmeb_pro/app/controller/admin/v1/product/StoreProductWords.php
class StoreProductWords extends AuthController
{
#[Inject]
protected StoreProductWordsServices $services;
}
列表方法:
php
public function index(Request $request)
{
$where = $request->postMore([
['name', '']
]);
$where['type'] = 0;
$where['relation_id'] = 0;
$where['is_del'] = 0;
return $this->success($this->services->getWordsList($where));
}
保存方法:
php
public function save(Request $request, $id)
{
if (!$this->checkSupplierPlatformResourceManageAuth()) {
return $this->fail('暂无权限');
}
$data = $request->postMore([
['name', ''],
['color', ''],
['bg_color', ''],
['border_color', ''],
['icon', ''],
['sort', 0],
['is_search', 1],
['is_show', 1]
]);
validate(\app\validate\admin\product\StoreProductWordsValidate::class)
->scene('get')
->check(['name' => $data['name']]);
$res = $this->services->saveData((int)$id, $data);
return $this->success($res ? '保存成功' : '保存失败');
}
这里 Controller 做了什么?
text
1. 权限入口:checkSupplierPlatformResourceManageAuth
2. 参数读取:postMore
3. 参数校验:Validate
4. 调 Services:saveData
5. 统一返回:success/fail
它没有直接查库,没有拼 SQL,也没有把缓存逻辑写在 Controller。
6. Validate:不要只靠前端校验
商品热词验证器:
php
// crmeb_pro/app/validate/admin/product/StoreProductWordsValidate.php
class StoreProductWordsValidate extends Validate
{
protected $rule = [
'name' => ['require', 'length' => '1,15'],
];
protected $message = [
'name.require' => '请填写热词名称',
'name.length' => '热词名称长度超过限制',
];
protected $scene = [
'save' => ['name'],
];
}
商品单位验证器也类似:
php
class StoreProductUnitValidate extends Validate
{
protected $rule = [
'name' => ['require', 'length' => '1,15'],
];
protected $message = [
'name.require' => '请填写单位名称',
'name.length' => '单位名称长度超过限制',
];
protected $scene = [
'save' => ['name'],
];
}
新增接口时,如果有保存、修改、审核、状态变更,一定要考虑后端验证器。
常见规则包括:
text
require:必填
length:长度
number:数字
integer:整数
in:枚举值
array:数组
url:链接
mobile:手机号
注意:前端校验只是体验,后端 Validate 才是数据入口的底线。
7. Services:业务规则、重复校验、缓存更新都在这里
商品热词 Services:
php
// crmeb_pro/app/services/product/product/StoreProductWordsServices.php
class StoreProductWordsServices extends BaseServices
{
#[Inject]
protected StoreProductWordsDao $dao;
}
保存逻辑:
php
public function saveData(int $id, array $data)
{
$words = $this->dao->getOne([
'name' => $data['name'],
'is_del' => 0,
'type' => 0
]);
if ($id) {
if ($words && $words['id'] != $id) {
throw new AdminException('热词名称已经存在');
}
if ($this->dao->update($id, $data)) {
$data['id'] = $id;
$this->cacheUpdate($data);
return true;
} else {
return false;
}
} else {
if ($words) {
throw new AdminException('热词已经存在,请勿重复添加');
}
$data['add_time'] = time();
if ($res = $this->dao->save($data)) {
$id = $res->id;
$data['id'] = $id;
$this->cacheUpdate($data);
return true;
} else {
return false;
}
}
}
这里有几个业务点:
text
新增时不能重名
编辑时不能和其他记录重名
新增时补 add_time
保存后更新缓存
抛出 AdminException 给后台统一处理
这些都不该塞进 Controller。
状态切换也在 Services:
php
public function setShow(int $id, int $is_show)
{
$res = $this->dao->update($id, ['is_show' => $is_show]);
if (!$res) {
throw new AdminException('设置失败');
}
if (!$is_show) {
$this->cacheDelById($id);
return;
}
$branInfo = $this->dao->cacheInfoById($id);
if ($branInfo) {
$branInfo['is_show'] = 1;
} else {
$branInfo = $this->dao->get($id);
if (!$branInfo) {
return;
}
$branInfo = $branInfo->toArray();
}
$this->dao->cacheUpdate($branInfo);
}
它不只是 update is_show,还要处理缓存。
8. Dao:封装列表、分页、排序和模型绑定
商品热词 Dao:
php
// crmeb_pro/app/dao/product/product/StoreProductWordsDao.php
class StoreProductWordsDao extends BaseDao
{
protected function setModel(): string
{
return StoreProductWords::class;
}
public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0)
{
return $this->search($where)->field($field)
->when($page && $limit, function ($query) use ($page, $limit) {
$query->page($page, $limit);
})->order('sort desc,id desc')->select()->toArray();
}
}
新增接口如果只是基础 CRUD,很多时候 Dao 只需要:
text
setModel()
getList()
其它 save/update/delete/get/getOne/count 可以复用 BaseDao。
9. Model:表名和搜索器别忘了
商品热词 Model:
php
// crmeb_pro/app/model/product/product/StoreProductWords.php
class StoreProductWords extends BaseModel
{
use ModelTrait;
protected $pk = 'id';
protected $name = 'store_product_words';
}
搜索器:
php
public function searchNameAttr($query, $value)
{
if ($value !== '') {
$query->whereLike('id|name', '%' . $value . '%');
}
}
public function searchIsShowAttr($query, $value, $data)
{
if ($value != '') {
$query->where('is_show', $value ?: 0);
}
}
public function searchIsSearchAttr($query, $value, $data)
{
if ($value != '') {
$query->where('is_search', $value ?: 0);
}
}
public function searchIsDelAttr($query, $value)
{
if ($value !== '') {
$query->where('is_del', $value);
}
}
如果你新增的是一个表,一定要准备:
text
Model 的 $pk
Model 的 $name
常用搜索器
必要关联关系
必要获取器/修改器
否则 Dao 的 search($where) 只能查基础条件,复杂列表就会散到外面。
10. 权限菜单保存后要清缓存
权限菜单 Controller 保存时:
php
public function save()
{
$data = $this->request->getMore([
['menu_name', ''],
['controller', ''],
['module', 'admin'],
['action', ''],
['api_url', ''],
['methods', ''],
['unique_auth', ''],
['pid', 0],
['type', 1],
['sort', 0],
['auth_type', 0],
['access', 1],
['is_show', 1],
['is_show_path', 0],
]);
if (!$data['menu_name']) {
return $this->fail('请填写按钮名称');
}
if ($this->services->save($data)) {
CacheService::redisHandler('system_menus')->clear();
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
更新菜单时也会清:
php
if ($this->services->update($id, $data)) {
CacheService::redisHandler('system_menus')->clear();
return $this->success('修改成功');
}
这说明新增后台接口后,如果涉及菜单权限,不只是"加一条记录"就完事,还要注意菜单缓存。
否则你可能会遇到:
text
菜单已配置,但页面权限没生效
按钮权限改了,但用户刷新后还是旧权限
搜索菜单能搜到,左侧菜单不更新
11. 新增一个后台小功能,可以按这个文件清单走
假设要新增一个"商品推荐词"功能,可以按这个清单:
text
1. route/admin.php
增加 product/recommend_words 相关路由,写清 real_name。
2. app/controller/admin/v1/product/StoreProductRecommendWords.php
继承 AuthController,注入 Services,写 index/save/delete/set_show。
3. app/validate/admin/product/StoreProductRecommendWordsValidate.php
定义 name、sort、is_show 等规则和错误信息。
4. app/services/product/product/StoreProductRecommendWordsServices.php
处理列表、保存、重名校验、状态切换、缓存。
5. app/dao/product/product/StoreProductRecommendWordsDao.php
绑定 Model,封装 getList。
6. app/model/product/product/StoreProductRecommendWords.php
绑定表名,写 searchNameAttr、searchIsShowAttr、searchIsDelAttr。
7. SQL 文件
完整安装 SQL 和升级 SQL 同步新增表或字段,并写 COMMENT。
8. 后台权限菜单
通过未添加权限规则列表补到菜单或按钮权限里。
9. 前端 API 和页面
按现有 request/api 风格新增,不要另起请求封装。
如果只是给已有表加接口,可能不需要新增 Model/Dao;但也要先检查是否已有可复用的 Dao 和搜索器。
12. 新增接口的 Controller 模板
可以参考这种结构:
php
class XxxController extends AuthController
{
#[Inject]
protected XxxServices $services;
public function index(Request $request)
{
$where = $request->getMore([
['name', ''],
['is_show', ''],
]);
$where['is_del'] = 0;
return $this->success($this->services->getList($where));
}
public function save(Request $request, $id)
{
$data = $request->postMore([
['name', ''],
['sort', 0],
['is_show', 1],
]);
validate(XxxValidate::class)->scene('save')->check($data);
$this->services->saveData((int)$id, $data);
return $this->success('保存成功');
}
public function delete($id)
{
if (!$id) {
return $this->fail('参数错误');
}
$this->services->deleteData((int)$id);
return $this->success('删除成功');
}
}
注意模板里不要出现:
text
Db::name()
return json()
new Redis()
直接拼 SQL
直接改订单/库存/积分/余额字段
13. 新增接口前,先问自己 10 个问题
text
1. 这个接口是否需要登录态?
2. 是否需要平台/供应商/渠道隔离?
3. 是否需要菜单权限或按钮权限?
4. route/admin.php 的 real_name 写清楚了吗?
5. 参数是否有 Validate?
6. 是否有重复数据校验?
7. 是否会影响缓存?
8. 是否会影响订单、库存、余额、积分、优惠券、退款?
9. 是否已有同类 Services/Dao/Model 可复用?
10. 是否需要同步 SQL 安装文件和升级文件?
这 10 个问题能避免大部分"能跑但不稳"的二开接口。
14. 发布前怎么自查
新增接口写完后,建议按这个顺序查:
text
1. rg 路由地址,确认没有重复
2. rg Controller 方法,确认命名和路由一致
3. rg Validate 场景,确认 scene 名称写对
4. rg Services 方法,确认业务判断不在 Controller
5. rg Dao getList/search,确认查询集中封装
6. rg Model searchXxxAttr,确认筛选条件能生效
7. 打开后台权限规则列表,确认接口能被识别
8. 用管理端页面点一遍新增、编辑、删除、状态切换
9. 看返回格式是否都是 success/fail
10. 涉及 SQL 时确认完整安装 SQL 和升级 SQL 都维护
后端 PHP 文件至少执行语法检查:
bash
php -l crmeb_pro/app/controller/admin/v1/product/StoreProductRecommendWords.php
php -l crmeb_pro/app/services/product/product/StoreProductRecommendWordsServices.php
php -l crmeb_pro/app/dao/product/product/StoreProductRecommendWordsDao.php
php -l crmeb_pro/app/model/product/product/StoreProductRecommendWords.php
php -l crmeb_pro/app/validate/admin/product/StoreProductRecommendWordsValidate.php
15. 注意事项
- 后台接口必须继承现有后台 Controller 体系,不要另写一套返回结构。
- 路由必须写
real_name,否则权限规则和后台识别会很难维护。 - 标准 CRUD 优先用
Route::resource(),特殊动作再单独写路由。 - Controller 只处理参数、权限入口、Validate 和 success/fail。
- 保存、重名校验、缓存、状态切换放 Services。
- 查询、分页、排序、字段选择放 Dao。
- 表名、主键、搜索器放 Model。
- 新增保存接口必须配 Validate,不要只靠前端校验。
- 涉及菜单权限变更时要注意
system_menus缓存。 - 涉及订单、库存、余额、积分、优惠券、退款、分销时,要先看完整业务链路,不要照小模块 CRUD 硬套。
标签建议
text
CRMEB Pro
CRMEB二开
ThinkPHP
后台接口
路由
权限菜单
验证器
Services
Dao
源码解析