diff --git a/app/Console/Commands/UpdateCompany.php b/app/Console/Commands/UpdateCompany.php index fcdc305..8581313 100755 --- a/app/Console/Commands/UpdateCompany.php +++ b/app/Console/Commands/UpdateCompany.php @@ -16,14 +16,14 @@ class UpdateCompany extends Command * * @var string */ - protected $signature = 'update_company {--user_id=}'; + protected $signature = 'update_company {--user_id=} {--address=0}'; /** * The console command description. * * @var string */ - protected $description = '批量公司信息'; + protected $description = '批量更新公司信息'; /** * Create a new command instance. @@ -43,10 +43,13 @@ class UpdateCompany extends Command public function handle() { $user_id = $this->option('user_id'); + $updateLocal = (int)$this->option('address'); // 更新公司信息 $this->compnay($user_id); - // 更新经纬度信息 - $this->local($user_id); + // 更新经纬度信息(可选) + if ($updateLocal) { + $this->local($user_id); + } return $this->info('全部更新完成'); } @@ -57,22 +60,43 @@ class UpdateCompany extends Command { if ($user_id) { // 强制单个更新 - $users = User::where('id', $user_id)->get(); + $users = User::whereHas('courseSigns', function ($query) { + $query->where('status', 1); + })->where('id', $user_id)->get(); } else { - // 批量更新 - $users = User::whereDoesntHave('company')->whereNotNull('company_name')->get(); + // 批量更新(只更新有报名审核通过的用户) + $users = User::whereHas('courseSigns', function ($query) { + $query->where('status', 1); + })->whereNotNull('company_name')->get(); + } + + $total = $users->count(); + if ($total == 0) { + return $this->info('没有需要更新的用户'); } + + $this->info("开始更新公司信息,共 {$total} 个用户"); + $bar = $this->output->createProgressBar($total); + $bar->start(); + $YuanheRepository = new YuanheRepository(); foreach ($users as $user) { // 获取公司详细信息 $result = $YuanheRepository->companyInfo(['enterpriseName' => $user->company_name]); if (!$result) { - $this->info($user->company_name . '公司不存在'); + $bar->setMessage($user->company_name . ' 公司不存在', 'status'); + $bar->advance(); continue; } // 如果$result['enterpriseName']存在数字,跳过 if (preg_match('/\d/', $result['enterpriseName'])) { - $this->info($user->company_name . '公司名称包含数字,跳过'); + $bar->setMessage($user->company_name . ' 公司名称包含数字,跳过', 'status'); + $bar->advance(); + continue; + } + if ($result['status'] == '未注册') { + $bar->setMessage($user->company_name . ' 公司未注册,跳过', 'status'); + $bar->advance(); continue; } $where = ['company_name' => $result['enterpriseName']]; @@ -102,13 +126,23 @@ class UpdateCompany extends Command 'stock_number' => $result['stockNumber'], 'stock_type' => $result['stockType'], 'company_tag' => implode(',', $result['tagList']), + // 更新日期 + 'update_date' => $result['updatedDate'] ?? null, + // 管理平台 + 'project_users' => $result['projectUsers'] ?? null, + // 股东信息 + 'partners' => $result['partners'] ?? null, ]; $company = Company::updateOrCreate($where, $data); // 更新用户关联 $user->company_id = $company->id; $user->save(); - $this->info($result['enterpriseName'] . '-更新成功'); + $bar->setMessage($result['enterpriseName'] . ' 更新成功', 'status'); + $bar->advance(); } + + $bar->finish(); + $this->newLine(); return $this->info('公司信息-全部更新完成'); } @@ -121,7 +155,7 @@ class UpdateCompany extends Command // 强制单个更新 $user = User::find($user_id); if (empty($user->company_id)) { - return false; + return $this->error('用户没有关联公司'); } $companys = Company::where('id', $user->company_id)->get(); } else { @@ -131,19 +165,33 @@ class UpdateCompany extends Command ->where('company_address', '!=', '') ->get(); } + + $total = $companys->count(); + if ($total == 0) { + return $this->info('没有需要更新经纬度的公司'); + } + + $this->info("开始更新经纬度信息,共 {$total} 个公司"); + $bar = $this->output->createProgressBar($total); + $bar->start(); + // 每3个数据分一个chunk 。接口限制了一秒只能3次请求 - $companys = $companys->chunk(3); - foreach ($companys as $company) { - foreach ($company as $item) { + $companysChunk = $companys->chunk(3); + foreach ($companysChunk as $companyChunk) { + foreach ($companyChunk as $item) { $local = Company::addressTolocation($item->company_address); $item->company_longitude = $local['lng']; $item->company_latitude = $local['lat']; $item->save(); - $this->info($item->company_name . "-{$local['lng']}-{$local['lat']}-经纬度信息更新成功"); + $bar->setMessage($item->company_name . " 经纬度({$local['lng']}, {$local['lat']})更新成功", 'status'); + $bar->advance(); } sleep(1); } - return true; + + $bar->finish(); + $this->newLine(); + return $this->info('经纬度信息-全部更新完成'); } } diff --git a/app/Http/Controllers/Admin/ArticleController.php b/app/Http/Controllers/Admin/ArticleController.php new file mode 100644 index 0000000..b950b4b --- /dev/null +++ b/app/Http/Controllers/Admin/ArticleController.php @@ -0,0 +1,218 @@ +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/article/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/article/save", + * tags={"文章"}, + * summary="保存", + * description="", + * @OA\Parameter(name="id", in="query", @OA\Schema(type="int"), required=true, description="Id(存在更新,不存在新增)"), + * @OA\Parameter(name="title", in="query", @OA\Schema(type="string", nullable=true), description="标题"), + * @OA\Parameter(name="content", in="query", @OA\Schema(type="string", nullable=true), description="内容"), + * @OA\Parameter(name="type", in="query", @OA\Schema(type="string", nullable=true), description="类型1校友动态2业界动态"), + * @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/article/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(); + } + +} diff --git a/app/Http/Controllers/Admin/CalendarsController.php b/app/Http/Controllers/Admin/CalendarsController.php index ba5cea1..1f54e14 100644 --- a/app/Http/Controllers/Admin/CalendarsController.php +++ b/app/Http/Controllers/Admin/CalendarsController.php @@ -115,6 +115,7 @@ class CalendarsController extends BaseController * @OA\Parameter(name="end_time", in="query", @OA\Schema(type="string", format="date-time"), required=false, description="结束时间(YYYY-MM-DD HH:MM:SS)"), * @OA\Parameter(name="is_publish", in="query", @OA\Schema(type="string"), required=true, description="是否向用户发布0否1是"), * @OA\Parameter(name="address", in="query", @OA\Schema(type="string"), required=true, description="地址"), + * @OA\Parameter(name="days", in="query", @OA\Schema(type="string"), required=true, description="天数"), * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"), * @OA\Response( * response="200", diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index d891290..6e1bdd2 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -30,6 +30,34 @@ class CompanyController extends BaseController parent::__construct(new Company()); } + /** + * @OA\Get( + * path="/api/admin/company/config", + * tags={"公司管理"}, + * summary="参数", + * description="", + * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"), + * @OA\Response( + * response="200", + * description="暂无" + * ) + * ) + */ + public function config() + { + // 企业标签 + $companiesTags = Company::where('company_tag', '!=', '')->pluck('company_tag'); + $companiesTags = $companiesTags->flatten()->implode(','); + $companiesTags =array_unique(explode(',', $companiesTags)); + // 去除$companiesTags中包含小数点的元素 + $companiesTags = array_filter($companiesTags, function ($item) { + return !strpos($item, '.'); + }); + $companiesTags = array_values($companiesTags); + + return $this->success(compact('companiesTags')); + } + /** * @OA\Get( diff --git a/app/Http/Controllers/Admin/CourseController.php b/app/Http/Controllers/Admin/CourseController.php index f7b8e31..faa5aae 100755 --- a/app/Http/Controllers/Admin/CourseController.php +++ b/app/Http/Controllers/Admin/CourseController.php @@ -58,91 +58,107 @@ class CourseController extends BaseController { $all = request()->all(); $list = $this->model->with(underlineToHump($all['show_relation'] ?? [])) - ->withCount(['courseSigns' => function ($query) { - $query->whereNotIn('status', [4, 5]); - }])->withCount(['courseSigns as sign_pass_total' => function ($query) { - $query->where('status', 1)->whereHas('user'); - }])->withCount(['courseSigns as sign_wait_total' => function ($query) { - $query->where('status', 0)->whereHas('user'); - }])->withCount(['courseSigns as sign_fault_total' => function ($query) { - $query->where('status', 2)->whereHas('user'); - }])->withCount(['courseSigns as sign_prepare_total' => function ($query) { - $query->where('status', 3)->whereHas('user'); - }])->withCount(['courseSigns as sign_cancel_total' => function ($query) { - $query->where('status', 4)->whereHas('user'); - }])->withCount(['courseSigns as sign_give_up_total' => function ($query) { - $query->where('status', 5)->whereHas('user'); - }])->withCount(['courseSigns as sign_black_total' => function ($query) { - $query->where('status', 6)->whereHas('user'); - }])->where(function ($query) use ($all) { - if (isset($all['has_course_forms']) && !empty($all['has_course_forms'])) { - $query->whereHas('courseForms'); + ->withCount([ + 'courseSigns' => function ($query) { + $query->whereNotIn('status', [4, 5]); } - if (isset($all['start_date'])) { - $query->where('start_date', '>=', $all['start_date']); - } - if (isset($all['end_date'])) { - $query->where('end_date', '<=', $all['end_date']); - } - if (isset($all['course_type_id'])) { - $course_type_id = explode(',', $all['course_type_id']); - $query->whereIn('type', $course_type_id); - } - 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)) { + ])->withCount([ + 'courseSigns as sign_pass_total' => function ($query) { + $query->where('status', 1)->whereHas('user'); + } + ])->withCount([ + 'courseSigns as sign_wait_total' => function ($query) { + $query->where('status', 0)->whereHas('user'); + } + ])->withCount([ + 'courseSigns as sign_fault_total' => function ($query) { + $query->where('status', 2)->whereHas('user'); + } + ])->withCount([ + 'courseSigns as sign_prepare_total' => function ($query) { + $query->where('status', 3)->whereHas('user'); + } + ])->withCount([ + 'courseSigns as sign_cancel_total' => function ($query) { + $query->where('status', 4)->whereHas('user'); + } + ])->withCount([ + 'courseSigns as sign_give_up_total' => function ($query) { + $query->where('status', 5)->whereHas('user'); + } + ])->withCount([ + 'courseSigns as sign_black_total' => function ($query) { + $query->where('status', 6)->whereHas('user'); + } + ])->where(function ($query) use ($all) { + if (isset($all['has_course_forms']) && !empty($all['has_course_forms'])) { + $query->whereHas('courseForms'); + } + if (isset($all['start_date'])) { + $query->where('start_date', '>=', $all['start_date']); + } + if (isset($all['end_date'])) { + $query->where('end_date', '<=', $all['end_date']); + } + if (isset($all['course_type_id'])) { + $course_type_id = explode(',', $all['course_type_id']); + $query->whereIn('type', $course_type_id); + } + 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; } - $query->whereBetween($key, [$from, $to]); - } - if ($op == 'in') { - $array = explode(',', $value); - $query->whereIn($key, $array); + // 等于 + 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]); + } + if ($op == 'in') { + $array = explode(',', $value); + $query->whereIn($key, $array); + } } } - } - }); + }); $list = $list->orderBy($all['sort_name'] ?? 'sign_status', $all['sort_type'] ?? 'asc'); if (isset($all['is_export']) && !empty($all['is_export'])) { $list = $list->limit(5000)->get()->toArray(); @@ -219,6 +235,8 @@ class CourseController extends BaseController * @OA\Parameter(name="latitude", in="query", @OA\Schema(type="string"), description="纬度"), * @OA\Parameter(name="address_detail", in="query", @OA\Schema(type="string"), description="详细地址"), * @OA\Parameter(name="url_title", in="query", @OA\Schema(type="string"), description="链接地址"), + * @OA\Parameter(name="is_ganbu", in="query", @OA\Schema(type="integer"), description="是否干部课程-0否1是"), + * @OA\Parameter(name="is_chart", in="query", @OA\Schema(type="integer"), description="是否参与统计-0否1是,默认1"), * @OA\Response( * response=200, * description="操作成功" @@ -240,7 +258,6 @@ class CourseController extends BaseController $all['admin_id'] = $this->getUserId(); $all['department_id'] = $this->getUser()->department_id; } - $original = $model->getOriginal(); $model->fill($all); $model->save(); DB::commit(); @@ -250,8 +267,6 @@ class CourseController extends BaseController if ($model->status == 1 && $model->start_date) { CourseAppointmentTotal::addByCourse($model->id); } - // 记录日志 - $this->saveLogs($original, $model); return $this->success($model); } catch (\Exception $exception) { DB::rollBack(); diff --git a/app/Http/Controllers/Admin/CourseSignController.php b/app/Http/Controllers/Admin/CourseSignController.php index e686103..66f8c75 100755 --- a/app/Http/Controllers/Admin/CourseSignController.php +++ b/app/Http/Controllers/Admin/CourseSignController.php @@ -144,12 +144,12 @@ class CourseSignController extends BaseController if (isset($all['course_type_id'])) { $course_type_id = explode(',', $all['course_type_id']); $courses = Course::where(function ($query) use ($all) { - if (isset($all['start_date'])) { - $query->where('start_date', '>=', $all['start_date']); - } - if (isset($all['end_date'])) { - $query->where('start_date', '<=', $all['end_date']); - } +// if (isset($all['start_date'])) { +// $query->where('start_date', '>=', $all['start_date']); +// } +// if (isset($all['end_date'])) { +// $query->where('start_date', '<=', $all['end_date']); +// } })->whereIn('type', $course_type_id)->get(); $query->whereNotIn('status', [4, 5]) ->where(function ($query) use ($courses) { diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index cccfe54..bf7c11c 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -7,6 +7,7 @@ use App\Jobs\CancelAppointMeet; use App\Models\Admin; use App\Models\Appointment; use App\Models\AppointmentConfig; +use App\Models\Article; use App\Models\Calendar; use App\Models\CarparkLog; use App\Models\Company; @@ -15,6 +16,8 @@ use App\Models\CourseType; 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; @@ -101,6 +104,103 @@ class OtherController extends CommonController return $this->success(compact('courseTypes', 'schoolmate', 'company', 'industryTotal', 'suzhou', 'country')); } + /** + * @OA\Get( + * path="/api/admin/other/home-v2", + * tags={"其他"}, + * summary="驾驶舱V2", + * description="", + * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"), + * @OA\Response( + * response="200", + * description="暂无" + * ) + * ) + */ + public function homeV2() + { + // 校友总数 + $list['schoolmate_total'] = User::where('is_schoolmate', 1)->count(); + // 今年新增校友数 + $list['schoolmate_year'] = User::where('is_schoolmate', 1)->where('created_at', 'like', '%' . date('Y') . '%')->count(); + // 投后企业 + $list['company_invested_total'] = Company::where('is_yh_invested', 1)->count(); + // 元和员工参与 + // 元和员工参与企业 + $companyNameKeyword = ['元禾控股', '元禾原点', '元禾厚望', '元禾重元', '元禾璞华', '元禾谷风', '元禾绿柳', '元禾辰坤', '元禾沙湖', '禾裕集团', '苏州科服', '信诚管理咨询', '集成电路公司', '常州团队', '国企元禾']; + // 获取公司名字包含$companyNameKeyword任意数据的公司,需要模糊匹配 + $list['company_join_total'] = Company::where(function ($query) use ($companyNameKeyword) { + foreach ($companyNameKeyword as $item) { + $query->orWhere('company_name', 'like', '%' . $item . '%'); + } + })->count(); + // 全市干部参与企业 + $list['company_ganbu_total'] = Company::whereHas('users', function ($query) { + $query->where('from', '跟班学员'); + })->count(); + // 三个全覆盖 + // 苏州头部企业 + $list['cover_head_total'] = Company::where('company_tag', 'like', '%' . '高新技术企业' . '%')->count(); + // 高层次人才 + // 获取人才培训课程 + $renCaiCourseIds = Course::whereHas('typeDetail', function ($query) { + $query->where('name', '人才培训'); + })->pluck('id'); + $list['cover_rencai_total'] = CourseSign::whereIn('course_id', $renCaiCourseIds)->where('status', 1)->count(); + // 重点上市公司 + $list['cover_stock_total'] = Company::where('company_market', 1)->count(); + // 本月课程 + $monthCourses = Calendar::with('course.teacher')->where('type', 1) + ->where('date', 'like', '%' . date('Y-m') . '%') + ->get(); + // 课程统计 + $courseTypes = CourseType::where('is_chart', 1)->get(); + $start_date = CourseType::START_DATE; + $end_date = date('Y-m-d'); + foreach ($courseTypes as $courseType) { + // 课程 + $courses = Course::where('type', $courseType->id)->get(); + // 已开设期数 + $courseType->course_periods_total = Course::where('type', $courseType->id)->count(); + // 培养人数去重 + $courseType->course_signs_total = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses->pluck('id'), null, $userIds); + } + // 苏州区域数据 + $suzhouArea = Company::where('company_city', '苏州市')->groupBy('company_area') + ->whereNotNull('company_area') + ->get(['company_area']); + $suzhou = []; + foreach ($suzhouArea as $item) { + $suzhou[] = [ + 'company_area' => $item->company_area, + 'company_total' => User::whereHas('company', function ($query) use ($item) { + $query->where('company_area', $item->company_area); + })->where('is_schoolmate', 1)->count() + ]; + } + + // 全国数据 + $countryArea = Company::groupBy('company_city')->whereNotNull('company_city') + ->get(['company_city']); + $country = []; + foreach ($countryArea as $item) { + $country[] = [ + 'company_city' => $item->company_city, + 'company_total' => User::whereHas('company', function ($query) use ($item) { + $query->where('company_city', $item->company_city); + })->where('is_schoolmate', 1)->count() + ]; + } + + // 时间轴 + $time_axis = TimeEvent::orderBy('sort', 'asc')->get(); + // 动态信息 + $article['xiaoyou'] = Article::where('type', 1)->limit(7)->orderBy('sort', 'desc')->get(); + $article['yejie'] = Article::where('type', 2)->limit(7)->orderBy('sort', 'desc')->get(); + $article['supply_demands'] = SupplyDemand::limit(7)->orderBy('created_at', 'desc')->get(); + return $this->success(compact('list', 'courseTypes', 'suzhou', 'country', 'monthCourses', 'time_axis', 'article')); + } + /** * @OA\Get( * path="/api/admin/other/courses-home", @@ -130,18 +230,18 @@ class OtherController extends CommonController $course_type_id = CourseType::pluck('id')->toArray(); } // 课程 - $courses = Course::where('start_date', '>=', $start_date) - ->where('start_date', '<=', $end_date) - ->whereIn('type', $course_type_id) + $courses = Course::whereIn('type', $course_type_id) + // ->where('start_date', '<=', $end_date) + // ->where('start_date', '>=', $start_date) ->get(); // 被投企业数 $list['course_signs_invested'] = CourseSign::yhInvested($start_date, $end_date); // 报名人数 $list['course_signs_total'] = CourseSign::courseSignsTotal($start_date, $end_date); // 审核通过人数 - $list['course_signs_pass'] = CourseSign::courseSignsTotal($start_date, $end_date, 1); + $list['course_signs_pass'] = CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses->pluck('id')); // 审核通过人数去重 - $list['course_signs_pass_unique'] = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1); + $list['course_signs_pass_unique'] = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses->pluck('id'), null); // 开课场次 $calendar = Calendar::whereIn('course_id', $courses->pluck('id'))->whereBetween('date', [$start_date, $end_date])->get(); $list['course_total'] = $calendar->count(); @@ -156,34 +256,27 @@ class OtherController extends CommonController $courseTypes = CourseType::whereIn('id', $course_type_id)->get(); foreach ($courseTypes as $courseType) { // 获取课程 - $courses2 = Course::where('start_date', '>=', $start_date) - ->where('start_date', '<=', $end_date) - ->where('type', $courseType->id) + $courses2 = Course::where('type', $courseType->id) + // ->where('start_date', '<=', $end_date) + // ->where('start_date', '>=', $start_date) ->get(); foreach ($courses2 as $course) { $courseTypesSum[] = [ 'course_type' => $courseType->name, // 培养人数 - 'course_type_signs_pass' => CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses2->pluck('id')->toArray()), + 'course_type_signs_pass' => CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses2->pluck('id')), // 去重培养人数 - 'course_type_signs_pass_unique' => CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses2->pluck('id')->toArray()), + 'course_type_signs_pass_unique' => CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses2->pluck('id'), null), 'course_name' => $course->name, 'course_signs_pass' => CourseSign::courseSignsTotal($start_date, $end_date, 1, [$course->id]), ]; } - } // 区域明细统计 $areas = ParameterDetail::where('parameter_id', 5)->get(); foreach ($areas as $area) { - $courseSignByArea = CourseSign::where('status', 1) - ->whereHas('user', function ($query) use ($area) { - $query->where('company_area', $area->value); - })->whereDate('created_at', '>=', $start_date) - ->whereDate('created_at', '<=', $end_date) - ->get(); - $area->course_signs_pass = $courseSignByArea->count(); - $area->course_signs_pass_unique = User::whereIn('id', $courseSignByArea->pluck('user_id'))->distinct('mobile')->count(); + $area->course_signs_pass = CourseSign::courseSignsTotal($start_date, $end_date, 1, $courses->pluck('id'), $area->value); + $area->course_signs_pass_unique = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $courses->pluck('id'), $area->value); } return $this->success(compact('list', 'courseTypesSum', 'areas')); } @@ -305,7 +398,7 @@ class OtherController extends CommonController { $model = new DoorRepository(); $result = $model->getAllDoorInfo(); - dd(json_encode($result,JSON_UNESCAPED_UNICODE)); + dd(json_encode($result, JSON_UNESCAPED_UNICODE)); } diff --git a/app/Http/Controllers/Admin/StatisticsConfigController.php b/app/Http/Controllers/Admin/StatisticsConfigController.php new file mode 100644 index 0000000..65446a6 --- /dev/null +++ b/app/Http/Controllers/Admin/StatisticsConfigController.php @@ -0,0 +1,279 @@ +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="show_type", in="query", @OA\Schema(type="string"), required=false, description="显示类型:statistics-统计数据,list-列表数据,默认statistics"), + * @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())]); + } + + // 根据key查找配置 + $config = $this->model->where('key', $all['key'])->first(); + if (empty($config)) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '配置不存在']); + } + + try { + // 调用模型中的统计方法 + $params = [ + 'page' => isset($all['page']) ? (int) $all['page'] : 1, + 'page_size' => isset($all['page_size']) ? (int) $all['page_size'] : 10, + 'show_type' => isset($all['show_type']) ? $all['show_type'] : 'statistics', + ]; + + $result = $config->calculateStatistics($params); + + return $this->success($result); + + } catch (\Exception $exception) { + return $this->fail([$exception->getCode(), $exception->getMessage()]); + } + } + +} diff --git a/app/Http/Controllers/Admin/TimeEventController.php b/app/Http/Controllers/Admin/TimeEventController.php new file mode 100644 index 0000000..8ab2990 --- /dev/null +++ b/app/Http/Controllers/Admin/TimeEventController.php @@ -0,0 +1,219 @@ +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/time-event/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/time-event/save", + * tags={"时间轴"}, + * summary="保存", + * description="", + * @OA\Parameter(name="id", in="query", @OA\Schema(type="int"), required=true, description="Id(存在更新,不存在新增)"), + * @OA\Parameter(name="title", in="query", @OA\Schema(type="string", nullable=true), description="标题"), + * @OA\Parameter(name="content", in="query", @OA\Schema(type="string", nullable=true), description="内容"), + * @OA\Parameter(name="type", in="query", @OA\Schema(type="string", nullable=true), description="类型1校友动态2业界动态"), + * @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/time-event/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(); + } + +} diff --git a/app/Models/Article.php b/app/Models/Article.php new file mode 100644 index 0000000..e310ceb --- /dev/null +++ b/app/Models/Article.php @@ -0,0 +1,7 @@ + 'json', 'partners' => 'json']; + public function users() { return $this->hasMany(User::class, 'company_id', 'id'); diff --git a/app/Models/CourseSign.php b/app/Models/CourseSign.php index cd4c156..32dc1b9 100755 --- a/app/Models/CourseSign.php +++ b/app/Models/CourseSign.php @@ -4,6 +4,7 @@ namespace App\Models; +use Illuminate\Support\Facades\Log; use OwenIt\Auditing\Models\Audit; class CourseSign extends SoftDeletesModel @@ -77,30 +78,53 @@ class CourseSign extends SoftDeletesModel /** * 指定时间内的报名信息(未去重) */ - public static function courseSignsTotal($start_date, $end_date, $status = null, $course_ids = null) + public static function courseSignsTotal($start_date, $end_date, $status = null, $course_ids = null, $area = null, $retList = false) { $total = CourseSign::whereDate('created_at', '>=', $start_date) ->whereDate('created_at', '<=', $end_date) - ->where(function ($query) use ($status, $course_ids) { + ->whereHas('user', function ($query) use ($area) { + if (isset($area)) { + if ($area == '苏州市外') { + $allArea = ParameterDetail::where('parameter_id', 5)->get(); + $query->whereNotIn('company_area', $allArea->pluck('value')); + } else { + $query->where('company_area', $area); + } + } + })->where(function ($query) use ($status, $course_ids) { if (isset($status)) { $query->where('status', $status); } if (isset($course_ids)) { $query->whereIn('course_id', $course_ids); } - })->whereNotIn('status', [4, 5]) - ->count(); - return $total; + })->whereNotIn('status', [4, 5]); + if ($retList) { + // 返回列表 + return $total->get(); + } else { + // 返回统计数据 + return $total->count(); + } } /** * 指定时间内的报名信息(去重) */ - public static function courseSignsTotalByUnique($start_date, $end_date, $status = null, $course_ids = null) + public static function courseSignsTotalByUnique($start_date, $end_date, $status = null, $course_ids = null, $area = null, $retList = false) { $courseSignByType = CourseSign::whereDate('created_at', '>=', $start_date) ->whereDate('created_at', '<=', $end_date) - ->where(function ($query) use ($status, $course_ids) { + ->whereHas('user', function ($query) use ($area) { + if ($area) { + if ($area == '苏州市外') { + $allArea = ParameterDetail::where('parameter_id', 5)->get(); + $query->whereNotIn('company_area', $allArea->pluck('value')); + } else { + $query->where('company_area', $area); + } + } + })->where(function ($query) use ($status, $course_ids) { if (isset($status)) { $query->where('status', $status); } @@ -109,7 +133,14 @@ class CourseSign extends SoftDeletesModel } })->whereNotIn('status', [4, 5]) ->get(); - return User::whereIn('id', $courseSignByType->pluck('user_id'))->distinct('mobile')->count(); + $user = User::whereIn('id', $courseSignByType->pluck('user_id'))->distinct('mobile'); + if ($retList) { + // 列表 + return $user->get(); + } else { + // 统计数据 + return $user->count(); + } } /** @@ -130,7 +161,7 @@ class CourseSign extends SoftDeletesModel ->get(); return Company::whereHas('users', function ($query) use ($courseSignByType) { $query->whereIn('id', $courseSignByType->pluck('user_id')); - })->count(); + })->where('is_yh_invested', 1)->count(); } } diff --git a/app/Models/CourseType.php b/app/Models/CourseType.php index a5f560c..a0ebea1 100755 --- a/app/Models/CourseType.php +++ b/app/Models/CourseType.php @@ -6,6 +6,8 @@ namespace App\Models; class CourseType extends SoftDeletesModel { + const START_DATE = '2020-01-01 00:00:00'; + public function courses() { return $this->hasMany(Course::class, 'type', 'id'); diff --git a/app/Models/Sms.php b/app/Models/Sms.php index 9e8286c..c5ad5bc 100755 --- a/app/Models/Sms.php +++ b/app/Models/Sms.php @@ -4,7 +4,7 @@ namespace App\Models; use Illuminate\Support\Facades\Cache; -class Sms +class Sms extends SoftDeletesModel { /** * 检查IP是否被锁定 diff --git a/app/Models/StatisticsConfig.php b/app/Models/StatisticsConfig.php new file mode 100644 index 0000000..24fec25 --- /dev/null +++ b/app/Models/StatisticsConfig.php @@ -0,0 +1,587 @@ + 'json', + ]; + + /** + * 根据配置执行统计计算 + * + * @param array $params 参数数组,包含:page, page_size, show_type + * @return array 返回统计结果数组 + * @throws \Exception + */ + public function calculateStatistics($params = []) + { + $page = isset($params['page']) ? (int) $params['page'] : 1; + $pageSize = isset($params['page_size']) ? (int) $params['page_size'] : 10; + $showType = isset($params['show_type']) ? $params['show_type'] : 'statistics'; + if (!in_array($showType, ['statistics', 'list'])) { + $showType = 'statistics'; + } + + $configJson = $this->config_json; + if (empty($configJson)) { + throw new \Exception('配置数据为空'); + } + + // 如果 config_json 是字符串,尝试解析 + if (is_string($configJson)) { + $configJson = json_decode($configJson, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('配置 JSON 格式错误:' . json_last_error_msg()); + } + } + + // 检查数据结构 + if (!isset($configJson['data_source'])) { + throw new \Exception('配置数据缺少 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'])) : '无'; + throw new \Exception('主模型名称未配置,data_source 中的键:' . $dataSourceKeys); + } + $mainModel = $this->getModel($mainModelName); + if (empty($mainModel)) { + throw new \Exception('主模型不存在:' . $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, $this->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, $this->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, $this->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, $this->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), $this->decimal_places); + } else { + $statisticsResult = round($listQuery->sum($statisticsField), $this->decimal_places); + } + } else { + $statisticsResult = round($listQuery->sum($statisticsField), $this->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), $this->decimal_places); + } else { + $statisticsResult = round($listQuery->max($statisticsField), $this->decimal_places); + } + } else { + $statisticsResult = round($listQuery->max($statisticsField), $this->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), $this->decimal_places); + } else { + $statisticsResult = round($listQuery->min($statisticsField), $this->decimal_places); + } + } else { + $statisticsResult = round($listQuery->min($statisticsField), $this->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) + ]; + } + + // 根据显示类型决定返回的数据 + if ($showType === 'statistics') { + // 只返回统计数据 + return [ + 'total' => $statisticsResult, + 'sql' => $statisticsSqlFull + ]; + } else { + // 只返回列表数据 + return [ + 'list' => $data, + 'pagination' => $pagination, + 'sql' => $statisticsSqlFull + ]; + } + } + + /** + * 获取模型实例 + */ + 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/TimeEvent.php b/app/Models/TimeEvent.php new file mode 100644 index 0000000..00b36da --- /dev/null +++ b/app/Models/TimeEvent.php @@ -0,0 +1,7 @@ +>>>>>> 8508a4aca7d83d334aa2ec18b4291c6d5750a769 - -#检测系统里是否存在用户名是git的用户,如果不存在,就创建一个,并且设置密码为git@2023 -if ! id -u git > /dev/null 2>&1; then - useradd git - echo "已创建用户 git" - echo git:git@2023 | chpasswd - echo "密码已设置为 Git@2018" -else - echo "git用户已存在" -fi - -# 检测系统里是否存在git用户组,如果不存在,就创建一个,并把git用户加入git用户组 -if grep "^git:" /etc/group >/dev/null 2>&1; then - echo "git用户组已存在" -else - echo "创建git用户组..." - groupadd git - usermod -aG git git -fi - -# 根据定义的裸仓库路径,在指定路径下创建裸仓库 -echo "创建裸仓库..." -mkdir -p $GIT_REPO_NAME && cd $GIT_REPO_NAME && git init --bare - -# 给上一步创建的裸仓库给予git用户组里git用户读写执行权限 -echo "给裸仓库设置权限..." -chown git:git * -R -cd - -chown git:git "$GIT_REPO_NAME" - -# 切换到当前脚本所在的目录,初始化git仓库,并且关联之前创建的本地裸仓库地址。 -cd "$(dirname "$0")" -if [ -d ".git" ]; then - echo "该目录是一个Git仓库" -else - echo "该目录不是一个Git仓库" - echo "初始化git仓库..." - git init -fi -echo "关联仓库" -git remote add origin $GIT_REPO_NAME - -# 输出这个裸仓库的本地链接地址和远程连接地址 -echo "远程仓库关联:git remote add production ssh://git@ip:$GIT_REPO_NAME" diff --git a/coursesHome统计逻辑说明.md b/coursesHome统计逻辑说明.md new file mode 100644 index 0000000..03a264a --- /dev/null +++ b/coursesHome统计逻辑说明.md @@ -0,0 +1,306 @@ +# 课程统计数据说明文档 + +## 一、文档说明 + +本文档用于说明课程统计系统中各项数据的含义和统计方式,帮助您更好地理解和使用统计数据。 + +--- + +## 二、如何查询统计数据 + +### 2.1 查询方式 + +通过系统后台的"课程统计"功能,您可以查询以下信息: + +- **时间范围**:选择需要统计的开始日期和结束日期 +- **课程类型**:可以选择全部课程类型,也可以选择特定的课程类型进行统计 + +### 2.2 统计时间说明 + +- 统计数据基于**报名记录的创建时间**,而不是课程开课时间 +- 例如:选择2024年1月1日到2024年12月31日,会统计在这个时间段内创建的所有报名记录 + +--- + +## 三、核心统计数据说明 + +### 3.1 被投企业数 + +**含义**:在统计时间段内,有多少家元禾已投资的企业有员工报名参加了课程。 + +**统计方式**: +- 统计所有在指定时间段内报名的用户 +- 查看这些用户所在的公司 +- 统计其中被元禾投资的公司数量 +- **重要**:如果一家公司有多个员工报名,这家公司只统计一次 + +**使用场景**:了解元禾投资企业的参与情况 + +--- + +### 3.2 报名人数 + +**含义**:在统计时间段内,总共有多少条报名记录。 + +**统计方式**: +- 统计所有在指定时间段内创建的报名记录 +- 包括所有状态的报名(待审核、审核通过、审核不通过等) +- 不包括已取消和主动放弃的报名 +- **重要**:如果同一个人报名了多个课程,会按报名次数分别计算 + +**使用场景**:了解课程的整体报名热度 + +**举例**: +- 张三报名了3个课程 → 统计为3人 +- 李四报名了1个课程 → 统计为1人 +- 总计:4人 + +--- + +### 3.3 审核通过人数 + +**含义**:在统计时间段内,有多少条报名记录通过了审核。 + +**统计方式**: +- 只统计状态为"审核通过"的报名记录 +- 不包括已取消和主动放弃的报名 +- **重要**:如果同一个人报名了多个课程且都通过了,会按通过次数分别计算 + +**使用场景**:了解实际可以参加课程的人数 + +**举例**: +- 张三报名了3个课程,都通过了 → 统计为3人 +- 李四报名了1个课程,通过了 → 统计为1人 +- 总计:4人 + +--- + +### 3.4 审核通过人数(去重) + +**含义**:在统计时间段内,有多少个不同的用户通过了审核。 + +**统计方式**: +- 只统计状态为"审核通过"的报名记录 +- 根据用户的手机号进行去重 +- 如果同一个手机号报名了多个课程,只统计一次 +- **重要**:这是真实的培养人数,不会重复计算同一个人 + +**使用场景**:了解实际培养了多少个不同的学员 + +**举例**: +- 张三(手机号13800138000)报名了3个课程,都通过了 → 统计为1人 +- 李四(手机号13900139000)报名了1个课程,通过了 → 统计为1人 +- 总计:2人(去重后) + +--- + +### 3.5 开课场次 + +**含义**:在统计时间段内,总共开了多少场课程。 + +**统计方式**: +- 统计指定课程类型下,在指定时间范围内的所有开课记录 +- 统计数据源来自于课程日历上的数据 +- **重要**:统计的是开课场次,不是课程数量 + +**使用场景**:了解课程开展的频率和规模 + + +--- + +### 3.6 开课天数 + +**含义**:在统计时间段内,所有课程总共开了多少天。 + +**统计方式**: +- 对每场课程计算开课天数(从开始日期到结束日期) +- 包含开始日期和结束日期 +- 将所有场次的天数加起来 +- 统计数据源来自于课程日历上的数据 + + +**使用场景**:了解课程的总时长 + +**举例**: +- 某场课程从2024年1月1日到1月3日 → 3天(包含1日、2日、3日) +- 某场课程从2024年2月10日到2月12日 → 3天 +- 总计:6天 + +--- + +## 四、课程分类明细统计说明 + +### 4.1 统计内容 + +系统会按照每个课程类型,详细统计以下信息: + +#### 课程类型名称 +显示课程体系的名称,例如:"高研班"、"初创班"等。 + +#### 培养人数(未去重,按课程类型) +**含义**:该课程类型下所有课程的审核通过报名人数总和。 + +**说明**: +- 如果同一个学员在该课程类型下报名了多个课程,会按报名次数分别计算 +- 例如:张三在"高研班"类型下报名了2个课程,都通过了 → 统计为2人 + +#### 培养人数(去重,按课程类型) +**含义**:该课程类型下有多少个不同的学员通过了审核。 + +**说明**: +- 根据学员的手机号去重 +- 同一个手机号在该课程类型下只统计一次 +- 例如:张三(手机号13800138000)在"高研班"类型下报名了2个课程,都通过了 → 统计为1人 + +#### 课程名称 +显示具体课程的名称,例如:"2024年第一期高研班"。 + +#### 培养人数(按单个课程) +**含义**:该单个课程的审核通过报名人数。 + +**说明**: +- 只统计该课程的审核通过人数 +- 不包括其他课程的数据 + +--- + +## 五、区域明细统计说明 + +### 5.1 统计内容 + +系统会按照每个区域,详细统计以下信息: + +#### 区域名称 +显示区域名称,例如:"工业园区"、"高新区"、"相城区"等。 + +**特殊区域**:"苏州市外"表示不在苏州所有区域内的用户。 + +#### 审核通过人数(未去重,按区域) +**含义**:该区域有多少条审核通过的报名记录。 + +**说明**: +- 根据学员所在公司的区域进行统计 +- 如果同一个学员在该区域报名了多个课程,会按报名次数分别计算 +- 例如:张三(公司位于工业园区)报名了2个课程,都通过了 → 统计为2人 + +#### 审核通过人数(去重,按区域) +**含义**:该区域有多少个不同的学员通过了审核。 + +**说明**: +- 根据学员的手机号去重 +- 同一个手机号在该区域只统计一次 +- 例如:张三(手机号13800138000,公司位于工业园区)报名了2个课程,都通过了 → 统计为1人 + +--- + + +## 七、数据关系说明 + +### 7.1 人数关系 + +通常情况下,数据之间存在以下关系: + +**报名人数** ≥ **审核通过人数** ≥ **审核通过人数(去重)** + +**解释**: +- 报名人数最多,因为包括所有状态的报名 +- 审核通过人数次之,因为只包括审核通过的 +- 去重人数最少,因为同一个学员只统计一次 + +**举例**: +- 报名人数:500人(包括待审核、审核通过、审核不通过等) +- 审核通过人数:450人(只包括审核通过的) +- 审核通过人数(去重):380人(同一个学员只统计一次) + +### 7.2 区域汇总与全局去重的关系 + +**区域汇总** 可能大于 **全局去重人数** + +**原因**: +- 区域汇总时,每个区域是分别统计的 +- 而全局去重是按所有区域统一去重的 + +--- + +## 八、报名状态说明 + +### 8.1 报名状态类型 + +报名记录有以下几种状态: + +| 状态名称 | 说明 | 是否参与统计 | +|---------|------|------------| +| 待审核 | 报名已提交,等待审核 | 参与"报名人数"统计,不参与"审核通过人数"统计 | +| 审核通过 | 报名已通过审核,可以参加课程 | 参与所有统计 | +| 审核不通过 | 报名未通过审核 | 参与"报名人数"统计,不参与"审核通过人数"统计 | +| 备选 | 报名作为备选 | 参与"报名人数"统计,不参与"审核通过人数"统计 | +| 已取消 | 报名已取消 | 不参与任何统计 | +| 主动放弃 | 学员主动放弃报名 | 不参与任何统计 | +| 黑名单 | 学员在黑名单中 | 参与"报名人数"统计,不参与"审核通过人数"统计 | + +### 8.2 统计规则 + +- **报名人数**:统计除"已取消"和"主动放弃"外的所有状态 +- **审核通过人数**:只统计"审核通过"状态的记录 +- **所有统计**:都不包括"已取消"和"主动放弃"的记录 + +--- + +## 九、常见问题解答 + +### Q1: 为什么"报名人数"比"审核通过人数"多? + +**A**: 因为"报名人数"包括所有状态的报名记录(待审核、审核通过、审核不通过、备选等),而"审核通过人数"只包括审核通过的记录。所以报名人数会更多。 + +### Q2: 为什么"审核通过人数"比"审核通过人数(去重)"多? + +**A**: 因为"审核通过人数"是报名记录数,如果同一个学员报名了多个课程,会按报名次数分别计算。而"审核通过人数(去重)"是按学员手机号去重的,同一个学员只统计一次。 + + +### Q4: "被投企业数"是如何统计的? + +**A**: 先统计所有报名的学员,然后查看这些学员所在的公司,统计其中被元禾投资的公司数量。如果一家公司有多个员工报名,这家公司只统计一次。 + +### Q5: "开课天数"是如何计算的? + +**A**: 对每场课程,计算从开始日期到结束日期的天数(包含开始日期和结束日期),然后将所有场次的天数加起来。例如:某场课程从1月1日到1月3日,算3天。 + +### Q6: 统计时间范围是什么意思? + +**A**: 统计时间范围是指报名记录的创建时间,不是课程的开课时间。例如:选择2024年1月1日到12月31日,会统计在这个时间段内创建的所有报名记录,即使这些课程可能在2025年才开课。 + +### Q7: 为什么选择不同的课程类型,统计数据会不同? + +**A**: 因为统计数据是基于选择的课程类型进行筛选的。如果选择全部课程类型,会统计所有课程的数据;如果选择特定的课程类型,只统计该类型下的课程数据。 + +--- + +## 十、使用建议 + +### 10.1 查看整体情况 + +- 使用"报名人数"了解课程的整体报名热度 +- 使用"审核通过人数(去重)"了解实际培养了多少个不同的学员 +- 使用"开课场次"和"开课天数"了解课程开展的频率和时长 + +### 10.2 查看分类情况 + +- 使用"课程分类明细统计"了解不同课程类型的培养情况 +- 使用"区域明细统计"了解不同区域的参与情况 + +### 10.3 数据对比 + +- 对比"报名人数"和"审核通过人数",了解审核通过率 +- 对比"审核通过人数"和"审核通过人数(去重)",了解学员的参与深度 +- 对比不同课程类型的数据,了解各类课程的受欢迎程度 + +--- + +## 十一、注意事项 + +1. **时间范围选择**:统计数据基于报名记录的创建时间,不是课程开课时间 +2. **去重逻辑**:去重统计使用学员的手机号作为依据 +3. **区域统计**:区域统计基于学员所在公司的区域 +4. **数据关系**:不同统计数据之间可能存在包含关系,请注意理解 +5. **汇总数据**:区域汇总可能包含跨区域的重复学员,请注意区分 + diff --git a/database/migrations/2025_11_11_140323_alert_companies_table.php b/database/migrations/2025_11_11_140323_alert_companies_table.php new file mode 100644 index 0000000..88ad941 --- /dev/null +++ b/database/migrations/2025_11_11_140323_alert_companies_table.php @@ -0,0 +1,37 @@ +dateTime('update_date')->nullable()->comment('更新日期'); + // 管理平台 + $table->json('project_users')->nullable()->comment('管理平台'); + // 股东信息 + $table->json('partners')->nullable()->comment('股东信息'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('companies', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2025_11_17_173017_create_articles_table.php b/database/migrations/2025_11_17_173017_create_articles_table.php new file mode 100644 index 0000000..82b2cdd --- /dev/null +++ b/database/migrations/2025_11_17_173017_create_articles_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('title')->comment('标题'); + $table->mediumText('content')->nullable()->comment('内容'); + // 类型 + $table->tinyInteger('type')->default(0)->comment('类型1校友动态2业界动态'); + // 排序 + $table->integer('sort')->default(0)->comment('排序'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('articles'); + } +}; diff --git a/database/migrations/2025_11_18_160801_add_days_to_calendars_table.php b/database/migrations/2025_11_18_160801_add_days_to_calendars_table.php new file mode 100644 index 0000000..0de0832 --- /dev/null +++ b/database/migrations/2025_11_18_160801_add_days_to_calendars_table.php @@ -0,0 +1,32 @@ +decimal('days', 10, 1)->nullable()->comment('天数')->after('end_time'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('calendars', function (Blueprint $table) { + $table->dropColumn('days'); + }); + } +}; diff --git a/database/migrations/2025_11_18_161527_add_fields_to_courses_table.php b/database/migrations/2025_11_18_161527_add_fields_to_courses_table.php new file mode 100644 index 0000000..eeaefce --- /dev/null +++ b/database/migrations/2025_11_18_161527_add_fields_to_courses_table.php @@ -0,0 +1,33 @@ +tinyInteger('is_ganbu')->nullable()->default(0)->comment('是否干部课程-0否1是'); + $table->tinyInteger('is_chart')->nullable()->default(1)->comment('是否参与统计-0否1是'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('courses', function (Blueprint $table) { + $table->dropColumn(['is_ganbu', 'is_chart']); + }); + } +}; diff --git a/database/migrations/2025_11_18_162248_create_time_events_table.php b/database/migrations/2025_11_18_162248_create_time_events_table.php new file mode 100644 index 0000000..eeec311 --- /dev/null +++ b/database/migrations/2025_11_18_162248_create_time_events_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('title')->comment('标题'); + $table->date('date')->nullable()->comment('日期'); + $table->integer('sort')->default(0)->comment('排序'); + $table->mediumText('content')->nullable()->comment('内容'); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('time_events'); + } +}; 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 19cf915..318ef4e 100755 --- a/routes/api.php +++ b/routes/api.php @@ -35,7 +35,10 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () { Route::get('users/index', [\App\Http\Controllers\Admin\UserController::class, "index"]); Route::get('other/table-fileds', [\App\Http\Controllers\Admin\OtherController::class, "tableFileds"]); Route::get('other/home', [\App\Http\Controllers\Admin\OtherController::class, "home"]); + Route::get('other/home-v2', [\App\Http\Controllers\Admin\OtherController::class, "homeV2"]); + Route::get('other/courses-home', [\App\Http\Controllers\Admin\OtherController::class, "coursesHome"]); + Route::get('other/courses-home-export', [\App\Http\Controllers\Admin\OtherController::class, "coursesHomeExport"]); // 验证码登陆 Route::get('auth/sms-login', [\App\Http\Controllers\Admin\AuthController::class, "smsLogin"]); @@ -228,6 +231,7 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () { Route::post('email-record/excel-show', [\App\Http\Controllers\Admin\EmailRecordController::class, "excelShow"]); // 企业管理 + Route::get('company/config', [\App\Http\Controllers\Admin\CompanyController::class, "config"]); Route::get('company/index', [\App\Http\Controllers\Admin\CompanyController::class, "index"]); Route::get('company/show', [\App\Http\Controllers\Admin\CompanyController::class, "show"]); Route::post('company/save', [\App\Http\Controllers\Admin\CompanyController::class, "save"]); @@ -238,6 +242,25 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () { Route::get('course-content-check/show', [\App\Http\Controllers\Admin\CourseContentCheckController::class, "show"]); Route::post('course-content-check/save', [\App\Http\Controllers\Admin\CourseContentCheckController::class, "save"]); Route::get('course-content-check/destroy', [\App\Http\Controllers\Admin\CourseContentCheckController::class, "destroy"]); + + // 文章管理 + Route::get('article/index', [\App\Http\Controllers\Admin\ArticleController::class, "index"]); + Route::get('article/show', [\App\Http\Controllers\Admin\ArticleController::class, "show"]); + Route::post('article/save', [\App\Http\Controllers\Admin\ArticleController::class, "save"]); + Route::get('article/destroy', [\App\Http\Controllers\Admin\ArticleController::class, "destroy"]); + + // 时间轴管理 + Route::get('time-event/index', [\App\Http\Controllers\Admin\TimeEventController::class, "index"]); + 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"]); }); }); @@ -287,14 +310,12 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () { Route::post('user/update-donates', [\App\Http\Controllers\Mobile\UserController::class, "updateDonates"]); // 课程信息 - Route::get('course/evaluation-detail', [\App\Http\Controllers\Mobile\CourseController::class, "evaluationDetail"]); Route::post('course/sign', [\App\Http\Controllers\Mobile\CourseController::class, "sign"]); Route::get('course/my-course', [\App\Http\Controllers\Mobile\CourseController::class, "myCourse"]); Route::get('course/my-course-content', [\App\Http\Controllers\Mobile\CourseController::class, "myCourseContent"]); - Route::post('course/course-form', [\App\Http\Controllers\Mobile\CourseController::class, "courseForm"]); Route::get('course/get-sign', [\App\Http\Controllers\Mobile\CourseController::class, "getSign"]); Route::post('course/update-sign', [\App\Http\Controllers\Mobile\CourseController::class, "updateSign"]); Route::post('course/course-content-form', [\App\Http\Controllers\Mobile\CourseController::class, "courseContentForm"]); 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 +