diff --git a/app/Exports/MultiSheetExport.php b/app/Exports/MultiSheetExport.php new file mode 100644 index 0000000..7cd6016 --- /dev/null +++ b/app/Exports/MultiSheetExport.php @@ -0,0 +1,94 @@ +sheets = $sheets; + } + + public function sheets(): array + { + return $this->sheets; + } +} + +class SheetExport implements FromCollection, WithStyles, WithColumnWidths, WithEvents +{ + protected $data; + protected $fields; + protected $sheetName; + + public function __construct($data, $fields, $sheetName = 'Sheet1') + { + $this->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/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 907e6b0..5fcace6 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -32,6 +32,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 { @@ -585,10 +587,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 ?? '', @@ -596,19 +600,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': 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) + +-- 执行前请备份数据库! +-- 建议在业务低峰期执行索引创建操作 +