- 后端
- 电商
- 前端
【免费下载链接】CRMEB
🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
导读:本文以 CRMEB 开源商城系统的 crmeb/app/dao/README.md 为骨架,系统讲解其数据访问对象(DAO)层的职责边界、抽象设计、通用能力矩阵与真实调用链。读者将理解 CRMEB 基于 ThinkPHP 6 的「控制器 → 服务层 → DAO → 模型」分层架构中,DAO 层如何屏蔽数据库差异、统一 CRUD 接口,并掌握在业务中新增一个 DAO、复用搜索器与高精度金额操作的完整实战路径。
CRMEB 是使用 ThinkPHP 6 框架开发的商用商城系统,包含小程序、H5、公众号、PC 商城与 App 等端。在它的app目录中,crmeb/app/dao承担了全部数据持久层访问职责。本文依据该目录的 README.md 展开,并结合 BaseDao.php、UserDao.php 等真实源码逐层拆解。
一、DAO 层是什么:CRMEB 数据访问层的职责定位
根据 crmeb/app/dao/README.md,DAO(Data Access Object)层位于控制器(Controller)与模型(Model)之间,主要职责包括:
- 实现数据持久层访问,对数据库进行 CURD(增删改查)操作;
- 依赖数据库连接,实现对表的基本增删改查功能;
- 封装数据库操作原语(查询、插入、更新等),简化开发难度;
- 与数据库解耦,提供统一接口,便于扩展和维护。
具体到代码组织方式,README 明确了三条规则:
crmeb/app/dao目录下每个文件对应一个数据表或业务模块(如user/UserDao.php对应user用户表,order/StoreOrderDao.php对应订单模块);- 文件内封装了对该表的基本操作方法,如查找、插入、更新、删除、计数、求和等;
- 方法的参数与返回值类型为模型对象(Model),实现数据与业务的解耦,并封装了丰富的查询条件方便上层调用;
- 底层利用ThinkPHP 的 ActiveRecord 机制实现数据操作。
从目录结构看,crmeb/app/dao下按业务域划分了activity、agent、article、diy、order、other、product、service、shipping、sms、system、user、wechat等子目录,对应商城系统的营销活动、分销代理、订单履约、商品、服务、物流、系统配置、用户、微信等核心业务模块。
DAO 层带来的工程收益
README 总结了使用 DAO 层的四大好处,这些收益在 CRMEB 这种多端、多业务模块的大型商城中体现得尤为明显:
- 提供对象化的数据操作接口:上层业务以方法调用替代手写 SQL,代码可读性与复用性大幅提升;
- 屏蔽数据库差异,提高移植性:CRMEB 通过配置文件统一管理数据库连接,业务代码不感知底层是 MySQL 还是其他数据库;
- 方便测试和扩展:DAO 方法职责单一、入参出参类型明确,便于单元测试与后续功能扩展;
- 实现业务和数据层的分离:服务层(Services)只关心业务规则,数据访问细节全部收敛到 DAO,这也是 CRMEB 前后端分离、便于二次开发的关键设计之一。
二、分层架构中的位置:控制器 → 服务层 → DAO → 模型
在 CRMEB 中,一次典型的数据请求遵循如下调用链:
Controller(控制器)→ Services(服务层)→ DAO(数据访问层)→ Model(模型)→ 数据库以用户模块为例:
- 控制器接收并校验 HTTP 请求参数;
- UserServices.php 通过构造函数注入
UserDao(见 UserServices.php,public function __construct(UserDao $dao) { $this->dao = $dao; }); UserServices调用$this->dao->getList(...)、$this->dao->get($uid, $field)等 DAO 方法获取数据;- DAO 内部通过
getModel()实例化对应的 Model,再由 ThinkPHP 的 ActiveRecord 执行 SQL。
服务层基类 BaseServices.php 中声明了protected $dao属性,并通过 PHPDoc@method标注了大量可直接透传到 DAO 的方法(get、getOne、batchUpdate、sum、update、be、value、count、getCount、getColumn、delete、save、saveAll、selectList、bcInc、bcDec、decStockIncSales、incStockDecSales等),服务层因此可以把 DAO 当作自己的“数据能力底座”直接使用。
这条链路清晰印证了 README 的结论——DAO 层负责项目底层数据操作,其他业务要操作数据库时统一调用它。
三、BaseDao 抽象基类:DAO 层的统一能力底座
crmeb/app/dao下所有 DAO 类都继承自 BaseDao.php(abstract class BaseDao)。它做了三件核心的事:统一模型来源、统一 CRUD 原语、统一查询搜索机制。
3.1 统一模型来源:setModel()
BaseDao 定义了唯一一个必须由子类实现的抽象方法:
abstract protected function setModel(): string;每个业务 DAO 只需返回自己对应的模型类名即可绑定数据表。以 UserDao.php 为例:
protected function setModel(): string { return User::class; }其余所有继承的方法都通过getModel()获得模型实例:
protected function getModel() { return app()->make($this->setModel()); }即用容器解析模型类。这样新增一个 DAO 的成本被压缩到极小:继承 BaseDao、实现 setModel、按需补充业务方法即可。此外 BaseDao 还提供了setJoinModel()钩子用于 join 联表查询时的模型设置。
3.2 通用 CRUD 能力矩阵
BaseDao 内置了一套覆盖面很全的通用方法,全部以array $where为查询条件入口,返回值以模型对象或think\Collection为主:
| 方法 | 作用 | 关键参数/默认值 |
|---|---|---|
count($where, $search = true) | 统计符合条件的记录数 | $search=true走搜索器机制 |
selectList(...) | 条件列表查询 | $field='*'、$page=0、$limit=0、$order=''、$with=[]、$search=false |
selectModel(...) | 返回可继续链式操作的模型 | 同上,返回BaseModel |
getCount($where) | 不带搜索器的简单计数 | 直接用where($where) |
getDistinctCount($where, $field) | 去重计数 | 生成COUNT(distinct(field)) |
get($id, $field, $with) | 按主键或条件取单条 | $id支持数组条件 |
be($map, $field) | 判断记录是否存在 | 返回 bool |
getOne($where, $field, $with) | 按条件取单条 | 内部转调get() |
value($where, $field) | 取单个字段值 | 默认取主键 |
getColumn($where, $field, $key) | 取字段值数组 | $key作为数组键名 |
delete($id, $key) | 删除 | 支持数组条件与指定键 |
destroy($id, $force) | 软删除/真删除 | 透传模型destroy |
update($id, $data, $key) | 更新 | 支持数组条件 |
batchUpdate($ids, $data, $key) | 批量更新 | 基于whereIn |
save($data) | 插入单条 | 调用模型create |
saveAll($data) | 批量插入 | 调用模型saveAll |
sum($where, $field, $search) | 字段求和 | 支持搜索器 |
getMax/getMin | 取最大/最小值 | 空条件时作用于全表 |
getFieldValue(...) | 取关联字段值 | 透传模型能力 |
3.3 搜索器机制:search() 与 withSearch
BaseDao 的search()是整个查询体系的核心入口,它与 ThinkPHP 的**模型搜索器(Search Scope)**深度结合:
public function search(array $where = [], bool $search = true) { if ($where) { return $this->withSearchSelect($where, $search); } else { return $this->getModel(); } }其实现逻辑(见getSearchData()与withSearchSelect(),BaseDao.php)为:
- 通过
ReflectionClass反射出当前模型类的所有方法; - 对
$where中的每个键,拼接出'search' . Str::studly($key) . 'Attr'方法名; - 若模型存在该方法,则将该键归入搜索器集合
$with,交给 ThinkPHP 的withSearch()处理; - 不存在的键则转为普通
where条件[字段, '=', 值],且三元数组条件会原样保留(支持自定义操作符如BETWEEN); - 通过
filterWhere()过滤掉表中不存在的字段,避免 SQL 报错(BaseDao.php)。
这正是 README 所说“提供丰富的查询条件以方便调用”的实现原理。以用户模型 User.php 为例,它定义了searchUidAttr、searchAccountAttr、searchPwdAttr、searchUidsAttr、searchLikeAttr、searchPhoneAttr、searchGroupIdAttr、searchIsPromoterAttr、searchStatusAttr、searchLevelAttr、searchSpreadUidAttr、searchUserTypeAttr、searchPayCountAttr、searchNicknameAttr等数十个搜索器(见 User.php)。其中searchUidAttr自动区分单值与数组:
public function searchUidAttr($query, $value) { if (is_array($value)) $query->whereIn('uid', $value); else $query->where('uid', $value); }这意味着业务层只要往$where里塞['uid' => [1,2,3]],DAO 就会自动生成IN查询,调用方无需关心 SQL 细节。
3.4 业务级复合操作:库存、金额与并发安全
除了基础 CRUD,BaseDao 还沉淀了商城业务高频使用的一组“复合操作”:
- 高精度金额增减:
bc()/bcInc()/bcDec()基于 PHP 的bcmath扩展实现金额的高精度加减,支持指定精度$acc(默认 2 位小数),减法时若余额不足返回 false,避免浮点误差与透支(BaseDao.php); - 减库存加销量 / 加库存减销量:
decStockIncSales()与incStockDecSales()在商品下单、退款场景中同步维护stock(库存)与sales(销量)两个字段,并支持秒杀等活动的quota(限购额度)联动;其中incStockDecSales会先读取当前销量,回滚时销量不会扣成负数(BaseDao.php); getFieldValue:透传模型的关联字段取值能力,用于组合查询。
这些复合操作把“多字段事务性更新”的细节封闭在 DAO 内,服务层无需重复实现。
四、业务 DAO 实战:以 UserDao 为例的调用示范
UserDao.php 是 CRMEB 中最典型的业务 DAO,它在继承 BaseDao 通用能力的基础上,针对用户模块补充了大量业务方法。下面从分页列表、关联预载入、聚合统计三个角度拆解其写法。
4.1 带搜索器的分页列表
public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0): array { return $this->search($where)->field($field)->with(['label']) ->when($page && $limit, function ($query) use ($page, $limit) { $query->page($page, $limit); })->select()->toArray(); }这里search($where)先应用模型搜索器,再通过with(['label'])预载入关联(用户标签),when($page && $limit, ...)实现条件分页,最终以数组返回。对应服务层 UserServices.php 中的getUserList():它先从getPageValue()取分页参数,再调用$this->dao->getList($where, $field, $page, $limit)与$this->getCount($where),组装出compact('list', 'count')返回给控制器。
4.2 关联聚合与条件预载入
分销场景的getAgentUserList()展示了 DAO 的“高级用法”——用闭包定义关联子查询做聚合统计:
return $this->search($where)->field($field)->with([ 'extract' => function ($query) { $query->field('sum(extract_price) as extract_count_price,count(id) as extract_count_num,uid') ->where('status', '1')->group('uid'); }, 'order' => function ($query) { $query->field('sum(pay_price) as order_price,count(id) as order_count,uid') ->where('paid', 1)->where('refund_status', 0)->whereIn('pid', [-1, 0])->group('uid'); }, 'bill' => function ($query) { $query->field('sum(number) as brokerage_money,uid') ->where('category', 'now_money')->where('type', 'brokerage') ->where('status', 1)->where('pm', 1)->group('uid'); }, 'spreadCount' => function ($query) { $query->field('count(*) as spread_count,spread_uid')->group('spread_uid'); }, 'spreadUser' => function ($query) { $query->field('uid,phone,nickname'); }, 'agentLevel' => function ($query) { $query->field('id,name'); } ])->when($page && $limit, function ($query) use ($page, $limit) { $query->page($page, $limit); })->order('uid desc')->select()->toArray();一次查询即可同时拿到该用户的提现统计、已支付订单统计、分销佣金总额、下级人数、上级信息、代理等级,是 DAO 层支撑分销报表类页面的典型实现。
4.3 原生聚合与时间窗口统计
getTrendData()展示了在 DAO 内直接使用 ThinkPHP 时间查询与 SQL 聚合函数:
public function getTrendData($time, $type, $timeType) { return $this->getModel() ->when($type != '', function ($query) use ($type) { $query->where('user_type', $type); }) ->where(function ($query) use ($time) { if ($time[0] == $time[1]) { $query->whereDay('add_time', $time[0]); } else { $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); $query->whereTime('add_time', 'between', $time); } }) ->field("FROM_UNIXTIME(add_time,'$timeType') as days,count(uid) as num") ->group('days')->select()->toArray(); }这类方法把时间粒度格式化($timeType如%Y-%m-%d)、日期区间边界处理都封装在 DAO 中,服务层只需传入起止时间与统计维度即可。
五、新增一个业务 DAO 的落地路径
结合 CRMEB 现有代码的约定,在二次开发中新增数据访问能力只需四步:
- 确定业务域与模型:在 crmeb/app/dao 对应子目录(没有则新建)创建
XxxDao.php,命名遵循「业务名 + Dao」,一个 DAO 对应一个数据表或业务模块; - 继承 BaseDao 并实现 setModel():返回对应模型类名,如
return Xxx::class;,即可获得 BaseDao 的全部通用能力; - 在模型上按需声明搜索器:在 crmeb/app/model 对应模型类中新增
searchXxxAttr($query, $value)方法,DAO 的search()即可自动识别并应用; - 在 DAO 中补充业务方法:将组合查询、聚合统计、库存/金额联动等操作封装为语义化方法,供服务层调用。
调用侧保持与服务层一致的惯例——在XxxServices的构造函数中注入XxxDao:
public function __construct(XxxDao $dao) { $this->dao = $dao; }常见注意事项
- 软删除与物理删除:需要保留数据时使用
destroy($id, $force = false)走模型软删除;delete()为直接删除,务必确认业务语义; - 金额操作必须走 bcmath:涉及余额、佣金、退款等场景请使用
bcInc/bcDec而非浮点运算,CRMEB 中用户余额操作(如 UserDao.php 的incPayCount、incField)均以inc/dec数据库原子自增实现,避免并发覆盖; - where 键名与模型字段一致:
filterWhere()会过滤表中不存在的条件字段,非表字段的自定义条件需借助模型搜索器承载; - 分页参数统一由服务层注入:参考 BaseServices.php 的
getPageValue(),它从请求参数读取page/limit并受database.page.limitMax等配置约束。
六、与模型层的关系:ActiveRecord 的最后一公里
BaseDao 的底层数据操作最终落在模型对象上。CRMEB 的模型统一继承自 BaseModel.php(class BaseModel extends Model,即 ThinkPHP 6 的think\Model),并通过ModelTrait混入额外的查询与业务辅助能力:
namespace crmeb\basic; use crmeb\traits\ModelTrait; use think\db\Query; use think\Model; class BaseModel extends Model { }这使得 DAO 返回的模型对象既是“数据行”又是“查询构建器”:既能直接->save()持久化修改,也能继续->with()、->page()、->group()等链式操作。DAO 方法(如selectModel())刻意返回BaseModel而不是数组,正是为了让上层有最大的链式自由度;而面向控制器输出时,再通过->toArray()或->select()->toArray()转成数组。
七、小结
回到 crmeb/app/dao/README.md 的核心论断:CRMEB 的 dao 目录负责项目底层数据操作,其他业务统一通过它访问数据库。通过本文可以确认,这一设计并非一句口号,而是由BaseDao的抽象方法 + 通用 CRUD 矩阵 + 搜索器反射机制 + 业务复合操作 + 各业务 DAO 的分层继承共同落实的:
- 面向数据表:一个 DAO 对应一张表/一个业务模块,
setModel()统一模型来源; - 面向业务:以模型对象为出入参,提供丰富条件查询与关联聚合,屏蔽 SQL 细节;
- 面向工程:屏蔽数据库差异、降低开发难度、便于测试与扩展,实现业务与数据层的彻底分离。
对于准备基于 CRMEB 做二次开发的开发者而言,理解 DAO 层这套「继承 BaseDao → 实现 setModel → 按需添加搜索器与业务方法」的模式,就等于拿到了通往 CRMEB 数据层的钥匙。
- 后端
- 电商
- 前端
【免费下载链接】CRMEB
🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
相关推荐
Java 数据访问对象(DAO)模式深度实战:在 java-design-patterns 项目中实现数据库抽象层
Java 数据访问对象(DAO)模式深度实战:在 java design patterns 项目中实现数据库抽象层 导读 本文围绕 java design pa
示例工程教程Actual-Server数据访问对象:数据持久化抽象层
Actual Server数据访问对象:数据持久化抽象层 痛点场景:本地优先应用的数据持久化挑战 你是否曾经在开发本地优先(Local First)应用时,面临
后端金融科技数据同步Actual-Server数据访问层:数据库操作抽象
Actual Server数据访问层:数据库操作抽象 痛点:传统数据库操作的复杂性 你是否曾经在Node.js项目中遇到过这样的困扰?每次编写数据库操作代码时,
后端金融科技数据同步
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考