From b9508c85ff8158a8ef18ab76d3430ba440598a6c Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Wed, 19 Nov 2025 16:34:39 +0800 Subject: [PATCH] update --- .../Controllers/Admin/OtherController.php | 34 +- .../Admin/StatisticsConfigController.php | 819 ++++++++++++++++++ app/Models/StatisticsConfig.php | 12 + ...7_create_user_statistics_configs_table.php | 36 + database/seeders/StatisticsConfigSeeder.php | 616 +++++++++++++ routes/api.php | 7 + user_statistics_config_json结构说明.md | 550 ++++++++++++ 7 files changed, 2044 insertions(+), 30 deletions(-) create mode 100644 app/Http/Controllers/Admin/StatisticsConfigController.php create mode 100644 app/Models/StatisticsConfig.php create mode 100644 database/migrations/2025_11_19_140617_create_user_statistics_configs_table.php create mode 100644 database/seeders/StatisticsConfigSeeder.php create mode 100644 user_statistics_config_json结构说明.md diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 8e2f07d..bc79eff 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -17,6 +17,7 @@ use App\Models\CustomFormField; use App\Models\Department; use App\Models\ParameterDetail; use App\Models\SupplyDemand; +use App\Models\TimeEvent; use App\Models\User; use App\Repositories\DoorRepository; use App\Repositories\EntranceRepository; @@ -118,11 +119,6 @@ class OtherController extends CommonController */ public function homeV2() { - // 校友总数 - $schoolmate['schoolmate_total'] = User::where('is_schoolmate', 1)->count(); - // 2025年校友数 - $schoolmate['schoolmate_year'] = User::where('is_schoolmate', 1)->where('created_at', 'like', '%' . date('Y') . '%')->count(); - // 课程统计 $courseTypes = CourseType::where('is_chart', 1)->get(); $start_date = '2020-01-01'; @@ -150,8 +146,7 @@ class OtherController extends CommonController } // 全国数据 - $countryArea = Company::groupBy('company_city')->whereNotNull('company_city') - ->get(['company_city']); + $countryArea = Company::groupBy('company_city')->whereNotNull('company_city')->get(['company_city']); $country = []; foreach ($countryArea as $item) { $country[] = [ @@ -163,34 +158,13 @@ class OtherController extends CommonController } // 本月课程 $monthCourses = Course::with('teacher')->where('start_date', 'like', '%' . date('Y-m') . '%')->get(); - // 投后企业 - $yuanhe['yh_invested_total'] = Company::where('is_yh_invested', 1)->count(); - // 元和员工参与企业 - $companyNameKeyword = ['元禾控股', '元禾原点', '元禾厚望', '元禾重元', '元禾璞华', '元禾谷风', '元禾绿柳', '元禾辰坤', '元禾沙湖', '禾裕集团', '苏州科服', '信诚管理咨询', '集成电路公司', '常州团队', '国企元禾']; - // 获取公司名字包含$companyNameKeyword任意数据的公司,需要模糊匹配 - $yuanhe['yh_join_company_total'] = Company::where(function ($query) use ($companyNameKeyword) { - foreach ($companyNameKeyword as $item) { - $query->orWhere('company_name', 'like', '%' . $item . '%'); - } - })->count(); - // 全市干部参与企业 - $yuanhe['yh_ganbu_total'] = Company::whereHas('users', function ($query) { - $query->where('from', '跟班学员'); - })->count(); - // 三个全覆盖 - // 苏州头部企业 - $cover['head_total'] = 0; - // 高层次人才 - $cover['high_total'] = 0; - // 重点上市公司 - $cover['stock_total'] = 0; // 时间轴 - $time_axis = []; + $time_axis = TimeEvent::orderBy('sort', 'asc')->orderBy('id', 'desc')->get(); // 动态信息 $article['xiaoyou'] = Article::where('type', 1)->limit(7)->orderBy('created_at', 'desc')->get(); $article['yejie'] = Article::where('type', 2)->limit(7)->orderBy('created_at', 'desc')->get(); $article['supply_demands'] = SupplyDemand::limit(7)->orderBy('created_at', 'desc')->get(); - return $this->success(compact('courseTypes', 'schoolmate', 'suzhou', 'country', 'monthCourses', 'yuanhe', 'time_axis')); + return $this->success(compact('courseTypes', 'suzhou', 'country', 'monthCourses', 'time_axis', 'article')); } /** diff --git a/app/Http/Controllers/Admin/StatisticsConfigController.php b/app/Http/Controllers/Admin/StatisticsConfigController.php new file mode 100644 index 0000000..d3ef441 --- /dev/null +++ b/app/Http/Controllers/Admin/StatisticsConfigController.php @@ -0,0 +1,819 @@ +all(); + $list = $this->model->where(function ($query) use ($all) { + if (isset($all['filter']) && !empty($all['filter'])) { + foreach ($all['filter'] as $condition) { + $key = $condition['key'] ?? null; + $op = $condition['op'] ?? null; + $value = $condition['value'] ?? null; + if (!isset($key) || !isset($op) || !isset($value)) { + continue; + } + // 等于 + if ($op == 'eq') { + $query->where($key, $value); + } + // 不等于 + if ($op == 'neq') { + $query->where($key, '!=', $value); + } + // 大于 + if ($op == 'gt') { + $query->where($key, '>', $value); + } + // 大于等于 + if ($op == 'egt') { + $query->where($key, '>=', $value); + } + // 小于 + if ($op == 'lt') { + $query->where($key, '<', $value); + } + // 小于等于 + if ($op == 'elt') { + $query->where($key, '<=', $value); + } + // 模糊搜索 + if ($op == 'like') { + $query->where($key, 'like', '%' . $value . '%'); + } + // 否定模糊搜索 + if ($op == 'notlike') { + $query->where($key, 'not like', '%' . $value . '%'); + } + // 范围搜索 + if ($op == 'range') { + list($from, $to) = explode(',', $value); + if (empty($from) || empty($to)) { + continue; + } + $query->whereBetween($key, [$from, $to]); + } + } + } + })->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc'); + if (isset($all['is_export']) && !empty($all['is_export'])) { + $list = $list->get()->toArray(); + $export_fields = $all['export_fields'] ?? []; + // 导出文件名字 + $tableName = $this->model->getTable(); + $filename = (new CustomForm())->getTableComment($tableName); + return Excel::download(new BaseExport($export_fields, $list, $tableName), $filename . date('YmdHis') . '.xlsx'); + } else { + // 输出 + $list = $list->paginate($all['page_size'] ?? 20); + } + return $this->success($list); + } + + /** + * @OA\Get( + * path="/api/admin/statistics-config/show", + * tags={"动态统计"}, + * summary="详情", + * description="", + * @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"), + * @OA\Parameter(name="show_relation", in="query", @OA\Schema(type="string"), required=false, description="需要输出的关联关系数组,填写输出指定数据"), + * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"), + * @OA\Response( + * response="200", + * description="暂无" + * ) + * ) + */ + public function show() + { + $all = \request()->all(); + $messages = [ + 'id.required' => 'Id必填', + ]; + $validator = Validator::make($all, [ + 'id' => 'required' + ], $messages); + if ($validator->fails()) { + return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]); + } + $detail = $this->model->find($all['id']); + return $this->success($detail); + } + + /** + * @OA\Post( + * path="/api/admin/statistics-config/save", + * tags={"动态统计"}, + * summary="保存统计数据配置", + * description="根据传入的id决定是更新现有配置还是新增新的配置。", + * @OA\Parameter(name="id", in="query", @OA\Schema(type="integer"), required=false, description="配置ID(存在则更新,不存在则新增)"), + * @OA\Parameter(name="name", in="query", @OA\Schema(type="string"), required=true, description="名字"), + * @OA\Parameter(name="key", in="query", @OA\Schema(type="string"), required=false, description="标识key"), + * @OA\Parameter(name="decimal_places", in="query", @OA\Schema(type="integer"), required=false, description="小数点位数,默认0"), + * @OA\Parameter(name="description", in="query", @OA\Schema(type="string"), required=false, description="描述"), + * @OA\Parameter(name="config_json", in="query", @OA\Schema(type="string"), required=false, description="配置json,包含数据来源、条件设置、统计方式等配置,详见配置文档"), + * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"), + * @OA\Response( + * response="200", + * description="操作成功" + * ) + * ) + */ + public function save() + { + $all = \request()->all(); + DB::beginTransaction(); + try { + if (isset($all['id'])) { + $model = $this->model->find($all['id']); + if (empty($model)) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '数据不存在']); + } + } else { + $model = $this->model; + $all['admin_id'] = $this->getUserId(); + $all['department_id'] = $this->getUser()->department_id; + } + $original = $model->getOriginal(); + $model->fill($all); + $model->save(); + DB::commit(); + return $this->success($model); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->fail([$exception->getCode(), $exception->getMessage()]); + } + } + + /** + * @OA\Get( + * path="/api/admin/statistics-config/destroy", + * tags={"动态统计"}, + * summary="删除", + * description="", + * @OA\Parameter(name="id", in="query", @OA\Schema(type="string"), required=true, description="id"), + * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"), + * @OA\Response( + * response="200", + * description="暂无" + * ) + * ) + */ + public function destroy() + { + return parent::destroy(); + } + + /** + * @OA\Get( + * path="/api/admin/statistics-config/calculate", + * tags={"动态统计"}, + * summary="根据配置key执行统计", + * description="根据配置的key获取对应的统计配置,然后根据config_json的配置执行统计查询,返回数据列表和统计结果", + * @OA\Parameter(name="key", in="query", @OA\Schema(type="string"), required=true, description="配置的key标识"), + * @OA\Parameter(name="page", in="query", @OA\Schema(type="integer"), required=false, description="页码,默认1"), + * @OA\Parameter(name="page_size", in="query", @OA\Schema(type="integer"), required=false, description="每页显示的条数,默认10"), + * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"), + * @OA\Response( + * response="200", + * description="返回统计数据" + * ) + * ) + */ + public function calculate() + { + $all = \request()->all(); + $messages = [ + 'key.required' => 'key必填', + ]; + $validator = Validator::make($all, [ + 'key' => 'required' + ], $messages); + if ($validator->fails()) { + return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]); + } + + // 获取分页参数 + $page = isset($all['page']) ? (int) $all['page'] : 1; + $pageSize = isset($all['page_size']) ? (int) $all['page_size'] : 10; + + // 根据key查找配置 + $config = $this->model->where('key', $all['key'])->first(); + if (empty($config)) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '配置不存在']); + } + + $configJson = $config->config_json; + if (empty($configJson)) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '配置数据为空']); + } + + // 如果 config_json 是字符串,尝试解析 + if (is_string($configJson)) { + $configJson = json_decode($configJson, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '配置 JSON 格式错误:' . json_last_error_msg()]); + } + } + + try { + // 检查数据结构 + if (!isset($configJson['data_source'])) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '配置数据缺少 data_source 字段,当前配置键:' . implode(', ', array_keys($configJson))]); + } + + // 获取主模型 + $mainModelName = $configJson['data_source']['main_model'] ?? ''; + if (empty($mainModelName)) { + $dataSourceKeys = isset($configJson['data_source']) ? implode(', ', array_keys($configJson['data_source'])) : '无'; + return $this->fail([ResponseCode::ERROR_BUSINESS, '主模型名称未配置,data_source 中的键:' . $dataSourceKeys]); + } + $mainModel = $this->getModel($mainModelName); + if (empty($mainModel)) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '主模型不存在:' . $mainModelName . ',可用的模型:user, company, course_sign, course, course_type']); + } + + $query = $mainModel::query(); + + // 获取主模型表名 + $tableName = $mainModel::make()->getTable(); + + // 加载关联模型 + $relations = $configJson['data_source']['relations'] ?? []; + if (!empty($relations)) { + foreach ($relations as $relation) { + $query->with($relation); + } + } + + // 应用条件 + $conditions = $configJson['conditions'] ?? []; + if (!empty($conditions['items'])) { + $logic = $conditions['logic'] ?? 'and'; + if ($logic === 'or') { + $query->where(function ($q) use ($conditions, $tableName) { + foreach ($conditions['items'] as $index => $item) { + if ($index === 0) { + $this->applyCondition($q, $item, 'and', 0, $tableName); + } else { + $q->orWhere(function ($subQ) use ($item, $tableName) { + $this->applyCondition($subQ, $item, 'and', 0, $tableName); + }); + } + } + }); + } else { + foreach ($conditions['items'] as $item) { + $this->applyCondition($query, $item, 'and', 0, $tableName); + } + } + } + + // 执行统计 + $statistics = $configJson['statistics'] ?? []; + $statisticsType = $statistics['type'] ?? 'count'; + $groupBy = $statistics['group_by'] ?? null; + // 确保空字符串也被视为不分组 + $groupBy = !empty($groupBy) ? $groupBy : null; + + // 保存原始查询用于获取列表 + $listQuery = clone $query; + + if ($groupBy) { + // 分组统计 + $groupParts = explode('.', $groupBy); + + if (count($groupParts) > 1) { + // 关联模型字段分组,需要 join + $relationName = $groupParts[0]; + $fieldName = $groupParts[1]; + $relationModel = $this->getRelationModel($mainModel, $relationName); + if ($relationModel) { + $relationTable = $relationModel::make()->getTable(); + $relationKey = $this->getRelationKey($mainModel, $relationName); + $query->join($relationTable, $tableName . '.' . $relationKey, '=', $relationTable . '.id'); + $selectFields = [$relationTable . '.' . $fieldName . ' as group_value']; + } else { + $selectFields = [$groupBy . ' as group_value']; + } + } else { + // 主模型字段分组 + $selectFields = [$tableName . '.' . $groupBy . ' as group_value']; + } + + // 根据统计类型构建 SQL + $statisticsField = $statistics['field'] ?? null; + if ($statisticsType === 'sum' && $statisticsField) { + // 处理关联模型字段 + $fieldParts = explode('.', $statisticsField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + // 如果还没有 join,需要 join + $query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $selectFields[] = DB::raw('SUM(' . $fieldRelationTable . '.' . $fieldFieldName . ') as total'); + } else { + $selectFields[] = DB::raw('SUM(' . $tableName . '.' . $statisticsField . ') as total'); + } + } else { + $selectFields[] = DB::raw('SUM(' . $tableName . '.' . $statisticsField . ') as total'); + } + } elseif ($statisticsType === 'max' && $statisticsField) { + // 处理关联模型字段 + $fieldParts = explode('.', $statisticsField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + // 如果还没有 join,需要 join + $query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $selectFields[] = DB::raw('MAX(' . $fieldRelationTable . '.' . $fieldFieldName . ') as total'); + } else { + $selectFields[] = DB::raw('MAX(' . $tableName . '.' . $statisticsField . ') as total'); + } + } else { + $selectFields[] = DB::raw('MAX(' . $tableName . '.' . $statisticsField . ') as total'); + } + } elseif ($statisticsType === 'min' && $statisticsField) { + // 处理关联模型字段 + $fieldParts = explode('.', $statisticsField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + // 如果还没有 join,需要 join + $query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $selectFields[] = DB::raw('MIN(' . $fieldRelationTable . '.' . $fieldFieldName . ') as total'); + } else { + $selectFields[] = DB::raw('MIN(' . $tableName . '.' . $statisticsField . ') as total'); + } + } else { + $selectFields[] = DB::raw('MIN(' . $tableName . '.' . $statisticsField . ') as total'); + } + } elseif ($statisticsType === 'count_distinct' && isset($statistics['distinct_field'])) { + // 去重数量统计 + $distinctField = $statistics['distinct_field']; + // 处理关联模型字段 + $fieldParts = explode('.', $distinctField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + // 如果还没有 join,需要 join + $query->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $selectFields[] = DB::raw('COUNT(DISTINCT ' . $fieldRelationTable . '.' . $fieldFieldName . ') as total'); + } else { + $selectFields[] = DB::raw('COUNT(DISTINCT ' . $tableName . '.' . $distinctField . ') as total'); + } + } else { + $selectFields[] = DB::raw('COUNT(DISTINCT ' . $tableName . '.' . $distinctField . ') as total'); + } + } else { + // count 统计总数量 + $selectFields[] = DB::raw('COUNT(*) as total'); + } + + $query->select($selectFields); + + if (count($groupParts) > 1) { + $query->groupBy($relationTable . '.' . $fieldName); + } else { + $query->groupBy($tableName . '.' . $groupBy); + } + } else { + // 不分组统计,获取所有数据列表 + // 列表查询保持原样,获取所有符合条件的记录 + } + + // 排序(分组统计时对分组结果排序,不分组时对列表排序) + if (!empty($statistics['order_by'])) { + $orderField = $statistics['order_by']['field'] ?? ($groupBy ? 'total' : 'id'); + $orderDirection = $statistics['order_by']['direction'] ?? 'desc'; + $query->orderBy($orderField, $orderDirection); + if (!$groupBy) { + // 不分组时,列表查询也需要排序 + $listQuery->orderBy($orderField, $orderDirection); + } + } else { + // 没有指定排序时,不分组情况使用默认排序 + if (!$groupBy) { + $listQuery->orderBy('id', 'desc'); + } + } + + // 获取统计查询的 SQL 语句(在执行查询前) + $statisticsSql = $query->toSql(); + $statisticsBindings = $query->getBindings(); + $statisticsSqlFull = $this->getFullSql($statisticsSql, $statisticsBindings); + + // 获取分组统计结果列表(分页) + // 先获取所有结果用于计算统计结果 + $allResults = $query->get(); + + // 对结果进行分页处理 + $totalCount = $allResults->count(); + $pagedResults = $allResults->slice(($page - 1) * $pageSize, $pageSize); + + // 格式化分组统计结果 + $data = []; + foreach ($pagedResults as $item) { + $row = [ + 'group_value' => $item->group_value ?? null, + 'total' => round($item->total, $config->decimal_places) + ]; + $data[] = $row; + } + + // 计算统计结果 + $statisticsResult = null; + $pagination = null; + + if ($groupBy) { + // 分组统计:根据统计类型计算最终结果 + $statisticsField = $statistics['field'] ?? null; + if ($statisticsType === 'sum' && $statisticsField) { + // 分组求和时,统计结果是所有分组的总和 + $allTotalValue = 0; + foreach ($allResults as $item) { + $allTotalValue += $item->total; + } + $statisticsResult = round($allTotalValue, $config->decimal_places); + } elseif ($statisticsType === 'max' && $statisticsField) { + // 分组最大值时,统计结果是所有分组中的最大值 + $maxValue = null; + foreach ($allResults as $item) { + if ($maxValue === null || $item->total > $maxValue) { + $maxValue = $item->total; + } + } + $statisticsResult = $maxValue !== null ? round($maxValue, $config->decimal_places) : 0; + } elseif ($statisticsType === 'min' && $statisticsField) { + // 分组最小值时,统计结果是所有分组中的最小值 + $minValue = null; + foreach ($allResults as $item) { + if ($minValue === null || $item->total < $minValue) { + $minValue = $item->total; + } + } + $statisticsResult = $minValue !== null ? round($minValue, $config->decimal_places) : 0; + } else { + // 分组计数时,统计结果是所有分组的计数总和 + $allTotalValue = 0; + foreach ($allResults as $item) { + $allTotalValue += $item->total; + } + $statisticsResult = $allTotalValue; + } + + // 分页信息 + $pagination = [ + 'current_page' => $page, + 'page_size' => $pageSize, + 'total' => $totalCount, + 'total_pages' => ceil($totalCount / $pageSize) + ]; + } else { + // 不分组统计:先获取 SQL,再执行查询 + // 克隆查询用于获取 SQL(避免执行后无法获取) + $calcQuery = clone $listQuery; + $listQueryForData = clone $listQuery; + + // 根据统计类型计算统计值 + $statisticsField = $statistics['field'] ?? null; + if ($statisticsType === 'sum' && $statisticsField) { + // 处理关联模型字段 + $fieldParts = explode('.', $statisticsField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + $calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $statisticsResult = round($calcQuery->sum($fieldRelationTable . '.' . $fieldFieldName), $config->decimal_places); + } else { + $statisticsResult = round($listQuery->sum($statisticsField), $config->decimal_places); + } + } else { + $statisticsResult = round($listQuery->sum($statisticsField), $config->decimal_places); + } + } elseif ($statisticsType === 'max' && $statisticsField) { + // 处理关联模型字段 + $fieldParts = explode('.', $statisticsField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + $calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $statisticsResult = round($calcQuery->max($fieldRelationTable . '.' . $fieldFieldName), $config->decimal_places); + } else { + $statisticsResult = round($listQuery->max($statisticsField), $config->decimal_places); + } + } else { + $statisticsResult = round($listQuery->max($statisticsField), $config->decimal_places); + } + } elseif ($statisticsType === 'min' && $statisticsField) { + // 处理关联模型字段 + $fieldParts = explode('.', $statisticsField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + $calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $statisticsResult = round($calcQuery->min($fieldRelationTable . '.' . $fieldFieldName), $config->decimal_places); + } else { + $statisticsResult = round($listQuery->min($statisticsField), $config->decimal_places); + } + } else { + $statisticsResult = round($listQuery->min($statisticsField), $config->decimal_places); + } + } elseif ($statisticsType === 'count_distinct' && isset($statistics['distinct_field'])) { + // 去重数量统计 + $distinctField = $statistics['distinct_field']; + // 处理关联模型字段 + $fieldParts = explode('.', $distinctField); + if (count($fieldParts) > 1) { + $fieldRelationName = $fieldParts[0]; + $fieldFieldName = $fieldParts[1]; + $fieldRelationModel = $this->getRelationModel($mainModel, $fieldRelationName); + if ($fieldRelationModel) { + $fieldRelationTable = $fieldRelationModel::make()->getTable(); + $fieldRelationKey = $this->getRelationKey($mainModel, $fieldRelationName); + $calcQuery->leftJoin($fieldRelationTable, $tableName . '.' . $fieldRelationKey, '=', $fieldRelationTable . '.id'); + $statisticsResult = $calcQuery->selectRaw('COUNT(DISTINCT ' . $fieldRelationTable . '.' . $fieldFieldName . ') as total')->value('total') ?? 0; + } else { + $statisticsResult = $listQuery->selectRaw('COUNT(DISTINCT ' . $distinctField . ') as total')->value('total') ?? 0; + } + } else { + $statisticsResult = $listQuery->selectRaw('COUNT(DISTINCT ' . $tableName . '.' . $distinctField . ') as total')->value('total') ?? 0; + } + } else { + // count 统计总数量 + $statisticsResult = $listQuery->count(); + } + + // 获取统计查询的 SQL(在计算完统计值之后,确保包含所有修改) + $statisticsSqlForCalc = $calcQuery->toSql(); + $statisticsBindingsForCalc = $calcQuery->getBindings(); + $statisticsSqlFull = $this->getFullSql($statisticsSqlForCalc, $statisticsBindingsForCalc); + + // 获取列表总数(用于分页) + $listTotalCount = $listQueryForData->count(); + + // 不分组时,获取分页数据作为列表 + $listResults = $listQueryForData->skip(($page - 1) * $pageSize)->take($pageSize)->get(); + $data = []; + foreach ($listResults as $item) { + $data[] = $item->toArray(); + } + + // 分页信息 + $pagination = [ + 'current_page' => $page, + 'page_size' => $pageSize, + 'total' => $listTotalCount, + 'total_pages' => ceil($listTotalCount / $pageSize) + ]; + } + + return $this->success([ + 'sql' => $statisticsSqlFull, + 'list' => $data, + 'total' => $statisticsResult, + 'pagination' => $pagination, + 'config' => $config + ]); + + } catch (\Exception $exception) { + return $this->fail([$exception->getCode(), $exception->getMessage()]); + } + } + + /** + * 获取模型实例 + */ + private function getModel($modelName) + { + $models = [ + 'user' => User::class, + 'company' => Company::class, + 'course_sign' => CourseSign::class, + 'course' => Course::class, + 'course_type' => CourseType::class, + ]; + return $models[$modelName] ?? null; + } + + /** + * 应用查询条件 + */ + private function applyCondition($query, $condition, $logic, $index, $tableName = null) + { + $key = $condition['key'] ?? ''; + $operator = $condition['operator'] ?? 'eq'; + $value = $condition['value'] ?? ''; + + if (empty($key)) { + return; + } + + // 处理关联模型的字段 + $keyParts = explode('.', $key); + if (count($keyParts) > 1) { + // 关联模型字段,使用 whereHas + $relationName = $keyParts[0]; + $fieldName = $keyParts[1]; + $query->whereHas($relationName, function ($q) use ($fieldName, $operator, $value) { + $this->applyOperator($q, $fieldName, $operator, $value); + }); + return; + } + + // 主模型字段,添加表名前缀避免字段歧义 + $qualifiedKey = $tableName ? $tableName . '.' . $key : $key; + $this->applyOperator($query, $qualifiedKey, $operator, $value); + } + + /** + * 获取关联模型 + */ + private function getRelationModel($mainModel, $relationName) + { + $model = new $mainModel(); + if (method_exists($model, $relationName)) { + $relation = $model->$relationName(); + return get_class($relation->getRelated()); + } + return null; + } + + /** + * 获取关联键名 + */ + private function getRelationKey($mainModel, $relationName) + { + $model = new $mainModel(); + if (method_exists($model, $relationName)) { + $relation = $model->$relationName(); + return $relation->getForeignKeyName(); + } + return 'id'; + } + + /** + * 获取完整的 SQL 语句(包含绑定参数) + */ + private function getFullSql($sql, $bindings) + { + if (empty($bindings)) { + return $sql; + } + + $fullSql = $sql; + foreach ($bindings as $binding) { + $value = is_numeric($binding) ? $binding : "'" . addslashes($binding) . "'"; + $fullSql = preg_replace('/\?/', $value, $fullSql, 1); + } + + return $fullSql; + } + + /** + * 应用操作符 + */ + private function applyOperator($query, $key, $operator, $value) + { + switch ($operator) { + case 'eq': + $query->where($key, $value); + break; + case 'neq': + $query->where($key, '!=', $value); + break; + case 'gt': + $query->where($key, '>', $value); + break; + case 'egt': + $query->where($key, '>=', $value); + break; + case 'lt': + $query->where($key, '<', $value); + break; + case 'elt': + $query->where($key, '<=', $value); + break; + case 'like': + $query->where($key, 'like', '%' . $value . '%'); + break; + case 'notlike': + $query->where($key, 'not like', '%' . $value . '%'); + break; + case 'in': + $array = explode(',', $value); + $query->whereIn($key, $array); + break; + case 'notin': + $array = explode(',', $value); + $query->whereNotIn($key, $array); + break; + case 'between': + list($from, $to) = explode(',', $value); + if (!empty($from) && !empty($to)) { + $query->whereBetween($key, [$from, $to]); + } + break; + case 'notbetween': + list($from, $to) = explode(',', $value); + if (!empty($from) && !empty($to)) { + $query->whereNotBetween($key, [$from, $to]); + } + break; + case 'isnull': + $query->whereNull($key); + break; + case 'isnotnull': + $query->whereNotNull($key); + break; + } + } + +} diff --git a/app/Models/StatisticsConfig.php b/app/Models/StatisticsConfig.php new file mode 100644 index 0000000..62cc6a7 --- /dev/null +++ b/app/Models/StatisticsConfig.php @@ -0,0 +1,12 @@ + 'json', + ]; + +} diff --git a/database/migrations/2025_11_19_140617_create_user_statistics_configs_table.php b/database/migrations/2025_11_19_140617_create_user_statistics_configs_table.php new file mode 100644 index 0000000..39799b5 --- /dev/null +++ b/database/migrations/2025_11_19_140617_create_user_statistics_configs_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name')->comment('名字'); + $table->string('key')->nullable()->comment('标识key'); + $table->tinyInteger('decimal_places')->default(0)->comment('小数点位数'); + $table->text('description')->nullable()->comment('描述'); + $table->json('config_json')->nullable()->comment('配置json'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('statistics_configs'); + } +}; diff --git a/database/seeders/StatisticsConfigSeeder.php b/database/seeders/StatisticsConfigSeeder.php new file mode 100644 index 0000000..9c4cde0 --- /dev/null +++ b/database/seeders/StatisticsConfigSeeder.php @@ -0,0 +1,616 @@ + '校友总数', + 'key' => 'schoolmate_total', + 'decimal_places' => 0, + 'description' => '统计所有校友的总数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'user', + 'relations' => [] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'is_schoolmate', + 'operator' => 'eq', + 'value' => '1' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'order_by' => [ + 'field' => 'id', + 'direction' => 'desc' + ] + ] + ] + ], + + // 2. 2025年校友数 - 对应 homeV2 中的 schoolmate_year + [ + 'name' => '2025年校友数', + 'key' => 'schoolmate_year', + 'decimal_places' => 0, + 'description' => '统计2025年创建的校友数量', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'user', + 'relations' => [] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'is_schoolmate', + 'operator' => 'eq', + 'value' => '1' + ], + [ + 'key' => 'created_at', + 'operator' => 'like', + 'value' => date('Y') + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'order_by' => [ + 'field' => 'created_at', + 'direction' => 'desc' + ] + ] + ] + ], + + // 3. 已开设期数 - 对应 homeV2 中的 course_periods_total(按课程类型) + [ + 'name' => '各课程类型已开设期数', + 'key' => 'course_periods_total_by_type', + 'decimal_places' => 0, + 'description' => '统计各课程类型已开设的期数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course', + 'relations' => [] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'type', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 4. 培养人数去重 - 对应 homeV2 中的 course_signs_total(按课程类型) + // 注意:去重逻辑需要在应用层处理,这里先统计总数 + [ + 'name' => '各课程类型培养人数', + 'key' => 'course_signs_total_by_type', + 'decimal_places' => 0, + 'description' => '统计各课程类型的审核通过报名人数(2020-01-01至今)', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course_sign', + 'relations' => ['course'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'status', + 'operator' => 'eq', + 'value' => '1' + ], + [ + 'key' => 'created_at', + 'operator' => 'between', + 'value' => '2020-01-01,' . date('Y-m-d') + ], + [ + 'key' => 'status', + 'operator' => 'neq', + 'value' => '4' + ], + [ + 'key' => 'status', + 'operator' => 'neq', + 'value' => '5' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'course.type', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 5. 苏州区域数据 - 对应 homeV2 中的 suzhou + [ + 'name' => '苏州各区域校友人数', + 'key' => 'suzhou_schoolmate_by_area', + 'decimal_places' => 0, + 'description' => '统计苏州各区域的校友人数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'user', + 'relations' => ['company'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'is_schoolmate', + 'operator' => 'eq', + 'value' => '1' + ], + [ + 'key' => 'company.company_city', + 'operator' => 'eq', + 'value' => '苏州市' + ], + [ + 'key' => 'company.company_area', + 'operator' => 'isnotnull' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'company.company_area', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 6. 全国数据 - 对应 homeV2 中的 country + [ + 'name' => '全国各城市校友人数', + 'key' => 'country_schoolmate_by_city', + 'decimal_places' => 0, + 'description' => '统计全国各城市的校友人数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'user', + 'relations' => ['company'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'is_schoolmate', + 'operator' => 'eq', + 'value' => '1' + ], + [ + 'key' => 'company.company_city', + 'operator' => 'isnotnull' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'company.company_city', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 7. 本月课程 - 对应 homeV2 中的 monthCourses + [ + 'name' => '本月课程列表', + 'key' => 'month_courses', + 'decimal_places' => 0, + 'description' => '获取本月开课的课程列表', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course', + 'relations' => ['teacher'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'start_date', + 'operator' => 'like', + 'value' => date('Y-m') + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'order_by' => [ + 'field' => 'start_date', + 'direction' => 'asc' + ] + ] + ] + ], + + // 8. 投后企业 - 对应 homeV2 中的 yh_invested_total + [ + 'name' => '投后企业总数', + 'key' => 'yh_invested_total', + 'decimal_places' => 0, + 'description' => '统计元禾已投企业的总数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'company', + 'relations' => [] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'is_yh_invested', + 'operator' => 'eq', + 'value' => '1' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'order_by' => [ + 'field' => 'id', + 'direction' => 'desc' + ] + ] + ] + ], + + // 9. 元和员工参与企业 - 对应 homeV2 中的 yh_join_company_total + [ + 'name' => '元和员工参与企业总数', + 'key' => 'yh_join_company_total', + 'decimal_places' => 0, + 'description' => '统计公司名称包含元禾相关关键词的企业总数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'company', + 'relations' => [] + ], + 'conditions' => [ + 'logic' => 'or', + 'items' => [ + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾控股' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾原点' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾厚望' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾重元' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾璞华' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾谷风' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾绿柳' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾辰坤' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '元禾沙湖' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '禾裕集团' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '苏州科服' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '信诚管理咨询' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '集成电路公司' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '常州团队' + ], + [ + 'key' => 'company_name', + 'operator' => 'like', + 'value' => '国企元禾' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'order_by' => [ + 'field' => 'id', + 'direction' => 'desc' + ] + ] + ] + ], + + // 10. 全市干部参与企业 - 对应 homeV2 中的 yh_ganbu_total + [ + 'name' => '全市干部参与企业总数', + 'key' => 'yh_ganbu_total', + 'decimal_places' => 0, + 'description' => '统计有"跟班学员"用户的企业总数', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'company', + 'relations' => ['users'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'users.from', + 'operator' => 'eq', + 'value' => '跟班学员' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'order_by' => [ + 'field' => 'id', + 'direction' => 'desc' + ] + ] + ] + ], + + // 11. 课程统计列表 - 对应 homeV2 中的 courseTypes(已开设期数) + [ + 'name' => '课程统计列表(已开设期数)', + 'key' => 'course_types_list_periods', + 'decimal_places' => 0, + 'description' => '统计各课程类型的已开设期数(仅统计 is_chart=1 的课程类型)', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course', + 'relations' => ['typeDetail'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'typeDetail.is_chart', + 'operator' => 'eq', + 'value' => '1' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'typeDetail.id', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 12. 课程统计列表 - 对应 homeV2 中的 courseTypes(培养人数) + [ + 'name' => '课程统计列表(培养人数)', + 'key' => 'course_types_list_signs', + 'decimal_places' => 0, + 'description' => '统计各课程类型的培养人数(2020-01-01至今,审核通过,仅统计 is_chart=1 的课程类型)', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course_sign', + 'relations' => ['course'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'status', + 'operator' => 'eq', + 'value' => '1' + ], + [ + 'key' => 'created_at', + 'operator' => 'between', + 'value' => '2020-01-01,' . date('Y-m-d') + ], + [ + 'key' => 'status', + 'operator' => 'neq', + 'value' => '4' + ], + [ + 'key' => 'status', + 'operator' => 'neq', + 'value' => '5' + ], + [ + 'key' => 'course.typeDetail.is_chart', + 'operator' => 'eq', + 'value' => '1' + ] + ] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'course.typeDetail.id', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 13. 课程分类下的课程数量统计 + [ + 'name' => '课程分类下的课程数量', + 'key' => 'course_count_by_type', + 'decimal_places' => 0, + 'description' => '统计各课程分类下的所有课程数量', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course', + 'relations' => ['typeDetail'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [] + ], + 'statistics' => [ + 'type' => 'count', + 'group_by' => 'typeDetail.id', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ], + + // 14. 课程分类列表及审核通过用户数量统计(按手机号去重) + [ + 'name' => '课程分类列表及审核通过用户数量统计', + 'key' => 'course_type_list_with_student_count', + 'decimal_places' => 0, + 'description' => '获取课程分类列表,并统计每个分类下审核通过的报名用户数量(按照手机号去重)', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course_sign', + 'relations' => ['course', 'user'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'status', + 'operator' => 'eq', + 'value' => '1' + ] + ] + ], + 'statistics' => [ + 'type' => 'count_distinct', + 'distinct_field' => 'user.mobile', + 'group_by' => 'course.type', + 'order_by' => [ + 'field' => 'group_value', + 'direction' => 'asc' + ] + ] + ] + ], + + // 15. 课程分类下按手机号去重的审核通过用户数量统计 + [ + 'name' => '课程分类下按手机号去重的审核通过用户数量', + 'key' => 'course_type_student_count_distinct', + 'decimal_places' => 0, + 'description' => '统计各课程分类下审核通过的报名用户数量,按照手机号去重', + 'config_json' => [ + 'data_source' => [ + 'main_model' => 'course_sign', + 'relations' => ['course', 'user'] + ], + 'conditions' => [ + 'logic' => 'and', + 'items' => [ + [ + 'key' => 'status', + 'operator' => 'eq', + 'value' => '1' + ] + ] + ], + 'statistics' => [ + 'type' => 'count_distinct', + 'distinct_field' => 'user.mobile', + 'group_by' => 'course.type', + 'order_by' => [ + 'field' => 'total', + 'direction' => 'desc' + ] + ] + ] + ] + ]; + + foreach ($configs as $config) { + StatisticsConfig::updateOrCreate( + ['key' => $config['key']], + $config + ); + } + + $this->command->info('已生成 ' . count($configs) . ' 条统计数据配置测试数据(基于 homeV2 方法)'); + } +} diff --git a/routes/api.php b/routes/api.php index d3f0a59..847adc7 100755 --- a/routes/api.php +++ b/routes/api.php @@ -252,6 +252,13 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () { Route::get('time-event/show', [\App\Http\Controllers\Admin\TimeEventController::class, "show"]); Route::post('time-event/save', [\App\Http\Controllers\Admin\TimeEventController::class, "save"]); Route::get('time-event/destroy', [\App\Http\Controllers\Admin\TimeEventController::class, "destroy"]); + + // 统计数据配置管理 + Route::get('statistics-configs/index', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "index"]); + Route::get('statistics-config/show', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "show"]); + Route::post('statistics-config/save', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "save"]); + Route::get('statistics-config/destroy', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "destroy"]); + Route::get('statistics-config/calculate', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "calculate"]); }); }); diff --git a/user_statistics_config_json结构说明.md b/user_statistics_config_json结构说明.md new file mode 100644 index 0000000..a42822e --- /dev/null +++ b/user_statistics_config_json结构说明.md @@ -0,0 +1,550 @@ +# 用户统计数据配置 JSON 结构说明 + +## 概述 + +`user_statistics_configs` 表的 `config_json` 字段用于存储动态统计配置,包含三个主要部分:数据来源、条件设置、统计方式。 + +## JSON 结构 + +```json +{ + "data_source": { + "main_model": "user|company|course_sign|course|course_type", + "relations": ["user", "company", "course_sign", "course", "course_type"] + }, + "conditions": { + "logic": "and|or", + "items": [ + { + "key": "字段名", + "operator": "操作类型", + "value": "值" + } + ] + }, + "statistics": { + "type": "sum|max|min|count|count_distinct", + "field": "统计字段(sum/max/min 时使用,可选)", + "distinct_field": "去重字段(count_distinct 时使用,可选)", + "group_by": "分组字段(可选,不设置则不分组)", + "order_by": { + "field": "排序字段(可选)", + "direction": "asc|desc" + } + } +} +``` + +--- + +## 一、数据来源(data_source) + +### 1.1 主模型(main_model) + +**说明**:指定统计数据的主要来源模型。 + +**可选值**: +- `user` - 用户模型 +- `company` - 公司模型 +- `course_sign` - 报名模型 +- `course` - 课程模型 +- `course_type` - 课程分类模型 + +**示例**: +```json +{ + "main_model": "user" +} +``` + +### 1.2 关联模型(relations) + +**说明**:指定需要关联的其他模型,可以关联多个模型。 + +**可选值**(数组): +- `user` - 用户模型 +- `company` - 公司模型 +- `course_sign` - 报名模型 +- `course` - 课程模型 +- `course_type` - 课程分类模型 + +**注意**: +- 关联模型不能包含主模型本身 +- 可以关联多个模型 +- 数组可以为空 + +**示例**: +```json +{ + "relations": ["company", "course_sign"] +} +``` + +--- + +## 二、条件设置(conditions) + +### 2.1 逻辑关系(logic) + +**说明**:指定多个条件之间的逻辑关系。 + +**可选值**: +- `and` - 所有条件都必须满足(AND) +- `or` - 至少一个条件满足(OR) + +**示例**: +```json +{ + "logic": "and" +} +``` + +### 2.2 条件项(items) + +**说明**:条件数组,每个条件包含键名、操作类型和值。 + +**条件项结构**: +```json +{ + "key": "字段名", + "operator": "操作类型", + "value": "值" +} +``` + +#### 字段说明 + +- **key**(字符串):要查询的字段名 + - 可以是主模型的字段 + - 可以是关联模型的字段(使用点号分隔,如 `company.name`) + +- **operator**(字符串):操作类型 + - `eq` - 等于 + - `neq` - 不等于 + - `gt` - 大于 + - `egt` - 大于等于 + - `lt` - 小于 + - `elt` - 小于等于 + - `like` - 模糊匹配 + - `notlike` - 不匹配 + - `in` - 在范围内(值为逗号分隔的字符串) + - `notin` - 不在范围内 + - `between` - 在范围内(值为逗号分隔的两个值) + - `notbetween` - 不在范围内 + - `isnull` - 为空(value 可省略) + - `isnotnull` - 不为空(value 可省略) + +- **value**(字符串/数字/数组):条件值 + - 根据操作类型不同,值的形式也不同 + - `in` 操作:值为逗号分隔的字符串,如 `"1,2,3"` + - `between` 操作:值为逗号分隔的两个值,如 `"2024-01-01,2024-12-31"` + - `isnull` 和 `isnotnull` 操作:value 可以省略 + +**示例**: + +```json +{ + "logic": "and", + "items": [ + { + "key": "is_schoolmate", + "operator": "eq", + "value": "1" + }, + { + "key": "company.is_yh_invested", + "operator": "eq", + "value": "1" + }, + { + "key": "created_at", + "operator": "between", + "value": "2024-01-01,2024-12-31" + } + ] +} +``` + +--- + +## 三、统计方式(statistics) + +### 3.1 统计类型(type) + +**说明**:指定统计的方式。 + +**可选值**: +- `sum` - 求和(需要指定 `field` 字段) +- `max` - 最大值(需要指定 `field` 字段) +- `min` - 最小值(需要指定 `field` 字段) +- `count` - 统计总数量(不需要指定 `field` 字段) +- `count_distinct` - 统计去重数量(需要指定 `distinct_field` 字段) + +**示例**: +```json +{ + "type": "count" +} +``` + +### 3.2 统计字段(field) + +**说明**:当统计类型为 `sum`、`max` 或 `min` 时,指定要统计的字段名。 + +**注意**: +- `type` 为 `sum`、`max`、`min` 时必须指定 `field` +- `type` 为 `count` 时可以省略 `field` +- 可以是主模型的字段 +- 可以是关联模型的字段(使用点号分隔,如 `company.company_fund`) + +**示例**: +```json +{ + "type": "sum", + "field": "company_fund" +} +``` + +```json +{ + "type": "max", + "field": "company.company_fund" +} +``` + +```json +{ + "type": "min", + "field": "created_at" +} +``` + +### 3.3 去重字段(distinct_field) + +**说明**:当统计类型为 `count_distinct` 时,指定要去重的字段名。 + +**注意**: +- `type` 为 `count_distinct` 时必须指定 `distinct_field` +- 可以是主模型的字段 +- 可以是关联模型的字段(使用点号分隔,如 `user.mobile`) +- **可以与 `group_by` 同时使用**:可以按某个字段分组,然后统计每个分组的去重数量 + +**示例1:不分组去重统计** +```json +{ + "type": "count_distinct", + "distinct_field": "mobile" +} +``` + +**示例2:关联模型字段去重** +```json +{ + "type": "count_distinct", + "distinct_field": "user.mobile" +} +``` + +**示例3:分组 + 去重统计(组合使用)** +```json +{ + "type": "count_distinct", + "distinct_field": "user.mobile", + "group_by": "course.type" +} +``` + +### 3.4 分组字段(group_by) + +**说明**:指定按哪个字段进行分组统计。这是一个可选配置,可以选择不分组或选择具体的分组字段。 + +**配置选项**: +- **不分组**:不设置 `group_by` 字段,或设置为 `null`,将返回所有符合条件的记录列表 +- **按字段分组**:设置具体的分组字段,将按该字段进行分组统计 + +**分组字段格式**: +- 可以是主模型的字段(如:`company_area`) +- 可以是关联模型的字段(使用点号分隔,如 `company.company_area`) + +**示例1:不分组统计** +```json +{ + "statistics": { + "type": "count" + // 不设置 group_by,表示不分组 + } +} +``` + +**示例2:按主模型字段分组** +```json +{ + "statistics": { + "type": "count", + "group_by": "company_area" + } +} +``` + +**示例3:按关联模型字段分组** +```json +{ + "statistics": { + "type": "count", + "group_by": "company.company_area" + } +} +``` + +**示例4:分组 + 去重统计(组合使用)** +```json +{ + "statistics": { + "type": "count_distinct", + "distinct_field": "user.mobile", + "group_by": "course.type" + } +} +``` + +### 3.4 排序方式(order_by) + +**说明**:指定结果的排序方式。 + +**结构**: +```json +{ + "field": "排序字段", + "direction": "asc|desc" +} +``` + +**字段说明**: +- **field**(字符串):排序字段名 + - 可以是主模型的字段 + - 可以是关联模型的字段(使用点号分隔) + - 可以是统计结果字段(如 `total`、`count`) + +- **direction**(字符串):排序方向 + - `asc` - 升序 + - `desc` - 降序 + +**示例**: +```json +{ + "order_by": { + "field": "total", + "direction": "desc" + } +} +``` + +--- + +## 完整示例 + +### 示例1:统计各区域的校友人数 + +```json +{ + "data_source": { + "main_model": "user", + "relations": ["company"] + }, + "conditions": { + "logic": "and", + "items": [ + { + "key": "is_schoolmate", + "operator": "eq", + "value": "1" + }, + { + "key": "created_at", + "operator": "between", + "value": "2024-01-01,2024-12-31" + } + ] + }, + "statistics": { + "type": "count", + "group_by": "company.company_area", + "order_by": { + "field": "count", + "direction": "desc" + } + } +} +``` + +### 示例2:统计各课程类型的报名人数 + +```json +{ + "data_source": { + "main_model": "course_sign", + "relations": ["course", "user"] + }, + "conditions": { + "logic": "and", + "items": [ + { + "key": "status", + "operator": "eq", + "value": "1" + }, + { + "key": "created_at", + "operator": "between", + "value": "2024-01-01,2024-12-31" + } + ] + }, + "statistics": { + "type": "count", + "group_by": "course.type", + "order_by": { + "field": "count", + "direction": "desc" + } + } +} +``` + +### 示例3:统计各公司的融资总额 + +```json +{ + "data_source": { + "main_model": "company", + "relations": ["user"] + }, + "conditions": { + "logic": "and", + "items": [ + { + "key": "is_yh_invested", + "operator": "eq", + "value": "1" + }, + { + "key": "company_fund", + "operator": "isnotnull" + } + ] + }, + "statistics": { + "type": "sum", + "field": "company_fund", + "group_by": "company_area", + "order_by": { + "field": "total", + "direction": "desc" + } + } +} +``` + +### 示例4:统计审核通过或待审核的报名人数 + +```json +{ + "data_source": { + "main_model": "course_sign", + "relations": [] + }, + "conditions": { + "logic": "or", + "items": [ + { + "key": "status", + "operator": "eq", + "value": "0" + }, + { + "key": "status", + "operator": "eq", + "value": "1" + } + ] + }, + "statistics": { + "type": "count", + "order_by": { + "field": "created_at", + "direction": "desc" + } + } +} +``` + +### 示例5:统计各课程类型的去重培养人数(按手机号去重) + +```json +{ + "data_source": { + "main_model": "course_sign", + "relations": ["user", "course"] + }, + "conditions": { + "logic": "and", + "items": [ + { + "key": "status", + "operator": "eq", + "value": "1" + }, + { + "key": "created_at", + "operator": "between", + "value": "2020-01-01," . date('Y-m-d') + } + ] + }, + "statistics": { + "type": "count_distinct", + "distinct_field": "user.mobile", + "group_by": "course.type", + "order_by": { + "field": "total", + "direction": "desc" + } + } +} +``` + +--- + +## 注意事项 + +1. **字段引用**: + - 主模型字段直接使用字段名 + - 关联模型字段使用 `模型名.字段名` 格式 + - 例如:`company.name`、`course.type` + +2. **数据类型**: + - 所有值在 JSON 中都存储为字符串 + - 系统会根据字段类型自动转换 + +3. **条件逻辑**: + - `and` 表示所有条件都必须满足 + - `or` 表示至少一个条件满足 + - 条件数组可以为空(表示无条件) + +4. **统计字段**: + - `sum` 类型必须指定 `field` + - `count` 类型不需要 `field` + - 分组字段可以为空(表示不分组) + +5. **排序字段**: + - 可以按任意字段排序 + - 可以按统计结果字段排序(如 `total`、`count`) + - 排序字段可以为空(使用默认排序) + +--- + +## 文档版本 + +- **创建日期**:2025-11-19 +- **最后更新**:2025-11-19 +