diff --git a/app/Console/Commands/SyncCompany.php b/app/Console/Commands/SyncCompany.php new file mode 100644 index 0000000..3874ce9 --- /dev/null +++ b/app/Console/Commands/SyncCompany.php @@ -0,0 +1,89 @@ +syncAllCompanies(); + return $this->info('全量同步完成'); + } + + /** + * 全量同步公司信息 + */ + public function syncAllCompanies() + { + // 获取所有有公司名称的用户(全量同步,不限制company_id) + $users = User::whereNotNull('company_name') + ->orderBy('id', 'desc') + ->get(); + + $total = $users->count(); + if ($total == 0) { + return $this->info('没有需要同步的用户'); + } + + $this->info("开始全量同步公司信息,共 {$total} 个用户"); + $bar = $this->output->createProgressBar($total); + $bar->start(); + + $successCount = 0; + $failCount = 0; + + foreach ($users as $user) { + // 调用模型方法同步公司信息(不包含经纬度和地址) + $result = Company::syncCompanyFromUser($user); + + if ($result['success']) { + $successCount++; + $bar->setMessage($result['company']->company_name . ' 同步成功', 'status'); + } else { + $failCount++; + $bar->setMessage($user->company_name . ' ' . $result['message'], 'status'); + } + $bar->advance(); + } + + $bar->finish(); + $this->newLine(); + $this->info("同步完成:成功 {$successCount} 个,失败 {$failCount} 个"); + return $this->info('公司信息-全量同步完成'); + } +} + diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index bf141d1..bbf5911 100755 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -32,6 +32,8 @@ class Kernel extends ConsoleKernel $schedule->command('auto_schoolmate')->everyThirtyMinutes(); // 更新公司信息 $schedule->command('update_company')->everyTenMinutes(); + // 全量同步公司信息(每天凌晨执行,不同步经纬度和地址) + $schedule->command('sync:company')->dailyAt('02:00'); // 同步老师课程方向 $schedule->command('sync:teacher_direction')->hourly(); } diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index ba62b16..9b5b557 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -407,7 +407,11 @@ class OtherController extends CommonController // 区域明细统计 $areas = CourseSign::area($start_date, $end_date, 1, $courses->pluck('id'), true); - return $this->success(compact('list', 'courseTypesSum', 'areas')); + + // 获取统计项元数据 + $statistics_metadata = Course::getStatisticsMetadata(); + + return $this->success(compact('list', 'courseTypesSum', 'areas', 'statistics_metadata')); } /** @@ -449,8 +453,79 @@ class OtherController extends CommonController switch ($export_type) { case 'course_signs_invested': // 被投企业明细 - 使用与coursesHome相同的算法 - $companies = CourseSign::yhInvestedTotal(CourseType::START_DATE, $end_date, $course_ids, true); + // 确保 $course_ids 是数组格式 + $courseIdsArray = $course_ids ? (is_array($course_ids) ? $course_ids : $course_ids->toArray()) : null; + $companies = CourseSign::yhInvestedTotal(CourseType::START_DATE, $end_date, $courseIdsArray, true); foreach ($companies as $company) { + // 获取该公司的学员信息 + // 使用与yhInvestedTotal相同的筛选逻辑:status=1,筛选course_ids和日期范围 + // 直接通过公司ID查询学员 + $userIds = User::where('company_id', $company->id)->pluck('id')->toArray(); + + if (empty($userIds)) { + // 如果没有学员,设置空值 + $userNamesStr = ''; + $courseNamesStr = ''; + $courseTypesStr = ''; + $totalCourseCount = 0; + } else { + $userCourseSigns = CourseSign::getStudentList(CourseType::START_DATE, $end_date, 1, $courseIdsArray) + ->whereIn('user_id', $userIds) + ->with(['user', 'course.typeDetail']) + ->get(); + + // 按学员分组 + $usersData = []; + foreach ($userCourseSigns as $sign) { + if (!$sign->user) { + continue; + } + $userId = $sign->user_id; + if (!isset($usersData[$userId])) { + $usersData[$userId] = [ + 'user' => $sign->user, + 'courseSigns' => [], + ]; + } + $usersData[$userId]['courseSigns'][] = $sign; + } + + // 收集所有学员的姓名、课程名称、课程体系 + $userNames = []; + $allCourseNames = []; + $allCourseTypes = []; + $totalCourseCount = 0; + + foreach ($usersData as $userData) { + $user = $userData['user']; + $courseSigns = collect($userData['courseSigns']); + + if ($courseSigns->isNotEmpty()) { + $userNames[] = $user->name ?? ''; + + // 收集课程名称 + $courseNames = $courseSigns->pluck('course.name')->filter()->unique()->values()->toArray(); + $allCourseNames = array_merge($allCourseNames, $courseNames); + + // 收集课程体系 + $courseTypes = $courseSigns->pluck('course.typeDetail.name') + ->filter() + ->unique() + ->values() + ->toArray(); + $allCourseTypes = array_merge($allCourseTypes, $courseTypes); + + // 累计报名课程数 + $totalCourseCount += $courseSigns->count(); + } + } + + // 去重并合并 + $userNamesStr = implode('、', array_filter(array_unique($userNames))); + $courseNamesStr = implode('、', array_filter(array_unique($allCourseNames))); + $courseTypesStr = implode('、', array_filter(array_unique($allCourseTypes))); + } + $data[] = [ 'company_name' => $company->company_name, 'company_legal_representative' => $company->company_legal_representative ?? '', @@ -461,6 +536,10 @@ class OtherController extends CommonController 'contact_mail' => $company->contact_mail ?? '', 'company_tag' => $company->company_tag ?? '', 'credit_code' => ' ' . $company->credit_code ?? '', + 'user_names' => $userNamesStr, + 'course_names' => $courseNamesStr, + 'course_types' => $courseTypesStr, + 'course_count' => $totalCourseCount, ]; } $fields = [ @@ -473,6 +552,10 @@ class OtherController extends CommonController 'contact_mail' => '联系邮箱', 'company_tag' => '企业资质', 'credit_code' => '统一社会信用代码', + 'user_names' => '学员姓名', + 'course_names' => '课程名称', + 'course_types' => '课程体系', + 'course_count' => '报名课程数', ]; $filename = '被投企业明细'; break; @@ -1398,9 +1481,33 @@ class OtherController extends CommonController case 'cover_head_total': // 苏州头部企业明细 - 使用模型方法 - $companies = CourseSign::toubuqiye($start_date, $end_date, $course_ids, true); + // 确保 $course_ids 是数组格式 + $courseIdsArray = $course_ids ? (is_array($course_ids) ? $course_ids : $course_ids->toArray()) : null; + $companies = CourseSign::toubuqiye($start_date, $end_date, $courseIdsArray, true); + + // 预先获取所有符合条件的课程报名记录(与toubuqiye方法逻辑一致) + $courseSignsQuery = CourseSign::getStudentList($start_date, $end_date, 1, $courseIdsArray); + $allCourseSigns = $courseSignsQuery->with(['user', 'course.typeDetail'])->get(); + + // 按公司ID分组课程报名记录 + $companyCourseSigns = []; + foreach ($allCourseSigns as $sign) { + if (!$sign->user || !$sign->user->company) { + continue; + } + $companyId = $sign->user->company->id; + if (!isset($companyCourseSigns[$companyId])) { + $companyCourseSigns[$companyId] = []; + } + $companyCourseSigns[$companyId][] = $sign; + } + foreach ($companies as $company) { - $data[] = [ + // 获取该公司的学员信息(只包含有课程报名的学员) + $userCourseSigns = collect($companyCourseSigns[$company->id] ?? []); + + // 公司基本信息(只在第一行使用) + $companyInfo = [ 'company_name' => $company->company_name, 'company_legal_representative' => $company->company_legal_representative ?? '', 'company_date' => $company->company_date ?? '', @@ -1412,6 +1519,85 @@ class OtherController extends CommonController 'contact_phone' => $company->contact_phone ?? '', 'contact_mail' => $company->contact_mail ?? '', ]; + + if ($userCourseSigns->isEmpty()) { + // 如果没有学员,仍然导出公司基本信息 + $data[] = array_merge($companyInfo, [ + 'user_name' => '', + 'course_names' => '', + 'course_types' => '', + 'course_count' => 0, + ]); + } else { + // 按学员分组 + $usersData = []; + foreach ($userCourseSigns as $sign) { + if (!$sign->user) { + continue; + } + $userId = $sign->user_id; + if (!isset($usersData[$userId])) { + $usersData[$userId] = [ + 'user' => $sign->user, + 'courseSigns' => [], + ]; + } + $usersData[$userId]['courseSigns'][] = $sign; + } + + // 每个学员一行 + $isFirstRow = true; + foreach ($usersData as $userData) { + $user = $userData['user']; + $courseSigns = collect($userData['courseSigns']); + + if ($courseSigns->isEmpty()) { + continue; + } + + // 获取课程名称列表,用中文顿号分隔 + $courseNames = $courseSigns->pluck('course.name')->filter()->unique()->values()->implode('、'); + + // 获取课程体系列表,用中文顿号分隔 + $courseTypes = $courseSigns->pluck('course.typeDetail.name') + ->filter() + ->unique() + ->values() + ->implode('、'); + + // 报名课程数 + $courseCount = $courseSigns->count(); + + if ($isFirstRow) { + // 第一行:显示公司信息 + $data[] = array_merge($companyInfo, [ + 'user_name' => $user->name ?? '', + 'course_names' => $courseNames, + 'course_types' => $courseTypes, + 'course_count' => $courseCount, + ]); + $isFirstRow = false; + } else { + // 后续行:公司信息为空 + $data[] = [ + 'company_name' => '', + 'company_legal_representative' => '', + 'company_date' => '', + 'company_address' => '', + 'company_city' => '', + 'company_area' => '', + 'company_tag' => '', + 'business_scope' => '', + 'contact_phone' => '', + 'contact_mail' => '', + 'user_name' => $user->name ?? '', + 'course_names' => $courseNames, + 'course_types' => $courseTypes, + 'course_count' => $courseCount, + ]; + } + } + } } $fields = [ 'company_name' => '企业名称', @@ -1424,16 +1610,68 @@ class OtherController extends CommonController 'business_scope' => '营业范围', 'contact_phone' => '联系电话', 'contact_mail' => '联系邮箱', + 'user_name' => '学员姓名', + 'course_names' => '课程名称', + 'course_types' => '课程体系', + 'course_count' => '报名课程数', ]; $filename = '苏州头部企业明细'; break; case 'cover_rencai_total': // 高层次人才明细 - 使用模型方法 - $users = CourseSign::rencai($start_date, $end_date, $course_ids, true); + // 确保 $course_ids 是数组格式 + $courseIdsArray = $course_ids ? (is_array($course_ids) ? $course_ids : $course_ids->toArray()) : null; + $users = CourseSign::rencai($start_date, $end_date, $courseIdsArray, true); // 加载关联关系 $users->load('company'); + + // 获取所有用户的课程报名记录(使用与rencai相同的筛选条件) + $userIds = $users->pluck('id')->toArray(); + $userCourseSigns = CourseSign::getStudentList($start_date, $end_date, 1, $courseIdsArray) + ->whereIn('user_id', $userIds) + ->with(['user', 'course.typeDetail']) + ->get(); + + // 按用户ID分组课程报名记录 + $userCourseSignsMap = []; + foreach ($userCourseSigns as $sign) { + if (!$sign->user) { + continue; + } + $userId = $sign->user_id; + if (!isset($userCourseSignsMap[$userId])) { + $userCourseSignsMap[$userId] = []; + } + $userCourseSignsMap[$userId][] = $sign; + } + foreach ($users as $user) { + // 获取该用户的课程报名记录 + $courseSigns = collect($userCourseSignsMap[$user->id] ?? []); + + if ($courseSigns->isEmpty()) { + // 如果没有课程报名记录,设置空值 + $courseNamesStr = ''; + $courseTypesStr = ''; + $totalCourseCount = 0; + } else { + // 收集课程名称 + $courseNames = $courseSigns->pluck('course.name')->filter()->unique()->values()->toArray(); + $courseNamesStr = implode('、', array_filter($courseNames)); + + // 收集课程体系 + $courseTypes = $courseSigns->pluck('course.typeDetail.name') + ->filter() + ->unique() + ->values() + ->toArray(); + $courseTypesStr = implode('、', array_filter($courseTypes)); + + // 统计报名课程数 + $totalCourseCount = $courseSigns->count(); + } + $data[] = [ 'user_name' => $user->name ?? '', 'mobile' => $user->mobile ?? '', @@ -1442,6 +1680,9 @@ class OtherController extends CommonController 'company_city' => $user->company->company_city ?? '', 'company_position' => $user->company_position ?? '', 'education' => $user->education ?? '', + 'course_names' => $courseNamesStr, + 'course_types' => $courseTypesStr, + 'course_count' => $totalCourseCount, ]; } $fields = [ @@ -1452,6 +1693,9 @@ class OtherController extends CommonController 'company_city' => '所在城市', 'company_position' => '职位', 'education' => '学历', + 'course_names' => '课程名称', + 'course_types' => '课程体系', + 'course_count' => '报名课程数', ]; $filename = '高层次人才明细'; break; @@ -1461,7 +1705,10 @@ class OtherController extends CommonController $companiesData = CourseSign::shangshi($start_date, $end_date, $course_ids, true); foreach ($companiesData as $item) { $company = $item['company']; - $data[] = [ + $users = $item['users'] ?? []; + + // 公司基本信息(只在第一行使用) + $companyInfo = [ 'company_name' => $company->company_name, 'company_legal_representative' => $company->company_legal_representative ?? '', 'company_date' => $company->company_date ?? '', @@ -1469,10 +1716,74 @@ class OtherController extends CommonController 'company_address' => $company->company_address ?? '', 'company_city' => $company->company_city ?? '', 'company_area' => $company->company_area ?? '', + 'company_tag' => $company->company_tag ?? '', 'business_scope' => $company->business_scope ?? '', 'contact_phone' => $company->contact_phone ?? '', 'contact_mail' => $company->contact_mail ?? '', ]; + + if (empty($users)) { + // 如果没有学员,仍然导出公司基本信息 + $data[] = array_merge($companyInfo, [ + 'user_name' => '', + 'course_names' => '', + 'course_types' => '', + 'course_count' => 0, + ]); + } else { + // 每个学员一行 + $isFirstRow = true; + foreach ($users as $userInfo) { + $user = $userInfo['user'] ?? null; + $courses = $userInfo['courses'] ?? []; + + if (empty($courses)) { + // 如果没有课程,仍然显示学员信息 + $courseNames = ''; + $courseTypes = ''; + $courseCount = 0; + } else { + // 收集课程名称 + $courseNames = collect($courses)->pluck('course_name')->filter()->unique()->values()->implode('、'); + + // 收集课程体系 + $courseTypes = collect($courses)->pluck('course_type')->filter()->unique()->values()->implode('、'); + + // 报名课程数 + $courseCount = count($courses); + } + + if ($isFirstRow) { + // 第一行:显示公司信息 + $data[] = array_merge($companyInfo, [ + 'user_name' => $userInfo['user_name'] ?? ($user->name ?? ''), + 'course_names' => $courseNames, + 'course_types' => $courseTypes, + 'course_count' => $courseCount, + ]); + $isFirstRow = false; + } else { + // 后续行:公司信息为空 + $data[] = [ + 'company_name' => '', + 'company_legal_representative' => '', + 'company_date' => '', + 'stock_date' => '', + 'company_address' => '', + 'company_city' => '', + 'company_area' => '', + 'company_tag' => '', + 'business_scope' => '', + 'contact_phone' => '', + 'contact_mail' => '', + 'user_name' => $userInfo['user_name'] ?? ($user->name ?? ''), + 'course_names' => $courseNames, + 'course_types' => $courseTypes, + 'course_count' => $courseCount, + ]; + } + } + } } $fields = [ 'company_name' => '企业名称', @@ -1482,9 +1793,14 @@ class OtherController extends CommonController 'company_address' => '地址', 'company_city' => '所在城市', 'company_area' => '所在区域', + 'company_tag' => '企业资质', 'business_scope' => '营业范围', 'contact_phone' => '联系电话', 'contact_mail' => '联系邮箱', + 'user_name' => '学员姓名', + 'course_names' => '课程名称', + 'course_types' => '课程体系', + 'course_count' => '报名课程数', ]; $filename = '重点上市公司明细'; break; diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 08025a0..629c7e1 100755 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -57,79 +57,79 @@ class UserController extends BaseController $all = request()->all(); $list = $this->model->with(underlineToHump($all['show_relation'] ?? [])) ->with([ - 'courseSigns' => function ($query) use ($all) { - $query->where('status', 1)->with('course.teacher', 'course.typeDetail'); - } - ])->where(function ($query) use ($all) { - if (isset($all['keyword'])) { - $query->whereHas('courses', function ($q) use ($all) { - $q->where('name', 'like', '%' . $all['keyword'] . '%'); - })->orWhere('name', 'like', '%' . $all['keyword'] . '%'); - } - if (isset($all['has_course']) && $all['has_course'] == 1) { - $query->whereHas('courseSigns', function ($q) { - $q->where('status', 1); - }); - } - 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)) { + 'courseSigns' => function ($query) use ($all) { + $query->where('status', 1)->with('course.teacher', 'course.typeDetail'); + } + ])->where(function ($query) use ($all) { + if (isset($all['keyword'])) { + $query->whereHas('courses', function ($q) use ($all) { + $q->where('name', 'like', '%' . $all['keyword'] . '%'); + })->orWhere('name', 'like', '%' . $all['keyword'] . '%'); + } + if (isset($all['has_course']) && $all['has_course'] == 1) { + $query->whereHas('courseSigns', function ($q) { + $q->where('status', 1); + }); + } + 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 . '%'); + } + // null搜索 + if ($op == 'null') { + $query->whereNull($key); + } + // notnull搜索 + if ($op == 'notnull') { + $query->whereNotNull($key); + } + // 范围搜索 + if ($op == 'range') { + list($from, $to) = explode(',', $value); + if (empty($from) || empty($to)) { 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 . '%'); - } - // null搜索 - if ($op == 'null') { - $query->whereNull($key); - } - // notnull搜索 - if ($op == 'notnull') { - $query->whereNotNull($key); - } - // 范围搜索 - if ($op == 'range') { - list($from, $to) = explode(',', $value); - if (empty($from) || empty($to)) { - continue; - } - $query->whereBetween($key, [$from, $to]); - } + $query->whereBetween($key, [$from, $to]); } } - })->orderBy($all['sort_name'] ?? 'id', $all['sort_type'] ?? 'desc'); + } + })->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'] ?? []; @@ -168,25 +168,26 @@ class UserController extends BaseController * @OA\Parameter(name="courses_end_date", in="query", @OA\Schema(type="string"), required=false, description="课程结束时间"), * @OA\Parameter(name="is_vip", in="query", @OA\Schema(type="string"), required=false, description="是否vip0否1是"), * @OA\Parameter(name="courses_ing", in="query", @OA\Schema(type="string"), required=false, description="是否课程进行中0否1是"), - * @OA\Parameter(name="is_schoolmate", in="query", @OA\Schema(type="string"), required=false, description="is_schoolmate"), - * @OA\Parameter(name="mobile", in="query", @OA\Schema(type="string"), required=true, description="mobile"), - * @OA\Parameter(name="status", in="query", @OA\Schema(type="string"), required=true, description="审核状态"), - * @OA\Parameter(name="course_type", in="query", @OA\Schema(type="string"), required=true, description="course_type"), - * @OA\Parameter(name="company_has_share", in="query", @OA\Schema(type="string"), required=true, description="是否有股份"), - * @OA\Parameter(name="keyword", in="query", @OA\Schema(type="string"), required=true, description="关键词"), - * @OA\Parameter(name="start_company_date", in="query", @OA\Schema(type="string"), required=true, description="开始成立日期"), - * @OA\Parameter(name="end_company_date", in="query", @OA\Schema(type="string"), required=true, description="结束成立日期"), - * @OA\Parameter(name="start_birthday", in="query", @OA\Schema(type="string"), required=true, description="开始出生日期"), - * @OA\Parameter(name="end_birthday", in="query", @OA\Schema(type="string"), required=true, description="结束出生日期"), + * @OA\Parameter(name="is_schoolmate", in="query", @OA\Schema(type="string"), required=false, description="是否校友0否1是"), + * @OA\Parameter(name="mobile", in="query", @OA\Schema(type="string"), required=false, description="手机号"), + * @OA\Parameter(name="status", in="query", @OA\Schema(type="string"), required=false, description="审核状态:0待审核,1通过,2不通过"), + * @OA\Parameter(name="course_type", in="query", @OA\Schema(type="string"), required=false, description="课程体系ID"), + * @OA\Parameter(name="company_has_share", in="query", @OA\Schema(type="string"), required=false, description="是否有股份0否1是"), + * @OA\Parameter(name="keyword", in="query", @OA\Schema(type="string"), required=false, description="关键词(搜索学校、专业、介绍)"), + * @OA\Parameter(name="start_company_date", in="query", @OA\Schema(type="string"), required=false, description="开始成立日期"), + * @OA\Parameter(name="end_company_date", in="query", @OA\Schema(type="string"), required=false, description="结束成立日期"), + * @OA\Parameter(name="start_birthday", in="query", @OA\Schema(type="string"), required=false, description="开始出生日期"), + * @OA\Parameter(name="end_birthday", in="query", @OA\Schema(type="string"), required=false, description="结束出生日期"), * @OA\Parameter(name="sign_start_date", in="query", @OA\Schema(type="string"), required=false, description="报名开始时间"), * @OA\Parameter(name="sign_end_date", in="query", @OA\Schema(type="string"), required=false, description="报名结束时间"), - * @OA\Parameter(name="company_need_fund", in="query", @OA\Schema(type="string"), required=true, description="是否需要融资"), - * @OA\Parameter(name="is_fee", in="query", @OA\Schema(type="string"), required=true, description="是否缴费0否1是"), - * @OA\Parameter(name="has_openid", in="query", @OA\Schema(type="string"), required=true, description="是否绑定小程序0否1是"), - * @OA\Parameter(name="year", in="query", @OA\Schema(type="string"), required=true, description="年份"), - * @OA\Parameter(name="is_black", in="query", @OA\Schema(type="string"), required=true, description="是否黑名单0否1是"), - * @OA\Parameter(name="is_yh_invested", in="query", @OA\Schema(type="string"), required=true, description="是否元和已投企业0否1是"), - * @OA\Parameter(name="company_tag", in="query", @OA\Schema(type="string"), required=true, description="企业标签"), + * @OA\Parameter(name="company_need_fund", in="query", @OA\Schema(type="string"), required=false, description="是否需要融资0否1是"), + * @OA\Parameter(name="is_fee", in="query", @OA\Schema(type="string"), required=false, description="是否缴费0否1是"), + * @OA\Parameter(name="has_openid", in="query", @OA\Schema(type="string"), required=false, description="是否绑定小程序0否1是"), + * @OA\Parameter(name="year", in="query", @OA\Schema(type="string"), required=false, description="年份,默认为当前年份"), + * @OA\Parameter(name="is_black", in="query", @OA\Schema(type="string"), required=false, description="是否黑名单0否1是"), + * @OA\Parameter(name="is_yh_invested", in="query", @OA\Schema(type="string"), required=false, description="是否元和已投企业0否1是"), + * @OA\Parameter(name="is_company_market", in="query", @OA\Schema(type="string"), required=false, description="是否上市公司0否1是"), + * @OA\Parameter(name="company_tag", in="query", @OA\Schema(type="string"), required=false, description="企业标签"), * @OA\Parameter(name="talent_tags", 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( @@ -210,10 +211,10 @@ class UserController extends BaseController 'company' ) ->with([ - 'courseSigns' => function ($query) { - $query->with('course.typeDetail')->orderBy('fee_status', 'desc'); - } - ])->withCount([ + 'courseSigns' => function ($query) { + $query->with('course.typeDetail')->orderBy('fee_status', 'desc'); + } + ])->withCount([ 'appointments' => function ($query) { $query->whereIn('status', [0, 1]); } @@ -228,6 +229,16 @@ class UserController extends BaseController } }); } + // 是否上市公司 + if (isset($all['is_company_market'])) { + $list = $list->whereHas('company', function ($query) use ($all) { + if ($all['is_company_market'] == 1) { + $query->where('company_market', 1); + } else { + $query->where('company_market', 0); + } + }); + } // company_tag等价于company_type,新旧数据兼容 if (isset($all['company_type'])) { $all['company_tag'] = $all['company_type']; diff --git a/app/Models/Company.php b/app/Models/Company.php index e289ffc..71adeef 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -176,11 +176,15 @@ class Company extends SoftDeletesModel } /** - * 根据用户信息更新公司信息 + * 根据用户信息更新/同步公司信息(统一方法) * @param User $user 用户对象 + * @param bool $skipIfHasCompany 如果已有公司关联(company_id > 0)是否跳过,默认true + * @param bool $updateAddress 是否更新地址,默认true + * @param bool $updateLocation 是否更新经纬度,默认true + * @param bool $setCompanyIdOnFail 失败时是否设置company_id=0,默认true * @return array 返回结果 ['success' => bool, 'message' => string, 'company' => Company|null] */ - public static function updateCompanyFromUser($user) + public static function updateCompanyFromUser($user, $skipIfHasCompany = true, $updateAddress = true, $updateLocation = true, $setCompanyIdOnFail = true) { if (!$user || empty($user->company_name)) { return ['success' => false, 'message' => '用户或公司名称为空', 'company' => null]; @@ -188,7 +192,7 @@ class Company extends SoftDeletesModel // 如果已经有有效的公司关联(company_id > 0),跳过 // 允许处理 company_id = -1(待更新)或 null(初始状态)的情况 - if ($user->company_id && $user->company_id > 0) { + if ($skipIfHasCompany && $user->company_id && $user->company_id > 0) { return ['success' => false, 'message' => '用户已有公司关联', 'company' => null]; } @@ -213,27 +217,32 @@ class Company extends SoftDeletesModel if (!$result) { // 标识一下未匹配到公司,后续可以根据这个字段筛选出未匹配到公司的用户 - $user->company_id = 0; - $user->save(); + if ($setCompanyIdOnFail) { + $user->company_id = 0; + $user->save(); + } return ['success' => false, 'message' => '公司不存在', 'company' => null]; } // 如果$result['enterpriseName']存在数字,跳过 if (preg_match('/\d/', $result['enterpriseName'])) { - $user->company_id = 0; - $user->save(); + if ($setCompanyIdOnFail) { + $user->company_id = 0; + $user->save(); + } return ['success' => false, 'message' => '公司名称包含数字,跳过', 'company' => null]; } if ($result['status'] == '未注册') { - $user->company_id = 0; - $user->save(); + if ($setCompanyIdOnFail) { + $user->company_id = 0; + $user->save(); + } return ['success' => false, 'message' => '公司未注册,跳过', 'company' => null]; } $where = ['company_name' => $result['enterpriseName']]; $data = [ - 'company_address' => $result['address'], 'business_scope' => $result['businessScope'], 'company_city' => $result['city'], 'contact_mail' => $result['contactMail'], @@ -266,6 +275,11 @@ class Company extends SoftDeletesModel 'partners' => $result['partners'] ?? null, ]; + // 根据参数决定是否更新地址 + if ($updateAddress) { + $data['company_address'] = $result['address']; + } + $company = Company::updateOrCreate($where, $data); // 更新用户关联 @@ -275,12 +289,25 @@ class Company extends SoftDeletesModel // 更新上市状态 self::updateMarketStatus($company->id); - // 更新位置 - self::updateLocation($company->id); + // 根据参数决定是否更新位置(经纬度) + if ($updateLocation) { + self::updateLocation($company->id); + } return ['success' => true, 'message' => '更新成功', 'company' => $company]; } + /** + * 全量同步公司信息(不包含地址和经纬度) + * @param User $user 用户对象 + * @return array 返回结果 ['success' => bool, 'message' => string, 'company' => Company|null] + */ + public static function syncCompanyFromUser($user) + { + // 调用统一方法,参数设置为:不跳过已有公司、不更新地址、不更新经纬度、失败时不设置company_id + return self::updateCompanyFromUser($user, false, false, false, false); + } + /** * 更新经纬度信息 * @param int $companyId 公司ID diff --git a/app/Models/Course.php b/app/Models/Course.php index f41d3fb..3fac59e 100755 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -10,8 +10,18 @@ use Illuminate\Filesystem\Filesystem; class Course extends SoftDeletesModel { protected $appends = [ - 'date_status', 'publicize', 'sign_date_status', 'qrcode', 'teacher_detail', - 'status_text', 'is_fee_text', 'is_arrange_text', 'show_txl_text', 'show_mobile_text', 'auto_schoolmate_text', 'is_virtual_text' + 'date_status', + 'publicize', + 'sign_date_status', + 'qrcode', + 'teacher_detail', + 'status_text', + 'is_fee_text', + 'is_arrange_text', + 'show_txl_text', + 'show_mobile_text', + 'auto_schoolmate_text', + 'is_virtual_text' ]; protected $casts = ['publicize_ids' => 'json']; @@ -72,7 +82,8 @@ class Course extends SoftDeletesModel public function getPublicizeAttribute($value) { - if (empty($this->publicize_ids)) return []; + if (empty($this->publicize_ids)) + return []; return Upload::whereIn('id', $this->publicize_ids)->get(); } @@ -285,5 +296,101 @@ class Course extends SoftDeletesModel return $url; } + /** + * 获取课程统计项元数据 + * 返回每个统计项的计算逻辑和验证方法说明 + * @return array + */ + public static function getStatisticsMetadata() + { + return [ + 'course_signs_total' => [ + 'name' => '报名人数', + 'from' => '统计在指定时间范围内、指定课程范围内的所有报名记录数量。包括所有状态的报名(待审核、已通过、已拒绝等),同时会加上历史课程数据中的人数统计。一个学员报名多个课程会计算多次。', + 'verify' => '' + ], + 'course_signs_pass' => [ + 'name' => '审核通过人数', + 'from' => '统计在指定时间范围内、指定课程范围内审核通过的报名记录数量。只统计状态为"已通过"的报名,不包括待审核、已拒绝等其他状态。同时会加上历史课程数据中已通过的人数统计。一个学员报名多个课程会计算多次。', + 'verify' => '' + ], + 'course_signs_pass_unique' => [ + 'name' => '审核通过人数去重', + 'from' => '统计在指定时间范围内、指定课程范围内审核通过的学员人数(按学员去重)。与"审核通过人数"的区别是:如果同一个学员报名了多个课程,这里只计算一次。通过学员的手机号进行去重,确保每个学员只统计一次。同时会加上历史课程数据中去重后的人数统计。', + 'verify' => '' + ], + 'course_total' => [ + 'name' => '开课场次', + 'from' => '统计在指定时间范围内、指定课程体系范围内的开课场次数。一个课程可能有多个场次(比如分多天上课),这里统计的是场次数,不是课程数。只要场次的开始时间或结束时间在指定时间范围内,就会被统计。', + 'verify' => '' + ], + 'course_day_total' => [ + 'name' => '开课天数', + 'from' => '统计在指定时间范围内、指定课程体系范围内的开课天数总和。只统计标记为"需要统计天数"的场次,每个场次可能有不同的天数(比如3天、5天等),将所有场次的天数相加得到总天数。', + 'verify' => '' + ], + 'company_market_total' => [ + 'name' => '上市公司数', + 'from' => '统计在指定时间范围内、指定课程范围内报名的学员中,所在公司为上市公司的公司数量。通过学员关联到其所在公司,筛选出已上市的公司,然后按公司去重统计。如果同一公司有多个学员报名,只计算一次。', + 'verify' => '' + ], + 'ganbu_total' => [ + 'name' => '跟班学员数', + 'from' => '统计在指定时间范围内、指定课程范围内的跟班学员人数。筛选条件:1)学员的"来源"字段包含"跟班学员";2)课程类型标记为"需要统计跟班学员"。同时会加上员工参与表中的额外数据。按学员去重,同一学员只计算一次。', + 'verify' => '' + ], + 'company_market_year_total' => [ + 'name' => '今年上市公司数量', + 'from' => '统计所有在今年(当前年份)上市的公司数量。直接从上市公司表中查询,统计上市日期在今年内的公司。这个统计不依赖学员报名记录,统计的是所有在今年上市的公司,不管是否有学员报名。', + 'verify' => '' + ], + 'company_market_after_enrollment_total' => [ + 'name' => '入学后上市公司数量', + 'from' => '统计所有标记为"入学后上市"的公司数量。直接从上市公司表中查询,统计标记为"入学后上市"的公司。这个统计不依赖学员报名记录,统计的是所有在学员入学后才上市的公司。', + 'verify' => '' + ], + 'course_signs_invested' => [ + 'name' => '累计被投企业数', + 'from' => '统计从课程开始日期到结束日期,在指定课程范围内报名的学员中,所在公司为被投企业的公司数量。通过学员关联到其所在公司,筛选出标记为"被投企业"的公司,检查公司的被投时间是否在截止日期之前(或没有记录被投时间),然后按公司去重统计。同一公司多个学员只计算一次。', + 'verify' => '' + ], + 'company_invested_after_enrollment_total' => [ + 'name' => '入学后被投企业数量', + 'from' => '统计在指定时间范围内报名的学员中,所在公司在学员入学后被投的公司数量。以学员报名的课程开课时间作为"入学时间",筛选出公司的被投时间晚于入学时间的公司,然后按公司去重统计。同一公司多个学员只计算一次。', + 'verify' => '' + ], + 'company_invested_year_total' => [ + 'name' => '今年被投企业数', + 'from' => '统计在指定时间范围内报名的学员中,所在公司在指定年份范围内被投的公司数量。从开始日期和结束日期中提取年份范围,筛选出公司的被投时间在指定年份范围内的公司,然后按公司去重统计。同一公司多个学员只计算一次。', + 'verify' => '' + ], + 'company_join_total' => [ + 'name' => '元和员工参与人数', + 'from' => '统计在指定时间范围内、指定课程范围内报名的学员中,所在公司为元和投资公司的学员人数。通过学员关联到其所在公司,筛选出元和投资的公司,然后按学员去重统计。同一学员报名多个课程只计算一次。', + 'verify' => '' + ], + 'company_ganbu_total' => [ + 'name' => '全市干部参与企业', + 'from' => '统计在指定时间范围内、指定课程范围内报名的学员中,来源为"跟班学员"且课程类型标记为"需要统计跟班学员"的学员人数。与"跟班学员数"的统计逻辑相同,同时会加上员工参与表中的额外数据。按学员去重,同一学员只计算一次。', + 'verify' => '' + ], + 'cover_head_total' => [ + 'name' => '苏州头部企业', + 'from' => '统计在指定时间范围内、指定课程范围内报名的学员中,所在公司为苏州头部企业的公司数量。通过学员关联到其所在公司,筛选出标记为"苏州头部企业"的公司,然后按公司去重统计。同一公司多个学员只计算一次。', + 'verify' => '' + ], + 'cover_rencai_total' => [ + 'name' => '高层次人才', + 'from' => '统计在指定时间范围内、指定课程范围内报名的学员中,标记为"高层次人才"的学员人数。通过学员的个人信息(如学历等)判断是否为高层次人才,然后按学员去重统计。同一学员报名多个课程只计算一次。', + 'verify' => '' + ], + 'cover_stock_total' => [ + 'name' => '重点上市公司', + 'from' => '统计在指定时间范围内、指定课程范围内报名的学员中,所在公司为重点上市公司的公司数量。通过学员关联到其所在公司,筛选出重点上市公司,然后按公司去重统计。与"上市公司数"的统计逻辑相同,但筛选的是重点上市公司。同一公司多个学员只计算一次。', + 'verify' => '' + ], + ]; + } + } diff --git a/app/Models/CourseSign.php b/app/Models/CourseSign.php index c2fe834..50ad16a 100755 --- a/app/Models/CourseSign.php +++ b/app/Models/CourseSign.php @@ -299,15 +299,15 @@ class CourseSign extends SoftDeletesModel $years = []; if ($start_date && $end_date) { // 从开始和结束日期中提取年份范围 - $startYear = (int)date('Y', strtotime($start_date)); - $endYear = (int)date('Y', strtotime($end_date)); + $startYear = (int) date('Y', strtotime($start_date)); + $endYear = (int) date('Y', strtotime($end_date)); // 生成所有年份的数组 for ($year = $startYear; $year <= $endYear; $year++) { $years[] = $year; } } else { // 如果没有提供日期,使用当前年份 - $years[] = (int)date('Y'); + $years[] = (int) date('Y'); } // 获取这些公司中标记为被投的公司 @@ -323,7 +323,7 @@ class CourseSign extends SoftDeletesModel foreach ($projectUsers as $item) { $investDate = $item['investDate'] ?? null; if ($investDate) { - $investYear = (int)date('Y', strtotime($investDate)); + $investYear = (int) date('Y', strtotime($investDate)); if (in_array($investYear, $years)) { $hasInvestInYears = true; break; @@ -431,18 +431,30 @@ class CourseSign extends SoftDeletesModel public static function companyMarketAfterEnrollment($start_date, $end_date, $course_ids = null, $retList = false) { $courseSignsQuery = self::getStudentList($start_date, $end_date, null, $course_ids); - $courseSignsForStock = $courseSignsQuery->with('user.company')->get(); + $courseSignsForStock = $courseSignsQuery->with(['user.company', 'course'])->get(); $companiesAfterEnrollment = []; foreach ($courseSignsForStock as $sign) { if ($sign->user && $sign->user->company && $sign->user->company->company_market == 1) { - $signDate = \Carbon\Carbon::parse($sign->created_at)->format('Y-m-d'); + // 入学时间:使用参与课程的课程开始时间(course->start_date),不使用报名时间(created_at) + $enrollmentDate = null; + if ($sign->course && $sign->course->start_date) { + $enrollmentDate = \Carbon\Carbon::parse($sign->course->start_date)->format('Y-m-d'); + } + + // 如果没有课程开始时间,跳过这条记录 + if (!$enrollmentDate) { + continue; + } + $stockDate = $sign->user->company->stock_date; - if ($stockDate && $stockDate >= $signDate) { + // 上市时间 >= 课程开始时间(入学时间),说明是入学后上市 + if ($stockDate && $stockDate >= $enrollmentDate) { $companyId = $sign->user->company->id; if (!isset($companiesAfterEnrollment[$companyId])) { $companiesAfterEnrollment[$companyId] = [ 'company' => $sign->user->company, - 'first_sign_date' => $signDate, + 'first_enrollment_date' => $enrollmentDate, // 首次入学时间(课程开始时间) + 'stock_date' => $stockDate, 'users' => [], ]; } @@ -475,18 +487,18 @@ class CourseSign extends SoftDeletesModel $companiesAfterEnrollment = []; foreach ($courseSignsForInvest as $sign) { if ($sign->user && $sign->user->company && $sign->user->company->is_yh_invested == 1) { - // 使用课程开课时间作为入学时间 + // 入学时间:使用参与课程的课程开始时间(course->start_date),不使用报名时间(created_at) $enrollmentDate = null; if ($sign->course && $sign->course->start_date) { $enrollmentDate = \Carbon\Carbon::parse($sign->course->start_date)->format('Y-m-d'); } - // 如果没有开课时间,跳过这条记录 + // 如果没有课程开始时间,跳过这条记录 if (!$enrollmentDate) { continue; } - // 从 project_users 中检查是否有任何一个被投时间 >= 课程开课时间 + // 从 project_users 中检查是否有任何一个被投时间 >= 课程开始时间(入学时间) $projectUsers = $sign->user->company->project_users; $hasInvestAfterEnrollment = false; $investDate = null; @@ -494,7 +506,7 @@ class CourseSign extends SoftDeletesModel foreach ($projectUsers as $projectUser) { if (!empty($projectUser['investDate'])) { $currentInvestDate = $projectUser['investDate']; - // 只要有一个被投时间 >= 课程开课时间,就满足条件 + // 只要有一个被投时间 >= 课程开始时间(入学时间),就满足条件 if ($currentInvestDate >= $enrollmentDate) { $hasInvestAfterEnrollment = true; // 记录满足条件的被投时间(如果有多个,记录最早的) @@ -505,13 +517,13 @@ class CourseSign extends SoftDeletesModel } } } - // 只要有一个被投时间 >= 课程开课时间,说明是入学后被投 + // 只要有一个被投时间 >= 课程开始时间(入学时间),说明是入学后被投 if ($hasInvestAfterEnrollment && $investDate) { $companyId = $sign->user->company->id; if (!isset($companiesAfterEnrollment[$companyId])) { $companiesAfterEnrollment[$companyId] = [ 'company' => $sign->user->company, - 'first_sign_date' => $enrollmentDate, + 'first_enrollment_date' => $enrollmentDate, // 首次入学时间(课程开始时间) 'invest_date' => $investDate, 'users' => [], ]; diff --git a/quick-commit.sh b/quick-commit.sh index dc740c3..91ff8e2 100755 --- a/quick-commit.sh +++ b/quick-commit.sh @@ -190,12 +190,27 @@ EOF return 0 } - # 首先尝试使用默认密码 + # 执行 push echo "" + echo "==========================================" + echo "执行 git push" + echo "==========================================" + echo "" + + # 使用局部变量保存当前使用的密码 + local current_password="$PASSWORD" + + # 首先尝试使用默认密码 push echo "执行: git push $selected_remote $current_branch (使用默认密码)" - if do_git_push "$PASSWORD" "$selected_remote" "$current_branch"; then + if do_git_push "$current_password" "$selected_remote" "$current_branch"; then + echo "" + echo "==========================================" + echo "执行结果" + echo "==========================================" + echo "✅ Push: 成功" echo "" echo "✅ 仓库 $repo_path 提交完成!" + echo "==========================================" else # 默认密码失败,提示用户输入密码 echo "" @@ -207,11 +222,23 @@ EOF echo "使用您输入的密码重新执行: git push $selected_remote $current_branch" if do_git_push "$user_password" "$selected_remote" "$current_branch"; then + echo "" + echo "==========================================" + echo "执行结果" + echo "==========================================" + echo "✅ Push: 成功" echo "" echo "✅ 仓库 $repo_path 提交完成!" + echo "==========================================" else + echo "" + echo "==========================================" + echo "执行结果" + echo "==========================================" + echo "❌ Push: 失败" echo "" echo "❌ 仓库 $repo_path push 失败,请检查密码和网络连接" + echo "==========================================" fi fi