You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Console\Commands;
use App\Models\Course;
use App\Models\CourseSign;
use App\Models\CourseType;
use App\Models\CourseTypeDataOverviewConfig;
use App\Models\HistoryCourse;
use Illuminate\Console\Command;
/**
* 比对 home-v2 的 yearConfigs 与 courses-home 的 courseTypesSum,
* 在指定时间段内 期数、课程数量、去重培养人数 的差异,并输出导致差异的 Course / HistoryCourse 明细。
*/
class DiffHomeV2CoursesHome extends Command
{
protected $signature = 'diff:home-v2-courses-home
{--start=2024-01-01 : 开始日期}
{--end=2027-01-01 : 结束日期}
{--config-id= : 可选,仅用该 yearConfig 的日期;不传则用 start/end}';
protected $description = '比对 home-v2(yearConfigs) 与 courses-home(courseTypesSum) 的期数、课程数量、去重培养人数差异';
protected $startDate;
protected $endDate;
public function handle()
{
$this->startDate = $this->option('start');
$this->endDate = $this->option('end');
$configId = $this->option('config-id');
if ($configId) {
$config = CourseTypeDataOverviewConfig::where('status', true)->find($configId);
if (!$config) {
$this->error("未找到 status=1 的 config id={$configId}");
return 1;
}
$this->startDate = $config->start_date;
$this->endDate = $config->end_date ?: date('Y-m-d', strtotime('+10 year'));
$this->info("使用 config#{$configId} 的日期: {$this->startDate} ~ {$this->endDate}");
} else {
$this->info("使用参数日期: {$this->startDate} ~ {$this->endDate}");
}
// 1) home-v2 逻辑:按 yearConfigs 的统计方式(只取 is_chart=1 & is_history=0 的 CourseType + 其他)
$homeV2 = $this->computeHomeV2($this->startDate, $this->endDate);
// 2) courses-home 逻辑:courseTypesSum 的课程数量、以及涉及的 Course / HistoryCourse
$coursesHome = $this->computeCoursesHome($this->startDate, $this->endDate);
$this->printComparison($homeV2, $coursesHome);
$this->printDetailDiff($homeV2, $coursesHome);
return 0;
}
/**
* home-v2 的 yearConfigs 统计(单一段 start~end)
* 返回:期数、去重人数、涉及的 course_ids、history_course_ids
*/
protected function computeHomeV2(string $start, string $end): array
{
$courseIds = [];
$historyIds = [];
$periodsTotal = 0;
$signsUniqueSum = 0;
$allCourseTypes = CourseType::where('is_chart', 1)->where('is_history', 0)->orderBy('sort')->get();
foreach ($allCourseTypes as $ct) {
// 历史:typeDetail name like,且仅 type 为 is_history=0(is_history=1 的由下方与 courses-home 口径一致的块统计)
$history = HistoryCourse::whereHas('typeDetail', function ($q) use ($ct) {
$q->where('name', 'like', '%' . $ct->name . '%')->where('is_history', 0);
})->where(function ($q) use ($start, $end) {
$q->whereBetween('start_time', [$start, $end])
->orWhereBetween('end_time', [$start, $end]);
})->get();
foreach ($history as $h) {
$historyIds[] = $h->id;
}
$historyPeriods = $history->count();
// 现在:Course type=$ct->id, is_chart=1
$courses = Course::where('type', $ct->id)->where('is_chart', 1)
->where(function ($q) use ($start, $end) {
$q->whereBetween('start_date', [$start, $end])
->orWhereBetween('end_date', [$start, $end]);
})->get();
foreach ($courses as $c) {
$courseIds[] = $c->id;
}
$nowPeriods = $courses->count();
$periodsTotal += $historyPeriods + $nowPeriods;
$signsUniqueSum += $history->sum('course_type_signs_pass_unique')
+ CourseSign::courseSignsTotalByUnique($start, $end, 1, $courses->pluck('id'), false, false);
}
// 其他 is_chart=0
$other = CourseType::getOtherStatistics($start, $end);
$oIds = CourseType::getOtherCourseIds($start, $end);
$otherH = HistoryCourse::whereIn('type', CourseType::where('is_chart', 0)->where('is_history', 0)->pluck('id'))
->where(function ($q) use ($start, $end) {
$q->whereBetween('start_time', [$start, $end])->orWhereBetween('end_time', [$start, $end]);
})->get();
foreach ($otherH as $h) {
$historyIds[] = $h->id;
}
foreach (Course::whereIn('id', $oIds)->get() as $c) {
$courseIds[] = $c->id;
}
$periodsTotal += $other->course_periods_total;
$signsUniqueSum += $other->course_signs_total;
// getOtherStatistics 内部已含 history,此处仅补 course ids(getOtherCourseIds 与 getOtherStatistics 的 Course 条件一致)
// 与 courses-home 口径一致:附加 is_history=1 的 HistoryCourse(whereHas calendar is_count_people=1,type=体系 id)
$courseTypesHistory = CourseType::where('is_history', 1)->get();
foreach ($courseTypesHistory as $hc) {
$historyList = 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)
->get();
foreach ($historyList as $h) {
$historyIds[] = $h->id;
}
$periodsTotal += $historyList->count();
$signsUniqueSum += $historyList->sum('course_type_signs_pass_unique');
}
return [
'periods_total' => $periodsTotal,
'signs_unique_sum' => $signsUniqueSum,
'course_ids' => array_values(array_unique($courseIds)),
'history_ids' => array_values(array_unique($historyIds)),
];
}
/**
* courses-home 的 courseTypesSum:按行展开的课程数量、去重(按类型聚合的那列会重复,这里只算 课程数量/期数)
* 返回:课程数量(行数)、涉及的 course_ids、history_course_ids
*/
protected function computeCoursesHome(string $start, string $end): array
{
$courseIds = [];
$historyIds = [];
$courseTypes = CourseType::whereIn('id', CourseType::pluck('id')->toArray())->get();
// 第一段:非历史 CourseType 下的 Course
foreach ($courseTypes as $ct) {
$courses2 = Course::where('type', $ct->id)
->where(function ($q) use ($start, $end) {
if ($start && $end) {
$q->whereBetween('start_date', [$start, $end])
->orWhereBetween('end_date', [$start, $end]);
}
})
->where('is_chart', 1)
->orderBy('start_date')
->get();
foreach ($courses2 as $c) {
$courseIds[] = $c->id;
}
}
// 第二段:is_history=1 的 HistoryCourse(与 courses-home 一致:whereHas calendar is_count_people=1)
$courseTypesHistory = CourseType::where('is_history', 1)->whereIn('id', CourseType::pluck('id')->toArray())->get();
foreach ($courseTypesHistory as $hc) {
$courses3 = HistoryCourse::whereHas('calendar', function ($q) {
$q->where('is_count_people', 1);
})
->where(function ($q) use ($start, $end) {
$q->whereBetween('start_time', [$start, $end])
->orWhereBetween('end_time', [$start, $end]);
})
->where('type', $hc->id)
->get();
foreach ($courses3 as $h) {
$historyIds[] = $h->id;
}
}
// 期数 = courseTypesSum 行数:每个 Course 一行,每个 HistoryCourse 一行
$periodsTotal = count($courseIds) + count($historyIds);
// 去重人数:用与 home-v2 同口径的「所有在 courseTypesSum 里的 course_id + history 对应类型」来算较复杂,
// 这里仅用 courses-home 实际用到的 Course 的 id 做 courseSignsTotalByUnique,历史用 sum(course_type_signs_pass_unique)
$allCids = array_values(array_unique($courseIds));
$signsFromCourse = $allCids
? (int) CourseSign::courseSignsTotalByUnique($start, $end, 1, $allCids, false, false)
: 0;
$signsFromHistory = 0;
foreach ($courseTypesHistory as $hc) {
$list = 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)
->get();
$signsFromHistory += $list->sum('course_type_signs_pass_unique');
}
$signsUniqueSum = $signsFromCourse + $signsFromHistory;
return [
'periods_total' => $periodsTotal,
'signs_unique_sum' => $signsUniqueSum,
'course_ids' => array_values(array_unique($courseIds)),
'history_ids' => array_values(array_unique($historyIds)),
];
}
protected function printComparison(array $homeV2, array $coursesHome): void
{
$this->line('');
$this->line('========== 汇总对比 ==========');
$this->table(
['指标', 'home-v2 (yearConfigs)', 'courses-home (courseTypesSum)', '差异 (courses-home - home-v2)'],
[
[
'期数/课程数量',
$homeV2['periods_total'],
$coursesHome['periods_total'],
$coursesHome['periods_total'] - $homeV2['periods_total'],
],
[
'去重培养人数',
$homeV2['signs_unique_sum'],
$coursesHome['signs_unique_sum'],
$coursesHome['signs_unique_sum'] - $homeV2['signs_unique_sum'],
],
[
'Course 数',
count($homeV2['course_ids']),
count($coursesHome['course_ids']),
count($coursesHome['course_ids']) - count($homeV2['course_ids']),
],
[
'HistoryCourse 数',
count($homeV2['history_ids']),
count($coursesHome['history_ids']),
count($coursesHome['history_ids']) - count($homeV2['history_ids']),
],
]
);
}
protected function printDetailDiff(array $homeV2, array $coursesHome): void
{
$this->line('');
$this->line('========== 明细差异(导致 courses-home 多出 期数 的条目) ==========');
$h2C = array_flip($homeV2['course_ids']);
$chC = array_flip($coursesHome['course_ids']);
$onlyInChC = array_keys(array_diff_key($chC, $h2C));
$onlyInH2C = array_keys(array_diff_key($h2C, $chC));
$h2H = array_flip($homeV2['history_ids']);
$chH = array_flip($coursesHome['history_ids']);
$onlyInChH = array_keys(array_diff_key($chH, $h2H));
$onlyInH2H = array_keys(array_diff_key($h2H, $chH));
if (!empty($onlyInChC)) {
$this->line('【仅 courses-home 有的 Course(会多算期数)】');
$rows = Course::whereIn('id', $onlyInChC)->get(['id', 'name', 'type', 'start_date', 'end_date', 'is_chart']);
$this->table(
['id', 'name', 'type', 'start_date', 'end_date', 'is_chart'],
$rows->map(fn($r) => [$r->id, $r->name, $r->type, $r->start_date, $r->end_date, $r->is_chart])->toArray()
);
$ctIds = $rows->pluck('type')->unique();
$cts = CourseType::whereIn('id', $ctIds)->get(['id', 'name', 'is_chart', 'is_history']);
$this->line('对应 CourseType: ' . $cts->map(fn($t) => "id={$t->id} name={$t->name} is_chart={$t->is_chart} is_history={$t->is_history}")->implode('; '));
}
if (!empty($onlyInChH)) {
$this->line('【仅 courses-home 有的 HistoryCourse(会多算期数)】');
$rows = HistoryCourse::whereIn('id', $onlyInChH)->get(['id', 'course_name', 'type', 'start_time', 'end_time', 'calendar_id']);
$this->table(
['id', 'course_name', 'type', 'start_time', 'end_time', 'calendar_id'],
$rows->map(fn($r) => [$r->id, $r->course_name, $r->type, $r->start_time, $r->end_time, $r->calendar_id])->toArray()
);
$ctIds = $rows->pluck('type')->unique();
$cts = CourseType::whereIn('id', $ctIds)->get(['id', 'name', 'is_chart', 'is_history']);
$this->line('对应 CourseType: ' . $cts->map(fn($t) => "id={$t->id} name={$t->name} is_chart={$t->is_chart} is_history={$t->is_history}")->implode('; '));
}
if (!empty($onlyInH2C)) {
$this->line('【仅 home-v2 有的 Course】');
$this->line(implode(', ', $onlyInH2C));
}
if (!empty($onlyInH2H)) {
$this->line('【仅 home-v2 有的 HistoryCourse】');
$this->line(implode(', ', $onlyInH2H));
}
if (empty($onlyInChC) && empty($onlyInChH) && empty($onlyInH2C) && empty($onlyInH2H)) {
$this->line('(无 ID 集合差异;若期数仍不一致,可能是同一条目在两个口径下 算/不算 的规则不同,例如 is_chart、is_count_people、日期区间、type 与 name 匹配等)');
}
}
}