diff --git a/1 b/1 deleted file mode 100644 index bedd795..0000000 --- a/1 +++ /dev/null @@ -1,5 +0,0 @@ -Merge branch 'master' of ssh://47.101.48.251:/data/git/wx.sstbc.com -# 请输入一个提交信息以解释此合并的必要性,尤其是将一个更新后的上游分支 -# 合并到主题分支。 -# -# 以 '#' 开始的行将被忽略,而空的提交说明将终止提交。 diff --git a/app/Console/Commands/SyncCompany.php b/app/Console/Commands/SyncCompany.php index 3874ce9..b28c9f6 100644 --- a/app/Console/Commands/SyncCompany.php +++ b/app/Console/Commands/SyncCompany.php @@ -3,7 +3,6 @@ namespace App\Console\Commands; use App\Models\Company; -use App\Models\User; use Illuminate\Console\Command; class SyncCompany extends Command @@ -39,7 +38,7 @@ class SyncCompany extends Command */ public function handle() { - // 全量同步所有有公司名称的用户 + // 全量同步所有公司信息 $this->syncAllCompanies(); return $this->info('全量同步完成'); } @@ -49,33 +48,33 @@ class SyncCompany extends Command */ public function syncAllCompanies() { - // 获取所有有公司名称的用户(全量同步,不限制company_id) - $users = User::whereNotNull('company_name') + // 获取所有公司(同步存量数据) + $companies = Company::whereNotNull('company_name') ->orderBy('id', 'desc') ->get(); - $total = $users->count(); + $total = $companies->count(); if ($total == 0) { - return $this->info('没有需要同步的用户'); + return $this->info('没有需要同步的公司'); } - $this->info("开始全量同步公司信息,共 {$total} 个用户"); + $this->info("开始全量同步公司信息,共 {$total} 家公司"); $bar = $this->output->createProgressBar($total); $bar->start(); $successCount = 0; $failCount = 0; - foreach ($users as $user) { + foreach ($companies as $company) { // 调用模型方法同步公司信息(不包含经纬度和地址) - $result = Company::syncCompanyFromUser($user); + $result = Company::syncCompanyInfo($company); if ($result['success']) { $successCount++; $bar->setMessage($result['company']->company_name . ' 同步成功', 'status'); } else { $failCount++; - $bar->setMessage($user->company_name . ' ' . $result['message'], 'status'); + $bar->setMessage($company->company_name . ' ' . $result['message'], 'status'); } $bar->advance(); } diff --git a/app/Console/Commands/UpdateCalendar.php b/app/Console/Commands/UpdateCalendar.php deleted file mode 100644 index c5d2647..0000000 --- a/app/Console/Commands/UpdateCalendar.php +++ /dev/null @@ -1,488 +0,0 @@ -argument('file'); - $filePath = base_path($fileName); - - if (!file_exists($filePath)) { - $this->error("文件不存在: {$filePath}"); - return; - } - - $this->info("开始处理文件: {$fileName}"); - - try { - // 读取Excel文件 - $spreadsheet = IOFactory::load($filePath); - $sheetCount = $spreadsheet->getSheetCount(); - - $this->info("Excel文件包含 {$sheetCount} 个工作表"); - - $totalCreated = 0; - $failedCourses = []; - $failedNews = []; - - // 处理每个工作表 - for ($sheetIndex = 0; $sheetIndex < $sheetCount; $sheetIndex++) { - $worksheet = $spreadsheet->getSheet($sheetIndex); - $sheetName = $worksheet->getTitle(); - - $this->info("正在处理工作表: {$sheetName}"); - - list($created, $sheetFailedCourses, $sheetFailedNews) = $this->processWorksheet($worksheet, $sheetName); - $totalCreated += $created; - $failedCourses = array_merge($failedCourses, $sheetFailedCourses); - $failedNews = array_merge($failedNews, $sheetFailedNews); - } - - $this->info("处理完成,总共创建了 {$totalCreated} 条日历记录"); - - // 显示匹配失败的课程 - if (!empty($failedCourses)) { - $this->warn("匹配失败的课程:"); - foreach (array_unique($failedCourses) as $failedCourse) { - $this->warn(" - {$failedCourse}"); - } - } - - // 显示匹配失败的新闻 - if (!empty($failedNews)) { - $this->warn("匹配失败的新闻:"); - foreach (array_unique($failedNews) as $failedNewsItem) { - $this->warn(" - {$failedNewsItem}"); - } - } - - } catch (\Exception $e) { - $this->error("处理Excel文件时发生错误: " . $e->getMessage()); - return; - } - } - - /** - * 处理单个工作表 - */ - private function processWorksheet($worksheet, $sheetName) - { - $highestRow = $worksheet->getHighestRow(); - $highestColumn = $worksheet->getHighestColumn(); - - $this->info("工作表 {$sheetName} 有 {$highestRow} 行,最高列为 {$highestColumn}"); - - // 读取第一行作为表头 - $headers = []; - $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); - - for ($col = 1; $col <= $highestColumnIndex; $col++) { - $cellValue = $worksheet->getCellByColumnAndRow($col, 1)->getCalculatedValue(); - $headers[$col] = trim($cellValue); - } - - // 找到"课程"、"开始时间"、"结束时间"、"跳转链接"列的位置 - $courseColumn = null; - $startTimeColumn = null; - $endTimeColumn = null; - $linkColumn = null; - - foreach ($headers as $colIndex => $header) { - if (strpos($header, '课程') !== false) { - $courseColumn = $colIndex; - } - if (strpos($header, '开始时间') !== false) { - $startTimeColumn = $colIndex; - } - if (strpos($header, '结束时间') !== false) { - $endTimeColumn = $colIndex; - } - if (strpos($header, '跳转链接') !== false) { - $linkColumn = $colIndex; - } - } - - if (!$courseColumn || !$startTimeColumn || !$endTimeColumn || !$linkColumn) { - $this->warn("工作表 {$sheetName} 中未找到必要的列(课程、开始时间、结束时间、跳转链接)"); - return [0, [], []]; - } - - $this->info("找到课程列: {$courseColumn},开始时间列: {$startTimeColumn},结束时间列: {$endTimeColumn},跳转链接列: {$linkColumn}"); - $created = 0; - $failedCourses = []; - $failedNews = []; - - // 处理数据行 - for ($row = 2; $row <= $highestRow; $row++) { - $courseName = trim($worksheet->getCellByColumnAndRow($courseColumn, $row)->getCalculatedValue()); - - // 获取开始时间和结束时间的原始值,避免格式化问题 - $startTimeCell = $worksheet->getCellByColumnAndRow($startTimeColumn, $row); - $endTimeCell = $worksheet->getCellByColumnAndRow($endTimeColumn, $row); - - // 优先使用原始值,如果没有则使用计算值 - $startTime = $startTimeCell->getValue(); - if ($startTime === null) { - $startTime = trim($startTimeCell->getCalculatedValue()); - } - - $endTime = $endTimeCell->getValue(); - if ($endTime === null) { - $endTime = trim($endTimeCell->getCalculatedValue()); - } - - $jumpLink = trim($worksheet->getCellByColumnAndRow($linkColumn, $row)->getCalculatedValue()); - - if (empty($courseName) || empty($startTime) || empty($endTime)) { - continue; - } - - $this->info("处理行 {$row}: 课程='{$courseName}', 开始时间='{$startTime}' (类型: " . gettype($startTime) . "), 结束时间='{$endTime}' (类型: " . gettype($endTime) . "), 跳转链接='{$jumpLink}'"); - - // 1. 匹配courses表 - $courseId = $this->matchCourse($courseName); - if (!$courseId) { - $this->warn("✗ 未找到匹配的课程: '{$courseName}'"); - $failedCourses[] = $courseName; - continue; - } - - // 2. 匹配phome_ecms_news表获取url和title - $url = null; - $title = null; - if (!empty($jumpLink)) { - list($title, $url) = $this->getTitleUrlFromNews($jumpLink); - if (!$url) { - $this->warn("✗ 未找到匹配的新闻标题: '{$jumpLink}'"); - $failedNews[] = $jumpLink; - } - } - - // 3. 更新courses表的url字段 - if ($url && $title) { - $this->updateCourseUrl($courseId, $url, $title); - } - - // 4. 创建calendar记录 - $calendarCreated = $this->createCalendarRecord($courseId, $courseName, $startTime, $endTime, $url, $title, $courseName); - if ($calendarCreated) { - $created++; - $this->info("✓ 成功创建日历记录: '{$courseName}'"); - } else { - $this->warn("✗ 创建日历记录失败: '{$courseName}'"); - } - } - - return [$created, $failedCourses, $failedNews]; - } - - /** - * 匹配courses表 - */ - private function matchCourse($courseName) - { - try { - // 直接匹配 - $course = Course::where('name', $courseName)->first(); - - if ($course) { - $this->info("通过直接匹配找到课程: '{$course->name}' (ID: {$course->id})"); - return $course->id; - } - - // 模糊匹配 - $course = Course::where('name', 'like', "%{$courseName}%") - ->whereNull('deleted_at') - ->first(); - - if ($course) { - $this->info("通过模糊匹配找到课程: '{$course->name}' (ID: {$course->id})"); - return $course->id; - } - - // 使用相似度匹配 - $courses = Course::whereNotNull('name')->where('name', '!=', '')->get(); - - $bestMatch = null; - $highestSimilarity = 0; - - foreach ($courses as $course) { - $similarity = $this->calculateSimilarity($courseName, $course->name); - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = $course; - } - } - - // 取相似度最高的作为结果,不设置阈值限制 - if ($bestMatch && $highestSimilarity > 0.3) { - $this->info("通过相似度匹配找到课程 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->name}' (ID: {$bestMatch->id})"); - return $bestMatch->id; - } - - } catch (\Exception $e) { - $this->error("查询courses表时发生错误: " . $e->getMessage()); - } - - return null; - } - - /** - * 从phome_ecms_news表获取titleurl - */ - private function getTitleUrlFromNews($title) - { - try { - // 直接匹配 - $news = DB::table('phome_ecms_news') - ->where('title', $title) - ->first(); - - if ($news && !empty($news->titleurl)) { - return [$news->title, $news->titleurl]; - } - - // 模糊匹配 - $news = DB::table('phome_ecms_news') - ->where('title', 'like', "%{$title}%") - ->first(); - - if ($news && !empty($news->titleurl)) { - $this->info("通过模糊匹配找到: '{$news->title}' -> '{$news->titleurl}'"); - return [$news->title, $news->titleurl]; - } - - // 使用相似度匹配 - $allNews = DB::table('phome_ecms_news') - ->whereNotNull('title') - ->whereNotNull('titleurl') - ->where('title', '!=', '') - ->where('titleurl', '!=', '') - ->get(); - - $bestMatch = null; - $highestSimilarity = 0; - - foreach ($allNews as $news) { - $similarity = $this->calculateSimilarity($title, $news->title); - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = $news; - } - } - - // 取相似度最高的作为结果,不设置阈值限制 - if ($bestMatch && $highestSimilarity > 0.3) { - $this->info("通过相似度匹配找到新闻 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->title}' -> '{$bestMatch->titleurl}'"); - return [$bestMatch->title, $bestMatch->titleurl]; - } - - } catch (\Exception $e) { - $this->error("查询phome_ecms_news表时发生错误: " . $e->getMessage()); - } - - return null; - } - - /** - * 更新courses表的url字段 - */ - private function updateCourseUrl($courseId, $titleUrl, $title) - { - try { - $course = Course::find($courseId); - if ($course) { - $course->url = $titleUrl; - $course->url_title = $title; - $course->save(); - $this->info("✓ 成功更新课程URL: '{$course->name}' -> '{$titleUrl}'"); - return true; - } - } catch (\Exception $e) { - $this->error("更新courses表时发生错误: " . $e->getMessage()); - } - - return false; - } - - /** - * 创建calendar记录 - */ - private function createCalendarRecord($courseId, $courseName, $startTime, $endTime, $url = null, $title = null, $calendarTitle = null) - { - try { - // 转换时间格式 - $startDateTime = $this->parseDateTime($startTime); - $endDateTime = $this->parseDateTime($endTime); - - if (!$startDateTime || !$endDateTime) { - $this->warn("时间格式解析失败: 开始时间='{$startTime}', 结束时间='{$endTime}'"); - return false; - } - - // 检查是否已存在相同的日历记录 - $existingCalendar = Calendar::where('course_id', $courseId) - ->where('start_time', $startDateTime) - ->where('end_time', $endDateTime) - ->first(); - - if ($existingCalendar) { - $this->info("日历记录已存在,跳过创建: '{$courseName}'"); - return true; - } - - // 创建新的日历记录 - $calendar = new Calendar(); - $calendar->type = 1; // 课程类型 - $calendar->course_id = $courseId; - $calendar->title = $calendarTitle ?: $courseName; // 使用Excel中的课程名字作为title - $calendar->start_time = $startDateTime; - $calendar->end_time = $endDateTime; - $calendar->date = $startDateTime->format('Y-m-d'); - $calendar->url = $url; - $calendar->is_publish = 1; // 默认发布 - $calendar->save(); - - return true; - - } catch (\Exception $e) { - $this->error("创建calendar记录时发生错误: " . $e->getMessage()); - } - - return false; - } - - /** - * 解析日期时间格式 - */ - private function parseDateTime($dateTimeString) - { - try { - // 处理Excel数字格式的日期时间 - if (is_numeric($dateTimeString)) { - $excelDate = (float)$dateTimeString; - - // Excel日期从1900年1月1日开始计算天数 - // 需要减去2是因为Excel错误地认为1900年是闰年 - $unixTimestamp = ($excelDate - 25569) * 86400; - - $dateTime = new \DateTime(); - $dateTime->setTimestamp($unixTimestamp); - - $this->info("Excel数字日期转换: {$dateTimeString} -> " . $dateTime->format('Y-m-d H:i:s')); - return $dateTime; - } - - // 尝试多种日期时间格式 - $formats = [ - 'Y-m-d H:i:s', - 'Y-m-d H:i', - 'Y/m/d H:i:s', - 'Y/m/d H:i', - 'Y-m-d', - 'Y/m/d', - 'd/m/Y H:i:s', - 'd/m/Y H:i', - 'd-m-Y H:i:s', - 'd-m-Y H:i' - ]; - - foreach ($formats as $format) { - $dateTime = \DateTime::createFromFormat($format, $dateTimeString); - if ($dateTime !== false) { - // 如果只有日期没有时间,设置默认时间 - if (strpos($format, 'H:i') === false) { - $dateTime->setTime(9, 0, 0); // 默认上午9点 - } - return $dateTime; - } - } - - // 尝试使用strtotime - $timestamp = strtotime($dateTimeString); - if ($timestamp !== false) { - $dateTime = new \DateTime(); - $dateTime->setTimestamp($timestamp); - return $dateTime; - } - - } catch (\Exception $e) { - $this->error("解析日期时间时发生错误: " . $e->getMessage()); - } - - return null; - } - - /** - * 计算字符串相似度 - */ - private function calculateSimilarity($str1, $str2) - { - // 移除空格并转换为小写 - $str1 = strtolower(preg_replace('/\s+/', '', $str1)); - $str2 = strtolower(preg_replace('/\s+/', '', $str2)); - - if ($str1 === $str2) { - return 1.0; - } - - if (empty($str1) || empty($str2)) { - return 0.0; - } - - // 使用Levenshtein距离计算相似度 - $maxLen = max(strlen($str1), strlen($str2)); - if ($maxLen == 0) { - return 1.0; - } - - $distance = levenshtein($str1, $str2); - $similarity = 1 - ($distance / $maxLen); - - // 如果其中一个字符串包含另一个,提高相似度 - if (strpos($str1, $str2) !== false || strpos($str2, $str1) !== false) { - $containsSimilarity = min(strlen($str1), strlen($str2)) / $maxLen; - $similarity = max($similarity, $containsSimilarity); - } - - return max(0, $similarity); - } -} diff --git a/app/Console/Commands/UpdatePosition.php b/app/Console/Commands/UpdatePosition.php deleted file mode 100755 index 2540d2a..0000000 --- a/app/Console/Commands/UpdatePosition.php +++ /dev/null @@ -1,63 +0,0 @@ -option('field'); - if (empty($field)) return self::FAILURE; - $users = User::get(); - foreach ($users as $user) { - $item = str_replace(',', ',', $user->$field); - $tempArray = explode(',', $item); - $tempArray = array_unique(array_filter($tempArray)); - $user->$field = implode(',', $tempArray); - $user->save(); - $this->info($user->username.'-处理完成'); - } - return self::SUCCESS; - } - - -} diff --git a/app/Exports/MultiSheetExport.php b/app/Exports/MultiSheetExport.php new file mode 100644 index 0000000..f287081 --- /dev/null +++ b/app/Exports/MultiSheetExport.php @@ -0,0 +1,21 @@ +sheets = $sheets; + } + + public function sheets(): array + { + return $this->sheets; + } +} + diff --git a/app/Exports/SheetExport.php b/app/Exports/SheetExport.php new file mode 100644 index 0000000..97499cc --- /dev/null +++ b/app/Exports/SheetExport.php @@ -0,0 +1,78 @@ +data = $data; + $this->fields = $fields; + $this->sheetName = $sheetName; + } + + public function collection() + { + $newList = []; + + // 添加表头 + $header = array_values($this->fields); + $newList[] = $header; + + // 添加数据行 + foreach ($this->data as $row) { + $temp = []; + foreach (array_keys($this->fields) as $field) { + $temp[] = $row[$field] ?? ''; + } + $newList[] = $temp; + } + + return new Collection($newList); + } + + public function styles(Worksheet $sheet) + { + return [ + 1 => [ + 'font' => ['bold' => true], + 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], + ], + ]; + } + + public function columnWidths(): array + { + $widths = []; + $column = 'A'; + foreach ($this->fields as $field) { + $widths[$column] = 15; + $column++; + } + return $widths; + } + + public function registerEvents(): array + { + return [ + AfterSheet::class => function (AfterSheet $event) { + $sheet = $event->sheet->getDelegate(); + $sheet->setTitle($this->sheetName); + }, + ]; + } +} + diff --git a/app/Http/Controllers/Admin/EmployeeParticipationController.php b/app/Http/Controllers/Admin/EmployeeParticipationController.php index c57fda6..5630223 100644 --- a/app/Http/Controllers/Admin/EmployeeParticipationController.php +++ b/app/Http/Controllers/Admin/EmployeeParticipationController.php @@ -5,10 +5,12 @@ namespace App\Http\Controllers\Admin; use App\Exports\BaseExport; use App\Helpers\ResponseCode; use App\Models\CustomForm; +use App\Models\CustomFormField; use App\Models\EmployeeParticipation; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Maatwebsite\Excel\Facades\Excel; +use Rap2hpoutre\FastExcel\FastExcel; class EmployeeParticipationController extends BaseController { @@ -156,6 +158,9 @@ class EmployeeParticipationController extends BaseController * @OA\Parameter(name="total", in="query", @OA\Schema(type="integer", format="int64"), required=false, description="数量"), * @OA\Parameter(name="course_type_id", in="query", @OA\Schema(type="integer", format="int64"), required=false, description="课程类型ID"), * @OA\Parameter(name="course_name", in="query", @OA\Schema(type="string", nullable=true), description="课程名称"), + * @OA\Parameter(name="company_name", in="query", @OA\Schema(type="string", nullable=true), description="公司名字"), + * @OA\Parameter(name="name", in="query", @OA\Schema(type="string", nullable=true), description="姓名"), + * @OA\Parameter(name="department", in="query", @OA\Schema(type="string", nullable=true), description="部门"), * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="认证token"), * @OA\Response( * response="200", @@ -205,5 +210,107 @@ class EmployeeParticipationController extends BaseController return parent::destroy(); } + /** + * @OA\Post( + * path="/api/admin/employee-participations/excel-show", + * tags={"员工参与"}, + * summary="导入预览", + * description="", + * @OA\Parameter(name="file", 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", + * description="暂无" + * ) + * ) + */ + public function excelShow() + { + $file = \request()->file('file'); + //判断文件是否有效 + if (!(\request()->hasFile('file') && $file->isValid())) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '文件不存在或无效']); + } + //获取文件大小 + $img_size = floor($file->getSize() / 1024); + if ($img_size >= 50 * 1024) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '文件必须小于50M']); + } + //过滤文件后缀 + $ext = $file->getClientOriginalExtension(); + if (!in_array($ext, ['xls', 'xlsx', 'csv'])) { + return $this->fail([ResponseCode::ERROR_BUSINESS, '仅支持xls/xlsx/csv格式']); + } + $tempFile = $file->getRealPath(); + $dataArray = (new FastExcel)->import($tempFile)->toArray(); + + // 固定的字段映射(Excel表头中文 => 数据库字段名) + $fieldMapping = [ + '类型' => 'type', + '开始日期' => 'start_date', + '结束日期' => 'end_date', + '数量' => 'total', + '课程类型ID' => 'course_type_id', + '课程名称' => 'course_name', + '公司名字' => 'company_name', + '姓名' => 'name', + '部门' => 'department', + ]; + + $list = []; + foreach ($dataArray as $key => $value) { + $list[$key] = []; + // 根据固定字段映射转换数据 + foreach ($fieldMapping as $excelHeader => $dbField) { + if (isset($value[$excelHeader])) { + $list[$key][$dbField] = $value[$excelHeader]; + } + } + } + return $this->success($list); + } + + /** + * @OA\Post( + * path="/api/admin/employee-participations/import", + * tags={"员工参与"}, + * summary="导入", + * description="", + * @OA\Parameter(name="data", 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", + * description="暂无" + * ) + * ) + */ + public function import() + { + $all = \request()->all(); + $messages = [ + 'data.required' => '数据必填', + ]; + $validator = Validator::make($all, [ + 'data' => 'required', + ], $messages); + if ($validator->fails()) { + return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]); + } + $records = $all['data']; + DB::beginTransaction(); + try { + $successCount = 0; + foreach ($records as $record) { + $this->model->create($record); + $successCount++; + } + DB::commit(); + return $this->success(['total' => count($records), 'success_count' => $successCount]); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->fail([$exception->getCode(), $exception->getMessage()]); + } + } + } diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 9b5b557..90271d6 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -17,6 +17,7 @@ use App\Models\CourseTypeDataOverviewConfig; use App\Models\CustomFormField; use App\Models\Department; use App\Models\HistoryCourse; +use App\Models\EmployeeParticipation; use App\Models\ParameterDetail; use App\Models\StockCompany; use App\Models\SupplyDemand; @@ -32,6 +33,8 @@ use EasyWeChat\Factory; use Illuminate\Filesystem\Filesystem; use Maatwebsite\Excel\Facades\Excel; use App\Exports\CommonExport; +use App\Exports\MultiSheetExport; +use App\Exports\SheetExport; class OtherController extends CommonController { @@ -312,7 +315,6 @@ class OtherController extends CommonController // 上市公司数(所有上市公司) $list['company_market_total'] = CourseSign::shangshi($start_date, $end_date, $course_ids); - // 跟班学员数(在指定时间范围内报名的学员中,from为'跟班学员'的数量) $list['ganbu_total'] = CourseSign::genban($start_date, $end_date, $course_ids); @@ -338,10 +340,10 @@ class OtherController extends CommonController $list['company_ganbu_total'] = CourseSign::ganbu($start_date, $end_date, $course_ids); // 苏州头部企业 $list['cover_head_total'] = CourseSign::toubuqiye($start_date, $end_date, $course_ids); - // 高层次人才 + // 苏州高层次人才 $list['cover_rencai_total'] = CourseSign::rencai($start_date, $end_date, $course_ids); - // 重点上市公司 - $list['cover_stock_total'] = CourseSign::shangshi($start_date, $end_date, $course_ids); + // 苏州重点上市公司 + $list['cover_stock_total'] = CourseSign::suzhoushangshi($start_date, $end_date, $course_ids); // 课程分类明细统计 @@ -462,12 +464,27 @@ class OtherController extends CommonController // 直接通过公司ID查询学员 $userIds = User::where('company_id', $company->id)->pluck('id')->toArray(); + // 公司基本信息 + $companyInfo = [ + 'company_name' => $company->company_name, + 'company_legal_representative' => $company->company_legal_representative ?? '', + 'company_date' => $company->company_date ?? '', + 'company_address' => $company->company_address ?? '', + 'business_scope' => $company->business_scope ?? '', + 'contact_phone' => $company->contact_phone ?? '', + 'contact_mail' => $company->contact_mail ?? '', + 'company_tag' => $company->company_tag ?? '', + // 'credit_code' => ($company->credit_code ? ' ' . $company->credit_code : ''), + ]; + if (empty($userIds)) { - // 如果没有学员,设置空值 - $userNamesStr = ''; - $courseNamesStr = ''; - $courseTypesStr = ''; - $totalCourseCount = 0; + // 如果没有学员,仍然导出公司基本信息 + $data[] = array_merge($companyInfo, [ + 'user_name' => '', + 'course_names' => '', + 'course_types' => '', + 'course_count' => 0, + ]); } else { $userCourseSigns = CourseSign::getStudentList(CourseType::START_DATE, $end_date, 1, $courseIdsArray) ->whereIn('user_id', $userIds) @@ -490,57 +507,56 @@ class OtherController extends CommonController $usersData[$userId]['courseSigns'][] = $sign; } - // 收集所有学员的姓名、课程名称、课程体系 - $userNames = []; - $allCourseNames = []; - $allCourseTypes = []; - $totalCourseCount = 0; - + // 每个学员一行,公司信息只在第一行显示,后续行公司信息为空 + $isFirstRow = true; 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); + // 获取课程名称列表,用中文顿号分隔 + $courseNames = $courseSigns->pluck('course.name')->filter()->unique()->values()->implode('、'); - // 收集课程体系 + // 获取课程体系列表,用中文顿号分隔 $courseTypes = $courseSigns->pluck('course.typeDetail.name') ->filter() ->unique() ->values() - ->toArray(); - $allCourseTypes = array_merge($allCourseTypes, $courseTypes); + ->implode('、'); + + // 报名课程数 + $courseCount = $courseSigns->count(); - // 累计报名课程数 - $totalCourseCount += $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' => '', + 'business_scope' => '', + 'contact_phone' => '', + 'contact_mail' => '', + 'company_tag' => '', + 'credit_code' => '', + 'user_name' => $user->name ?? '', + 'course_names' => $courseNames, + 'course_types' => $courseTypes, + 'course_count' => $courseCount, + ]; + } } } - - // 去重并合并 - $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 ?? '', - 'company_date' => $company->company_date ?? '', - 'company_address' => $company->company_address ?? '', - 'business_scope' => $company->business_scope ?? '', - 'contact_phone' => $company->contact_phone ?? '', - '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 = [ 'company_name' => '企业名称', @@ -551,8 +567,8 @@ class OtherController extends CommonController 'contact_phone' => '联系电话', 'contact_mail' => '联系邮箱', 'company_tag' => '企业资质', - 'credit_code' => '统一社会信用代码', - 'user_names' => '学员姓名', + // 'credit_code' => '统一社会信用代码', + 'user_name' => '学员姓名', 'course_names' => '课程名称', 'course_types' => '课程体系', 'course_count' => '报名课程数', @@ -593,10 +609,12 @@ class OtherController extends CommonController // 审核通过人数明细 - 使用courseSignsTotal方法获取列表(与coursesHome算法一致) $courseSigns = CourseSign::courseSignsTotal($start_date, $end_date, 1, $course_ids, true); // 加载关联关系 - $courseSigns->load(['user', 'course']); + $courseSigns->load(['user.company', 'course.typeDetail']); + // 当前课程数据 + $currentData = []; foreach ($courseSigns as $sign) { - $data[] = [ + $currentData[] = [ 'user_name' => $sign->user->name ?? '', 'mobile' => $sign->user->mobile ?? '', 'company_name' => $sign->user->company->company_name ?? '', @@ -604,19 +622,70 @@ class OtherController extends CommonController 'company_industry' => $sign->user->company->company_industry ?? '', 'course_name' => $sign->course->name ?? '', 'course_type' => $sign->course->typeDetail->name ?? '', - // 'created_at' => $sign->created_at ? $sign->created_at->format('Y-m-d H:i:s') : '', ]; } - $fields = [ + $currentFields = [ 'user_name' => '学员姓名', 'mobile' => '手机号', 'company_name' => '企业名称', 'company_area' => '所在区域', + 'company_industry' => '所在行业', 'course_name' => '课程名称', 'course_type' => '课程类型', - // 'created_at' => '报名时间', ]; + + // 历史课程数据 + $historyData = []; + $course_type_id_array = $course_type_id ? (is_array($course_type_id) ? $course_type_id : explode(',', $course_type_id)) : []; + $historyCourses = HistoryCourse::whereHas('calendar', function ($query) { + $query->where('is_count_people', 1); + })->whereHas('typeDetail', function ($query) { + $query->where('is_history', 1); + })->where(function ($query) use ($start_date, $end_date) { + if ($start_date && $end_date) { + $query->whereBetween('start_time', [$start_date, $end_date]) + ->orWhereBetween('end_time', [$start_date, $end_date]); + } + })->where(function ($query) use ($course_type_id_array) { + if (!empty($course_type_id_array)) { + $query->whereIn('type', $course_type_id_array); + } + })->with('typeDetail')->get(); + + foreach ($historyCourses as $historyCourse) { + $historyData[] = [ + 'course_type' => $historyCourse->typeDetail->name ?? '', + 'course_name' => $historyCourse->course_name ?? '', + 'start_time' => $historyCourse->start_time ?? '', + 'end_time' => $historyCourse->end_time ?? '', + 'course_type_signs_pass' => $historyCourse->course_type_signs_pass ?? 0, + 'course_type_signs_pass_unique' => $historyCourse->course_type_signs_pass_unique ?? 0, + 'course_signs_pass' => $historyCourse->course_signs_pass ?? 0, + ]; + } + $historyFields = [ + 'course_type' => '课程体系', + 'course_name' => '课程名称', + 'start_time' => '开始时间', + 'end_time' => '结束时间', + 'course_type_signs_pass' => '培养人数未去重', + 'course_type_signs_pass_unique' => '培养人数去重', + 'course_signs_pass' => '课程培养人数', + ]; + + // 创建多 sheet 导出 + $sheets = [ + new SheetExport($currentData, $currentFields, '当前课程数据'), + new SheetExport($historyData, $historyFields, '历史课程数据'), + ]; + $filename = '审核通过人数明细'; + + // 直接返回多 sheet 导出 + return Excel::download( + new MultiSheetExport($sheets), + $filename . '_' . date('YmdHis') . '.xlsx' + ); break; case 'course_signs_pass_unique': @@ -624,6 +693,9 @@ class OtherController extends CommonController $users = CourseSign::courseSignsTotalByUnique($start_date, $end_date, 1, $course_ids, true); // 预加载 company 关系,避免 N+1 查询问题 $users->load('company'); + + // 当前课程数据(已去重) + $currentData = []; foreach ($users as $user) { // 获取该学员报名的课程列表 - 使用与getStudentList完全一致的逻辑 $userCourseSigns = CourseSign::where(function ($query) use ($course_ids) { @@ -674,7 +746,7 @@ class OtherController extends CommonController ? $user->company->company_industry : ($user->company_industry ?? ''); - $data[] = [ + $currentData[] = [ 'user_name' => $user->name ?? '', 'mobile' => $user->mobile ?? '', 'qcc_company_name' => $qccCompanyName, @@ -687,7 +759,7 @@ class OtherController extends CommonController 'course_count' => $userCourseSigns->count(), ]; } - $fields = [ + $currentFields = [ 'user_name' => '学员姓名', 'mobile' => '手机号', 'qcc_company_name' => '企查查企业', @@ -699,7 +771,59 @@ class OtherController extends CommonController 'course_names' => '报名课程', 'course_count' => '报名课程数', ]; + + // 历史课程数据 + $historyData = []; + $course_type_id_array = $course_type_id ? (is_array($course_type_id) ? $course_type_id : explode(',', $course_type_id)) : []; + $historyCourses = HistoryCourse::whereHas('calendar', function ($query) { + $query->where('is_count_people', 1); + })->whereHas('typeDetail', function ($query) { + $query->where('is_history', 1); + })->where(function ($query) use ($start_date, $end_date) { + if ($start_date && $end_date) { + $query->whereBetween('start_time', [$start_date, $end_date]) + ->orWhereBetween('end_time', [$start_date, $end_date]); + } + })->where(function ($query) use ($course_type_id_array) { + if (!empty($course_type_id_array)) { + $query->whereIn('type', $course_type_id_array); + } + })->with('typeDetail')->get(); + + foreach ($historyCourses as $historyCourse) { + $historyData[] = [ + 'course_type' => $historyCourse->typeDetail->name ?? '', + 'course_name' => $historyCourse->course_name ?? '', + 'start_time' => $historyCourse->start_time ?? '', + 'end_time' => $historyCourse->end_time ?? '', + 'course_type_signs_pass' => $historyCourse->course_type_signs_pass ?? 0, + 'course_type_signs_pass_unique' => $historyCourse->course_type_signs_pass_unique ?? 0, + 'course_signs_pass' => $historyCourse->course_signs_pass ?? 0, + ]; + } + $historyFields = [ + 'course_type' => '课程体系', + 'course_name' => '课程名称', + 'start_time' => '开始时间', + 'end_time' => '结束时间', + 'course_type_signs_pass' => '培养人数未去重', + 'course_type_signs_pass_unique' => '培养人数去重', + 'course_signs_pass' => '课程培养人数', + ]; + + // 创建多 sheet 导出 + $sheets = [ + new SheetExport($currentData, $currentFields, '当前课程数据'), + new SheetExport($historyData, $historyFields, '历史课程数据'), + ]; + $filename = '审核通过人数去重明细'; + + // 直接返回多 sheet 导出 + return Excel::download( + new MultiSheetExport($sheets), + $filename . '_' . date('YmdHis') . '.xlsx' + ); break; case 'courseTypesSum': @@ -802,21 +926,30 @@ class OtherController extends CommonController break; case 'company_market_total': - // 上市公司明细 - 所有上市公司,关联学员、课程信息 - // 数据结构:主表是公司,子数据是学员信息 - // 导出时:公司信息只在第一行显示,后续行公司信息为空 - $companiesData = CourseSign::shangshi($start_date, $end_date, $course_ids->toArray(), true); + // 上市公司明细 - 与coursesHome统计逻辑保持一致 + // 使用CourseSign::shangshi方法获取数据,确保与统计逻辑一致 + $shangshiList = CourseSign::shangshi($start_date, $end_date, $course_ids, true); - foreach ($companiesData as $item) { - $company = $item['company']; + foreach ($shangshiList as $item) { + $company = $item['company'] ?? null; $users = $item['users'] ?? []; - // 公司基本信息(只在第一行使用) + if (!$company) { + continue; + } + + // 获取StockCompany信息(如果有) + $stockCompany = StockCompany::where('company_id', $company->id) + ->orWhere('company_name', $company->company_name) + ->first(); + + // 公司基本信息 $companyInfo = [ - 'company_name' => $company->company_name, + 'company_name' => $company->company_name ?? '', 'company_legal_representative' => $company->company_legal_representative ?? '', 'company_date' => $company->company_date ?? '', - 'stock_date' => $company->stock_date ?? '', + 'stock_date' => $stockCompany->stock_date ?? '', + 'is_after_enrollment' => $stockCompany && $stockCompany->is_after_enrollment == 1 ? '是' : '否', 'company_address' => $company->company_address ?? '', 'company_city' => $company->company_city ?? '', 'company_area' => $company->company_area ?? '', @@ -824,40 +957,29 @@ class OtherController extends CommonController ]; if (empty($users)) { - // 如果没有学员报名记录,仍然导出公司基本信息 + // 如果没有学员,仍然导出公司基本信息 $data[] = array_merge($companyInfo, [ 'user_name' => '', - 'course_name' => '', - 'course_type' => '', + 'course_names' => '', 'course_types' => '', 'course_count' => 0, ]); } else { - // 每个学员一行,多个课程合并显示 + // 每个学员一行,公司信息只在第一行显示,后续行公司信息为空 $isFirstRow = true; - foreach ($users as $userInfo) { - $courses = $userInfo['courses'] ?? []; + foreach ($users as $userData) { + $user = $userData['user'] ?? null; + $courses = $userData['courses'] ?? []; - // 合并同一学员的多个课程:格式为"课程体系-课程名称,课程体系-课程名称" - $courseList = []; - foreach ($courses as $courseInfo) { - $courseType = $courseInfo['course_type'] ?? ''; - $courseName = $courseInfo['course_name'] ?? ''; - if ($courseType && $courseName) { - $courseList[] = $courseType . '-' . $courseName; - } elseif ($courseName) { - $courseList[] = $courseName; - } + if (!$user) { + continue; } - $courseDisplay = implode("\r\n", $courseList); + + // 获取课程名称列表,用中文顿号分隔 + $courseNames = collect($courses)->pluck('course_name')->filter()->unique()->values()->implode('、'); // 获取课程体系列表,用中文顿号分隔 - $courseTypes = collect($courses) - ->pluck('course_type') - ->filter() - ->unique() - ->values() - ->implode('、'); + $courseTypes = collect($courses)->pluck('course_type')->filter()->unique()->values()->implode('、'); // 报名课程数 $courseCount = count($courses); @@ -865,9 +987,8 @@ class OtherController extends CommonController if ($isFirstRow) { // 第一行:显示公司信息 $data[] = array_merge($companyInfo, [ - 'user_name' => $userInfo['user_name'] ?? '', - 'course_name' => $courseDisplay, - 'course_type' => '', // 课程类型已合并到课程名称中 + 'user_name' => $userData['user_name'] ?? $user->name ?? '', + 'course_names' => $courseNames, 'course_types' => $courseTypes, 'course_count' => $courseCount, ]); @@ -879,12 +1000,13 @@ class OtherController extends CommonController 'company_legal_representative' => '', 'company_date' => '', 'stock_date' => '', + 'is_after_enrollment' => '', 'company_address' => '', 'company_city' => '', 'company_area' => '', 'company_tag' => '', - 'user_name' => $userInfo['user_name'] ?? '', - 'course_name' => $courseDisplay, + 'user_name' => $userData['user_name'] ?? $user->name ?? '', + 'course_names' => $courseNames, 'course_types' => $courseTypes, 'course_count' => $courseCount, ]; @@ -897,12 +1019,13 @@ class OtherController extends CommonController 'company_legal_representative' => '法人', 'company_date' => '成立时间', 'stock_date' => '上市日期', + 'is_after_enrollment' => '是否入学后上市', 'company_address' => '地址', 'company_city' => '所在城市', 'company_area' => '所在区域', 'company_tag' => '企业资质', 'user_name' => '学员姓名', - 'course_name' => '课程信息', + 'course_names' => '课程名称', 'course_types' => '课程体系', 'course_count' => '报名课程数', ]; @@ -1232,7 +1355,7 @@ class OtherController extends CommonController 'company_legal_representative' => $company->company_legal_representative ?? '', 'company_date' => $company->company_date ?? '', 'invest_date' => $item['invest_date'] ?? '', - 'first_sign_date' => $item['first_sign_date'], + 'first_sign_date' => $item['first_enrollment_date'] ?? '', 'user_names' => $userNames, 'company_address' => $company->company_address ?? '', 'company_city' => $company->company_city ?? '', @@ -1402,6 +1525,9 @@ class OtherController extends CommonController $users = CourseSign::companyJoin($start_date, $end_date, $course_ids, true, true); // 加载关联关系 $users->load('company'); + + // 当前学员数据 + $currentData = []; foreach ($users as $user) { // 获取该学员的课程报名记录 // 使用与 companyJoin 方法完全相同的 getStudentList 逻辑,确保数据一致 @@ -1423,7 +1549,7 @@ class OtherController extends CommonController // 报名课程数 $courseCount = $userCourseSigns->count(); - $data[] = [ + $currentData[] = [ 'user_name' => $user->name ?? '', 'mobile' => $user->mobile ?? '', 'company_name' => $user->company->company_name ?? $user->company_name, @@ -1438,7 +1564,7 @@ class OtherController extends CommonController 'course_count' => $courseCount, ]; } - $fields = [ + $currentFields = [ 'user_name' => '学员姓名', 'mobile' => '手机号', 'company_name' => '企业名称', @@ -1452,14 +1578,57 @@ class OtherController extends CommonController 'course_types' => '课程体系', 'course_count' => '报名课程数', ]; + + // 自定义数据(EmployeeParticipation type=1 员工参与数) + $customData = []; + $employeeParticipations = EmployeeParticipation::where(function ($query) use ($start_date, $end_date) { + // 开始结束日期的筛选。or查询 + if ($start_date && $end_date) { + $query->whereBetween('start_date', [$start_date, $end_date]) + ->orWhereBetween('end_date', [$start_date, $end_date]); + } + })->where('type', 1)->with('courseType')->get(); + + foreach ($employeeParticipations as $participation) { + $customData[] = [ + 'course_type' => $participation->courseType->name ?? '', + 'course_name' => $participation->course_name ?? '', + 'start_date' => $participation->start_date ?? '', + 'end_date' => $participation->end_date ?? '', + 'total' => $participation->total ?? 0, + ]; + } + $customFields = [ + 'course_type' => '课程体系', + 'course_name' => '课程名称', + 'start_date' => '开始日期', + 'end_date' => '结束日期', + 'total' => '参与数量', + ]; + + // 创建多 sheet 导出 + $sheets = [ + new SheetExport($currentData, $currentFields, '学员数据'), + new SheetExport($customData, $customFields, '自定义数据'), + ]; + $filename = '元和员工参与企业明细'; + + // 直接返回多 sheet 导出 + return Excel::download( + new MultiSheetExport($sheets), + $filename . '_' . date('YmdHis') . '.xlsx' + ); break; case 'company_ganbu_total': // 全市干部参与企业明细 - 使用模型方法 $users = CourseSign::ganbu($start_date, $end_date, $course_ids, true); + + // 当前学员数据 + $currentData = []; foreach ($users as $user) { - $data[] = [ + $currentData[] = [ 'user_name' => $user->name ?? '', 'mobile' => $user->mobile ?? '', 'company_name' => $user->company->company_name ?? '', @@ -1468,7 +1637,7 @@ class OtherController extends CommonController 'company_position' => $user->company_position ?? '', ]; } - $fields = [ + $currentFields = [ 'user_name' => '学员姓名', 'mobile' => '手机号', 'company_name' => '企业名称', @@ -1476,7 +1645,47 @@ class OtherController extends CommonController 'company_city' => '所在城市', 'company_position' => '职位', ]; + + // 自定义数据(EmployeeParticipation type=2 干部培训数) + $customData = []; + $employeeParticipations = EmployeeParticipation::where(function ($query) use ($start_date, $end_date) { + // 开始结束日期的筛选。or查询 + if ($start_date && $end_date) { + $query->whereBetween('start_date', [$start_date, $end_date]) + ->orWhereBetween('end_date', [$start_date, $end_date]); + } + })->where('type', 2)->with('courseType')->get(); + + foreach ($employeeParticipations as $participation) { + $customData[] = [ + 'course_type' => $participation->courseType->name ?? '', + 'course_name' => $participation->course_name ?? '', + 'start_date' => $participation->start_date ?? '', + 'end_date' => $participation->end_date ?? '', + 'total' => $participation->total ?? 0, + ]; + } + $customFields = [ + 'course_type' => '课程体系', + 'course_name' => '课程名称', + 'start_date' => '开始日期', + 'end_date' => '结束日期', + 'total' => '参与数量', + ]; + + // 创建多 sheet 导出 + $sheets = [ + new SheetExport($currentData, $currentFields, '学员数据'), + new SheetExport($customData, $customFields, '自定义数据'), + ]; + $filename = '全市干部参与企业明细'; + + // 直接返回多 sheet 导出 + return Excel::download( + new MultiSheetExport($sheets), + $filename . '_' . date('YmdHis') . '.xlsx' + ); break; case 'cover_head_total': @@ -1701,8 +1910,8 @@ class OtherController extends CommonController break; case 'cover_stock_total': - // 重点上市公司明细 - 使用模型方法 - $companiesData = CourseSign::shangshi($start_date, $end_date, $course_ids, true); + // 重点上市公司明细 - 使用模型方法,与统计逻辑保持一致 + $companiesData = CourseSign::suzhoushangshi($start_date, $end_date, $course_ids, true); foreach ($companiesData as $item) { $company = $item['company']; $users = $item['users'] ?? []; diff --git a/app/Http/Controllers/Admin/TraineeStudentController.php b/app/Http/Controllers/Admin/TraineeStudentController.php new file mode 100644 index 0000000..1c53eb3 --- /dev/null +++ b/app/Http/Controllers/Admin/TraineeStudentController.php @@ -0,0 +1,217 @@ +all(); + $list = $this->model->with(underlineToHump($all['show_relation'] ?? []))->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 . '%'); + } + // 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]); + } + } + } + })->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/trainee-students/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->with(underlineToHump($all['show_relation'] ?? []))->find($all['id']); + return $this->success($detail); + } + + /** + * @OA\Post( + * path="/api/admin/trainee-students/save", + * tags={"跟班学员管理"}, + * summary="保存", + * description="", + * @OA\Parameter(name="id", in="query", @OA\Schema(type="int"), required=true, description="Id(存在更新,不存在新增)"), + * @OA\Parameter(name="name", in="query", @OA\Schema(type="string", nullable=true), description="名字"), + * @OA\Parameter(name="start_date", in="query", @OA\Schema(type="string", format="date", nullable=true), description="开始日期"), + * @OA\Parameter(name="end_date", in="query", @OA\Schema(type="string", format="date", nullable=true), description="结束日期"), + * @OA\Parameter(name="total", in="query", @OA\Schema(type="integer", nullable=true), description="加减数据(正数表示加,负数表示减)"), + * @OA\Parameter(name="remark", in="query", @OA\Schema(type="string", format="textarea", nullable=true), description="备注"), + * @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(); + // 记录日志 + $this->saveLogs($original, $model); + return $this->success($model); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->fail([$exception->getCode(), $exception->getMessage()]); + } + } + + /** + * @OA\Get( + * path="/api/admin/trainee-students/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/Company.php b/app/Models/Company.php index 71adeef..745e3fc 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -176,15 +176,11 @@ 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, $skipIfHasCompany = true, $updateAddress = true, $updateLocation = true, $setCompanyIdOnFail = true) + public static function updateCompanyFromUser($user) { if (!$user || empty($user->company_name)) { return ['success' => false, 'message' => '用户或公司名称为空', 'company' => null]; @@ -192,7 +188,7 @@ class Company extends SoftDeletesModel // 如果已经有有效的公司关联(company_id > 0),跳过 // 允许处理 company_id = -1(待更新)或 null(初始状态)的情况 - if ($skipIfHasCompany && $user->company_id && $user->company_id > 0) { + if ($user->company_id && $user->company_id > 0) { return ['success' => false, 'message' => '用户已有公司关联', 'company' => null]; } @@ -217,27 +213,21 @@ class Company extends SoftDeletesModel if (!$result) { // 标识一下未匹配到公司,后续可以根据这个字段筛选出未匹配到公司的用户 - if ($setCompanyIdOnFail) { - $user->company_id = 0; - $user->save(); - } + $user->company_id = 0; + $user->save(); return ['success' => false, 'message' => '公司不存在', 'company' => null]; } // 如果$result['enterpriseName']存在数字,跳过 if (preg_match('/\d/', $result['enterpriseName'])) { - if ($setCompanyIdOnFail) { - $user->company_id = 0; - $user->save(); - } + $user->company_id = 0; + $user->save(); return ['success' => false, 'message' => '公司名称包含数字,跳过', 'company' => null]; } if ($result['status'] == '未注册') { - if ($setCompanyIdOnFail) { - $user->company_id = 0; - $user->save(); - } + $user->company_id = 0; + $user->save(); return ['success' => false, 'message' => '公司未注册,跳过', 'company' => null]; } @@ -273,13 +263,10 @@ class Company extends SoftDeletesModel 'project_users' => $result['projectUsers'] ?? null, // 股东信息 'partners' => $result['partners'] ?? null, + // 更新地址 + 'company_address' => $result['address'], ]; - // 根据参数决定是否更新地址 - if ($updateAddress) { - $data['company_address'] = $result['address']; - } - $company = Company::updateOrCreate($where, $data); // 更新用户关联 @@ -289,23 +276,89 @@ class Company extends SoftDeletesModel // 更新上市状态 self::updateMarketStatus($company->id); - // 根据参数决定是否更新位置(经纬度) - if ($updateLocation) { - self::updateLocation($company->id); - } + // 更新位置(经纬度) + self::updateLocation($company->id); return ['success' => true, 'message' => '更新成功', 'company' => $company]; } /** - * 全量同步公司信息(不包含地址和经纬度) - * @param User $user 用户对象 + * 直接同步公司信息(根据公司名称从接口获取最新信息更新) + * @param Company $company 公司对象 * @return array 返回结果 ['success' => bool, 'message' => string, 'company' => Company|null] */ - public static function syncCompanyFromUser($user) + public static function syncCompanyInfo($company) { - // 调用统一方法,参数设置为:不跳过已有公司、不更新地址、不更新经纬度、失败时不设置company_id - return self::updateCompanyFromUser($user, false, false, false, false); + if (!$company || empty($company->company_name)) { + return ['success' => false, 'message' => '公司或公司名称为空', 'company' => null]; + } + + // 清理公司名称 + $cleanedCompanyName = trim($company->company_name); + $cleanedCompanyName = preg_replace('/[\r\n\t]+/', '', $cleanedCompanyName); + $cleanedCompanyName = preg_replace('/\s+/', ' ', $cleanedCompanyName); + $cleanedCompanyName = trim($cleanedCompanyName); + + if (empty($cleanedCompanyName)) { + return ['success' => false, 'message' => '公司名称无效', 'company' => null]; + } + + $YuanheRepository = new YuanheRepository(); + + // 获取公司详细信息 + $result = $YuanheRepository->companyInfo(['enterpriseName' => $cleanedCompanyName]); + + if (!$result) { + return ['success' => false, 'message' => '公司不存在', 'company' => null]; + } + + // 如果$result['enterpriseName']存在数字,跳过 + if (preg_match('/\d/', $result['enterpriseName'])) { + return ['success' => false, 'message' => '公司名称包含数字,跳过', 'company' => null]; + } + + if ($result['status'] == '未注册') { + return ['success' => false, 'message' => '公司未注册,跳过', 'company' => null]; + } + + // 更新公司数据(不包含地址和经纬度) + $data = [ + 'business_scope' => $result['businessScope'], + 'company_city' => $result['city'], + 'contact_mail' => $result['contactMail'], + 'contact_phone' => $result['contactPhone'], + 'company_area' => $result['country'], + 'credit_code' => $result['creditCode'], + 'enterprise_id' => $result['enterpriseId'], + 'company_name' => $result['enterpriseName'], + 'is_abroad' => $result['isAbroad'], + 'company_market' => $result['isOnStock'], + 'is_yh_invested' => $result['isYhInvested'], + 'logo' => $result['logo'], + 'company_legal_representative' => $result['operName'], + 'company_province' => $result['province'], + 'company_industry' => combineKeyValue($result['qccIndustry']), + 'regist_amount' => $result['registAmount'], + 'regist_capi_type' => $result['registCapiType'], + 'company_date' => $result['startDate'], + 'status' => $result['status'], + 'stock_date' => $result['stockDate'], + 'currency_type' => $result['currencyType'], + '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->fill($data); + $company->save(); + + // 更新上市状态 + self::updateMarketStatus($company->id); + + return ['success' => true, 'message' => '更新成功', 'company' => $company]; } /** @@ -344,9 +397,27 @@ class Company extends SoftDeletesModel $stockCodePattern = '/\.(SWR|SW|WR|SS|RS|SB|PK|TO|AX|WS|PR|DB|UN|RT|WT|SH|SZ|BJ|TW|HK|SG|US|DE|FR|JP|KR|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|U|V|W|X|Y|Z)(?![A-Za-z0-9])/i'; $hasStockCode = preg_match($stockCodePattern, $company->company_tag); - // 检查是否包含"新三板" - $hasXinsanban = strpos($company->company_tag, '新三板') !== false; - // 如果匹配到股票代码或包含"新三板",则标记为上市 + + // 不属于新三板上市公司的关键字(需要排除) + $excludeXinsanbanKeywords = [ + '新三板摘牌', + '新三板挂牌审核', + '新三板终止', + '新三板退市', + '新三板撤销', + '新三板注销', + '新三板中止', + ]; + + // 检查是否包含排除关键字 + $hasExcludeKeyword = array_reduce($excludeXinsanbanKeywords, function ($carry, $keyword) use ($company) { + return $carry || strpos($company->company_tag, $keyword) !== false; + }, false); + + // 检查是否包含"新三板",且不包含排除关键字 + $hasXinsanban = !$hasExcludeKeyword && strpos($company->company_tag, '新三板') !== false; + + // 如果匹配到股票代码或包含"新三板"(且非排除关键字),则标记为上市 $newMarketStatus = ($hasStockCode || $hasXinsanban) ? 1 : 0; // 只有状态变化才更新 diff --git a/app/Models/Course.php b/app/Models/Course.php index 3fac59e..8a764e5 100755 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -299,97 +299,12 @@ class Course extends SoftDeletesModel /** * 获取课程统计项元数据 * 返回每个统计项的计算逻辑和验证方法说明 + * 从 statistics_metadata 表中读取 * @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' => '' - ], - ]; + return StatisticsMetadata::getAllAsArray(); } } diff --git a/app/Models/CourseSign.php b/app/Models/CourseSign.php index 50ad16a..7836291 100755 --- a/app/Models/CourseSign.php +++ b/app/Models/CourseSign.php @@ -684,6 +684,27 @@ class CourseSign extends SoftDeletesModel } } + /** + * 判断是否为苏州地区 + * @param \App\Models\User|null $user 用户对象 + * @param \App\Models\Company|null $company 公司对象 + * @return bool + */ + public static function isSuzhou($user = null, $company = null) + { + // 判断是否为苏州地区:user表的company_address包含"苏州",或关联公司的company_address包含"苏州",或company_city包含"苏州" + if ($user && $user->company_address && strpos($user->company_address, '苏州') !== false) { + return true; + } + if ($company && $company->company_address && strpos($company->company_address, '苏州') !== false) { + return true; + } + if ($company && $company->company_city && strpos($company->company_city, '苏州') !== false) { + return true; + } + return false; + } + /** * 头部企业(统计或列表) * @param string $start_date 开始日期 @@ -695,16 +716,53 @@ class CourseSign extends SoftDeletesModel public static function toubuqiye($start_date = null, $end_date = null, $course_ids = null, $retList = false) { $courseSignsQuery = self::getStudentList($start_date, $end_date, 1, $course_ids); - $courseSignByType = $courseSignsQuery->get(); - $list = Company::approvedStudents()->whereHas('users', function ($query) use ($courseSignByType) { - $query->whereIn('id', $courseSignByType->pluck('user_id')); - })->where('company_tag', 'like', '%' . '高新技术企业' . '%')->get(); + $courseSigns = $courseSignsQuery->with(['user.company'])->get(); + + // 获取所有高新技术企业的公司ID + $companyIds = $courseSigns->pluck('user.company.id') + ->filter() + ->unique() + ->toArray(); + + // 获取这些公司中标记为高新技术企业的公司 + $companies = Company::approvedStudents()->whereIn('id', $companyIds) + ->where('company_tag', 'like', '%' . '高新技术企业' . '%') + ->get() + ->keyBy('id'); + + // 筛选苏州地区的公司 + $suzhouCompanyIds = []; + foreach ($courseSigns as $courseSign) { + if (!$courseSign->user || !$courseSign->user->company) { + continue; + } + + $companyId = $courseSign->user->company->id; + // 只处理高新技术企业的记录 + if (!isset($companies[$companyId])) { + continue; + } + + $user = $courseSign->user; + $company = $user->company; + + // 使用通用方法判断是否为苏州地区 + if (self::isSuzhou($user, $company)) { + $suzhouCompanyIds[$companyId] = true; + } + } + + // 只保留苏州地区的高新技术企业 + $suzhouCompanies = $companies->filter(function ($company) use ($suzhouCompanyIds) { + return isset($suzhouCompanyIds[$company->id]); + }); + if ($retList) { // 返回列表 - return $list; + return $suzhouCompanies->values(); } else { // 返回统计数据 - return $list->count(); + return count($suzhouCompanyIds); } } @@ -720,10 +778,10 @@ class CourseSign extends SoftDeletesModel $query->whereHas('typeDetail', function ($q) { $q->where('name', '人才培训'); }); - })->get(); + })->with(['user.company'])->get(); // 条件2:data 字段中 name="个人荣誉" 且 value != "其他" 的用户 - $courseSigns2 = $courseSignsQuery->get()->filter(function ($courseSign) { + $courseSigns2 = $courseSignsQuery->with(['user.company'])->get()->filter(function ($courseSign) { if (empty($courseSign->data) || !is_array($courseSign->data)) { return false; } @@ -743,10 +801,37 @@ class CourseSign extends SoftDeletesModel ->unique() ->filter(); + // 筛选苏州地区的用户 + $suzhouUserIds = []; + $allCourseSigns = $courseSigns1->merge($courseSigns2); + + foreach ($allCourseSigns as $courseSign) { + if (!$courseSign->user) { + continue; + } + + $userId = $courseSign->user->id; + // 只处理符合条件的用户 + if (!in_array($userId, $allUserIds->toArray())) { + continue; + } + + $user = $courseSign->user; + $company = $user->company; + + // 使用通用方法判断是否为苏州地区 + if (self::isSuzhou($user, $company)) { + $suzhouUserIds[$userId] = true; + } + } + + // 只保留苏州地区的用户ID + $suzhouUserIds = array_keys($suzhouUserIds); + if ($retList) { - return User::whereIn('id', $allUserIds)->get(); + return User::whereIn('id', $suzhouUserIds)->get(); } else { - return $allUserIds->count(); + return count($suzhouUserIds); } } @@ -831,6 +916,234 @@ class CourseSign extends SoftDeletesModel } } + + /** + * 苏州上市公司(统计或列表) + * @param string|null $start_date 开始日期 + * @param string|null $end_date 结束日期 + * @param array|null $course_ids 课程ID数组,不传则统计所有课程 + * @param bool $retList 是否返回列表,false返回数量,true返回列表(包含学员、课程信息) + * @return int|array + */ + public static function suzhoushangshi($start_date = null, $end_date = null, $course_ids = null, $retList = false) + { + $courseSignsQuery = self::getStudentList($start_date, $end_date, 1, $course_ids); + $courseSigns = $courseSignsQuery->with(['user.company', 'course.typeDetail'])->get(); + + // 获取所有上市公司的ID + $companyIds = $courseSigns->pluck('user.company.id') + ->filter() + ->unique() + ->toArray(); + + // 获取这些公司中标记为上市的公司 + $companies = Company::approvedStudents()->whereIn('id', $companyIds) + ->where('company_market', 1) + ->get() + ->keyBy('id'); + + // 筛选苏州地区的公司 + $suzhouCompanyIds = []; + foreach ($courseSigns as $courseSign) { + if (!$courseSign->user || !$courseSign->user->company) { + continue; + } + + $companyId = $courseSign->user->company->id; + // 只处理上市公司的记录 + if (!isset($companies[$companyId])) { + continue; + } + + $user = $courseSign->user; + $company = $user->company; + + // 使用通用方法判断是否为苏州地区 + if (self::isSuzhou($user, $company)) { + $suzhouCompanyIds[$companyId] = true; + } + } + + // 只保留苏州地区的上市公司 + $suzhouCompanies = $companies->filter(function ($company) use ($suzhouCompanyIds) { + return isset($suzhouCompanyIds[$company->id]); + }); + + if ($retList) { + // 返回详细列表:主表是公司,子数据是学员信息 + $result = []; + foreach ($courseSigns as $courseSign) { + if (!$courseSign->user || !$courseSign->user->company) { + continue; + } + + $companyId = $courseSign->user->company->id; + // 只处理苏州上市公司的记录 + if (!isset($suzhouCompanyIds[$companyId])) { + continue; + } + + $company = $suzhouCompanies[$companyId] ?? $courseSign->user->company; + + // 如果公司还没有在结果中,初始化 + if (!isset($result[$companyId])) { + $result[$companyId] = [ + 'company' => $company, + 'users' => [], + ]; + } + + // 按学员分组,收集每个学员的课程信息 + $userId = $courseSign->user->id; + if (!isset($result[$companyId]['users'][$userId])) { + $result[$companyId]['users'][$userId] = [ + 'user' => $courseSign->user, + 'user_name' => $courseSign->user->name ?? '', + 'mobile' => $courseSign->user->mobile ?? '', + 'courses' => [], + ]; + } + + // 添加该学员的课程信息 + $result[$companyId]['users'][$userId]['courses'][] = [ + 'course_name' => $courseSign->course->name ?? '', + 'course_type' => $courseSign->course->typeDetail->name ?? '', + 'course_sign' => $courseSign, + ]; + } + + // 将 users 转换为数组(去掉 user_id 作为 key) + foreach ($result as $companyId => $item) { + $result[$companyId]['users'] = array_values($item['users']); + } + + // 转换为数组并返回 + return array_values($result); + } else { + // 返回统计数据 + return count($suzhouCompanyIds); + } + } + + /** + * 苏州重点上市公司(统计或列表) + * 在上市公司基础上,限定为苏州地区的公司 + * 条件:user表的company_address包含"苏州",或关联公司的company_address包含"苏州",或company_city包含"苏州" + * @param string|null $start_date 开始日期 + * @param string|null $end_date 结束日期 + * @param array|null $course_ids 课程ID数组,不传则统计所有课程 + * @param bool $retList 是否返回列表,false返回数量,true返回列表(包含学员、课程信息) + * @return int|array + */ + public static function suzhouStock($start_date = null, $end_date = null, $course_ids = null, $retList = false) + { + $courseSignsQuery = self::getStudentList($start_date, $end_date, 1, $course_ids); + $courseSigns = $courseSignsQuery->with(['user.company', 'course.typeDetail'])->get(); + + // 获取所有上市公司的ID + $companyIds = $courseSigns->pluck('user.company.id') + ->filter() + ->unique() + ->toArray(); + + // 获取这些公司中标记为上市的公司 + $companies = Company::approvedStudents()->whereIn('id', $companyIds) + ->where('company_market', 1) + ->get() + ->keyBy('id'); + + // 筛选苏州地区的公司 + $suzhouCompanyIds = []; + foreach ($courseSigns as $courseSign) { + if (!$courseSign->user || !$courseSign->user->company) { + continue; + } + + $companyId = $courseSign->user->company->id; + // 只处理上市公司的记录 + if (!isset($companies[$companyId])) { + continue; + } + + $user = $courseSign->user; + $company = $user->company; + + // 判断是否为苏州地区:user表的company_address包含"苏州",或关联公司的company_address包含"苏州",或company_city包含"苏州" + $isSuzhou = false; + if ($user->company_address && strpos($user->company_address, '苏州') !== false) { + $isSuzhou = true; + } elseif ($company->company_address && strpos($company->company_address, '苏州') !== false) { + $isSuzhou = true; + } elseif ($company->company_city && strpos($company->company_city, '苏州') !== false) { + $isSuzhou = true; + } + + if ($isSuzhou) { + $suzhouCompanyIds[$companyId] = true; + } + } + + // 只保留苏州地区的上市公司 + $suzhouCompanies = $companies->filter(function ($company) use ($suzhouCompanyIds) { + return isset($suzhouCompanyIds[$company->id]); + }); + + if ($retList) { + // 返回详细列表:主表是公司,子数据是学员信息 + $result = []; + foreach ($courseSigns as $courseSign) { + if (!$courseSign->user || !$courseSign->user->company) { + continue; + } + + $companyId = $courseSign->user->company->id; + // 只处理苏州上市公司的记录 + if (!isset($suzhouCompanyIds[$companyId])) { + continue; + } + + $company = $suzhouCompanies[$companyId] ?? $courseSign->user->company; + + // 如果公司还没有在结果中,初始化 + if (!isset($result[$companyId])) { + $result[$companyId] = [ + 'company' => $company, + 'users' => [], + ]; + } + + // 按学员分组,收集每个学员的课程信息 + $userId = $courseSign->user->id; + if (!isset($result[$companyId]['users'][$userId])) { + $result[$companyId]['users'][$userId] = [ + 'user' => $courseSign->user, + 'user_name' => $courseSign->user->name ?? '', + 'mobile' => $courseSign->user->mobile ?? '', + 'courses' => [], + ]; + } + + // 添加该学员的课程信息 + $result[$companyId]['users'][$userId]['courses'][] = [ + 'course_name' => $courseSign->course->name ?? '', + 'course_type' => $courseSign->course->typeDetail->name ?? '', + 'course_sign' => $courseSign, + ]; + } + + // 将 users 转换为数组(去掉 user_id 作为 key) + foreach ($result as $companyId => $item) { + $result[$companyId]['users'] = array_values($item['users']); + } + + // 转换为数组并返回 + return array_values($result); + } else { + // 返回统计数据 + return count($suzhouCompanyIds); + } + } + /** * 今年上市公司(统计或列表) * @param string|null $start_date 开始日期 diff --git a/app/Models/StatisticsMetadata.php b/app/Models/StatisticsMetadata.php new file mode 100644 index 0000000..ba138f3 --- /dev/null +++ b/app/Models/StatisticsMetadata.php @@ -0,0 +1,36 @@ +first(); + } + + /** + * 获取所有统计元数据,以 key 为索引的数组 + * @return array + */ + public static function getAllAsArray() + { + $items = self::all(); + $result = []; + foreach ($items as $item) { + $result[$item->key] = [ + 'name' => $item->name, + 'from' => $item->from, + 'verify' => $item->verify, + ]; + } + return $result; + } +} diff --git a/app/Models/TraineeStudent.php b/app/Models/TraineeStudent.php new file mode 100644 index 0000000..6ef9fe2 --- /dev/null +++ b/app/Models/TraineeStudent.php @@ -0,0 +1,8 @@ + 1, + 'company_area' => 1, + 'company_industry' => 1, + 'company_type' => 1, + 'is_vip' => ['普通学员', 'VIP学员'], + 'is_schoolmate' => ['否', '是'], + ]; + + /** + * Boot 方法 - 处理模型事件 + */ + protected static function boot() { - return $this->username; + parent::boot(); + + static::saving(function ($user) { + // 当 is_schoolmate 被设置为 1 时,自动设置成为校友时间 + if ($user->isDirty('is_schoolmate')) { + if ($user->is_schoolmate == 1) { + // 如果是从非校友变成校友,设置成为校友时间为当前时间 + if ($user->getOriginal('is_schoolmate') != 1) { + $user->schoolmate_time = now(); + } + } elseif ($user->is_schoolmate == 0 || $user->is_schoolmate === null) { + // 如果设置为 0 或 null,清空成为校友时间 + $user->schoolmate_time = null; + } + } + }); } public function getMobileAttribute($value) @@ -173,19 +206,7 @@ class User extends Authenticatable implements Auditable return $this->is_company_schoolmate == 1 ? '是' : '否'; } - /** - * 数据类型转换,数字转字符串。 - * 值是id则是数据字典的顶级id,或者是枚举型数组 - * @var array[] - */ - public static $intToString = [ - 'company_position' => 1, - 'company_area' => 1, - 'company_industry' => 1, - 'company_type' => 1, - 'is_vip' => ['普通学员', 'VIP学员'], - 'is_schoolmate' => ['否', '是'], - ]; + public function courses() { @@ -227,6 +248,8 @@ class User extends Authenticatable implements Auditable return $this->hasOne(Company::class, 'id', 'company_id'); } + + /** * 获取预约剩余次数 */ diff --git a/database/migrations/2026_01_16_111947_create_statistics_metadata_table.php b/database/migrations/2026_01_16_111947_create_statistics_metadata_table.php new file mode 100644 index 0000000..a3840eb --- /dev/null +++ b/database/migrations/2026_01_16_111947_create_statistics_metadata_table.php @@ -0,0 +1,38 @@ +comment('统计指标元数据'); + $table->increments('id'); + $table->string('key')->unique()->comment('统计项标识,如 course_signs_total'); + $table->string('name')->comment('统计项名称'); + $table->text('from')->nullable()->comment('统计逻辑说明'); + $table->text('verify')->nullable()->comment('验证方法说明'); + $table->text('remark')->nullable()->comment('备注'); + $table->dateTime('created_at')->nullable(); + $table->dateTime('updated_at')->nullable(); + $table->dateTime('deleted_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('statistics_metadata'); + } +}; diff --git a/database/migrations/2026_01_17_094313_create_trainee_students_table.php b/database/migrations/2026_01_17_094313_create_trainee_students_table.php new file mode 100644 index 0000000..335ade0 --- /dev/null +++ b/database/migrations/2026_01_17_094313_create_trainee_students_table.php @@ -0,0 +1,38 @@ +id(); + $table->integer('admin_id')->nullable(); + $table->integer('department_id')->nullable(); + $table->string('name')->nullable()->comment('名字'); + $table->date('start_date')->nullable()->comment('开始日期'); + $table->date('end_date')->nullable()->comment('结束日期'); + $table->integer('total')->default(0)->comment('加减数据(正数表示加,负数表示减)'); + $table->text('remark')->nullable()->comment('备注'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('trainee_students'); + } +}; diff --git a/database/migrations/2026_01_17_095245_add_fields_to_employee_participations_table.php b/database/migrations/2026_01_17_095245_add_fields_to_employee_participations_table.php new file mode 100644 index 0000000..ee8fd39 --- /dev/null +++ b/database/migrations/2026_01_17_095245_add_fields_to_employee_participations_table.php @@ -0,0 +1,33 @@ +string('company_name')->nullable()->comment('公司名字')->after('course_name'); + $table->string('name')->nullable()->comment('姓名')->after('company_name'); + $table->string('department')->nullable()->comment('部门')->after('name'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('employee_participations', function (Blueprint $table) { + $table->dropColumn(['company_name', 'name', 'department']); + }); + } +}; diff --git a/database/migrations/2026_01_17_110058_add_schoolmate_date_to_users_table.php b/database/migrations/2026_01_17_110058_add_schoolmate_date_to_users_table.php new file mode 100644 index 0000000..148212c --- /dev/null +++ b/database/migrations/2026_01_17_110058_add_schoolmate_date_to_users_table.php @@ -0,0 +1,31 @@ +dateTime('schoolmate_time')->nullable()->comment('成为校友时间')->after('is_schoolmate'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('schoolmate_time'); + }); + } +}; diff --git a/database/seeders/StatisticsMetadataSeeder.php b/database/seeders/StatisticsMetadataSeeder.php new file mode 100644 index 0000000..642321d --- /dev/null +++ b/database/seeders/StatisticsMetadataSeeder.php @@ -0,0 +1,56 @@ +command->warn('统计指标说明.json 文件不存在,跳过数据填充'); + return; + } + + $jsonContent = File::get($jsonPath); + $data = json_decode($jsonContent, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->command->error('JSON 文件解析失败:' . json_last_error_msg()); + return; + } + + if (!is_array($data)) { + $this->command->error('JSON 数据格式不正确,应为对象/数组'); + return; + } + + // 清空现有数据(可选,根据需要决定是否保留) + // StatisticsMetadata::truncate(); + + // 插入数据 + foreach ($data as $key => $item) { + StatisticsMetadata::updateOrCreate( + ['key' => $key], + [ + 'name' => $item['name'] ?? '', + 'from' => $item['from'] ?? '', + 'verify' => $item['verify'] ?? '', + ] + ); + } + + $this->command->info('统计指标元数据填充完成,共处理 ' . count($data) . ' 条记录'); + } +} diff --git a/routes/api.php b/routes/api.php index 7196cf7..7be9ba6 100755 --- a/routes/api.php +++ b/routes/api.php @@ -283,6 +283,14 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () { Route::get('employee-participations/show', [\App\Http\Controllers\Admin\EmployeeParticipationController::class, "show"]); Route::post('employee-participations/save', [\App\Http\Controllers\Admin\EmployeeParticipationController::class, "save"]); Route::get('employee-participations/destroy', [\App\Http\Controllers\Admin\EmployeeParticipationController::class, "destroy"]); + Route::post('employee-participations/excel-show', [\App\Http\Controllers\Admin\EmployeeParticipationController::class, "excelShow"]); + Route::post('employee-participations/import', [\App\Http\Controllers\Admin\EmployeeParticipationController::class, "import"]); + + // 跟班学员 + Route::get('trainee-students/index', [\App\Http\Controllers\Admin\TraineeStudentController::class, "index"]); + Route::get('trainee-students/show', [\App\Http\Controllers\Admin\TraineeStudentController::class, "show"]); + Route::post('trainee-students/save', [\App\Http\Controllers\Admin\TraineeStudentController::class, "save"]); + Route::get('trainee-students/destroy', [\App\Http\Controllers\Admin\TraineeStudentController::class, "destroy"]); // 统计数据配置管理 Route::get('statistics-configs/index', [\App\Http\Controllers\Admin\StatisticsConfigController::class, "index"]); diff --git a/数据库索引优化建议.sql b/数据库索引优化建议.sql new file mode 100644 index 0000000..4c144b3 --- /dev/null +++ b/数据库索引优化建议.sql @@ -0,0 +1,295 @@ +-- ============================================ +-- 数据库索引优化建议 +-- 基于 coursesHome 统计逻辑分析 +-- ============================================ + +-- ============================================ +-- 1. course_signs 表索引 +-- ============================================ + +-- 1.1 核心查询索引:status + course_id(最常用) +-- 用于:courseSignsTotal, courseSignsTotalByUnique, getStudentList +-- 查询条件:status, course_id, whereHas('course') +ALTER TABLE `course_signs` +ADD INDEX `idx_status_course_id` (`status`, `course_id`); + +-- 1.2 用户去重查询索引:user_id + status +-- 用于:courseSignsTotalByUnique(按手机号去重) +ALTER TABLE `course_signs` +ADD INDEX `idx_user_id_status` (`user_id`, `status`); + +-- 1.3 排除状态索引:status(用于 whereNotIn status [4,5,6]) +-- 注意:如果 status 字段选择性不高,可考虑与 course_id 组合 +ALTER TABLE `course_signs` +ADD INDEX `idx_status_not_in` (`status`); + +-- 1.4 复合索引:status + course_id + user_id(用于关联查询优化) +ALTER TABLE `course_signs` +ADD INDEX `idx_status_course_user` (`status`, `course_id`, `user_id`); + +-- ============================================ +-- 2. courses 表索引 +-- ============================================ + +-- 2.1 图表统计索引:is_chart + 日期范围 +-- 用于:getStudentList 中的 whereHas('course') +-- 查询条件:is_chart=1, start_date/end_date BETWEEN +ALTER TABLE `courses` +ADD INDEX `idx_is_chart_dates` (`is_chart`, `start_date`, `end_date`); + +-- 2.2 课程体系索引:type + is_chart +-- 用于:按课程体系筛选课程 +ALTER TABLE `courses` +ADD INDEX `idx_type_is_chart` (`type`, `is_chart`); + +-- 2.3 复合索引:type + is_chart + start_date + end_date +-- 用于:课程分类明细统计 +ALTER TABLE `courses` +ADD INDEX `idx_type_chart_dates` (`type`, `is_chart`, `start_date`, `end_date`); + +-- ============================================ +-- 3. calendars 表索引 +-- ============================================ + +-- 3.1 日期范围查询索引:start_time + end_time +-- 用于:getCourseTotal, getCourseDayTotal +-- 查询条件:start_time/end_time BETWEEN +ALTER TABLE `calendars` +ADD INDEX `idx_dates_range` (`start_time`, `end_time`); + +-- 3.2 课程体系索引:course_type_id + 日期 +-- 用于:按课程体系筛选日历 +ALTER TABLE `calendars` +ADD INDEX `idx_course_type_dates` (`course_type_id`, `start_time`, `end_time`); + +-- 3.3 统计天数索引:is_count_days + 日期 +-- 用于:getCourseDayTotal(开课天数统计) +ALTER TABLE `calendars` +ADD INDEX `idx_count_days_dates` (`is_count_days`, `start_time`, `end_time`); + +-- 3.4 课程关联索引:course_id + course_type_id +-- 用于:通过 course.type 匹配课程体系 +ALTER TABLE `calendars` +ADD INDEX `idx_course_type_id` (`course_id`, `course_type_id`); + +-- ============================================ +-- 4. companies 表索引 +-- ============================================ + +-- 4.1 上市公司索引:company_market +-- 用于:shangshi, suzhouStock(上市公司统计) +ALTER TABLE `companies` +ADD INDEX `idx_company_market` (`company_market`); + +-- 4.2 被投企业索引:is_yh_invested +-- 用于:yhInvestedTotal, companyInvestedYear(被投企业统计) +ALTER TABLE `companies` +ADD INDEX `idx_is_yh_invested` (`is_yh_invested`); + +-- 4.3 企业标签索引:company_tag(用于 LIKE 查询) +-- 用于:toubuqiye(高新技术企业筛选) +-- 注意:LIKE '%高新技术企业%' 无法使用索引,但可以优化前缀匹配 +ALTER TABLE `companies` +ADD INDEX `idx_company_tag` (`company_tag`(100)); + +-- 4.4 城市区域索引:company_city + company_area +-- 用于:area(区域统计), isSuzhou(苏州筛选) +ALTER TABLE `companies` +ADD INDEX `idx_city_area` (`company_city`, `company_area`); + +-- 4.5 地址索引:company_address(用于 LIKE 查询) +-- 用于:isSuzhou(苏州筛选) +-- 注意:LIKE '%苏州%' 无法使用索引,但可以优化前缀匹配 +ALTER TABLE `companies` +ADD INDEX `idx_company_address` (`company_address`(100)); + +-- 4.6 复合索引:company_market + company_city(用于苏州上市公司) +ALTER TABLE `companies` +ADD INDEX `idx_market_city` (`company_market`, `company_city`); + +-- ============================================ +-- 5. users 表索引 +-- ============================================ + +-- 5.1 公司关联索引:company_id +-- 用于:通过用户关联公司 +ALTER TABLE `users` +ADD INDEX `idx_company_id` (`company_id`); + +-- 5.2 跟班学员索引:from(用于 LIKE 查询) +-- 用于:genban, ganbu(跟班学员统计) +-- 注意:LIKE '%跟班学员%' 无法使用索引,但可以优化前缀匹配 +ALTER TABLE `users` +ADD INDEX `idx_from` (`from`(50)); + +-- 5.3 手机号索引:mobile(用于去重) +-- 用于:courseSignsTotalByUnique(按手机号去重) +ALTER TABLE `users` +ADD INDEX `idx_mobile` (`mobile`); + +-- 5.4 公司名称索引:company_name(用于 LIKE 查询) +-- 用于:companyJoin(元和员工筛选) +-- 注意:LIKE '%元禾控股%' 等无法使用索引 +ALTER TABLE `users` +ADD INDEX `idx_company_name` (`company_name`(100)); + +-- 5.5 用户地址索引:company_address(用于 LIKE 查询) +-- 用于:isSuzhou(苏州筛选) +ALTER TABLE `users` +ADD INDEX `idx_user_company_address` (`company_address`(100)); + +-- 5.6 复合索引:company_id + from(用于跟班学员筛选) +ALTER TABLE `users` +ADD INDEX `idx_company_from` (`company_id`, `from`(50)); + +-- ============================================ +-- 6. stock_companies 表索引 +-- ============================================ + +-- 6.1 上市日期索引:stock_date +-- 用于:company_market_year_total(今年上市公司数量) +ALTER TABLE `stock_companys` +ADD INDEX `idx_stock_date` (`stock_date`); + +-- 6.2 入学后上市索引:is_after_enrollment +-- 用于:company_market_after_enrollment_total +ALTER TABLE `stock_companys` +ADD INDEX `idx_after_enrollment` (`is_after_enrollment`); + +-- 6.3 公司关联索引:company_id +-- 用于:关联 companies 表 +ALTER TABLE `stock_companys` +ADD INDEX `idx_company_id` (`company_id`); + +-- 6.4 复合索引:is_after_enrollment + stock_date +ALTER TABLE `stock_companys` +ADD INDEX `idx_enrollment_date` (`is_after_enrollment`, `stock_date`); + +-- ============================================ +-- 7. history_courses 表索引 +-- ============================================ + +-- 7.1 日期范围索引:start_time + end_time +-- 用于:历史课程统计 +ALTER TABLE `history_courses` +ADD INDEX `idx_history_dates` (`start_time`, `end_time`); + +-- 7.2 课程类型索引:type + 日期 +-- 用于:按课程体系筛选历史课程 +ALTER TABLE `history_courses` +ADD INDEX `idx_type_dates` (`type`, `start_time`, `end_time`); + +-- 7.3 日历关联索引:calendar_id +-- 用于:whereHas('calendar', is_count_people=1) +ALTER TABLE `history_courses` +ADD INDEX `idx_calendar_id` (`calendar_id`); + +-- 7.4 复合索引:type + calendar_id + 日期 +ALTER TABLE `history_courses` +ADD INDEX `idx_type_calendar_dates` (`type`, `calendar_id`, `start_time`, `end_time`); + +-- ============================================ +-- 8. course_types 表索引 +-- ============================================ + +-- 8.1 历史课程索引:is_history +-- 用于:筛选历史课程类型 +ALTER TABLE `course_types` +ADD INDEX `idx_is_history` (`is_history`); + +-- 8.2 跟班学员统计索引:is_count_genban +-- 用于:genban(筛选需要统计跟班学员的课程) +ALTER TABLE `course_types` +ADD INDEX `idx_is_count_genban` (`is_count_genban`); + +-- ============================================ +-- 9. 关联查询优化索引 +-- ============================================ + +-- 9.1 course_signs 关联 users 优化 +-- 已通过 user_id 索引优化 + +-- 9.2 users 关联 course_signs 优化 +-- 需要在 course_signs 表已有 user_id 索引 + +-- 9.3 companies 关联 users 优化 +-- 需要在 users 表已有 company_id 索引 + +-- 9.4 courses 关联 course_types 优化 +-- 需要在 courses 表已有 type 索引 + +-- ============================================ +-- 10. 特殊查询优化建议 +-- ============================================ + +-- 10.1 JSON 字段查询优化 +-- companies.project_users 字段(JSON)无法直接建立索引 +-- 建议:如果 investDate 查询频繁,考虑单独建立 invest_dates 表或字段 + +-- 10.2 LIKE 查询优化 +-- 对于 '%关键词%' 类型的 LIKE 查询,无法使用普通索引 +-- 建议: +-- 1. 如果可能,改为前缀匹配 '关键词%' 可以使用索引 +-- 2. 考虑使用全文索引(FULLTEXT): +ALTER TABLE `companies` +ADD FULLTEXT INDEX `ft_company_tag` (`company_tag`); + +ALTER TABLE `users` +ADD FULLTEXT INDEX `ft_company_name` (`company_name`); + +ALTER TABLE `users` +ADD FULLTEXT INDEX `ft_from` (`from`); + +-- 10.3 日期范围查询优化 +-- 对于 start_date/end_date BETWEEN 查询,确保日期字段有索引 +-- 对于 orWhereBetween 查询,MySQL 可能无法同时使用两个索引 +-- 建议:如果性能问题,考虑拆分为两个查询 UNION + +-- ============================================ +-- 11. 索引使用说明 +-- ============================================ + +-- 11.1 索引创建顺序 +-- 建议按照表的数据量和查询频率,优先创建高频查询的索引 + +-- 11.2 索引维护 +-- 定期使用 EXPLAIN 分析查询计划,确认索引被正确使用 +-- 示例:EXPLAIN SELECT * FROM course_signs WHERE status=1 AND course_id IN (1,2,3); + +-- 11.3 索引监控 +-- 使用以下查询监控索引使用情况: +-- SELECT * FROM sys.schema_unused_indexes; +-- SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage; + +-- 11.4 注意事项 +-- 1. 索引会占用存储空间,增加写入成本 +-- 2. 不要过度索引,每个表建议不超过 5-7 个索引 +-- 3. 复合索引的顺序很重要,将选择性高的字段放在前面 +-- 4. 定期分析表,更新统计信息:ANALYZE TABLE table_name; + +-- ============================================ +-- 12. 性能优化建议 +-- ============================================ + +-- 12.1 查询优化 +-- 1. 避免在 WHERE 子句中使用函数 +-- 2. 使用 EXISTS 替代 IN(当子查询结果集较大时) +-- 3. 合理使用 JOIN,避免过度嵌套 + +-- 12.2 分页优化 +-- 对于大数据量分页,考虑使用游标分页替代 OFFSET + +-- 12.3 缓存策略 +-- 对于统计类查询,考虑使用 Redis 缓存结果 + +-- ============================================ +-- 索引创建脚本执行顺序建议 +-- ============================================ + +-- 1. 先创建核心表索引(course_signs, courses, calendars) +-- 2. 再创建关联表索引(companies, users) +-- 3. 最后创建辅助表索引(stock_companies, history_courses) + +-- 执行前请备份数据库! +-- 建议在业务低峰期执行索引创建操作 + diff --git a/统计指标说明.json b/统计指标说明.json new file mode 100644 index 0000000..f460385 --- /dev/null +++ b/统计指标说明.json @@ -0,0 +1,87 @@ +{ + "course_signs_total": { + "name": "报名人数", + "from": "1、统计指定时间范围和课程范围内的所有报名记录数量,包括所有状态的报名(待审核、已通过、已拒绝等);2、加上额外添加的课程数据中的人数统计;3、注意:一个学员报名多个课程会计算多次。", + "verify": "" + }, + "course_signs_pass": { + "name": "培养人次(未去重)", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录数量;2、统计额外添加的课程数据(关联的分类是历史课程,关联的日历是统计人数选项)里满足时间范围内的课程(课程开始或结束时间在时间范围内)培养人数未去重的数据;3、将两部分数据相加得到总数。", + "verify": "报名管理栏目,累加统计对比" + }, + "course_signs_pass_unique": { + "name": "培养人数(已去重)", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录数量按照手机号去重;2、统计额外添加的课程数据(关联的分类是历史课程,关联的日历是统计人数选项)里满足时间范围内的课程(课程开始或结束时间在时间范围内)培养人数去重的数据;3、将两部分数据相加得到总数。", + "verify": "" + }, + "course_total": { + "name": "开课场次", + "from": "1、统计指定时间范围内、指定课程体系范围内的日历里记录数量", + "verify": "日历里的数据导出对比" + }, + "course_day_total": { + "name": "开课天数", + "from": "1、在开课场次的基础上;2、获取设置了统计天数的记录;3、计算天数总和。", + "verify": "日历里的数据导出对比" + }, + "company_market_total": { + "name": "重点上市公司数", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户总数;2、如果一个用户报名多个课程只算一个有效用户;3、筛选出关联的公司是上市公司的用户数量。", + "verify": "企业资质中包含了上市代码的就识别成上市公司" + }, + "ganbu_total": { + "name": "跟班学员数", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户总数;2、如果一个用户报名多个课程只算一个有效用户;3、筛选出有\"跟班学员\"标签的用户总数。", + "verify": "学员中心,学员标签筛选对比" + }, + "company_market_year_total": { + "name": "今年上市公司数量", + "from": "1、从自定义的上市公司数据里;2、统计上市年份在今年的公司数量。", + "verify": "自定义上市公司数据对比" + }, + "company_market_after_enrollment_total": { + "name": "入学后上市公司数量", + "from": "1、直接从上市公司表中查询;2、统计所有标记为\"入学后上市\"的公司数量;3、注意:这个统计不依赖学员报名记录,统计的是自定义的上市的公司。", + "verify": "自定义上市公司数据对比" + }, + "course_signs_invested": { + "name": "累计被投企业数", + "from": "1、指定课程体系下,从2000-01-01到结束日期的所有课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户关联的公司总数;2、筛选被投企业标签的公司;3、再次筛选存在被投日期在时间范围内的公司总数。", + "verify": "" + }, + "company_invested_after_enrollment_total": { + "name": "入学后被投企业数量", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户关联的公司总数;2、筛选被投企业标签的公司;3、再次筛选存在被投日期并且被投日期在用户报名的课程的开始时间之后的公司数量。", + "verify": "" + }, + "company_invested_year_total": { + "name": "今年被投企业数", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户关联的公司总数;2、筛选出有被投企业标签的公司;3、再次筛选存在被投日期并且被投日期是今年的公司数量。", + "verify": "" + }, + "company_join_total": { + "name": "元和员工参与人数", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下报名记录的用户总数(如果一个用户报名多个课程只算一个有效用户);2、筛选自己填写的公司名称包含以下关键词的用户总数:元禾控股、元禾原点、元禾厚望、元禾重元、元禾璞华、元禾谷风、元禾绿柳、元禾辰坤、元禾沙湖、禾裕集团、苏州科服、信诚管理咨询、集成电路公司、常州团队、国器元禾;3、加上自定义的元禾员工/干部培训数据里满足时间范围内的元禾员工总和;4、将两部分数据相加得到总数。", + "verify": "公司管理,筛选公司名字加起来对比" + }, + "company_ganbu_total": { + "name": "全市干部参与企业", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户总数(如果一个用户报名多个课程只算一个有效用户);2、加上\"元禾员工/干部培训\"栏目里满足时间范围内的\"元禾员工参与\"记录的参与数量的总和;3、将两部分数据相加得到总数。", + "verify": "学员中心,学员标签筛选对比" + }, + "cover_head_total": { + "name": "苏州头部企业", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户;2、筛选关联的公司包含\"高新技术企业\"字符串的公司,并且用户填写的公司地址,或者关联的企查查公司地址包含苏州的公司;3、统计公司数量。", + "verify": "" + }, + "cover_rencai_total": { + "name": "苏州高层次人才", + "from": "1、统计指定课程体系下的指定时间范围内的课程(课程开始或结束时间在时间范围内)下审核通过的报名记录的用户;2、筛选满足以下条件之一的用户:参加过\"人才培训\"课程类型的用户,或填写的所有报名表单里个人荣誉字段不包含\"其他\"的用户(剩下的选项是国家人才、市级人才等)。并且用户填写的公司地址,或者关联的企查查公司地址包含苏州的公司;3、取两者的并集得到用户总数。", + "verify": "" + }, + "cover_stock_total": { + "name": "苏州重点上市公司", + "from": "1、统计在指定时间范围内、指定课程范围内报名的学员;2、通过学员关联到其所在公司;3、筛选出上市公司;4、并且用户填写的公司地址,或者关联的企查查公司地址包含苏州的公司", + "verify": "" + } +} \ No newline at end of file