首页 > 学习中心 > 开发技术 > PHP

文档大纲

    ThinkPHP 企业级分层架构的完整链路学习

    从一次「新增视频功能」看 ThinkPHP 企业级分层架构的完整链路

    作者按 :这是一篇从真实业务代码出发的架构拆解文。我们用一个后台「新增短视频」接口作为样本,把 Controller → Validate → Service → Model 每一层的职责、边界和协作方式讲清楚。适合正在从"能跑就行"过渡到"知道为什么这么写"的 PHP 开发者。


    一、问题起点

    后台新增一条短视频,表单里有:标题、文件、分组、关联商品……

    接口看起来很简单,但真正落地时你要面对的是:

    • 参数格式怎么拦?
    • 主表和关联表怎么保证同时成功?
    • 多商户数据怎么隔离?
    • 缓存什么时候清?
    • Service 和 Model 的边界到底在哪?

    我们直接看一段生产级add 方法,把它从头到尾拆开。


    二、先看全貌:四层各司其职

    Controller       接参数、归一化、调校验、交棒
        ↓
    Validate         格式守门员(require / integer / 自定义规则)
        ↓
    Service          业务大脑(事务、多表、缓存、业务断言)
        ↓
    Model            纯数据层(白名单、时间戳、单表 CRUD)
        ↓
    MySQL            最终真相源
    

    每一层只说一句话:

    口头禅
    Controller "参数我洗好了,校验你上"
    Validate "长得不对,直接拦"
    Service "要么全成,要么全败"
    Model "我只管安全地把这一行写进这张表"

    三、Controller 做了什么(你可能忽略的预处理)

    在 Service 被调用之前,Controller 其实已经干完了三件关键事:

    // 1. 拆表单壳(前端 name="video[title]" 结构)
    $data = $this->postDataDefault('video');
    
    // 2. 关联字段单独剥离、归一化成纯 int 数组
    $goodsIds = $this->normalizeIds($data['goods_ids'] ?? []);
    unset($data['goods_ids']);
    
    // 3. 组装"给验证器看的完整数据"去校验
    $checkData = array_merge($data, ['goods_ids' => $goodsIds]);
    $this->validateOrFail($checkData, 'add', $this->videoValidate);
    

    这里有个 新手容易错的设计点

    $data(给主表用)和 $checkData(给校验用)是两份数据。

    • 校验阶段需要 goods_ids 参与规则判断
    • 入库阶段主表没有 goods_ids 这个字段

    校验用完整语义数据,入库用干净表字段 ——两份数据故意不一样,是高级写法,不是多此一举。


    四、Service::add 完整拆解

    public function add(array $data, array $goodsIds, int $wxappId): array
    {
        $data['wxapp_id'] = $wxappId;
        unset($data['goods_ids']);
    
        Db::startTrans();
        try {
            if (empty($data['video_title'])) {
                throw new Exception('视频标题不能为空');
            }
            if (empty($data['video_file_id'])) {
                throw new Exception('请上传视频文件');
            }
    
            $videoId = $this->model->insertVideo($data);
    
            if (!empty($goodsIds)) {
                VideoGoodsRel::bindGoods($videoId, $goodsIds, $wxappId);
            }
    
            $this->clearCache($wxappId);
            Db::commit();
            return [$videoId, ''];
        } catch (Throwable $e) {
            Db::rollback();
            return [false, $e->getMessage()];
        }
    }
    

    4.1 第一行:强制打上归属标记

    $data['wxapp_id'] = $wxappId;
    

    多租户系统的铁律: wxapp_id 永远由服务端从登录态取,绝不信任前端传值 。Controller 不管归属,Service 在写库前强制打标。

    4.2 再 unset 一次:防御性编程

    Controller 已经清过 goods_ids,Service 为什么再清?

    因为 Service 是公共业务入口 ,它不假设"上游一定干净":

    • 未来可能有定时脚本直接调 Service
    • 可能有其他 Controller 复用
    • 重构时上游忘了处理

    每一层只信自己手里的数据。 ​ 这是分层架构能长期维护的核心原因。

    4.3 开事务:草稿模式

    Db::startTrans();
    

    MySQL 进入"暂存模式"——后续所有写操作对外部连接 不可见 ,只有 commit 那一刻同时生效。

    4.4 事务内的业务断言

    if (empty($data['video_title'])) {
        throw new Exception('视频标题不能为空');
    }
    

    你可能会问:验证器不是校验过了吗?

    对,但职责不同:

    位置 校验的是什么
    Validate HTTP 请求参数的格式合法性
    Service 这里 业务上下文里"能不能干这件事"

    Validate 可以被绕过(内部调用、脚本、别人 new 了 Service 直接用)。Service 里的断言是 最后一道代码级保险 ,而且放在事务开头——还没写任何东西就发现不对,rollback 成本为零。

    4.5 insertGetId:数据什么时候真正落盘?

    $videoId = $this->model->insertVideo($data);
    

    Model 内部:

    public function insertVideo(array $data): int
    {
        $data['create_time'] = time();
        $data['update_time'] = time();
        return $this->allowField($this->allowField)->insertGetId($data);
    }
    

    三件事:

    1. 补时间戳(Model 管表约定字段,Service 不管)
    2. allowField 白名单过滤(多余字段直接丢弃,防脏写)
    3. insertGetId 执行 INSERT + 取 LAST_INSERT_ID()

    关键认知: 拿到 $videoId 的那一瞬间,数据已经写进表了 (事务内可见,外部不可见)。不是先插再查,是 MySQL 自增分配完 ID 后顺手把号码还给你。高并发下每个连接拿到的 LAST_INSERT_ID 互相隔离,不会串。

    4.6 写关联表:用刚拿到的 ID 串起来

    VideoGoodsRel::bindGoods($videoId, $goodsIds, $wxappId);
    

    内部逻辑(精简后):

    // 先清旧关联(新增时实际无旧数据,但方法复用于编辑)
    self::where('video_id', $videoId)->delete();
    
    $data = [];
    $sort = 100;
    foreach ($goodsIds as $goodsId) {
        $data[] = [
            'video_id'    => $videoId,
            'goods_id'    => $goodsId,
            'goods_sort'  => $sort,
            'wxapp_id'    => $wxappId,
            'create_time' => time()
        ];
        $sort += 10;
    }
    (new self)->insertAll($data);
    

    主表一条 + 关联表 N 条, $videoId 串联,在同一个事务里

    4.7 清缓存 → commit

    $this->clearCache($wxappId);
    Db::commit();
    

    commit 之前:数据在事务内存在,外部查不到。

    commit 之后: 主表 + 关联表同时对全库可见 ,缓存已清,下次读拿新数据。

    4.8 任何一步炸了 → 全撤

    catch (Throwable $e) {
        Db::rollback();
        return [false, $e->getMessage()];
    }
    
    炸在哪儿 结果
    标题为空(throw) 一条没写,回滚
    主表写入成功,关联表失败 主表也撤,无孤儿数据
    数据库连接异常 全撤

    不会出现"视频有、关联没有"的中间态。

    Service 返回统一约定 [成功时ID, ''][false, 错误信息],Controller 只管翻译成 HTTP 响应格式,不碰业务逻辑。


    五、一张表看全链路

    阶段 位置 动作 数据状态
    接参 Controller 拆壳、归一化 $data干净
    校验 Validate scene + rule 不过直接拦
    打标 Service 加 wxapp_id 归属明确
    开事务 Service startTrans 草稿模式
    断言 Service try 业务兜底 早失败
    写主表 Model INSERT + 白名单 + 时间戳 表里有数据(事务内)
    拿 ID insertGetId 返回 MySQL 自增回执 $videoId
    写关联 RelModel insertAll N条 两张表都有
    清缓存 Service del key 读不脏
    提交 commit 对外可见 ✅ 成功
    异常 rollback 全部撤销 ✅ 干净

    六、为什么这段代码值得你 Ctrl+C

    30 行,同时覆盖了:

    • ✅ 多租户数据隔离
    • ✅ 事务双写(主表 + 关联表)
    • ✅ 分层职责零越界
    • ✅ 防御性 unset
    • ✅ 事务内前置断言
    • ✅ 缓存一致性
    • ✅ 统一返回约定

    以后你写任何「主表 + 关联表」的新增接口(文章/商品/SKU/活动),把这个方法 改字段名就能用


    七、给新手的三个思考题

    1. 如果把 insertVideo 放在事务外面,关联表写失败了,主表数据回得去吗?
    2. 如果 Service 不 unset 直接把 $data 丢给 Model,Model 有 allowField 兜底,为什么 Service 还要清?
    3. 编辑接口里 bindGoodsdeleteinsertAll,而不是逐条 diff 更新——你觉得是懒还是对?

    最后一句 :分层架构的价值不在于"多写几个文件夹",而在于 每一层被单独调用时,都不会把系统搞脏 。你项目里这个 add 方法,就是这句话最好的注脚。

    推荐商品

    更多
    来团GEO-AI搜索优化系统 用AI打造企业品牌

    来团GEO-AI搜索优化系统 用AI打造企业品牌

    来团科技GEO优化&AI搜索优化系统,是通过大模型内容投喂+训练,将企业品牌及产品信息在多平台AI生成的答案中获取优先展现,更精准触达潜在目标客户,让企业品牌出现在AI搜索里。让客户一搜就看到你,实现一问就有你,一查就信你,一看就找你的营销效果。

    来团智慧商业小程序零代码开发平台 多行业适用

    来团智慧商业小程序零代码开发平台 多行业适用

    来团智慧商业小程序零代码开发平台,多行业适配。无需代码,拖拽式设计,轻松打造订货商城、会员制商城、分销商城及小程序官网。不仅能满足通用需求,还支持定制化,从页面布局到功能模块,随心定制,助您快速搭建专属商业小程序,抢占市场先机。

    微名通名片 VIP年卡会员 | SVIP永久会员

    微名通名片 VIP年卡会员 | SVIP永久会员

    来团科技微名通不止是电子名片,更是你的商业连接器。比起传统名片,它更像你的 “迷你商业工具”:信息多、好携带、能互动,还不浪费纸张。不管是跑业务、拓人脉,还是展示企业,一张「微名通」电子名片,就能帮你把商机揣在手机里。

    来团LTCRM客户管理系统 可独立部署

    来团LTCRM客户管理系统 可独立部署

    来团科技CRM客户管理系统,帮你把 “线索→成交→回款” 全流程管明白。这就是一套 “让销售省心、老板放心” 的客户管理工具,从获客到回款,帮你把生意攥在手里。

    大纲

    文档目录

      联系我们
      联系方式
      • 官方服务热线:17721141027
      • 邮箱:kf@ilaituan.com
      • QQ:20262336
      扫码添加客服
      微信