master
cody 2 months ago
parent 9822619069
commit 4e0089700c

@ -1,296 +0,0 @@
<?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 统计(单一段 startend
* 返回:期数、去重人数、涉及的 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=0is_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 idsgetOtherCourseIds 与 getOtherStatistics 的 Course 条件一致)
// 与 courses-home 口径一致:附加 is_history=1 的 HistoryCoursewhereHas calendar is_count_people=1type=体系 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 匹配等)');
}
}
}

@ -1,78 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Course;
use App\Models\CourseSign;
use App\Models\CourseType;
use App\Models\User;
use Illuminate\Console\Command;
/**
* 对比 study 接口(苏州+人才)的 list.total 与 courses-home 的 cover_rencai_total
* 在相同 start_date、end_date、is_chart、address=苏州、is_rencai=1 下应一致。
*/
class DiffStudyCoverRencai extends Command
{
protected $signature = 'diff:study-cover-rencai
{--start=2025-01-01 : 开始日期}
{--end=2025-12-31 : 结束日期}';
protected $description = '对比 study(苏州+人才) 的 list.total 与 courses-home 的 cover_rencai_total';
public function handle()
{
$start = $this->option('start');
$end = $this->option('end');
$course_type_id = CourseType::pluck('id')->toArray();
$courses = Course::whereIn('type', $course_type_id)->get();
$course_ids = $courses->pluck('id');
$rencaiCount = CourseSign::rencai($start, $end, $course_ids);
// 复现 study 的 User 数量address=苏州, is_rencai=1, courses_start/end, is_chart=1, status=1
// is_rencai 的「人才培训」已与 rencai 一致:仅在 2025、is_chart=1 的报名中认定
$studyCount = User::query()
->whereHas('courseSigns', function ($query) use ($start, $end) {
$query->where('status', 1)->whereHas('course', function ($q) use ($start, $end) {
$q->where('is_chart', 1)->where(function ($q2) use ($start, $end) {
$q2->whereBetween('start_date', [$start, $end])
->orWhereBetween('end_date', [$start, $end]);
});
});
})
->whereHas('company', function ($c) {
$c->where('company_address', 'like', '%苏州%')
->orWhere('company_city', 'like', '%苏州%');
})
->where(function ($q) use ($start, $end) {
$q->whereHas('courseSigns', function ($cs) use ($start, $end) {
$cs->where('status', 1)->whereHas('course', function ($c) use ($start, $end) {
$c->where('is_chart', 1)
->where(function ($q2) use ($start, $end) {
$q2->whereBetween('start_date', [$start, $end])
->orWhereBetween('end_date', [$start, $end]);
})
->whereHas('typeDetail', function ($t) {
$t->where('name', '人才培训');
});
});
})->orWhere('type', 'like', '%人才%')->orWhere('talent_tags', 'like', '%人才%');
})
->count();
$diff = $studyCount - $rencaiCount;
$this->table(
['指标', 'study (User 数)', 'cover_rencai_total (rencai)', '差异 study - rencai'],
[['苏州人才', $studyCount, $rencaiCount, $diff]]
);
if ($diff !== 0) {
$this->warn("两者相差 {$diff},请检查 study 的 is_rencai 与 CourseSign::rencai 的细微口径。");
return 1;
}
$this->info('study 与 cover_rencai_total 一致。');
return 0;
}
}

@ -1,137 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Company;
use App\Models\User;
use Illuminate\Console\Command;
/**
* 对比 studyis_schoolmate=1, address=上海, is_chart=1与 home-v2 country 上海市 的人数差异,
* 输出各自 User id 及「仅 study 有 / 仅 home-v2 有」的明细,便于修正。
*/
class DiffStudyHomeV2Country extends Command
{
protected $signature = 'diff:study-home-v2-country
{--address=上海 : 地址关键词,与 study 的 address 对应}';
protected $description = '对比 study(上海+校友+is_chart) 与 home-v2 country 上海市 的 User 差异';
public function handle()
{
$address = $this->option('address');
// 1) studyis_schoolmate=1, address=上海, is_chart=1courses_start/end、status 为空(不限制课程日期与报名状态)
$studyQuery = User::query()
->where('is_schoolmate', 1)
->whereHas('company', function ($c) use ($address) {
$c->where('company_address', 'like', '%' . $address . '%')
->orWhere('company_city', 'like', '%' . $address . '%');
})
->whereHas('courseSigns', function ($cs) {
$cs->whereHas('course', function ($c) {
$c->where('is_chart', 1);
});
});
$studyIds = $studyQuery->pluck('id')->toArray();
// 2) home-v2 上海市 当前逻辑address like + is_schoolmate + whereHas courseSigns(course is_chart=1)(与 study 完全一致)
$term = preg_replace('/市$/', '', '上海市') ?: '上海市';
$homeV2Query = User::query()
->where('is_schoolmate', 1)
->whereHas('company', function ($q) use ($term) {
$q->where(function ($q2) use ($term) {
$q2->where('company_address', 'like', '%' . $term . '%')
->orWhere('company_city', 'like', '%' . $term . '%');
});
})
->whereHas('courseSigns', function ($cs) {
$cs->whereHas('course', function ($c) {
$c->where('is_chart', 1);
});
});
$homeV2Ids = $homeV2Query->pluck('id')->toArray();
// 3) 仅用 address 匹配 + is_schoolmate即与 home-v2 同样的 address 条件,不含 courseSigns
$addressOnlyQuery = User::query()
->where('is_schoolmate', 1)
->whereHas('company', function ($q) use ($address) {
$q->where('company_address', 'like', '%' . $address . '%')
->orWhere('company_city', 'like', '%' . $address . '%');
});
$addressOnlyIds = $addressOnlyQuery->pluck('id')->toArray();
$this->line('======== 人数 ========');
$this->table(
['口径', '人数'],
[
['study (address+is_schoolmate+courseSigns is_chart=1)', count($studyIds)],
['home-v2 上海市 (address like+is_schoolmate, term=' . $term . ')', count($homeV2Ids)],
['仅 address+is_schoolmate (不含 courseSigns)', count($addressOnlyIds)],
]
);
$onlyStudy = array_values(array_diff($studyIds, $homeV2Ids));
$onlyHomeV2 = array_values(array_diff($homeV2Ids, $studyIds));
$this->line('study ids 与 home-v2 上海市 ids 是否完全一致: ' . (count($onlyStudy) === 0 && count($onlyHomeV2) === 0 ? '是' : '否'));
if (!empty($onlyStudy)) {
$this->line('');
$this->line('【仅 study 有、home-v2 没有】 count=' . count($onlyStudy));
$rows = User::with('company')->whereIn('id', $onlyStudy)->get(['id', 'name', 'mobile', 'is_schoolmate', 'company_id']);
$out = [];
foreach ($rows as $u) {
$c = $u->company;
$out[] = [
$u->id,
$u->name,
$u->mobile,
$u->company_id,
$c ? ($c->company_city ?? '') : '',
$c ? ($c->company_address ?? '') : '',
$c ? (strpos((string)$c->company_address, $address) !== false || strpos((string)$c->company_city, $address) !== false ? 'Y' : 'N') : 'N',
];
}
$this->table(['user_id', 'name', 'mobile', 'company_id', 'company_city', 'company_address', 'match_' . $address . '?'], $out);
}
if (!empty($onlyHomeV2)) {
$this->line('');
$this->line('【仅 home-v2 有、study 没有】 count=' . count($onlyHomeV2));
$this->line(implode(', ', $onlyHomeV2));
}
// 4) 检查 $countryArea 中是否有 上海市,以及 approvedStudents 与 普通 company 的差别
$this->line('');
$this->line('======== 补充:$countryArea 与 approvedStudents ========');
$countryArea = Company::approvedStudents()->groupBy('company_city')->whereNotNull('company_city')->get(['company_city']);
$shCity = $countryArea->firstWhere('company_city', '上海市');
$this->line('countryArea 中是否存在 company_city=上海市: ' . ($shCity ? '是' : '否'));
$hasShanghai = $countryArea->contains(fn ($i) => $i->company_city === '上海' || strpos((string)$i->company_city, '上海') !== false);
$this->line('countryArea 中是否有含「上海」的 company_city: ' . ($hasShanghai ? '是' : '否'));
// 5) 若 study 多 1 人:检查该人 company 是否 whereHas 能匹配 (company_address or company_city) like 上海
if (count($onlyStudy) === 1) {
$u = User::with('company')->find($onlyStudy[0]);
if ($u && $u->company) {
$c = $u->company;
$a = (string)($c->company_address ?? '');
$city = (string)($c->company_city ?? '');
$like = 'like \'%' . $term . '%\'';
$this->line('');
$this->line('该 User 的 company: company_city=' . $city . ' company_address=' . $a);
$this->line('company_address ' . $like . ' => ' . (strpos($a, $term) !== false ? 'true' : 'false'));
$this->line('company_city ' . $like . ' => ' . (strpos($city, $term) !== false ? 'true' : 'false'));
$this->line('company 是否 approvedStudents: ' . ($this->companyIsApprovedStudents($c->id) ? 'Y' : 'N'));
}
}
return 0;
}
private function companyIsApprovedStudents($companyId): bool
{
return Company::approvedStudents()->where('id', $companyId)->exists();
}
}

@ -85,23 +85,42 @@ class UpdateUserNo extends Command
$no = $baseNo;
$attempt = 0;
$maxAttempts = 1000; // 防止无限循环
$saved = false;
// 尝试保存学号,如果遇到唯一性冲突则重新生成
while (!$saved && $attempt < $maxAttempts) {
// 检查学号是否已存在,如果存在则顺位+1继续尝试
if (User::where('no', $no)->where('id', '!=', $user->id)->exists()) {
$i++;
$no = $course->student_prefix . str_pad($i, 3, '0', STR_PAD_LEFT);
$attempt++;
continue;
}
// 检查学号是否已存在,如果存在则顺位+1继续尝试
while (User::where('no', $no)->where('id', '!=', $user->id)->exists() && $attempt < $maxAttempts) {
$i++;
$no = $course->student_prefix . str_pad($i, 3, '0', STR_PAD_LEFT);
$attempt++;
// 尝试更新用户编号
try {
$user->no = $no;
$user->save();
$saved = true;
} catch (\Illuminate\Database\QueryException $e) {
// 捕获唯一性约束冲突异常
if ($e->getCode() == 23000 && strpos($e->getMessage(), 'Duplicate entry') !== false) {
// 学号已被其他用户占用,重新生成
$i++;
$no = $course->student_prefix . str_pad($i, 3, '0', STR_PAD_LEFT);
$attempt++;
continue;
}
// 其他异常直接抛出
throw $e;
}
}
if ($attempt >= $maxAttempts) {
if (!$saved) {
$this->warn('课程: ' . ($course->name ?? $course->id) . ', 用户: ' . ($user->name ?? $user->id) . ' 无法生成唯一学号,跳过');
continue;
}
// 更新用户编号
$user->no = $no;
$user->save();
if ($no != $baseNo) {
$this->info('课程: ' . ($course->name ?? $course->id) . ', 原学号: ' . $baseNo . ' 已存在,使用: ' . $no . ', 用户: ' . ($user->name ?? $user->id));
} else {

Loading…
Cancel
Save