From 5dea483f6b7c6845ae17d6592d5b8b4b4009b981 Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 16:47:46 +0800 Subject: [PATCH 01/12] update --- app/Console/Commands/AutoSchoolmate.php | 2 +- app/Http/Controllers/Admin/UserController.php | 5 ++++ app/Models/User.php | 23 ------------------- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/app/Console/Commands/AutoSchoolmate.php b/app/Console/Commands/AutoSchoolmate.php index 29ec123..d648524 100755 --- a/app/Console/Commands/AutoSchoolmate.php +++ b/app/Console/Commands/AutoSchoolmate.php @@ -75,7 +75,7 @@ class AutoSchoolmate extends Command ->orWhereNull('is_schoolmate'); }) //->where('is_black',0) - ->update(['is_schoolmate' => 1]); + ->update(['is_schoolmate' => 1, 'schoolmate_time' => now()]); $totalUpdated += $updated; } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index f36f68e..5638556 100755 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -742,6 +742,7 @@ class UserController extends BaseController $data = []; if (isset($all['is_schoolmate'])) { $data['is_schoolmate'] = $all['is_schoolmate']; + $data['schoolmate_time'] = ($all['is_schoolmate'] == 1) ? now() : null; } $this->model->whereIn('id', $idsArray)->update($data); return $this->success('批量更新成功'); @@ -794,6 +795,10 @@ class UserController extends BaseController return $this->fail([StarterResponseCode::START_ERROR_PARAMETER, '没有可更新的有效字段']); } + if (array_key_exists('is_schoolmate', $data)) { + $data['schoolmate_time'] = ($data['is_schoolmate'] == 1) ? now() : null; + } + // 解析用户ID $idsArray = explode(',', $all['ids']); $idsArray = array_filter(array_map('trim', $idsArray)); diff --git a/app/Models/User.php b/app/Models/User.php index 35d18ba..5c6d362 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -125,29 +125,6 @@ class User extends Authenticatable implements Auditable 'is_schoolmate' => ['否', '是'], ]; - /** - * Boot 方法 - 处理模型事件 - */ - protected static function boot() - { - 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) { // 如果url中包含admin字符串,则所有的手机号显示中间4位星号代替 From c02a423e6096da305f182ccd82a40a94ad777737 Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 17:08:39 +0800 Subject: [PATCH 02/12] update --- app/Http/Controllers/Admin/OtherController.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 195f425..95a7cc7 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -263,18 +263,8 @@ class OtherController extends CommonController $config->courseTypes = $courseTypes; // 总期数(包含"其他") $config->course_periods_total = $courseTypes->sum('course_periods_total'); - // 总去重人数(包含"其他") - $coursesAll = Course::whereIn('type', $allCourseTypes->pluck('id')) - ->where('is_chart', 1) - ->where(function ($query) use ($configStartDate, $configEndDate) { - $query->whereBetween('start_date', [$configStartDate, $configEndDate]) - ->orWhereBetween('end_date', [$configStartDate, $configEndDate]); - })->get(); - - // 获取"其他"课程类型的课程ID列表 - $otherCourseIds = CourseType::getOtherCourseIds($configStartDate, $configEndDate); - - $config->course_signs_unique_total = $courseTypes->sum('history_course_signs_total') + CourseSign::courseSignsTotalByUnique($configStartDate, $configEndDate, 1, $coursesAll->pluck('id')->merge($otherCourseIds), false, false); + // 总去重人数(包含"其他"),与 courseTypes 各项 course_signs_total 之和保持一致 + $config->course_signs_unique_total = $courseTypes->sum('course_signs_total'); } return $this->success(compact('list', 'suzhou', 'country', 'monthCourses', 'time_axis', 'article', 'yearConfigs')); From ea82817fdc73ebeaa77b4f73a5355d69594f225f Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 17:10:56 +0800 Subject: [PATCH 03/12] update --- app/Http/Controllers/Admin/OtherController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 95a7cc7..f0c3d09 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -254,10 +254,10 @@ class OtherController extends CommonController } // 统计 is_chart=0 的课程类型数据,组成"其他"统计项 - $otherCourseType = CourseType::getOtherStatistics($configStartDate, $configEndDate); + // $otherCourseType = CourseType::getOtherStatistics($configStartDate, $configEndDate); - // 将"其他"添加到 courseTypes 集合中 - $courseTypes->push($otherCourseType); + // // 将"其他"添加到 courseTypes 集合中 + // $courseTypes->push($otherCourseType); // 将统计数据直接组合到配置对象中 $config->courseTypes = $courseTypes; From 56da20ffc931b30dae1a5691db76d449fc207038 Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 17:11:07 +0800 Subject: [PATCH 04/12] update --- app/Http/Controllers/Admin/OtherController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index f0c3d09..95a7cc7 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -254,10 +254,10 @@ class OtherController extends CommonController } // 统计 is_chart=0 的课程类型数据,组成"其他"统计项 - // $otherCourseType = CourseType::getOtherStatistics($configStartDate, $configEndDate); + $otherCourseType = CourseType::getOtherStatistics($configStartDate, $configEndDate); - // // 将"其他"添加到 courseTypes 集合中 - // $courseTypes->push($otherCourseType); + // 将"其他"添加到 courseTypes 集合中 + $courseTypes->push($otherCourseType); // 将统计数据直接组合到配置对象中 $config->courseTypes = $courseTypes; From 102bec9bad7f86e1aae1017b2342dead39eefb61 Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 18:03:11 +0800 Subject: [PATCH 05/12] update --- .../Controllers/Admin/OtherController.php | 37 +++++++++---------- app/Models/CourseType.php | 5 ++- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 95a7cc7..439b8c2 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -205,10 +205,9 @@ class OtherController extends CommonController ->orderBy('sort', 'asc') ->get(); - // 2. 查询对应的 CourseType + // 2. 查询对应的 CourseType(含 is_chart=1 的 is_history=0 与 is_history=1,与 courses-home 口径一致) $allCourseTypes = CourseType::where('is_chart', 1) ->orderBy('sort', 'asc') - ->where('is_history', 0) ->get(); // 3. 循环所有配置,对每个配置进行统计 @@ -222,34 +221,34 @@ class OtherController extends CommonController return clone $item; }); - // 对每个 CourseType 进行统计 + // 对每个 CourseType 进行统计(与 courses-home 口径一致) foreach ($courseTypes as $courseType) { - // 历史课程数据(添加时间范围限制) - $historyCourse = HistoryCourse::whereHas('typeDetail', function ($query) use ($courseType) { - $query->where('name', 'like', '%' . $courseType->name . '%'); - })->where(function ($query) use ($configStartDate, $configEndDate) { - $query->whereBetween('start_time', [$configStartDate, $configEndDate]) - ->orWhereBetween('end_time', [$configStartDate, $configEndDate]); - })->get(); - // 实际课程数据(添加时间范围限制) + // 实际课程数据(与 courses-home 一致) $courses = Course::where('type', $courseType->id)->where('is_chart', 1) ->where(function ($query) use ($configStartDate, $configEndDate) { $query->whereBetween('start_date', [$configStartDate, $configEndDate]) ->orWhereBetween('end_date', [$configStartDate, $configEndDate]); })->get(); - // 历史课程期数 + + // 历史课程:仅 is_history=1 类型统计,且与 courses-home 一致(type=id + calendar.is_count_people=1) + if (($courseType->is_history ?? 0) == 1) { + $historyCourse = HistoryCourse::where('type', $courseType->id) + ->whereHas('calendar', function ($query) { + $query->where('is_count_people', 1); + }) + ->where(function ($query) use ($configStartDate, $configEndDate) { + $query->whereBetween('start_time', [$configStartDate, $configEndDate]) + ->orWhereBetween('end_time', [$configStartDate, $configEndDate]); + })->get(); + } else { + $historyCourse = collect(); + } + $courseType->history_course_periods_total = $historyCourse->count(); - // 现在课程期数(添加时间范围限制) $courseType->now_course_periods_total = $courses->count(); - - // 历史课程培养人数去重 $courseType->history_course_signs_total = $historyCourse->sum('course_type_signs_pass_unique'); - // 现在课程培养人数(使用配置的日期范围) $courseType->now_course_signs_total = CourseSign::courseSignsTotalByUnique($configStartDate, $configEndDate, 1, $courses->pluck('id'), false, false); - - // 已开设期数 $courseType->course_periods_total = $courseType->now_course_periods_total + $courseType->history_course_periods_total; - // 培养人数去重 $courseType->course_signs_total = $courseType->history_course_signs_total + $courseType->now_course_signs_total; } diff --git a/app/Models/CourseType.php b/app/Models/CourseType.php index 97d6d4e..9e11d1e 100755 --- a/app/Models/CourseType.php +++ b/app/Models/CourseType.php @@ -52,8 +52,11 @@ class CourseType extends SoftDeletesModel return $otherCourseType; } - // 历史课程数据(所有 is_chart=0 的课程类型) + // 历史课程数据(所有 is_chart=0 的课程类型,与 courses-home 一致:calendar.is_count_people=1) $otherHistoryCourse = HistoryCourse::whereIn('type', $otherCourseTypeIds) + ->whereHas('calendar', function ($query) { + $query->where('is_count_people', 1); + }) ->where(function ($query) use ($startDate, $endDate) { $query->whereBetween('start_time', [$startDate, $endDate]) ->orWhereBetween('end_time', [$startDate, $endDate]); From 6a519aa145e59def3ce1694798abd87d1d782bb2 Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 18:05:38 +0800 Subject: [PATCH 06/12] update --- .../Controllers/Admin/OtherController.php | 37 ++++++++++--------- app/Models/CourseType.php | 5 +-- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 439b8c2..95a7cc7 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -205,9 +205,10 @@ class OtherController extends CommonController ->orderBy('sort', 'asc') ->get(); - // 2. 查询对应的 CourseType(含 is_chart=1 的 is_history=0 与 is_history=1,与 courses-home 口径一致) + // 2. 查询对应的 CourseType $allCourseTypes = CourseType::where('is_chart', 1) ->orderBy('sort', 'asc') + ->where('is_history', 0) ->get(); // 3. 循环所有配置,对每个配置进行统计 @@ -221,34 +222,34 @@ class OtherController extends CommonController return clone $item; }); - // 对每个 CourseType 进行统计(与 courses-home 口径一致) + // 对每个 CourseType 进行统计 foreach ($courseTypes as $courseType) { - // 实际课程数据(与 courses-home 一致) + // 历史课程数据(添加时间范围限制) + $historyCourse = HistoryCourse::whereHas('typeDetail', function ($query) use ($courseType) { + $query->where('name', 'like', '%' . $courseType->name . '%'); + })->where(function ($query) use ($configStartDate, $configEndDate) { + $query->whereBetween('start_time', [$configStartDate, $configEndDate]) + ->orWhereBetween('end_time', [$configStartDate, $configEndDate]); + })->get(); + // 实际课程数据(添加时间范围限制) $courses = Course::where('type', $courseType->id)->where('is_chart', 1) ->where(function ($query) use ($configStartDate, $configEndDate) { $query->whereBetween('start_date', [$configStartDate, $configEndDate]) ->orWhereBetween('end_date', [$configStartDate, $configEndDate]); })->get(); - - // 历史课程:仅 is_history=1 类型统计,且与 courses-home 一致(type=id + calendar.is_count_people=1) - if (($courseType->is_history ?? 0) == 1) { - $historyCourse = HistoryCourse::where('type', $courseType->id) - ->whereHas('calendar', function ($query) { - $query->where('is_count_people', 1); - }) - ->where(function ($query) use ($configStartDate, $configEndDate) { - $query->whereBetween('start_time', [$configStartDate, $configEndDate]) - ->orWhereBetween('end_time', [$configStartDate, $configEndDate]); - })->get(); - } else { - $historyCourse = collect(); - } - + // 历史课程期数 $courseType->history_course_periods_total = $historyCourse->count(); + // 现在课程期数(添加时间范围限制) $courseType->now_course_periods_total = $courses->count(); + + // 历史课程培养人数去重 $courseType->history_course_signs_total = $historyCourse->sum('course_type_signs_pass_unique'); + // 现在课程培养人数(使用配置的日期范围) $courseType->now_course_signs_total = CourseSign::courseSignsTotalByUnique($configStartDate, $configEndDate, 1, $courses->pluck('id'), false, false); + + // 已开设期数 $courseType->course_periods_total = $courseType->now_course_periods_total + $courseType->history_course_periods_total; + // 培养人数去重 $courseType->course_signs_total = $courseType->history_course_signs_total + $courseType->now_course_signs_total; } diff --git a/app/Models/CourseType.php b/app/Models/CourseType.php index 9e11d1e..97d6d4e 100755 --- a/app/Models/CourseType.php +++ b/app/Models/CourseType.php @@ -52,11 +52,8 @@ class CourseType extends SoftDeletesModel return $otherCourseType; } - // 历史课程数据(所有 is_chart=0 的课程类型,与 courses-home 一致:calendar.is_count_people=1) + // 历史课程数据(所有 is_chart=0 的课程类型) $otherHistoryCourse = HistoryCourse::whereIn('type', $otherCourseTypeIds) - ->whereHas('calendar', function ($query) { - $query->where('is_count_people', 1); - }) ->where(function ($query) use ($startDate, $endDate) { $query->whereBetween('start_time', [$startDate, $endDate]) ->orWhereBetween('end_time', [$startDate, $endDate]); From b21c5d355be213f4b25e9acdd9c0b1dd7582301e Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Mon, 19 Jan 2026 18:28:49 +0800 Subject: [PATCH 07/12] update --- .../Commands/DiffHomeV2CoursesHome.php | 233 ++++++++++++++++++ .../Controllers/Admin/OtherController.php | 26 +- 2 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/DiffHomeV2CoursesHome.php diff --git a/app/Console/Commands/DiffHomeV2CoursesHome.php b/app/Console/Commands/DiffHomeV2CoursesHome.php new file mode 100644 index 0000000..ca51ff7 --- /dev/null +++ b/app/Console/Commands/DiffHomeV2CoursesHome.php @@ -0,0 +1,233 @@ +option('start'); + $end = $this->option('end'); + $this->info("=== 对比时间段: {$start} ~ {$end} ===\n"); + + // 1. 找到覆盖该时间段的 yearConfig(或使用该时间段模拟) + $config = CourseTypeDataOverviewConfig::where('status', true) + ->where('start_date', '<=', $start) + ->where(function ($q) use ($end) { + $q->where('end_date', '>=', $end)->orWhereNull('end_date'); + }) + ->orderBy('sort') + ->first(); + + if (!$config) { + $config = CourseTypeDataOverviewConfig::where('status', true) + ->whereBetween('start_date', [$start, $end]) + ->orWhereBetween('end_date', [$start, $end]) + ->orderBy('sort') + ->first(); + } + + $configStart = $config ? $config->start_date : $start; + $configEnd = $config && $config->end_date ? $config->end_date : $end; + $this->info("yearConfig: " . ($config ? "id={$config->id} [{$configStart} ~ {$configEnd}]" : "无覆盖配置,使用 {$configStart} ~ {$configEnd}")); + + // 2. 按 home-v2 逻辑统计(仅用 2024-01-01~2027-01-01 做筛选,与 config 自身范围不一致时以 2024-2027 为准则需单独算) + // 为与 courses-home 可比,我们在这里用 start~end 作为统一日期范围 + $homeV2 = $this->computeHomeV2Style($start, $end); + $this->info("\n【home-v2 逻辑】期数: {$homeV2['course_periods_total']}, 去重培养人数: {$homeV2['course_signs_unique_total']}"); + + // 3. 按 courses-home 逻辑统计 + $coursesHome = $this->computeCoursesHomeStyle($start, $end); + $this->info("【courses-home 逻辑】课程数量(行数): {$coursesHome['course_count']}, 去重培养人数: {$coursesHome['course_signs_unique_total']}"); + + // 4. 差异 + $diffCount = $coursesHome['course_count'] - $homeV2['course_periods_total']; + $this->info("\n--- 差异: 课程数量 courses-home 多 " . $diffCount . " ---"); + + // 5. 定位多出来的课程来源 + $this->findExtraSources($start, $end, $homeV2, $coursesHome); + + return 0; + } + + /** + * home-v2 风格:is_chart=1 且 is_history=0 的 CourseType + 按 name like 的 HistoryCourse + 其他 + */ + protected function computeHomeV2Style(string $start, string $end): array + { + $allCourseTypes = CourseType::where('is_chart', 1)->where('is_history', 0)->orderBy('sort')->get(); + $coursePeriodsTotal = 0; + $courseSignsUniqueTotal = 0; + + $dateFilter = function ($q) use ($start, $end) { + $q->whereBetween('start_date', [$start, $end])->orWhereBetween('end_date', [$start, $end]); + }; + $historyDateFilter = function ($q) use ($start, $end) { + $q->whereBetween('start_time', [$start, $end])->orWhereBetween('end_time', [$start, $end]); + }; + + foreach ($allCourseTypes as $ct) { + $historyCourse = HistoryCourse::whereHas('typeDetail', fn($q) => $q->where('name', 'like', '%' . $ct->name . '%')) + ->where($historyDateFilter)->get(); + $courses = Course::where('type', $ct->id)->where('is_chart', 1)->where($dateFilter)->get(); + + $coursePeriodsTotal += $historyCourse->count() + $courses->count(); + $courseSignsUniqueTotal += $historyCourse->sum('course_type_signs_pass_unique') + + (int)CourseSign::courseSignsTotalByUnique($start, $end, 1, $courses->pluck('id'), false, false); + } + + $other = CourseType::getOtherStatistics($start, $end); + $coursePeriodsTotal += $other->course_periods_total; + $courseSignsUniqueTotal += $other->course_signs_total; + + return ['course_periods_total' => $coursePeriodsTotal, 'course_signs_unique_total' => $courseSignsUniqueTotal]; + } + + /** + * courses-home 风格:全部 CourseType(含 is_history=1)的 Course(is_chart=1) + is_history=1 的 HistoryCourse(calendar.is_count_people=1) + */ + protected function computeCoursesHomeStyle(string $start, string $end): array + { + $course_type_id = CourseType::pluck('id')->toArray(); + $courseTypesSum = []; + $allCourseIdsForUnique = []; + + $dateFilter = function ($q) use ($start, $end) { + if ($start && $end) { + $q->whereBetween('start_date', [$start, $end])->orWhereBetween('end_date', [$start, $end]); + } + }; + + // 第一循环:所有 CourseType(含 is_history=1) + $courseTypes = CourseType::whereIn('id', $course_type_id)->get(); + foreach ($courseTypes as $ct) { + $courses2 = Course::where('type', $ct->id)->where($dateFilter)->where('is_chart', 1)->orderBy('start_date')->get(); + foreach ($courses2 as $c) { + $courseTypesSum[] = ['source' => 'Course', 'course_type_id' => $ct->id, 'course_type_name' => $ct->name, 'is_history' => $ct->is_history ?? 0, 'course_id' => $c->id, 'course_name' => $c->name]; + $allCourseIdsForUnique[] = $c->id; + } + } + + // 第二循环:is_history=1 的 HistoryCourse(与 home-v2 一致:仅 typeDetail.name 能匹配某个 is_chart=1 且 is_history=0 的 name) + $chartHistoryTypeNames = CourseType::where('is_chart', 1)->where('is_history', 0)->pluck('name')->toArray(); + $courseTypesHistory = CourseType::where('is_history', 1)->whereIn('id', $course_type_id)->get(); + $historySignsTotal = 0; + foreach ($courseTypesHistory as $hc) { + $historyQ = HistoryCourse::whereHas('calendar', fn($q) => $q->where('is_count_people', 1)) + ->where(fn($q) => $q->whereBetween('start_time', [$start, $end])->orWhereBetween('end_time', [$start, $end])) + ->where('type', $hc->id); + if (!empty($chartHistoryTypeNames)) { + $historyQ->whereHas('typeDetail', function ($query) use ($chartHistoryTypeNames) { + $query->where(function ($q2) use ($chartHistoryTypeNames) { + foreach ($chartHistoryTypeNames as $n) { + $q2->orWhere('name', 'like', '%' . $n . '%'); + } + }); + }); + } else { + $historyQ->whereHas('typeDetail', fn($query) => $query->whereRaw('1=0')); + } + $courses3 = $historyQ->get(); + foreach ($courses3 as $c) { + $courseTypesSum[] = ['source' => 'HistoryCourse', 'course_type_id' => $hc->id, 'course_type_name' => $hc->name, 'is_history' => 1, 'history_course_id' => $c->id, 'course_name' => $c->course_name]; + $historySignsTotal += (int)($c->course_type_signs_pass_unique ?? 0); + } + } + + $courseSignsUniqueTotal = (int)CourseSign::courseSignsTotalByUnique($start, $end, 1, $allCourseIdsForUnique ?: [], false, false) + $historySignsTotal; + + return [ + 'course_count' => count($courseTypesSum), + 'course_signs_unique_total' => $courseSignsUniqueTotal, + 'rows' => $courseTypesSum, + ]; + } + + /** + * 找出 courses-home 多出来的课程来源 + */ + protected function findExtraSources(string $start, string $end, array $homeV2, array $coursesHome): void + { + $allCourseTypes = CourseType::where('is_chart', 1)->where('is_history', 0)->pluck('id')->toArray(); + $historyDateFilter = function ($q) use ($start, $end) { + $q->whereBetween('start_time', [$start, $end])->orWhereBetween('end_time', [$start, $end]); + }; + $dateFilter = function ($q) use ($start, $end) { + $q->whereBetween('start_date', [$start, $end])->orWhereBetween('end_date', [$start, $end]); + }; + + // home-v2 会计入的:1) Course: type in is_history=0, is_chart=1, 日期 2) HistoryCourse: typeDetail.name like (is_history=0 的 name), 日期 3) 其他 + $homeV2CourseIds = []; + foreach (CourseType::where('is_chart', 1)->where('is_history', 0)->get() as $ct) { + $ids = Course::where('type', $ct->id)->where('is_chart', 1)->where($dateFilter)->pluck('id')->toArray(); + $homeV2CourseIds = array_merge($homeV2CourseIds, $ids); + } + $otherCourseIds = Course::whereIn('type', CourseType::where('is_chart', 0)->where('is_history', 0)->pluck('id')) + ->where('is_chart', 1)->where($dateFilter)->pluck('id')->toArray(); + $homeV2CourseIds = array_unique(array_merge($homeV2CourseIds, $otherCourseIds)); + + $homeV2HistoryIds = []; + foreach (CourseType::where('is_chart', 1)->where('is_history', 0)->get() as $ct) { + $ids = HistoryCourse::whereHas('typeDetail', fn($q) => $q->where('name', 'like', '%' . $ct->name . '%')) + ->where($historyDateFilter)->pluck('id')->toArray(); + $homeV2HistoryIds = array_merge($homeV2HistoryIds, $ids); + } + $otherHistory = HistoryCourse::whereIn('type', CourseType::where('is_chart', 0)->where('is_history', 0)->pluck('id')) + ->where($historyDateFilter)->pluck('id')->toArray(); + $homeV2HistoryIds = array_unique(array_merge($homeV2HistoryIds, $otherHistory)); + + // courses-home 多出的:在 rows 里但不在 home-v2 的 + $extras = []; + foreach ($coursesHome['rows'] as $r) { + if ($r['source'] === 'Course') { + if (!in_array($r['course_id'], $homeV2CourseIds)) { + $extras[] = $r; + } + } else { + if (!in_array($r['history_course_id'], $homeV2HistoryIds)) { + $extras[] = $r; + } + } + } + + $this->info("\n【多出的课程/期(courses-home 有、home-v2 无)】共 " . count($extras) . " 条:"); + foreach ($extras as $e) { + $this->line(sprintf( + " - %s | type_id=%s is_history=%s | %s | %s", + $e['source'], + $e['course_type_id'], + $e['is_history'], + $e['course_name'] ?? '-', + isset($e['course_id']) ? "course_id={$e['course_id']}" : "history_id={$e['history_course_id']}" + )); + } + + // 单独列出:type 属于 is_history=1 的 Course(home-v2 不统计此类) + $isHistory1TypeIds = CourseType::where('is_history', 1)->pluck('id')->toArray(); + $courseFromHistory1Type = Course::whereIn('type', $isHistory1TypeIds) + ->where('is_chart', 1) + ->where($dateFilter) + ->get(); + if ($courseFromHistory1Type->isNotEmpty()) { + $this->info("\n【属于 is_history=1 课程类型的 Course(home-v2 不统计)】共 " . $courseFromHistory1Type->count() . " 条:"); + foreach ($courseFromHistory1Type as $c) { + $ct = CourseType::find($c->type); + $this->line(" - course_id={$c->id} type={$c->type} ({$ct->name}) | {$c->name} | {$c->start_date}~{$c->end_date}"); + } + } + } +} diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 95a7cc7..8dba8d6 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -377,11 +377,22 @@ class OtherController extends CommonController ]; } } - // 附加历史课程数据 + // 附加历史课程数据(与 home-v2 yearConfigs 口径一致:仅统计 typeDetail.name 能匹配某个 is_chart=1 且 is_history=0 的 CourseType 的 HistoryCourse) + $chartHistoryTypeNames = CourseType::where('is_chart', 1)->where('is_history', 0)->pluck('name')->toArray(); $courseTypesHistory = CourseType::where('is_history', 1)->whereIn('id', $course_type_id)->get(); foreach ($courseTypesHistory as $historyCourse) { $courses3 = HistoryCourse::whereHas('calendar', function ($query) { $query->where('is_count_people', 1); + })->whereHas('typeDetail', function ($query) use ($chartHistoryTypeNames) { + if (empty($chartHistoryTypeNames)) { + $query->whereRaw('1=0'); + return; + } + $query->where(function ($q) use ($chartHistoryTypeNames) { + foreach ($chartHistoryTypeNames as $n) { + $q->orWhere('name', 'like', '%' . $n . '%'); + } + }); })->where(function ($query) use ($start_date, $end_date) { // 开始结束日期的筛选。or查询 $query->whereBetween('start_time', [$start_date, $end_date]) @@ -863,11 +874,22 @@ class OtherController extends CommonController ]; } } - // 附加历史课程数据 + // 附加历史课程数据(与 home-v2 yearConfigs 口径一致:仅统计 typeDetail.name 能匹配某个 is_chart=1 且 is_history=0 的 CourseType 的 HistoryCourse) + $chartHistoryTypeNames = CourseType::where('is_chart', 1)->where('is_history', 0)->pluck('name')->toArray(); $courseTypesHistory = CourseType::where('is_history', 1)->whereIn('id', $course_type_id)->get(); foreach ($courseTypesHistory as $historyCourse) { $courses3 = HistoryCourse::whereHas('calendar', function ($query) { $query->where('is_count_people', 1); + })->whereHas('typeDetail', function ($query) use ($chartHistoryTypeNames) { + if (empty($chartHistoryTypeNames)) { + $query->whereRaw('1=0'); + return; + } + $query->where(function ($q) use ($chartHistoryTypeNames) { + foreach ($chartHistoryTypeNames as $n) { + $q->orWhere('name', 'like', '%' . $n . '%'); + } + }); })->where(function ($query) use ($start_date, $end_date) { // 开始结束日期的筛选。or查询 $query->whereBetween('start_time', [$start_date, $end_date]) From d253af6dad6a517134255df824c18396909311cd Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Tue, 20 Jan 2026 11:54:37 +0800 Subject: [PATCH 08/12] update --- app/Http/Controllers/Admin/EmployeeParticipationController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/Admin/EmployeeParticipationController.php b/app/Http/Controllers/Admin/EmployeeParticipationController.php index 2206346..b829ff4 100644 --- a/app/Http/Controllers/Admin/EmployeeParticipationController.php +++ b/app/Http/Controllers/Admin/EmployeeParticipationController.php @@ -285,6 +285,7 @@ class EmployeeParticipationController extends BaseController $list[$key]['company_name'] = $value['公司名称'] ?? null; $list[$key]['name'] = $value['姓名'] ?? null; $list[$key]['department'] = $value['部门'] ?? null; + $list[$key]['total'] = $value['数量'] ?? 1; // 根据课程信息填充其他字段 if ($course) { From d8c404f960a8b5bf7fb60ec083a6fdcaa5c77f25 Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Tue, 20 Jan 2026 13:37:03 +0800 Subject: [PATCH 09/12] update --- .../Commands/MergeDuplicateUsersByMobile.php | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 app/Console/Commands/MergeDuplicateUsersByMobile.php diff --git a/app/Console/Commands/MergeDuplicateUsersByMobile.php b/app/Console/Commands/MergeDuplicateUsersByMobile.php new file mode 100644 index 0000000..258b7ef --- /dev/null +++ b/app/Console/Commands/MergeDuplicateUsersByMobile.php @@ -0,0 +1,194 @@ +option('dry-run'); + if ($dryRun) { + $this->warn('【预检模式】不会修改数据库'); + } + + $duplicateGroups = $this->getDuplicateMobileUsers(); + if ($duplicateGroups->isEmpty()) { + $this->info('没有需要合并的重复手机号。'); + return 0; + } + + $this->info('共 ' . $duplicateGroups->count() . ' 组重复手机号待处理。'); + + $merged = 0; + $failed = 0; + + foreach ($duplicateGroups as $mobile => $users) { + // 按 id 升序,id 最大为“新”,保留;其余为“旧”,合并后软删 + $sorted = $users->sortBy('id')->values(); + $newUser = $sorted->last(); + $oldUsers = $sorted->slice(0, -1); + + $newId = (int) $newUser->id; + $oldIds = $oldUsers->pluck('id')->map(fn($v) => (int) $v)->toArray(); + + $this->line(''); + $this->line("手机号: {$mobile} | 保留用户 id={$newId} ({$newUser->name}), 合并并删除: " . implode(', ', $oldIds)); + + if ($dryRun) { + $this->listTransfers($newId, $oldIds); + $merged++; + continue; + } + + try { + DB::transaction(function () use ($newId, $oldIds) { + foreach ($oldIds as $oldId) { + $this->transferRelations($oldId, $newId); + } + User::whereIn('id', $oldIds)->delete(); // 软删除 + }); + $this->info(" 已合并并软删除旧用户: " . implode(', ', $oldIds)); + $merged++; + } catch (\Throwable $e) { + $this->error(" 失败: " . $e->getMessage()); + $failed++; + } + } + + $this->line(''); + $this->info("完成: 成功 {$merged} 组" . ($failed > 0 ? ", 失败 {$failed} 组" : '') . ($dryRun ? '(未写入)' : '')); + return $failed > 0 ? 1 : 0; + } + + /** + * 查询:有报名审核通过且手机号重复的用户,按手机号分组 + */ + protected function getDuplicateMobileUsers() + { + $sql = " + SELECT u.id, u.name, u.mobile + FROM users u + WHERE u.deleted_at IS NULL + AND u.mobile IS NOT NULL AND TRIM(u.mobile) != '' + AND EXISTS ( + SELECT 1 FROM course_signs cs + WHERE cs.user_id = u.id AND cs.status = 1 AND cs.deleted_at IS NULL + ) + AND u.mobile IN ( + SELECT u2.mobile + FROM users u2 + INNER JOIN course_signs cs2 ON cs2.user_id = u2.id AND cs2.status = 1 AND cs2.deleted_at IS NULL + WHERE u2.deleted_at IS NULL + AND u2.mobile IS NOT NULL AND TRIM(u2.mobile) != '' + GROUP BY u2.mobile + HAVING COUNT(DISTINCT u2.id) > 1 + ) + ORDER BY u.mobile, u.id + "; + $rows = DB::select($sql); + return collect($rows)->groupBy('mobile'); + } + + /** + * 把 oldUserId 的关联全部改为 newUserId + */ + protected function transferRelations(int $oldUserId, int $newUserId): void + { + foreach ($this->tablesWithUserId as $table) { + if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'user_id')) { + continue; + } + $n = DB::table($table)->where('user_id', $oldUserId)->update(['user_id' => $newUserId]); + if ($n > 0) { + $this->line(" {$table}.user_id: {$oldUserId} -> {$newUserId}, 更新 {$n} 行"); + } + } + + foreach ($this->tablesWithToUserId as $table) { + if (!Schema::hasTable($table)) { + continue; + } + if (Schema::hasColumn($table, 'user_id')) { + $n = DB::table($table)->where('user_id', $oldUserId)->update(['user_id' => $newUserId]); + if ($n > 0) { + $this->line(" {$table}.user_id: {$oldUserId} -> {$newUserId}, 更新 {$n} 行"); + } + } + if (Schema::hasColumn($table, 'to_user_id')) { + $n = DB::table($table)->where('to_user_id', $oldUserId)->update(['to_user_id' => $newUserId]); + if ($n > 0) { + $this->line(" {$table}.to_user_id: {$oldUserId} -> {$newUserId}, 更新 {$n} 行"); + } + } + } + } + + /** + * dry-run:只统计并打印每个表将更新的行数 + */ + protected function listTransfers(int $newUserId, array $oldIds): void + { + foreach ($oldIds as $oldId) { + foreach ($this->tablesWithUserId as $table) { + if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'user_id')) { + continue; + } + $n = DB::table($table)->where('user_id', $oldId)->count(); + if ($n > 0) { + $this->line(" [拟] {$table}.user_id: {$oldId} -> {$newUserId}, 约 {$n} 行"); + } + } + foreach ($this->tablesWithToUserId as $table) { + if (!Schema::hasTable($table)) { + continue; + } + if (Schema::hasColumn($table, 'user_id')) { + $n = DB::table($table)->where('user_id', $oldId)->count(); + if ($n > 0) { + $this->line(" [拟] {$table}.user_id: {$oldId} -> {$newUserId}, 约 {$n} 行"); + } + } + if (Schema::hasColumn($table, 'to_user_id')) { + $n = DB::table($table)->where('to_user_id', $oldId)->count(); + if ($n > 0) { + $this->line(" [拟] {$table}.to_user_id: {$oldId} -> {$newUserId}, 约 {$n} 行"); + } + } + } + } + } +} From 072fcb7d98f6b02612ed117b6c50bb4b24d481ca Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Tue, 20 Jan 2026 14:10:09 +0800 Subject: [PATCH 10/12] update --- app/Http/Controllers/Admin/CompanyController.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index 6f10d31..92d0af1 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -284,9 +284,11 @@ class CompanyController extends BaseController $statistics = [ 'course_signs_invested' => 0, 'company_invested_after_enrollment_total' => 0, + 'company_invested_after_enrollment_current_total' => 0, 'company_invested_year_total' => 0, 'course_signs_invested_companies' => [], 'company_invested_after_enrollment_companies' => [], + 'company_invested_after_enrollment_current_companies' => [], 'company_invested_year_companies' => [], ]; $start_date = $start_year ? $start_year . '-01-01' : date('Y-01-01'); @@ -324,6 +326,16 @@ class CompanyController extends BaseController $statistics['company_invested_year_companies'] = $yearInvestedCompaniesCollection->pluck('company_name')->filter()->unique()->values()->toArray(); } + // 当前入学后被投企业数(与 courses-home 的 company_invested_after_enrollment_total 口径一致:被投时间在 yearStart-yearEnd 内且入学≤被投) + // companyInvestedAfterEnrollment 需传日期;yearStart/yearEnd 为空时用 CourseType::START_DATE 与 end_date 表示全周期 + $currentAfterStart = $yearStart ?? CourseType::START_DATE; + $currentAfterEnd = $yearEnd ?? $end_date; + $currentAfterEnrollmentCompanies = CourseSign::companyInvestedAfterEnrollment($currentAfterStart, $currentAfterEnd, $course_ids, true); + if ($currentAfterEnrollmentCompanies) { + $statistics['company_invested_after_enrollment_current_total'] = count($currentAfterEnrollmentCompanies); + $statistics['company_invested_after_enrollment_current_companies'] = collect($currentAfterEnrollmentCompanies)->pluck('company.company_name')->filter()->unique()->values()->toArray(); + } + // 入学后被投企业数量(与 courses-home 的 company_invested_after_enrollment_total_cumulative 口径一致) $afterEnrollmentCompanies = CourseSign::companyInvestedAfterEnrollment(CourseType::START_DATE, $end_date, $course_ids, true); if ($afterEnrollmentCompanies) { From 2db5f6b4c01a965a44ba70c37e38883ffe46513d Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Tue, 20 Jan 2026 15:21:54 +0800 Subject: [PATCH 11/12] update --- app/Models/CourseSign.php | 2 +- .../2026_01_17_094313_create_trainee_students_table.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/CourseSign.php b/app/Models/CourseSign.php index 582eb70..c114c73 100755 --- a/app/Models/CourseSign.php +++ b/app/Models/CourseSign.php @@ -806,7 +806,7 @@ class CourseSign extends SoftDeletesModel // 条件2:user.type 包含「人才」的用户 $courseSigns2 = $courseSignsQuery->whereHas('user', function ($q) { - $q->where('type', 'like', '%人才%'); + $q->where('type', 'like', '%人才%')->orWhere('talent_tags', 'like', '%人才%'); })->with(['user.company'])->get(); // 合并两个条件的结果(或关系),并去重 user_id 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 index 335ade0..126db18 100644 --- a/database/migrations/2026_01_17_094313_create_trainee_students_table.php +++ b/database/migrations/2026_01_17_094313_create_trainee_students_table.php @@ -19,7 +19,7 @@ return new class extends Migration { $table->string('name')->nullable()->comment('名字'); $table->date('start_date')->nullable()->comment('开始日期'); $table->date('end_date')->nullable()->comment('结束日期'); - $table->integer('total')->default(0)->comment('加减数据(正数表示加,负数表示减)'); + $table->unsignedInteger('total')->default(1)->comment('数量,必须为正数,默认1'); $table->text('remark')->nullable()->comment('备注'); $table->timestamps(); $table->softDeletes(); From 26ce258ba5ccffbdf615ce0aae0b61cec0199c2b Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Tue, 20 Jan 2026 15:34:24 +0800 Subject: [PATCH 12/12] update --- app/Http/Controllers/Admin/UserController.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 5638556..a7512cc 100755 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -190,6 +190,7 @@ class UserController extends BaseController * @OA\Parameter(name="is_company_market", in="query", @OA\Schema(type="string"), required=false, description="是否上市公司0否1是"), * @OA\Parameter(name="company_tag", in="query", @OA\Schema(type="string"), required=false, description="企业标签"), * @OA\Parameter(name="talent_tags", in="query", @OA\Schema(type="string"), required=false, description="人才标签,多个英文逗号分隔"), + * @OA\Parameter(name="address", in="query", @OA\Schema(type="string"), required=false, description="公司地址,模糊匹配关联 company 的 company_address 或 company_city"), * @OA\Parameter(name="token", in="query", @OA\Schema(type="string"), required=true, description="token"), * @OA\Response( * response="200", @@ -369,6 +370,13 @@ class UserController extends BaseController $company_area = explode(',', $all['company_area']); $query->whereIn('company_area', $company_area); } + // 公司地址:模糊匹配关联 company 的 company_address 或 company_city + if (isset($all['address']) && $all['address'] !== '') { + $query->whereHas('company', function ($c) use ($all) { + $c->where('company_address', 'like', '%' . $all['address'] . '%') + ->orWhere('company_city', 'like', '%' . $all['address'] . '%'); + }); + } if (isset($all['company_industry'])) { $company_industry = explode(',', $all['company_industry']); $query->where(function ($q) use ($company_industry) {