diff --git a/app/Console/Commands/CheckBirthday.php b/app/Console/Commands/CheckBirthday.php index 265885b..0f8c4af 100755 --- a/app/Console/Commands/CheckBirthday.php +++ b/app/Console/Commands/CheckBirthday.php @@ -44,14 +44,13 @@ class CheckBirthday extends Command */ public function handle() { - // 匹配今天生日(格式:YYYY-MM-DD 或 MM-DD) - $today = date('m-d'); + // 仅完整生日(YYYY-MM-DD)参与提醒,避免 YYYY-MM 被自动补成 01 误判 $users = User::where('is_schoolmate', 1) - ->where(function ($query) use ($today) { - $query->where('birthday', 'like', '%-' . $today) - ->orWhere('birthday', 'like', $today . '%'); - }) + ->whereNotNull('birthday') ->get(); + $users = $users->filter(function ($user) { + return User::isBirthdayToday($user->birthday); + })->values(); $birthdayCount = $users->count(); diff --git a/app/Console/Commands/CheckMissingSchoolmateData.php b/app/Console/Commands/CheckMissingSchoolmateData.php new file mode 100644 index 0000000..ca68387 --- /dev/null +++ b/app/Console/Commands/CheckMissingSchoolmateData.php @@ -0,0 +1,157 @@ +allowedCourseTypeNames; + $allowedCourseNames = $this->allowedCourseNames; + + $missingUsers = User::query() + ->where(function ($query) { + $query->whereNull('is_schoolmate') + ->orWhere('is_schoolmate', '!=', 1); + }) + ->whereHas('courseSigns', function ($query) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $query->where('status', 1) + ->whereHas('course', function ($courseQuery) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $courseQuery->where(function ($eligibleQuery) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $eligibleQuery->where(function ($autoSchoolmateQuery) use ($today) { + $autoSchoolmateQuery->where('auto_schoolmate', 1) + ->whereNotNull('start_date') + ->where('start_date', '<=', $today); + })->orWhereHas('typeDetail', function ($typeQuery) use ($allowedCourseTypeNames) { + $typeQuery->whereIn('name', $allowedCourseTypeNames); + })->orWhereIn('name', $allowedCourseNames); + }); + }); + }) + ->with([ + 'courseSigns' => function ($query) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $query->where('status', 1) + ->whereHas('course', function ($courseQuery) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $courseQuery->where(function ($eligibleQuery) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $eligibleQuery->where(function ($autoSchoolmateQuery) use ($today) { + $autoSchoolmateQuery->where('auto_schoolmate', 1) + ->whereNotNull('start_date') + ->where('start_date', '<=', $today); + })->orWhereHas('typeDetail', function ($typeQuery) use ($allowedCourseTypeNames) { + $typeQuery->whereIn('name', $allowedCourseTypeNames); + })->orWhereIn('name', $allowedCourseNames); + }); + }) + ->with('course.typeDetail'); + } + ]) + ->orderBy('id') + ->get(); + + $this->info('检测口径:用户当前不是校友,但存在以下任一合格来源,则判定为“应进入校友库但未进入”:'); + $this->line('1. 已审核通过,且课程开启自动入校友,且课程已开课'); + $this->line('2. 已审核通过,且课程属于高研班体系'); + $this->line('3. 已审核通过,且课程属于攀峰班体系'); + $this->line('4. 已审核通过,且课程为“首期技术经理人领航班”'); + + if ($missingUsers->isEmpty()) { + $this->info('检测完成:当前未发现应进入校友库但未进入的用户。'); + return self::SUCCESS; + } + + $this->warn('检测完成:发现 ' . $missingUsers->count() . ' 位符合条件但未进入校友库的用户。'); + $this->newLine(); + + $rows = []; + foreach ($missingUsers as $user) { + $eligibleCourses = $user->courseSigns + ->map(function ($courseSign) use ($allowedCourseTypeNames, $allowedCourseNames, $today) { + if (empty($courseSign->course)) { + return '课程已删除'; + } + + $course = $courseSign->course; + $courseTypeName = $course->typeDetail->name ?? '空'; + $reasons = []; + + if ($course->auto_schoolmate == 1 && !empty($course->start_date) && $course->start_date <= $today) { + $reasons[] = '自动入校友'; + } + if (in_array($courseTypeName, $allowedCourseTypeNames, true)) { + $reasons[] = '体系白名单'; + } + if (in_array($course->name, $allowedCourseNames, true)) { + $reasons[] = '课程白名单'; + } + + return sprintf( + '%s[课程体系=%s,auto_schoolmate=%s,start_date=%s,命中规则=%s]', + $course->name, + $courseTypeName, + (string) $course->auto_schoolmate, + $course->start_date ?: '空', + empty($reasons) ? '无' : implode('+', $reasons) + ); + }) + ->filter() + ->unique() + ->values() + ->implode(';'); + + $rows[] = [ + '用户ID' => $user->id, + '姓名' => $user->name, + '手机号' => $user->mobile, + '当前校友状态' => (string) ($user->is_schoolmate ?? '空'), + '应入校友依据' => $eligibleCourses ?: '无', + '不合格原因' => '符合校友认定条件,但当前未进入校友库', + ]; + } + + $this->table( + ['用户ID', '姓名', '手机号', '当前校友状态', '应入校友依据', '不合格原因'], + $rows + ); + + $this->info('本次仅在命令行输出异常名单,不再生成 txt 文件。'); + + return self::FAILURE; + } +} diff --git a/app/Console/Commands/CheckSchoolmateData.php b/app/Console/Commands/CheckSchoolmateData.php new file mode 100644 index 0000000..2db2095 --- /dev/null +++ b/app/Console/Commands/CheckSchoolmateData.php @@ -0,0 +1,177 @@ +allowedCourseTypeNames; + $allowedCourseNames = $this->allowedCourseNames; + $fixedUsers = User::query() + ->where('is_schoolmate', 1) + ->where(function ($query) { + $query->whereNull('mobile') + ->orWhere('mobile', ''); + }) + ->orderBy('id') + ->get(); + + $fixedCount = 0; + foreach ($fixedUsers as $user) { + $user->is_schoolmate = 0; + $user->schoolmate_time = null; + if ($user->isDirty()) { + $user->save(); + $fixedCount++; + } + } + + $invalidUsers = User::query() + ->where('is_schoolmate', 1) + ->whereDoesntHave('courseSigns', function ($query) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $query->where('status', 1) + ->whereHas('course', function ($courseQuery) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $courseQuery->where(function ($eligibleQuery) use ($today, $allowedCourseTypeNames, $allowedCourseNames) { + $eligibleQuery->where(function ($autoSchoolmateQuery) use ($today) { + $autoSchoolmateQuery->where('auto_schoolmate', 1) + ->whereNotNull('start_date') + ->where('start_date', '<=', $today); + })->orWhereHas('typeDetail', function ($typeQuery) use ($allowedCourseTypeNames) { + $typeQuery->whereIn('name', $allowedCourseTypeNames); + })->orWhereIn('name', $allowedCourseNames); + }); + }); + }) + ->with([ + 'courseSigns' => function ($query) { + $query->where('status', 1)->with('course.typeDetail'); + } + ]) + ->orderBy('id') + ->get(); + + $schoolmateTotal = User::where('is_schoolmate', 1)->count(); + $invalidCount = $invalidUsers->count(); + + $this->info("当前校友总数:{$schoolmateTotal}"); + $this->info("已自动修正无手机号但仍是校友的用户数:{$fixedCount}"); + $this->info('检测口径:用户当前是校友,但不存在以下任一合格来源,则判定为不符合要求:'); + $this->line('1. 已审核通过,且课程开启自动入校友,且课程已开课'); + $this->line('2. 已审核通过,且课程属于高研班体系'); + $this->line('3. 已审核通过,且课程属于攀峰班体系'); + $this->line('4. 已审核通过,且课程为“首期技术经理人领航班”'); + + if ($invalidUsers->isEmpty()) { + $this->info('检测完成:当前所有校友数据都符合校友认定规则。'); + return self::SUCCESS; + } + + $this->warn('检测完成:发现 ' . $invalidCount . ' 位校友不符合校友认定规则,以下用户将被自动取消校友身份。'); + $this->newLine(); + + $rows = []; + foreach ($invalidUsers as $user) { + $passedCourses = $user->courseSigns + ->map(function ($courseSign) use ($allowedCourseTypeNames, $allowedCourseNames, $today) { + if (empty($courseSign->course)) { + return '课程已删除'; + } + + $course = $courseSign->course; + $courseTypeName = $course->typeDetail->name ?? '空'; + $reasons = []; + + if ($course->auto_schoolmate == 1 && !empty($course->start_date) && $course->start_date <= $today) { + $reasons[] = '自动入校友'; + } + if (in_array($courseTypeName, $allowedCourseTypeNames, true)) { + $reasons[] = '体系白名单'; + } + if (in_array($course->name, $allowedCourseNames, true)) { + $reasons[] = '课程白名单'; + } + + return sprintf( + '%s[课程体系=%s,auto_schoolmate=%s,start_date=%s,命中规则=%s]', + $course->name, + $courseTypeName, + (string) $course->auto_schoolmate, + $course->start_date ?: '空', + empty($reasons) ? '无' : implode('+', $reasons) + ); + }) + ->filter() + ->unique() + ->values() + ->implode(';'); + + $rows[] = [ + '用户ID' => $user->id, + '姓名' => $user->name, + '手机号' => $user->mobile, + '成为校友时间' => $user->schoolmate_time ?: '空', + '已通过课程' => $passedCourses ?: '无', + '不符合原因' => '没有符合自动入校友规则的已通过课程', + ]; + } + + $this->table( + ['用户ID', '姓名', '手机号', '成为校友时间', '已通过课程', '不符合原因'], + $rows + ); + + $updatedCount = 0; + foreach ($invalidUsers as $user) { + $user->is_schoolmate = 0; + $user->schoolmate_time = null; + if ($user->isDirty()) { + $user->save(); + $updatedCount++; + } + } + + $this->newLine(); + $this->info("已自动取消 {$updatedCount} 位异常校友的校友身份。"); + $this->info('本次仅在命令行输出处理结果,不再生成 txt 文件。'); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Admin/CourseController.php b/app/Http/Controllers/Admin/CourseController.php index 6854257..cc2bb78 100755 --- a/app/Http/Controllers/Admin/CourseController.php +++ b/app/Http/Controllers/Admin/CourseController.php @@ -281,6 +281,7 @@ class CourseController extends BaseController * @OA\Parameter(name="url_title", in="query", @OA\Schema(type="string"), description="链接地址"), * @OA\Parameter(name="is_ganbu", in="query", @OA\Schema(type="integer"), description="是否干部课程-0否1是"), * @OA\Parameter(name="is_chart", in="query", @OA\Schema(type="integer"), description="是否参与统计-0否1是,默认1"), + * @OA\Parameter(name="free_enter_schoolmate", in="query", @OA\Schema(type="integer"), description="免费/公益课程是否计入进入校友库资格-0否1是"), * @OA\Response( * response=200, * description="操作成功" diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 12f5dc6..40d314c 100755 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -711,7 +711,38 @@ class UserController extends BaseController */ public function import() { - return parent::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 { + $tableName = $this->model->getTable(); + $existingColumns = (new CustomFormField)->getRowTableFieldsByComment($tableName); + + $filteredRecords = array_map(function ($record) use ($existingColumns) { + return array_intersect_key($record, $existingColumns); + }, $records); + $filteredRecords = array_filter($filteredRecords); + + foreach ($filteredRecords as $record) { + $this->model->create($record); + } + + DB::commit(); + return $this->success(['total' => count($records), 'filter_total' => count($filteredRecords)]); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->fail([$exception->getCode(), $exception->getMessage()]); + } } /** @@ -834,20 +865,46 @@ class UserController extends BaseController if (empty($idsArray)) { return $this->fail([StarterResponseCode::START_ERROR_PARAMETER, '编号不能为空']); } - if (isset($all['is_schoolmate'])) { - if ($all['is_schoolmate'] == 1) { - // 仅当 非校友→校友 时写入 schoolmate_time;已是校友的不更新,避免覆盖 - $this->model->whereIn('id', $idsArray) - ->whereRaw('COALESCE(is_schoolmate, 0) != 1') - ->update(['is_schoolmate' => 1, 'schoolmate_time' => now()]); - } else { - $this->model->whereIn('id', $idsArray)->update([ - 'is_schoolmate' => $all['is_schoolmate'], - 'schoolmate_time' => null, - ]); + DB::beginTransaction(); + try { + $updatedCount = 0; + + if (isset($all['is_schoolmate'])) { + if ($all['is_schoolmate'] == 1) { + // 改为逐条 save(),确保触发审计日志 + $users = $this->model->whereIn('id', $idsArray) + ->whereRaw('COALESCE(is_schoolmate, 0) != 1') + ->get(); + + foreach ($users as $user) { + $user->is_schoolmate = 1; + $user->schoolmate_time = now(); + if ($user->isDirty()) { + $user->save(); + $updatedCount++; + } + } + } else { + // 改为逐条 save(),确保触发审计日志 + $users = $this->model->whereIn('id', $idsArray)->get(); + + foreach ($users as $user) { + $user->is_schoolmate = $all['is_schoolmate']; + $user->schoolmate_time = null; + if ($user->isDirty()) { + $user->save(); + $updatedCount++; + } + } + } } + + DB::commit(); + return $this->success('批量更新成功,共更新 ' . $updatedCount . ' 条记录'); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->fail([$exception->getCode(), $exception->getMessage()]); } - return $this->success('批量更新成功'); } /** @@ -911,9 +968,20 @@ class UserController extends BaseController DB::beginTransaction(); try { - $this->model->whereIn('id', $idsArray)->update($data); + $users = $this->model->whereIn('id', $idsArray)->get(); + $updatedCount = 0; + + foreach ($users as $user) { + // 改为逐条 save(),确保触发审计日志 + $user->fill($data); + if ($user->isDirty()) { + $user->save(); + $updatedCount++; + } + } + DB::commit(); - return $this->success('批量更新成功,共更新 ' . count($idsArray) . ' 条记录'); + return $this->success('批量更新成功,共更新 ' . $updatedCount . ' 条记录'); } catch (\Exception $exception) { DB::rollBack(); return $this->fail([$exception->getCode(), $exception->getMessage()]); diff --git a/app/Http/Controllers/Mobile/CourseController.php b/app/Http/Controllers/Mobile/CourseController.php index f872de5..84b4f25 100755 --- a/app/Http/Controllers/Mobile/CourseController.php +++ b/app/Http/Controllers/Mobile/CourseController.php @@ -1077,17 +1077,67 @@ class CourseController extends CommonController if ($validator->fails()) { return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]); } + $monthTs = strtotime($all['month'] . '-01'); + if ($monthTs === false) { + return $this->fail([ResponseCode::ERROR_PARAMETER, '月份格式错误']); + } + $monthStart = date('Y-m-01 00:00:00', $monthTs); + $monthEnd = date('Y-m-t 23:59:59', $monthTs); + $list = Calendar::with('course', 'courseContent') ->where(function ($query) use ($all) { if (isset($all['type'])) { $query->where('type', $all['type']); } - })->where('start_time', 'like', '%' . $all['month'] . '%') + })->where(function ($query) use ($monthStart, $monthEnd) { + $query->whereBetween('start_time', [$monthStart, $monthEnd]) + ->orWhereBetween('end_time', [$monthStart, $monthEnd]) + ->orWhere(function ($sub) use ($monthStart, $monthEnd) { + $sub->where('start_time', '<=', $monthStart) + ->where('end_time', '>=', $monthEnd); + }); + }) ->where('is_publish', 1) ->orderBy('start_time', 'asc') ->get(); + $list->each(function ($calendar) { + $rawStartTime = $calendar->start_time ?? null; + $rawEndTime = $calendar->end_time ?? null; + $calendar->start_time = $this->normalizeCalendarDateTime($rawStartTime, $rawEndTime, true); + $calendar->end_time = $this->normalizeCalendarDateTime($rawStartTime, $rawEndTime, false); + }); + return $this->success($list); } + private function formatDateTimeWithSeconds($value) + { + if (empty($value)) { + return $value; + } + $timestamp = strtotime($value); + if ($timestamp === false) { + return $value; + } + return date('Y-m-d H:i:s', $timestamp); + } + + private function normalizeCalendarDateTime($startValue, $endValue, $isStart = true) + { + $start = $this->formatDateTimeWithSeconds($startValue); + $end = $this->formatDateTimeWithSeconds($endValue); + $target = $isStart ? $start : $end; + if (empty($target)) { + return $target; + } + + // 跨天事件统一按“日期维度”返回,避免前端以时分秒切段时漏首日 + if (!empty($start) && !empty($end) && date('Y-m-d', strtotime($start)) !== date('Y-m-d', strtotime($end))) { + return date('Y-m-d 00:00:00', strtotime($target)); + } + + return $target; + } + } diff --git a/app/Http/Controllers/Mobile/OtherController.php b/app/Http/Controllers/Mobile/OtherController.php index b7a0f04..d4c2697 100755 --- a/app/Http/Controllers/Mobile/OtherController.php +++ b/app/Http/Controllers/Mobile/OtherController.php @@ -143,6 +143,7 @@ class OtherController extends CommonController if ($cache && !empty($cache->payload)) { $cache->last_matched_at = now(); $cache->save(); + $this->syncCurrentUserCompanyType($cache->payload); return $this->success($cache->payload); } @@ -163,9 +164,28 @@ class OtherController extends CommonController 'fetched_at' => now(), ] ); + $this->syncCurrentUserCompanyType($result); return $this->success($result); } + protected function syncCurrentUserCompanyType(array $result): void + { + $user = $this->getUser(); + + if (!$user) { + return; + } + + $companyType = implode(',', $result['tagList'] ?? []); + + if ($user->company_type === $companyType) { + return; + } + + $user->company_type = $companyType; + $user->save(); + } + /** * @OA\Get( * path="/api/mobile/other/company-list", diff --git a/app/Http/Controllers/Mobile/UserController.php b/app/Http/Controllers/Mobile/UserController.php index dbf3b0d..6931c20 100755 --- a/app/Http/Controllers/Mobile/UserController.php +++ b/app/Http/Controllers/Mobile/UserController.php @@ -275,11 +275,18 @@ class UserController extends CommonController } // 是否有资格进入校友库 $enter_schoolmate = User::whereHas('courseSigns', function ($query) { - $query->where('fee_status', 1)->where('status', 1); + $query->where('status', 1) + ->where(function ($schoolmateQuery) { + $schoolmateQuery->where('fee_status', 1) + ->orWhereHas('course', function ($courseQuery) { + $courseQuery->where('is_fee', 0) + ->where('free_enter_schoolmate', 1); + }); + }); })->where('id', $this->getUserId())->count(); // 是否生日 $is_birthday = 0; - if (isset($user->birthday) && date('m-d', strtotime($user->birthday)) == date('m-d')) { + if (User::isBirthdayToday($user->birthday ?? null)) { $is_birthday = 1; } return $this->success(compact('user', 'door_appointments', 'course_signs', 'enter_schoolmate', 'is_birthday')); diff --git a/app/Models/Company.php b/app/Models/Company.php index fdb679b..0d8827d 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -488,8 +488,6 @@ class Company extends SoftDeletesModel '^', '&', '*', - '(', - ')', '[', ']', '{', @@ -516,8 +514,6 @@ class Company extends SoftDeletesModel '·', '~', '¥', - '(', - ')', '【', '】', '《', diff --git a/app/Models/Course.php b/app/Models/Course.php index 8a764e5..f93e62d 100755 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -21,6 +21,7 @@ class Course extends SoftDeletesModel 'show_txl_text', 'show_mobile_text', 'auto_schoolmate_text', + 'free_enter_schoolmate_text', 'is_virtual_text' ]; @@ -62,6 +63,12 @@ class Course extends SoftDeletesModel return $array[$this->attributes['auto_schoolmate']]; } + public function getFreeEnterSchoolmateTextAttribute() + { + $array = [0 => '否', 1 => '是']; + return $array[$this->attributes['free_enter_schoolmate'] ?? 0]; + } + public function getIsVirtualTextAttribute() { $array = [0 => '否', 1 => '是']; @@ -308,4 +315,3 @@ class Course extends SoftDeletesModel } } - diff --git a/app/Models/User.php b/app/Models/User.php index f0266a4..e7bf63a 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -125,6 +125,97 @@ class User extends Authenticatable implements Auditable 'is_schoolmate' => ['否', '是'], ]; + public static function normalizeBirthday($birthday): ?string + { + if ($birthday instanceof DateTimeInterface) { + return Carbon::instance(\DateTime::createFromInterface($birthday))->format('Y-m-d'); + } + + if ($birthday === null) { + return null; + } + + $birthday = trim((string) $birthday); + if ($birthday === '') { + return null; + } + + $normalized = str_replace( + ['年', '月', '日', '/', '.', '_'], + ['-', '-', '', '-', '-', '-'], + $birthday + ); + $normalized = preg_replace('/\s+/', '', $normalized); + $normalized = preg_replace('/-+/', '-', $normalized); + $normalized = trim($normalized, '-'); + + if (preg_match('/^(\d{4})(\d{2})$/', $normalized, $matches)) { + $normalized = $matches[1] . '-' . $matches[2]; + } elseif (preg_match('/^(\d{4})(\d{2})(\d{2})$/', $normalized, $matches)) { + $normalized = $matches[1] . '-' . $matches[2] . '-' . $matches[3]; + } + + if (preg_match('/^(\d{4})-(\d{1,2})$/', $normalized, $matches)) { + $year = (int) $matches[1]; + $month = (int) $matches[2]; + + if ($month >= 1 && $month <= 12) { + return sprintf('%04d-%02d', $year, $month); + } + } + + if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $normalized, $matches)) { + $year = (int) $matches[1]; + $month = (int) $matches[2]; + $day = (int) $matches[3]; + + if (checkdate($month, $day, $year)) { + return sprintf('%04d-%02d-%02d', $year, $month, $day); + } + } + + return null; + } + + public static function hasBirthdayFormat(?string $birthday): bool + { + if (empty($birthday)) { + return false; + } + + if (preg_match('/^\d{4}-\d{2}$/', $birthday)) { + [, $month] = array_map('intval', explode('-', $birthday)); + return $month >= 1 && $month <= 12; + } + + return self::hasCompleteBirthday($birthday); + } + + public static function hasCompleteBirthday(?string $birthday): bool + { + if (empty($birthday) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) { + return false; + } + + [$year, $month, $day] = array_map('intval', explode('-', $birthday)); + + return checkdate($month, $day, $year); + } + + public static function isBirthdayToday(?string $birthday): bool + { + if (!self::hasCompleteBirthday($birthday)) { + return false; + } + + return substr($birthday, 5, 5) === date('m-d'); + } + + public function setBirthdayAttribute($value): void + { + $this->attributes['birthday'] = self::normalizeBirthday($value); + } + public function getMobileAttribute($value) { // 如果url中包含admin字符串,则所有的手机号显示中间4位星号代替 @@ -258,13 +349,22 @@ class User extends Authenticatable implements Auditable // 将课程开课时间转换为 datetime 格式 $schoolmateTime = Carbon::parse($courseStartDate)->format('Y-m-d H:i:s'); - // 批量更新:只更新还不是校友的学员;从 非校友→校友 时使用课程开课时间作为 schoolmate_time - return self::whereIn('id', $userIds) + // 改为逐条 save(),确保自动任务也能触发审计日志 + $users = self::whereIn('id', $userIds) ->whereRaw('COALESCE(is_schoolmate, 0) != 1') - ->update([ - 'is_schoolmate' => 1, - 'schoolmate_time' => $schoolmateTime - ]); + ->get(); + + $updatedCount = 0; + foreach ($users as $user) { + $user->is_schoolmate = 1; + $user->schoolmate_time = $schoolmateTime; + if ($user->isDirty()) { + $user->save(); + $updatedCount++; + } + } + + return $updatedCount; } } diff --git a/config/audit.php b/config/audit.php index ad0ae2d..c92f614 100755 --- a/config/audit.php +++ b/config/audit.php @@ -195,5 +195,5 @@ return [ | */ - 'console' => false, + 'console' => true, ]; diff --git a/database/migrations/2026_04_02_094504_change_users_birthday_back_to_string.php b/database/migrations/2026_04_02_094504_change_users_birthday_back_to_string.php new file mode 100644 index 0000000..429bcfe --- /dev/null +++ b/database/migrations/2026_04_02_094504_change_users_birthday_back_to_string.php @@ -0,0 +1,31 @@ +string('birthday')->nullable()->comment('生日')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->date('birthday')->nullable()->comment('生日')->change(); + }); + } +}; diff --git a/database/migrations/2026_04_03_100000_add_free_enter_schoolmate_to_courses_table.php b/database/migrations/2026_04_03_100000_add_free_enter_schoolmate_to_courses_table.php new file mode 100644 index 0000000..6cc65c5 --- /dev/null +++ b/database/migrations/2026_04_03_100000_add_free_enter_schoolmate_to_courses_table.php @@ -0,0 +1,35 @@ +boolean('free_enter_schoolmate') + ->default(0) + ->comment('免费/公益课程是否计入进入校友库资格-0否1是') + ->after('auto_schoolmate'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('courses', function (Blueprint $table) { + $table->dropColumn('free_enter_schoolmate'); + }); + } +}; diff --git a/doc/is_schoolmate字段更新说明.md b/doc/is_schoolmate字段更新说明.md new file mode 100644 index 0000000..262e5cd --- /dev/null +++ b/doc/is_schoolmate字段更新说明.md @@ -0,0 +1,20 @@ +目前会影响“校友状态”的主要有 3 类操作: + +## 1. 后台批量把一批人设为校友 + +后台支持批量操作,可以一次把多个人加入校友库,也可以一次把多个人从校友库移除。 + +这种方式的特点:一次会改很多人。过去这类操作没有审计日志,现在追加上了。 + +## 2. 导入数据时把用户设为校友 + +如果导入的表格里本身带有“是否校友”这一列,系统在导入时也可能把用户设为校友。 + +## 3. 系统自动把符合条件的学员加入校友库 + +系统有一个半小时一次的自动任务,检测课程开启了自动加入校友库设置,并且课程已经开始的审核通过的学员设置成校友。 + +## 问题排查说明 +- 经排查,创业课-企业出海与境外投资法律风险及规划。这里人员被设置成校友,来源于后台的批量设置功能。比如沈杰,是在2025-11-20 10:56:00被批量设置成校友的,一批的还有:周炫坊,李融。王洋是在2026-01-12 15:33:42被批量设置成校友的,同一批的还有杨湾湾,王婷。王文岩是在2026-01-23 17:40:02被批量设置成校友的。 + +- 终身校友的人员进入了校友库,是由于3.20日反馈有人员是终身校友,但是没有校友权益的问题时候,我理解错误并且误操作把终身校友课程下人员批量设置成了校友但是没有及时发现导致的。 \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 4610a18..950b083 100755 --- a/routes/api.php +++ b/routes/api.php @@ -344,8 +344,7 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () { Route::get('other/banner', [\App\Http\Controllers\Mobile\OtherController::class, "banner"]); // 公司查询 Route::get('other/company', [\App\Http\Controllers\Mobile\OtherController::class, "company"]); - // 公司详情 - Route::get('other/company-detail', [\App\Http\Controllers\Mobile\OtherController::class, "companyDetail"]); + // 公司查询 Route::get('other/company-list', [\App\Http\Controllers\Mobile\OtherController::class, "companyList"]); // 通知 @@ -368,6 +367,8 @@ Route::group(["namespace" => "Mobile", "prefix" => "mobile"], function () { // 支付回调 Route::any('course/pay_callback', [\App\Http\Controllers\Mobile\CourseController::class, "payCallback"]); Route::group(['middleware' => ['sanctum.jwt:mobile']], function () { + // 公司详情 + Route::get('other/company-detail', [\App\Http\Controllers\Mobile\OtherController::class, "companyDetail"]); // 其他 Route::post('upload-file', [\App\Http\Controllers\Mobile\UploadController::class, "uploadFile"]); // 用户信息