插件搜索功能开发指南
1. 搜索模型
搜索模型用于处理复杂的搜索条件和数据过滤,通常继承自对应的业务模型。
1.1 搜索模型创建
<?php
namespace addons\diandi_mall\models\search;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use addons\diandi_mall\models\goods\Goods;
/**
* 商品搜索模型
*/
class GoodsSearch extends Goods
{
public $keyword; // 关键词搜索
public $category_id; // 分类ID
public $min_price; // 最低价格
public $max_price; // 最高价格
public $start_date; // 开始日期
public $end_date; // 结束日期
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'category_id', 'stock', 'status'], 'integer'],
[['keyword', 'start_date', 'end_date'], 'safe'],
[['min_price', 'max_price', 'price'], 'number'],
];
}
/**
* {@inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = Goods::find();
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 10,
],
'sort' => [
'defaultOrder' => [
'created_at' => SORT_DESC,
],
],
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// 关键词搜索
if (!empty($this->keyword)) {
$query->andFilterWhere([
'or',
['like', 'name', $this->keyword],
['like', 'description', $this->keyword],
['like', 'sku', $this->keyword]
]);
}
// 分类过滤
if (!empty($this->category_id)) {
$query->andFilterWhere(['category_id' => $this->category_id]);
}
// 价格范围过滤
if (!empty($this->min_price)) {
$query->andFilterWhere(['>=', 'price', $this->min_price]);
}
if (!empty($this->max_price)) {
$query->andFilterWhere(['<=', 'price', $this->max_price]);
}
// 日期范围过滤
if (!empty($this->start_date)) {
$query->andFilterWhere(['>=', 'created_at', strtotime($this->start_date)]);
}
if (!empty($this->end_date)) {
$query->andFilterWhere(['<=', 'created_at', strtotime($this->end_date . ' 23:59:59')]);
}
// 状态过滤
$query->andFilterWhere([
'status' => $this->status,
]);
return $dataProvider;
}
}
2. 搜索控制器
搜索控制器用于处理前端的搜索请求,调用搜索模型执行搜索逻辑。
2.1 搜索控制器创建
<?php
namespace addons\diandi_mall\api;
use common\controllers\addons\ApiController;
use addons\diandi_mall\models\search\GoodsSearch;
use Yii;
/**
* 商品搜索控制器
*/
class SearchController extends ApiController
{
/**
* 商品搜索
* @return array
*/
public function actionGoods()
{
$searchModel = new GoodsSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->success([
'list' => $dataProvider->getModels(),
'total' => $dataProvider->getTotalCount(),
'page' => Yii::$app->request->get('page', 1),
'pageSize' => $dataProvider->pagination->pageSize,
]);
}
/**
* 高级搜索
* @return array
*/
public function actionAdvanced()
{
$params = Yii::$app->request->queryParams;
// 构建搜索条件
$searchParams = [
'keyword' => $params['keyword'] ?? '',
'category_id' => $params['category_id'] ?? null,
'min_price' => $params['min_price'] ?? null,
'max_price' => $params['max_price'] ?? null,
'status' => $params['status'] ?? null,
'start_date' => $params['start_date'] ?? null,
'end_date' => $params['end_date'] ?? null,
];
$searchModel = new GoodsSearch();
$dataProvider = $searchModel->search($searchParams);
return $this->success([
'list' => $dataProvider->getModels(),
'total' => $dataProvider->getTotalCount(),
'page' => $params['page'] ?? 1,
'pageSize' => $dataProvider->pagination->pageSize,
'params' => $searchParams,
]);
}
}
3. 搜索配置
3.1 API路由配置
在 config/api.php 中配置搜索相关的路由:
<?php
return [
'search' => [
'class' => 'yii\rest\UrlRule',
'controller' => ['diandi_mall/search'],
'pluralize' => false,
'extraPatterns' => [
'GET goods' => 'goods',
'GET advanced' => 'advanced',
],
],
];
3.2 后台路由配置
在 config/admin.php 中配置后台搜索相关的路由:
<?php
return [
'search' => [
'class' => 'yii\rest\UrlRule',
'controller' => ['diandi_mall/admin-search'],
'pluralize' => false,
'extraPatterns' => [
'GET goods' => 'goods',
'GET order' => 'order',
],
],
];
4. 搜索功能实现
4.1 关键词搜索
// 关键词搜索实现
if (!empty($this->keyword)) {
$query->andFilterWhere([
'or',
['like', 'name', $this->keyword],
['like', 'description', $this->keyword],
['like', 'sku', $this->keyword],
['like', 'barcode', $this->keyword]
]);
}
4.2 分类搜索
// 分类搜索实现
if (!empty($this->category_id)) {
// 单个分类
if (is_numeric($this->category_id)) {
$query->andFilterWhere(['category_id' => $this->category_id]);
}
// 多个分类
elseif (is_array($this->category_id)) {
$query->andFilterWhere(['in', 'category_id', $this->category_id]);
}
}
4.3 价格范围搜索
// 价格范围搜索实现
if (!empty($this->min_price)) {
$query->andFilterWhere(['>=', 'price', $this->min_price]);
}
if (!empty($this->max_price)) {
$query->andFilterWhere(['<=', 'price', $this->max_price]);
}
4.4 日期范围搜索
// 日期范围搜索实现
if (!empty($this->start_date)) {
$query->andFilterWhere(['>=', 'created_at', strtotime($this->start_date)]);
}
if (!empty($this->end_date)) {
$query->andFilterWhere(['<=', 'created_at', strtotime($this->end_date . ' 23:59:59')]);
}
4.5 状态搜索
// 状态搜索实现
if ($this->status !== null) {
$query->andFilterWhere(['status' => $this->status]);
}
5. 高级搜索技巧
5.1 关联查询搜索
// 关联查询搜索
$query->joinWith(['category', 'brand']);
// 搜索分类名称
if (!empty($this->category_name)) {
$query->andFilterWhere(['like', 'category.name', $this->category_name]);
}
// 搜索品牌名称
if (!empty($this->brand_name)) {
$query->andFilterWhere(['like', 'brand.name', $this->brand_name]);
}
5.2 自定义排序
// 自定义排序
if (!empty($params['sort'])) {
$sortField = $params['sort'];
$sortOrder = !empty($params['order']) ? $params['order'] : SORT_ASC;
switch ($sortField) {
case 'price':
$query->orderBy(['price' => $sortOrder]);
break;
case 'sales':
$query->orderBy(['sales' => $sortOrder]);
break;
case 'rating':
$query->orderBy(['rating' => $sortOrder]);
break;
default:
$query->orderBy(['created_at' => SORT_DESC]);
}
}
5.3 分页和限制
// 自定义分页
$pageSize = !empty($params['pageSize']) ? $params['pageSize'] : 10;
$page = !empty($params['page']) ? $params['page'] : 1;
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => $pageSize,
'page' => $page - 1,
],
]);
// 限制结果数量
if (!empty($params['limit'])) {
$query->limit($params['limit']);
}
6. 前端搜索实现
6.1 搜索表单
<form id="search-form">
<div class="form-group">
<label for="keyword">关键词</label>
<input type="text" id="keyword" name="keyword" class="form-control" placeholder="请输入关键词">
</div>
<div class="form-group">
<label for="category_id">分类</label>
<select id="category_id" name="category_id" class="form-control">
<option value="">全部分类</option>
<!-- 分类选项 -->
</select>
</div>
<div class="form-group">
<label>价格范围</label>
<div class="row">
<div class="col-md-6">
<input type="number" name="min_price" class="form-control" placeholder="最低价格">
</div>
<div class="col-md-6">
<input type="number" name="max_price" class="form-control" placeholder="最高价格">
</div>
</div>
</div>
<div class="form-group">
<label for="status">状态</label>
<select id="status" name="status" class="form-control">
<option value="">全部状态</option>
<option value="1">激活</option>
<option value="0">inactive</option>
</select>
</div>
<button type="button" id="search-btn" class="btn btn-primary">搜索</button>
<button type="button" id="reset-btn" class="btn btn-default">重置</button>
</form>
6.2 AJAX搜索请求
// 搜索按钮点击事件
$('#search-btn').on('click', function() {
var params = {
keyword: $('#keyword').val(),
category_id: $('#category_id').val(),
min_price: $('#min_price').val(),
max_price: $('#max_price').val(),
status: $('#status').val(),
page: 1
};
// 发送搜索请求
$.ajax({
url: '/api/addons/diandi_mall/search/goods',
type: 'GET',
data: params,
success: function(response) {
if (response.code === 200) {
// 处理搜索结果
renderResult(response.data);
} else {
alert('搜索失败: ' + response.message);
}
},
error: function() {
alert('网络错误,请重试');
}
});
});
// 渲染搜索结果
function renderResult(data) {
var html = '';
if (data.list.length > 0) {
data.list.forEach(function(item) {
html += '<div class="result-item">';
html += '<h4>' + item.name + '</h4>';
html += '<p>价格: ¥' + item.price + '</p>';
html += '<p>库存: ' + item.stock + '</p>';
html += '<p>状态: ' + (item.status === 1 ? '激活' : 'inactive') + '</p>';
html += '</div>';
});
} else {
html = '<div class="no-result">没有找到匹配的结果</div>';
}
$('#result-container').html(html);
// 渲染分页
renderPagination(data.page, Math.ceil(data.total / data.pageSize));
}
// 渲染分页
function renderPagination(currentPage, totalPages) {
var html = '';
for (var i = 1; i <= totalPages; i++) {
html += '<button class="page-btn" data-page="' + i + '">' + i + '</button>';
}
$('#pagination').html(html);
// 分页点击事件
$('.page-btn').on('click', function() {
var page = $(this).data('page');
// 重新发送搜索请求
// ...
});
}
7. 性能优化
7.1 索引优化
为常用的搜索字段添加数据库索引:
-- 为商品表添加索引
CREATE INDEX idx_goods_name ON diandi_mall_goods(name);
CREATE INDEX idx_goods_price ON diandi_mall_goods(price);
CREATE INDEX idx_goods_status ON diandi_mall_goods(status);
CREATE INDEX idx_goods_category_id ON diandi_mall_goods(category_id);
CREATE INDEX idx_goods_created_at ON diandi_mall_goods(created_at);
7.2 查询优化
- **避免使用 SELECT ***:只查询需要的字段
- 使用 WHERE 条件:减少返回的数据量
- 合理使用 JOIN:避免不必要的关联查询
- 使用 LIMIT:限制返回的记录数
- 缓存搜索结果:对于频繁的搜索请求使用缓存
7.3 缓存策略
// 使用缓存优化搜索
use yii\caching\FileCache;
public function actionGoods() {
$params = Yii::$app->request->queryParams;
$cacheKey = 'search_goods_' . md5(http_build_query($params));
$cache = new FileCache();
$data = $cache->get($cacheKey);
if ($data === false) {
$searchModel = new GoodsSearch();
$dataProvider = $searchModel->search($params);
$data = [
'list' => $dataProvider->getModels(),
'total' => $dataProvider->getTotalCount(),
'page' => $params['page'] ?? 1,
'pageSize' => $dataProvider->pagination->pageSize,
];
// 缓存搜索结果,有效期5分钟
$cache->set($cacheKey, $data, 300);
}
return $this->success($data);
}
8. 常见问题
8.1 搜索性能问题
可能原因:
- 数据库索引缺失
- 查询条件过于复杂
- 数据量过大
- 缓存策略不当
解决方案:
- 添加适当的数据库索引
- 优化查询条件,减少不必要的过滤
- 实现分页和限制,避免一次性返回过多数据
- 使用缓存,减少数据库查询次数
8.2 搜索结果不准确
可能原因:
- 搜索条件配置错误
- 数据模型关联问题
- 数据一致性问题
解决方案:
- 检查搜索条件的配置是否正确
- 确认数据模型的关联关系是否正确
- 确保数据的一致性和完整性
8.3 搜索功能无响应
可能原因:
- API 路由配置错误
- 控制器方法不存在
- 参数传递错误
- 服务器错误
解决方案:
- 检查 API 路由配置是否正确
- 确认控制器和方法是否存在
- 验证参数传递是否正确
- 查看服务器日志,排查错误原因
9. 最佳实践
9.1 搜索功能设计建议
- 简洁易用:搜索界面应简洁明了,避免过多的搜索条件
- 即时反馈:提供即时的搜索结果反馈,如自动补全、搜索建议等
- 错误处理:优雅处理搜索错误,提供友好的错误提示
- 性能优先:优化搜索性能,确保搜索速度快
- 用户体验:提供清晰的搜索结果展示和分页导航
9.2 代码规范
- 命名规范:搜索模型和控制器使用 Search 后缀
- 注释规范:为搜索逻辑添加详细的注释
- 错误处理:使用 try-catch 捕获和处理异常
- 日志记录:记录搜索请求和错误信息
9.3 安全考虑
- 参数验证:对所有搜索参数进行验证,防止 SQL 注入
- 权限控制:对敏感数据的搜索添加权限控制
- 频率限制:防止恶意的搜索请求攻击
- 数据过滤:过滤掉敏感信息,保护用户隐私
10. 示例代码
10.1 完整的搜索控制器示例
<?php
namespace addons\diandi_mall\api;
use common\controllers\addons\ApiController;
use addons\diandi_mall\models\search\GoodsSearch;
use addons\diandi_mall\models\search\OrderSearch;
use Yii;
use yii\caching\FileCache;
/**
* 搜索控制器
*/
class SearchController extends ApiController
{
/**
* 商品搜索
* @return array
*/
public function actionGoods()
{
$params = Yii::$app->request->queryParams;
$cacheKey = 'search_goods_' . md5(http_build_query($params));
$cache = new FileCache();
$data = $cache->get($cacheKey);
if ($data === false) {
$searchModel = new GoodsSearch();
$dataProvider = $searchModel->search($params);
$data = [
'list' => $dataProvider->getModels(),
'total' => $dataProvider->getTotalCount(),
'page' => $params['page'] ?? 1,
'pageSize' => $dataProvider->pagination->pageSize,
];
// 缓存搜索结果,有效期5分钟
$cache->set($cacheKey, $data, 300);
}
return $this->success($data);
}
/**
* 订单搜索
* @return array
*/
public function actionOrder()
{
$params = Yii::$app->request->queryParams;
$searchModel = new OrderSearch();
$dataProvider = $searchModel->search($params);
return $this->success([
'list' => $dataProvider->getModels(),
'total' => $dataProvider->getTotalCount(),
'page' => $params['page'] ?? 1,
'pageSize' => $dataProvider->pagination->pageSize,
]);
}
/**
* 高级搜索
* @return array
*/
public function actionAdvanced()
{
$params = Yii::$app->request->queryParams;
$type = $params['type'] ?? 'goods';
switch ($type) {
case 'goods':
$searchModel = new GoodsSearch();
break;
case 'order':
$searchModel = new OrderSearch();
break;
default:
return $this->error('搜索类型错误');
}
$dataProvider = $searchModel->search($params);
return $this->success([
'list' => $dataProvider->getModels(),
'total' => $dataProvider->getTotalCount(),
'page' => $params['page'] ?? 1,
'pageSize' => $dataProvider->pagination->pageSize,
'type' => $type,
]);
}
}
10.2 完整的搜索模型示例
<?php
namespace addons\diandi_mall\models\search;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use addons\diandi_mall\models\order\Order;
/**
* 订单搜索模型
*/
class OrderSearch extends Order
{
public $keyword; // 关键词搜索(订单号、商品名称、用户名称)
public $user_id; // 用户ID
public $status; // 订单状态
public $payment_method; // 支付方式
public $min_amount; // 最低金额
public $max_amount; // 最高金额
public $start_date; // 开始日期
public $end_date; // 结束日期
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'user_id', 'status', 'payment_method'], 'integer'],
[['keyword', 'start_date', 'end_date'], 'safe'],
[['min_amount', 'max_amount', 'amount'], 'number'],
];
}
/**
* {@inheritdoc}
*/
public function scenarios()
{
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = Order::find();
// 关联查询
$query->joinWith(['user', 'items']);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 10,
],
'sort' => [
'defaultOrder' => [
'created_at' => SORT_DESC,
],
],
]);
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
// 关键词搜索
if (!empty($this->keyword)) {
$query->andFilterWhere([
'or',
['like', 'order.order_sn', $this->keyword],
['like', 'user.username', $this->keyword],
['like', 'items.name', $this->keyword]
]);
}
// 用户ID过滤
if (!empty($this->user_id)) {
$query->andFilterWhere(['order.user_id' => $this->user_id]);
}
// 状态过滤
if (!empty($this->status)) {
$query->andFilterWhere(['order.status' => $this->status]);
}
// 支付方式过滤
if (!empty($this->payment_method)) {
$query->andFilterWhere(['order.payment_method' => $this->payment_method]);
}
// 金额范围过滤
if (!empty($this->min_amount)) {
$query->andFilterWhere(['>=', 'order.amount', $this->min_amount]);
}
if (!empty($this->max_amount)) {
$query->andFilterWhere(['<=', 'order.amount', $this->max_amount]);
}
// 日期范围过滤
if (!empty($this->start_date)) {
$query->andFilterWhere(['>=', 'order.created_at', strtotime($this->start_date)]);
}
if (!empty($this->end_date)) {
$query->andFilterWhere(['<=', 'order.created_at', strtotime($this->end_date . ' 23:59:59')]);
}
return $dataProvider;
}
}
