CRMEB Pro 新增后台接口全链路:路由、权限、验证器、返回格式一次讲清

摘要

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. 注意事项

  1. 后台接口必须继承现有后台 Controller 体系,不要另写一套返回结构。
  2. 路由必须写 real_name,否则权限规则和后台识别会很难维护。
  3. 标准 CRUD 优先用 Route::resource(),特殊动作再单独写路由。
  4. Controller 只处理参数、权限入口、Validate 和 success/fail。
  5. 保存、重名校验、缓存、状态切换放 Services。
  6. 查询、分页、排序、字段选择放 Dao。
  7. 表名、主键、搜索器放 Model。
  8. 新增保存接口必须配 Validate,不要只靠前端校验。
  9. 涉及菜单权限变更时要注意 system_menus 缓存。
  10. 涉及订单、库存、余额、积分、优惠券、退款、分销时,要先看完整业务链路,不要照小模块 CRUD 硬套。

标签建议

text 复制代码
CRMEB Pro
CRMEB二开
ThinkPHP
后台接口
路由
权限菜单
验证器
Services
Dao
源码解析
相关推荐
考虑考虑1 小时前
Java实现hmacsha1加密算法
java·后端·java ee
泉城老铁2 小时前
springboot+vue+ ffmpeg 实现视频的拉流播放
前端
程序边界2 小时前
lac_agent自愈链路上篇——crontab守护的那些坑与健康检查实战
后端
PedroQue992 小时前
uni-router v1.8.0新增冷启动守卫补执行
前端·uni-app
xiaok2 小时前
部署之后,本地浏览器还在读取旧缓存导致页面一直显示loading中
前端
用户059540174462 小时前
Redis缓存一致性踩坑实录:线上故障排查6小时,我用pytest+内存快照把它永久关进了笼子
前端·css
笨鸟飞不快2 小时前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
程序员威哥2 小时前
C#也能玩转YOLO:工业视觉原生推理方案,零Python依赖
后端
星栈2 小时前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:第一版先把列表和详情跑通
前端·rust·前端框架