21 changed files with 2253 additions and 469 deletions
@ -0,0 +1,172 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\adminapi\controller\sys; |
||||
|
|
||||
|
use app\service\admin\sys\SysMenuService; |
||||
|
use core\base\BaseAdminController; |
||||
|
use think\Response; |
||||
|
|
||||
|
/** |
||||
|
* 系统菜单管理控制器 |
||||
|
*/ |
||||
|
class SysMenu extends BaseAdminController |
||||
|
{ |
||||
|
/** |
||||
|
* 获取菜单列表 |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function lists() |
||||
|
{ |
||||
|
$data = $this->request->params([ |
||||
|
['keyword', ''], |
||||
|
['status', ''], |
||||
|
['page', 1], |
||||
|
['limit', 20] |
||||
|
]); |
||||
|
|
||||
|
$service = new SysMenuService(); |
||||
|
return success($service->getPage($data)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取菜单详情 |
||||
|
* @param int $id |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function info(int $id) |
||||
|
{ |
||||
|
$service = new SysMenuService(); |
||||
|
return success($service->getInfo($id)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加菜单 |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function add() |
||||
|
{ |
||||
|
$data = $this->request->params([ |
||||
|
['menu_key', ''], |
||||
|
['menu_name', ''], |
||||
|
['menu_icon', ''], |
||||
|
['menu_path', ''], |
||||
|
['menu_params', ''], |
||||
|
['sort_order', 0], |
||||
|
['status', 1], |
||||
|
['description', ''] |
||||
|
]); |
||||
|
|
||||
|
$this->validate($data, [ |
||||
|
'menu_key' => 'require|unique:sys_menus', |
||||
|
'menu_name' => 'require', |
||||
|
'menu_path' => 'require' |
||||
|
]); |
||||
|
|
||||
|
$service = new SysMenuService(); |
||||
|
$id = $service->add($data); |
||||
|
return success(['id' => $id], '添加成功'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 编辑菜单 |
||||
|
* @param int $id |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function edit(int $id) |
||||
|
{ |
||||
|
$data = $this->request->params([ |
||||
|
['menu_key', ''], |
||||
|
['menu_name', ''], |
||||
|
['menu_icon', ''], |
||||
|
['menu_path', ''], |
||||
|
['menu_params', ''], |
||||
|
['sort_order', 0], |
||||
|
['status', 1], |
||||
|
['description', ''] |
||||
|
]); |
||||
|
|
||||
|
$this->validate($data, [ |
||||
|
'menu_key' => 'require', |
||||
|
'menu_name' => 'require', |
||||
|
'menu_path' => 'require' |
||||
|
]); |
||||
|
|
||||
|
$service = new SysMenuService(); |
||||
|
$service->edit($id, $data); |
||||
|
return success([], '编辑成功'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除菜单 |
||||
|
* @param int $id |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function del(int $id) |
||||
|
{ |
||||
|
$service = new SysMenuService(); |
||||
|
$service->del($id); |
||||
|
return success([], '删除成功'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改菜单状态 |
||||
|
* @param int $id |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function modifyStatus(int $id) |
||||
|
{ |
||||
|
$data = $this->request->params([ |
||||
|
['status', 1] |
||||
|
]); |
||||
|
|
||||
|
$service = new SysMenuService(); |
||||
|
$service->modifyStatus($id, $data['status']); |
||||
|
return success([], '操作成功'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有菜单(用于角色权限配置) |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function getAllMenus() |
||||
|
{ |
||||
|
$service = new SysMenuService(); |
||||
|
return success($service->getAllMenus()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取角色菜单权限 |
||||
|
* @param int $roleId |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function getRoleMenus(int $roleId) |
||||
|
{ |
||||
|
$service = new SysMenuService(); |
||||
|
return success($service->getRoleMenus($roleId)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置角色菜单权限 |
||||
|
* @param int $roleId |
||||
|
* @return Response |
||||
|
*/ |
||||
|
public function setRoleMenus(int $roleId) |
||||
|
{ |
||||
|
$data = $this->request->params([ |
||||
|
['menu_ids', []] |
||||
|
]); |
||||
|
|
||||
|
$service = new SysMenuService(); |
||||
|
$service->setRoleMenus($roleId, $data['menu_ids']); |
||||
|
return success([], '设置成功'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\adminapi\controller\uniapp; |
||||
|
|
||||
|
use core\base\BaseAdminController; |
||||
|
use app\service\admin\uniapp\UniappAuthService; |
||||
|
|
||||
|
/** |
||||
|
* UniApp权限管理控制器 |
||||
|
* Class UniappAuth |
||||
|
* @package app\adminapi\controller\uniapp |
||||
|
*/ |
||||
|
class UniappAuth extends BaseAdminController |
||||
|
{ |
||||
|
/** |
||||
|
* 获取所有UniApp菜单(用于权限配置) |
||||
|
*/ |
||||
|
public function getMenus() |
||||
|
{ |
||||
|
return success((new UniappAuthService())->getAllMenus()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取角色的UniApp菜单权限 |
||||
|
* @param int $role_id |
||||
|
* @return \think\Response |
||||
|
*/ |
||||
|
public function getRoleMenus(int $role_id) |
||||
|
{ |
||||
|
return success((new UniappAuthService())->getRoleMenus($role_id)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置角色的UniApp菜单权限 |
||||
|
* @param int $role_id |
||||
|
* @return \think\Response |
||||
|
*/ |
||||
|
public function setRoleMenus(int $role_id) |
||||
|
{ |
||||
|
$data = $this->request->params([ |
||||
|
['menu_ids', []] |
||||
|
]); |
||||
|
(new UniappAuthService())->setRoleMenus($role_id, $data['menu_ids']); |
||||
|
return success([], '设置成功'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
use think\facade\Route; |
||||
|
|
||||
|
/** |
||||
|
* 系统菜单管理路由 |
||||
|
*/ |
||||
|
Route::group('sys_menu', function () { |
||||
|
|
||||
|
// 菜单列表 |
||||
|
Route::get('lists', 'app\adminapi\controller\sys\SysMenu@lists'); |
||||
|
|
||||
|
// 菜单详情 |
||||
|
Route::get('info/:id', 'app\adminapi\controller\sys\SysMenu@info'); |
||||
|
|
||||
|
// 添加菜单 |
||||
|
Route::post('add', 'app\adminapi\controller\sys\SysMenu@add'); |
||||
|
|
||||
|
// 编辑菜单 |
||||
|
Route::put('edit/:id', 'app\adminapi\controller\sys\SysMenu@edit'); |
||||
|
|
||||
|
// 删除菜单 |
||||
|
Route::delete('del/:id', 'app\adminapi\controller\sys\SysMenu@del'); |
||||
|
|
||||
|
// 修改菜单状态 |
||||
|
Route::put('modify_status/:id', 'app\adminapi\controller\sys\SysMenu@modifyStatus'); |
||||
|
|
||||
|
// 获取所有菜单(用于角色权限配置) |
||||
|
Route::get('all_menus', 'app\adminapi\controller\sys\SysMenu@getAllMenus'); |
||||
|
|
||||
|
// 获取角色菜单权限 |
||||
|
Route::get('role_menus/:role_id', 'app\adminapi\controller\sys\SysMenu@getRoleMenus'); |
||||
|
|
||||
|
// 设置角色菜单权限 |
||||
|
Route::post('set_role_menus/:role_id', 'app\adminapi\controller\sys\SysMenu@setRoleMenus'); |
||||
|
|
||||
|
})->middleware([ |
||||
|
app\adminapi\middleware\AdminCheckToken::class, |
||||
|
app\adminapi\middleware\AdminCheckRole::class, |
||||
|
app\adminapi\middleware\AdminLog::class |
||||
|
]); |
||||
@ -0,0 +1,32 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
use think\facade\Route; |
||||
|
|
||||
|
/** |
||||
|
* UniApp权限管理路由 |
||||
|
*/ |
||||
|
Route::group('uniapp_auth', function () { |
||||
|
|
||||
|
// 获取所有UniApp菜单(用于权限配置) |
||||
|
Route::get('menus', 'app\adminapi\controller\uniapp\UniappAuth@getMenus'); |
||||
|
|
||||
|
// 获取角色的UniApp菜单权限 |
||||
|
Route::get('role_menus/:role_id', 'app\adminapi\controller\uniapp\UniappAuth@getRoleMenus'); |
||||
|
|
||||
|
// 设置角色的UniApp菜单权限 |
||||
|
Route::post('set_role_menus/:role_id', 'app\adminapi\controller\uniapp\UniappAuth@setRoleMenus'); |
||||
|
|
||||
|
})->middleware([ |
||||
|
app\adminapi\middleware\AdminCheckToken::class, |
||||
|
app\adminapi\middleware\AdminCheckRole::class, |
||||
|
app\adminapi\middleware\AdminLog::class |
||||
|
]); |
||||
@ -0,0 +1,272 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\job\transfer\schedule; |
||||
|
|
||||
|
use app\model\course\Course; |
||||
|
use app\model\course_booking\CourseBooking; |
||||
|
use think\facade\Db; |
||||
|
use core\base\BaseJob; |
||||
|
use think\facade\Log; |
||||
|
|
||||
|
/** |
||||
|
* 定时处理课程有效状态任务 |
||||
|
* 检查课程的有效期,更新过期课程状态 |
||||
|
*/ |
||||
|
class CourseValidityJob extends BaseJob |
||||
|
{ |
||||
|
/** |
||||
|
* 执行定时任务 |
||||
|
* @param mixed ...$data 任务参数 |
||||
|
* @return bool 处理结果 |
||||
|
*/ |
||||
|
public function doJob(...$data) |
||||
|
{ |
||||
|
Log::write('开始执行课程有效状态检查任务'); |
||||
|
|
||||
|
try { |
||||
|
// 处理过期课程 |
||||
|
$expiredResult = $this->processExpiredCourses(); |
||||
|
|
||||
|
// 处理孩子的固定课 |
||||
|
$warningResult = $this->processStudentCourses(); |
||||
|
|
||||
|
Log::write('课程有效状态检查任务执行完成-' . print_r([ |
||||
|
'expired_courses' => $expiredResult['count'], |
||||
|
'warning_courses' => $warningResult['count'], |
||||
|
], true)); |
||||
|
|
||||
|
return true; |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
Log::write('课程有效状态检查任务执行失败:' . $e->getMessage()); |
||||
|
throw $e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理课程的状态 |
||||
|
* 根据school_course_schedule表的course_date和time_slot更新课程状态 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
private function processExpiredCourses() |
||||
|
{ |
||||
|
$currentDate = date('Y-m-d'); |
||||
|
$currentTime = date('H:i:s'); |
||||
|
$currentDateTime = date('Y-m-d H:i:s'); |
||||
|
|
||||
|
$count = 0; |
||||
|
|
||||
|
try { |
||||
|
// 1. 更新已结束的课程 - course_date小于当前日期 |
||||
|
$completedCount = Db::name('school_course_schedule') |
||||
|
->where('course_date', '<', $currentDate) |
||||
|
->where('status', '<>', 'completed') |
||||
|
->update([ |
||||
|
'status' => 'completed', |
||||
|
'updated_at' => $currentDateTime |
||||
|
]); |
||||
|
|
||||
|
Log::write("更新已结束课程数量: {$completedCount}"); |
||||
|
$count += $completedCount; |
||||
|
|
||||
|
// 2. 处理今天的课程,根据time_slot判断状态 |
||||
|
$todaySchedules = Db::name('school_course_schedule') |
||||
|
->where('course_date', $currentDate) |
||||
|
->where('status', '<>', 'completed') |
||||
|
->select(); |
||||
|
|
||||
|
foreach ($todaySchedules as $schedule) { |
||||
|
$newStatus = $this->calculateCourseStatus($schedule['time_slot'], $currentTime); |
||||
|
|
||||
|
if ($newStatus && $newStatus != $schedule['status']) { |
||||
|
Db::name('school_course_schedule') |
||||
|
->where('id', $schedule['id']) |
||||
|
->update([ |
||||
|
'status' => $newStatus, |
||||
|
'updated_at' => $currentDateTime |
||||
|
]); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("更新课程状态: ID {$schedule['id']}, 时间段 {$schedule['time_slot']}, 状态 {$schedule['status']} -> {$newStatus}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
Log::write('处理课程状态失败: ' . $e->getMessage()); |
||||
|
throw $e; |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理学员固定课的安排 |
||||
|
* 查看school_person_course_schedule表中schedule_type=2且person_type=student的课程 |
||||
|
* 自动为下周相同时间新增上课记录 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
private function processStudentCourses() |
||||
|
{ |
||||
|
$currentDate = date('Y-m-d'); |
||||
|
$currentDateTime = date('Y-m-d H:i:s'); |
||||
|
$count = 0; |
||||
|
|
||||
|
try { |
||||
|
// 查找学员的固定课程安排 |
||||
|
$studentFixedCourses = Db::name('school_person_course_schedule') |
||||
|
->where('schedule_type', 2) |
||||
|
->where('person_type', 'student') |
||||
|
->where('status', 1) // 假设1为有效状态 |
||||
|
->select(); |
||||
|
|
||||
|
foreach ($studentFixedCourses as $studentCourse) { |
||||
|
// 通过schedule_id获取课程安排信息 |
||||
|
$courseSchedule = Db::name('school_course_schedule') |
||||
|
->where('id', $studentCourse['schedule_id']) |
||||
|
->find(); |
||||
|
|
||||
|
if (!$courseSchedule) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 计算下周同一天的日期 |
||||
|
$courseDate = $courseSchedule['course_date']; |
||||
|
$dayOfWeek = date('w', strtotime($courseDate)); // 0(周日)到6(周六) |
||||
|
$nextWeekDate = $this->getNextWeekSameDay($courseDate); |
||||
|
|
||||
|
// 检查下周那天是否有相同时间段的课程安排 |
||||
|
$nextWeekSchedule = Db::name('school_course_schedule') |
||||
|
->where('course_date', $nextWeekDate) |
||||
|
->where('time_slot', $courseSchedule['time_slot']) |
||||
|
->where('venue_id', $courseSchedule['venue_id']) // 同一场地 |
||||
|
->find(); |
||||
|
|
||||
|
if ($nextWeekSchedule) { |
||||
|
// 检查该学员是否已经有下周的上课记录 |
||||
|
$existingRecord = Db::name('school_person_course_schedule') |
||||
|
->where('person_id', $studentCourse['person_id']) |
||||
|
->where('schedule_id', $nextWeekSchedule['id']) |
||||
|
->where('person_type', 'student') |
||||
|
->count(); |
||||
|
|
||||
|
if ($existingRecord == 0) { |
||||
|
// 为学员新增下周的上课记录 |
||||
|
$newRecord = [ |
||||
|
'person_id' => $studentCourse['person_id'], |
||||
|
'schedule_id' => $nextWeekSchedule['id'], |
||||
|
'person_type' => 'student', |
||||
|
'schedule_type' => 2, // 固定课 |
||||
|
'status' => 1, |
||||
|
'created_at' => $currentDateTime, |
||||
|
'updated_at' => $currentDateTime |
||||
|
]; |
||||
|
|
||||
|
// 复制其他可能的字段 |
||||
|
if (isset($studentCourse['course_id'])) { |
||||
|
$newRecord['course_id'] = $studentCourse['course_id']; |
||||
|
} |
||||
|
if (isset($studentCourse['venue_id'])) { |
||||
|
$newRecord['venue_id'] = $studentCourse['venue_id']; |
||||
|
} |
||||
|
|
||||
|
Db::name('school_person_course_schedule')->insert($newRecord); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("为学员新增下周固定课: 学员ID {$studentCourse['person_id']}, 日期 {$nextWeekDate}, 时间段 {$courseSchedule['time_slot']}"); |
||||
|
} |
||||
|
} else { |
||||
|
Log::write("下周无对应课程安排,跳过: 日期 {$nextWeekDate}, 时间段 {$courseSchedule['time_slot']}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
Log::write('处理学员固定课安排失败: ' . $e->getMessage()); |
||||
|
throw $e; |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据时间段和当前时间计算课程状态 |
||||
|
* @param string $timeSlot 时间段,格式如 "11:30-12:30" |
||||
|
* @param string $currentTime 当前时间,格式如 "14:30:00" |
||||
|
* @return string|null 课程状态:upcoming, ongoing, completed 或 null(无需更新) |
||||
|
*/ |
||||
|
private function calculateCourseStatus($timeSlot, $currentTime) |
||||
|
{ |
||||
|
if (empty($timeSlot)) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 解析时间段,格式如 "11:30-12:30" |
||||
|
$timeParts = explode('-', $timeSlot); |
||||
|
if (count($timeParts) != 2) { |
||||
|
Log::write("时间段格式错误: {$timeSlot}"); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
$startTime = trim($timeParts[0]) . ':00'; // 转换为 "11:30:00" |
||||
|
$endTime = trim($timeParts[1]) . ':00'; // 转换为 "12:30:00" |
||||
|
|
||||
|
$currentTimestamp = strtotime($currentTime); |
||||
|
$startTimestamp = strtotime($startTime); |
||||
|
$endTimestamp = strtotime($endTime); |
||||
|
$sixHoursBeforeStart = $startTimestamp - (6 * 3600); // 开始时间前6小时 |
||||
|
|
||||
|
if ($currentTimestamp <= $sixHoursBeforeStart) { |
||||
|
// 当前时间距离开始时间超过6小时,不需要更新状态 |
||||
|
return null; |
||||
|
} elseif ($currentTimestamp <= $startTimestamp) { |
||||
|
// 当前时间在开始前6小时内,状态为即将开始 |
||||
|
return 'upcoming'; |
||||
|
} elseif ($currentTimestamp >= $startTimestamp && $currentTimestamp <= $endTimestamp) { |
||||
|
// 当前时间在课程时间范围内,状态为进行中 |
||||
|
return 'ongoing'; |
||||
|
} elseif ($currentTimestamp > $endTimestamp) { |
||||
|
// 当前时间已过结束时间,状态为已结束 |
||||
|
return 'completed'; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算下周同一天的日期 |
||||
|
* @param string $courseDate 课程日期,格式如 "2025-01-08" |
||||
|
* @return string 下周同一天的日期 |
||||
|
*/ |
||||
|
private function getNextWeekSameDay($courseDate) |
||||
|
{ |
||||
|
$timestamp = strtotime($courseDate); |
||||
|
$nextWeekTimestamp = $timestamp + (7 * 24 * 3600); // 加7天 |
||||
|
return date('Y-m-d', $nextWeekTimestamp); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送课程即将过期的警告通知 |
||||
|
* @param Course $course 课程对象 |
||||
|
*/ |
||||
|
protected function sendExpiryWarning($course) |
||||
|
{ |
||||
|
// 这里可以实现具体的通知逻辑 |
||||
|
// 比如发送邮件、短信、系统内通知等 |
||||
|
Log::write("发送过期提醒通知:课程 {$course->course_name} 将在 {$course->valid_until} 过期"); |
||||
|
|
||||
|
// 示例:可以调用通知服务 |
||||
|
// NotificationService::send([ |
||||
|
// 'type' => 'course_expiry_warning', |
||||
|
// 'course_id' => $course->id, |
||||
|
// 'message' => "您的课程《{$course->course_name}》将在{$course->valid_until}过期" |
||||
|
// ]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,277 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\job\transfer\schedule; |
||||
|
|
||||
|
use app\model\staff\Staff; |
||||
|
use app\model\staff\StaffAttendance; |
||||
|
use core\base\BaseJob; |
||||
|
use think\facade\Log; |
||||
|
|
||||
|
/** |
||||
|
* 定时处理员工考勤状态任务 |
||||
|
* 自动生成考勤记录、处理迟到早退、计算工作时长等 |
||||
|
*/ |
||||
|
class StaffAttendanceJob extends BaseJob |
||||
|
{ |
||||
|
/** |
||||
|
* 执行定时任务 |
||||
|
* @param mixed ...$data 任务参数 |
||||
|
* @return bool 处理结果 |
||||
|
*/ |
||||
|
public function doJob(...$data) |
||||
|
{ |
||||
|
Log::write('开始执行员工考勤状态处理任务'); |
||||
|
|
||||
|
try { |
||||
|
$currentDate = date('Y-m-d'); |
||||
|
$currentTime = date('H:i:s'); |
||||
|
|
||||
|
// 处理当日考勤记录生成 |
||||
|
$generateResult = $this->generateDailyAttendance($currentDate); |
||||
|
|
||||
|
// 处理迟到状态更新 |
||||
|
$lateResult = $this->processLateArrivals($currentDate, $currentTime); |
||||
|
|
||||
|
// 处理早退状态更新 |
||||
|
$earlyResult = $this->processEarlyDepartures($currentDate, $currentTime); |
||||
|
|
||||
|
// 处理缺勤状态更新 |
||||
|
$absentResult = $this->processAbsences($currentDate); |
||||
|
|
||||
|
// 计算工作时长 |
||||
|
$workHoursResult = $this->calculateWorkHours($currentDate); |
||||
|
|
||||
|
Log::write('员工考勤状态处理任务执行完成', [ |
||||
|
'generated_records' => $generateResult['count'], |
||||
|
'late_arrivals' => $lateResult['count'], |
||||
|
'early_departures' => $earlyResult['count'], |
||||
|
'absences' => $absentResult['count'], |
||||
|
'calculated_hours' => $workHoursResult['count'] |
||||
|
]); |
||||
|
|
||||
|
return true; |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
Log::write('员工考勤状态处理任务执行失败:' . $e->getMessage()); |
||||
|
throw $e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成当日考勤记录 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function generateDailyAttendance($date) |
||||
|
{ |
||||
|
// 获取所有在职员工 |
||||
|
$activeStaff = Staff::where('status', 1)->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($activeStaff as $staff) { |
||||
|
// 检查是否已有当日考勤记录 |
||||
|
$exists = StaffAttendance::where('staff_id', $staff->id) |
||||
|
->where('attendance_date', $date) |
||||
|
->count(); |
||||
|
|
||||
|
if ($exists == 0) { |
||||
|
// 创建考勤记录 |
||||
|
$attendance = new StaffAttendance(); |
||||
|
$attendance->staff_id = $staff->id; |
||||
|
$attendance->attendance_date = $date; |
||||
|
$attendance->status = 'pending'; // 待考勤状态 |
||||
|
$attendance->work_schedule_start = '09:00:00'; // 默认上班时间 |
||||
|
$attendance->work_schedule_end = '18:00:00'; // 默认下班时间 |
||||
|
$attendance->created_at = date('Y-m-d H:i:s'); |
||||
|
$attendance->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("为员工生成考勤记录:员工ID {$staff->id}, 日期: {$date}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理迟到状态 |
||||
|
* @param string $date 日期 |
||||
|
* @param string $currentTime 当前时间 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function processLateArrivals($date, $currentTime) |
||||
|
{ |
||||
|
// 查找今日还未签到但已过上班时间的员工 |
||||
|
$lateThreshold = '09:30:00'; // 迟到阈值:9:30后算迟到 |
||||
|
|
||||
|
$lateAttendances = StaffAttendance::where('attendance_date', $date) |
||||
|
->where('check_in_time', null) |
||||
|
->where('work_schedule_start', '<', $currentTime) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($lateAttendances as $attendance) { |
||||
|
if ($currentTime > $lateThreshold) { |
||||
|
$attendance->status = 'late'; |
||||
|
$attendance->late_minutes = $this->calculateLateMinutes($attendance->work_schedule_start, $currentTime); |
||||
|
$attendance->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("员工迟到:员工ID {$attendance->staff_id}, 迟到 {$attendance->late_minutes} 分钟"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理早退状态 |
||||
|
* @param string $date 日期 |
||||
|
* @param string $currentTime 当前时间 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function processEarlyDepartures($date, $currentTime) |
||||
|
{ |
||||
|
// 查找今日已签到但提前签退的员工 |
||||
|
$earlyAttendances = StaffAttendance::where('attendance_date', $date) |
||||
|
->where('check_in_time', '<>', null) |
||||
|
->where('check_out_time', '<>', null) |
||||
|
->where('check_out_time', '<', 'work_schedule_end') |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($earlyAttendances as $attendance) { |
||||
|
$attendance->status = 'early_departure'; |
||||
|
$attendance->early_minutes = $this->calculateEarlyMinutes($attendance->check_out_time, $attendance->work_schedule_end); |
||||
|
$attendance->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("员工早退:员工ID {$attendance->staff_id}, 早退 {$attendance->early_minutes} 分钟"); |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理缺勤状态 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function processAbsences($date) |
||||
|
{ |
||||
|
$currentTime = date('H:i:s'); |
||||
|
$absentThreshold = '18:30:00'; // 18:30后还未签到算缺勤 |
||||
|
|
||||
|
if ($currentTime < $absentThreshold) { |
||||
|
return ['count' => 0]; // 还没到判断缺勤的时间 |
||||
|
} |
||||
|
|
||||
|
// 查找今日全天未签到的员工 |
||||
|
$absentAttendances = StaffAttendance::where('attendance_date', $date) |
||||
|
->where('check_in_time', null) |
||||
|
->where('status', '!=', 'leave') // 排除请假的员工 |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($absentAttendances as $attendance) { |
||||
|
$attendance->status = 'absent'; |
||||
|
$attendance->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("员工缺勤:员工ID {$attendance->staff_id}, 日期: {$date}"); |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算工作时长 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function calculateWorkHours($date) |
||||
|
{ |
||||
|
// 查找当日已完成签到签退的考勤记录 |
||||
|
$completedAttendances = StaffAttendance::where('attendance_date', $date) |
||||
|
->where('check_in_time', '<>', null) |
||||
|
->where('check_out_time', '<>', null) |
||||
|
->where('work_hours', null) // 还未计算工作时长的 |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($completedAttendances as $attendance) { |
||||
|
$workHours = $this->calculateHoursBetween($attendance->check_in_time, $attendance->check_out_time); |
||||
|
|
||||
|
$attendance->work_hours = $workHours; |
||||
|
$attendance->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("计算员工工作时长:员工ID {$attendance->staff_id}, 工作时长: {$workHours} 小时"); |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算迟到分钟数 |
||||
|
* @param string $scheduleTime 计划时间 |
||||
|
* @param string $actualTime 实际时间 |
||||
|
* @return int 迟到分钟数 |
||||
|
*/ |
||||
|
protected function calculateLateMinutes($scheduleTime, $actualTime) |
||||
|
{ |
||||
|
$schedule = strtotime($scheduleTime); |
||||
|
$actual = strtotime($actualTime); |
||||
|
|
||||
|
if ($actual > $schedule) { |
||||
|
return intval(($actual - $schedule) / 60); |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算早退分钟数 |
||||
|
* @param string $actualTime 实际时间 |
||||
|
* @param string $scheduleTime 计划时间 |
||||
|
* @return int 早退分钟数 |
||||
|
*/ |
||||
|
protected function calculateEarlyMinutes($actualTime, $scheduleTime) |
||||
|
{ |
||||
|
$actual = strtotime($actualTime); |
||||
|
$schedule = strtotime($scheduleTime); |
||||
|
|
||||
|
if ($schedule > $actual) { |
||||
|
return intval(($schedule - $actual) / 60); |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算两个时间之间的小时数 |
||||
|
* @param string $startTime 开始时间 |
||||
|
* @param string $endTime 结束时间 |
||||
|
* @return float 小时数 |
||||
|
*/ |
||||
|
protected function calculateHoursBetween($startTime, $endTime) |
||||
|
{ |
||||
|
$start = strtotime($startTime); |
||||
|
$end = strtotime($endTime); |
||||
|
|
||||
|
if ($end > $start) { |
||||
|
return round(($end - $start) / 3600, 2); |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,372 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\job\transfer\schedule; |
||||
|
|
||||
|
use app\model\member\Member; |
||||
|
use app\model\course\Course; |
||||
|
use app\model\course_booking\CourseBooking; |
||||
|
use app\model\notification\Notification; |
||||
|
use core\base\BaseJob; |
||||
|
use think\facade\Log; |
||||
|
|
||||
|
/** |
||||
|
* 定时检查学员课程有效期和课时数量发送提醒任务 |
||||
|
* 监控学员课程状态,及时发送相关提醒通知 |
||||
|
*/ |
||||
|
class StudentCourseReminderJob extends BaseJob |
||||
|
{ |
||||
|
/** |
||||
|
* 执行定时任务 |
||||
|
* @param mixed ...$data 任务参数 |
||||
|
* @return bool 处理结果 |
||||
|
*/ |
||||
|
public function doJob(...$data) |
||||
|
{ |
||||
|
Log::write('开始执行学员课程提醒任务'); |
||||
|
|
||||
|
try { |
||||
|
// 检查课程有效期提醒 |
||||
|
$expiryResult = $this->checkCourseExpiryReminders(); |
||||
|
|
||||
|
// 检查课时余量提醒 |
||||
|
$hoursResult = $this->checkRemainingHoursReminders(); |
||||
|
|
||||
|
// 检查预约课程提醒 |
||||
|
$bookingResult = $this->checkUpcomingBookingReminders(); |
||||
|
|
||||
|
// 检查长期未上课提醒 |
||||
|
$inactiveResult = $this->checkInactiveStudentReminders(); |
||||
|
|
||||
|
// 检查课程完成祝贺 |
||||
|
$completionResult = $this->checkCourseCompletionCongratulations(); |
||||
|
|
||||
|
Log::write('学员课程提醒任务执行完成', [ |
||||
|
'expiry_reminders' => $expiryResult['count'], |
||||
|
'hours_reminders' => $hoursResult['count'], |
||||
|
'booking_reminders' => $bookingResult['count'], |
||||
|
'inactive_reminders' => $inactiveResult['count'], |
||||
|
'completion_congratulations' => $completionResult['count'] |
||||
|
]); |
||||
|
|
||||
|
return true; |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
Log::write('学员课程提醒任务执行失败:' . $e->getMessage()); |
||||
|
throw $e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查课程有效期提醒 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function checkCourseExpiryReminders() |
||||
|
{ |
||||
|
$currentDate = date('Y-m-d'); |
||||
|
|
||||
|
// 查找7天内即将过期的课程 |
||||
|
$expiringCourses = Course::where('valid_until', '>', $currentDate) |
||||
|
->where('valid_until', '<=', date('Y-m-d', strtotime('+7 days'))) |
||||
|
->where('status', 1) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($expiringCourses as $course) { |
||||
|
// 检查是否已发送过提醒 |
||||
|
$reminderSent = $this->checkReminderSent($course->member_id, 'course_expiry', $course->id); |
||||
|
|
||||
|
if (!$reminderSent) { |
||||
|
$daysLeft = ceil((strtotime($course->valid_until) - strtotime($currentDate)) / 86400); |
||||
|
|
||||
|
$this->sendCourseExpiryReminder($course, $daysLeft); |
||||
|
$this->recordReminderSent($course->member_id, 'course_expiry', $course->id); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("发送课程过期提醒:学员ID {$course->member_id}, 课程ID {$course->id}, 剩余 {$daysLeft} 天"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查课时余量提醒 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function checkRemainingHoursReminders() |
||||
|
{ |
||||
|
// 查找剩余课时少于3节的有效课程 |
||||
|
$lowHoursCourses = Course::where('remaining_hours', '>', 0) |
||||
|
->where('remaining_hours', '<=', 3) |
||||
|
->where('status', 1) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($lowHoursCourses as $course) { |
||||
|
// 检查是否已发送过提醒 |
||||
|
$reminderSent = $this->checkReminderSent($course->member_id, 'low_hours', $course->id); |
||||
|
|
||||
|
if (!$reminderSent) { |
||||
|
$this->sendLowHoursReminder($course); |
||||
|
$this->recordReminderSent($course->member_id, 'low_hours', $course->id); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("发送课时不足提醒:学员ID {$course->member_id}, 课程ID {$course->id}, 剩余 {$course->remaining_hours} 课时"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查预约课程提醒 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function checkUpcomingBookingReminders() |
||||
|
{ |
||||
|
$tomorrow = date('Y-m-d', strtotime('+1 day')); |
||||
|
|
||||
|
// 查找明天的预约课程 |
||||
|
$tomorrowBookings = CourseBooking::where('booking_date', $tomorrow) |
||||
|
->where('status', 1) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($tomorrowBookings as $booking) { |
||||
|
// 检查是否已发送过提醒 |
||||
|
$reminderSent = $this->checkReminderSent($booking->member_id, 'booking_reminder', $booking->id); |
||||
|
|
||||
|
if (!$reminderSent) { |
||||
|
$this->sendBookingReminder($booking); |
||||
|
$this->recordReminderSent($booking->member_id, 'booking_reminder', $booking->id); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("发送预约提醒:学员ID {$booking->member_id}, 预约ID {$booking->id}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查长期未上课提醒 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function checkInactiveStudentReminders() |
||||
|
{ |
||||
|
$thirtyDaysAgo = date('Y-m-d', strtotime('-30 days')); |
||||
|
|
||||
|
// 查找有有效课程但30天内没有上课的学员 |
||||
|
$inactiveMembers = Member::whereHas('courses', function($query) { |
||||
|
$query->where('status', 1)->where('remaining_hours', '>', 0); |
||||
|
})->whereDoesntHave('courseBookings', function($query) use ($thirtyDaysAgo) { |
||||
|
$query->where('booking_date', '>=', $thirtyDaysAgo) |
||||
|
->where('status', 'completed'); |
||||
|
})->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($inactiveMembers as $member) { |
||||
|
// 检查是否已发送过提醒 |
||||
|
$reminderSent = $this->checkReminderSent($member->id, 'inactive_student', $member->id); |
||||
|
|
||||
|
if (!$reminderSent) { |
||||
|
$this->sendInactiveStudentReminder($member); |
||||
|
$this->recordReminderSent($member->id, 'inactive_student', $member->id); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("发送长期未上课提醒:学员ID {$member->id}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查课程完成祝贺 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function checkCourseCompletionCongratulations() |
||||
|
{ |
||||
|
// 查找今日刚完成的课程(剩余课时为0且今日有完成的预约) |
||||
|
$today = date('Y-m-d'); |
||||
|
|
||||
|
$completedCourses = Course::where('remaining_hours', 0) |
||||
|
->where('status', 1) |
||||
|
->whereHas('courseBookings', function($query) use ($today) { |
||||
|
$query->where('booking_date', $today) |
||||
|
->where('status', 'completed'); |
||||
|
})->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($completedCourses as $course) { |
||||
|
// 检查是否已发送过祝贺 |
||||
|
$congratsSent = $this->checkReminderSent($course->member_id, 'course_completion', $course->id); |
||||
|
|
||||
|
if (!$congratsSent) { |
||||
|
$this->sendCourseCompletionCongratulations($course); |
||||
|
$this->recordReminderSent($course->member_id, 'course_completion', $course->id); |
||||
|
|
||||
|
// 更新课程状态为已完成 |
||||
|
$course->status = 3; // 假设3为已完成状态 |
||||
|
$course->completed_at = date('Y-m-d H:i:s'); |
||||
|
$course->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("发送课程完成祝贺:学员ID {$course->member_id}, 课程ID {$course->id}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送课程过期提醒 |
||||
|
* @param Course $course 课程对象 |
||||
|
* @param int $daysLeft 剩余天数 |
||||
|
*/ |
||||
|
protected function sendCourseExpiryReminder($course, $daysLeft) |
||||
|
{ |
||||
|
$member = Member::find($course->member_id); |
||||
|
if (!$member) return; |
||||
|
|
||||
|
$message = "亲爱的{$member->nickname},您的课程《{$course->course_name}》将在{$daysLeft}天后过期,请尽快安排上课时间。"; |
||||
|
|
||||
|
$this->createNotification($member->id, 'course_expiry', '课程即将过期提醒', $message, [ |
||||
|
'course_id' => $course->id, |
||||
|
'days_left' => $daysLeft |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送课时不足提醒 |
||||
|
* @param Course $course 课程对象 |
||||
|
*/ |
||||
|
protected function sendLowHoursReminder($course) |
||||
|
{ |
||||
|
$member = Member::find($course->member_id); |
||||
|
if (!$member) return; |
||||
|
|
||||
|
$message = "亲爱的{$member->nickname},您的课程《{$course->course_name}》还剩{$course->remaining_hours}节课,建议及时续费以确保学习连续性。"; |
||||
|
|
||||
|
$this->createNotification($member->id, 'low_hours', '课时不足提醒', $message, [ |
||||
|
'course_id' => $course->id, |
||||
|
'remaining_hours' => $course->remaining_hours |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送预约课程提醒 |
||||
|
* @param CourseBooking $booking 预约对象 |
||||
|
*/ |
||||
|
protected function sendBookingReminder($booking) |
||||
|
{ |
||||
|
$member = Member::find($booking->member_id); |
||||
|
$course = Course::find($booking->course_id); |
||||
|
if (!$member || !$course) return; |
||||
|
|
||||
|
$message = "亲爱的{$member->nickname},您预约的课程《{$course->course_name}》将在明天{$booking->time_slot}开始,请准时参加。"; |
||||
|
|
||||
|
$this->createNotification($member->id, 'booking_reminder', '课程预约提醒', $message, [ |
||||
|
'booking_id' => $booking->id, |
||||
|
'course_id' => $booking->course_id, |
||||
|
'booking_date' => $booking->booking_date, |
||||
|
'time_slot' => $booking->time_slot |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送长期未上课提醒 |
||||
|
* @param Member $member 学员对象 |
||||
|
*/ |
||||
|
protected function sendInactiveStudentReminder($member) |
||||
|
{ |
||||
|
$message = "亲爱的{$member->nickname},您已经很久没有上课了。坚持学习才能看到更好的效果,快来预约课程吧!"; |
||||
|
|
||||
|
$this->createNotification($member->id, 'inactive_student', '久未上课提醒', $message, [ |
||||
|
'member_id' => $member->id |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送课程完成祝贺 |
||||
|
* @param Course $course 课程对象 |
||||
|
*/ |
||||
|
protected function sendCourseCompletionCongratulations($course) |
||||
|
{ |
||||
|
$member = Member::find($course->member_id); |
||||
|
if (!$member) return; |
||||
|
|
||||
|
$message = "恭喜{$member->nickname}!您已成功完成《{$course->course_name}》课程的全部学习,感谢您的坚持和努力!"; |
||||
|
|
||||
|
$this->createNotification($member->id, 'course_completion', '课程完成祝贺', $message, [ |
||||
|
'course_id' => $course->id, |
||||
|
'completion_date' => date('Y-m-d') |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查是否已发送过提醒 |
||||
|
* @param int $memberId 学员ID |
||||
|
* @param string $type 提醒类型 |
||||
|
* @param int $relatedId 相关ID |
||||
|
* @return bool |
||||
|
*/ |
||||
|
protected function checkReminderSent($memberId, $type, $relatedId) |
||||
|
{ |
||||
|
$today = date('Y-m-d'); |
||||
|
|
||||
|
$exists = Notification::where('member_id', $memberId) |
||||
|
->where('type', $type) |
||||
|
->where('related_id', $relatedId) |
||||
|
->where('created_at', '>=', $today . ' 00:00:00') |
||||
|
->count(); |
||||
|
|
||||
|
return $exists > 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 记录已发送的提醒 |
||||
|
* @param int $memberId 学员ID |
||||
|
* @param string $type 提醒类型 |
||||
|
* @param int $relatedId 相关ID |
||||
|
*/ |
||||
|
protected function recordReminderSent($memberId, $type, $relatedId) |
||||
|
{ |
||||
|
// 这个方法可以用于记录提醒发送日志,避免重复发送 |
||||
|
Log::write("记录提醒发送:学员ID {$memberId}, 类型: {$type}, 相关ID: {$relatedId}"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建通知记录 |
||||
|
* @param int $memberId 学员ID |
||||
|
* @param string $type 通知类型 |
||||
|
* @param string $title 标题 |
||||
|
* @param string $message 消息内容 |
||||
|
* @param array $extra 额外数据 |
||||
|
*/ |
||||
|
protected function createNotification($memberId, $type, $title, $message, $extra = []) |
||||
|
{ |
||||
|
$notification = new Notification(); |
||||
|
$notification->member_id = $memberId; |
||||
|
$notification->type = $type; |
||||
|
$notification->title = $title; |
||||
|
$notification->message = $message; |
||||
|
$notification->extra_data = json_encode($extra); |
||||
|
$notification->status = 'sent'; |
||||
|
$notification->created_at = date('Y-m-d H:i:s'); |
||||
|
$notification->save(); |
||||
|
|
||||
|
// 这里可以扩展实际的通知发送逻辑 |
||||
|
// 比如调用短信服务、邮件服务、推送服务等 |
||||
|
Log::write("创建通知:学员ID {$memberId}, 类型: {$type}, 标题: {$title}"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,310 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\job\transfer\schedule; |
||||
|
|
||||
|
use app\model\staff\Staff; |
||||
|
use app\model\service\TeachingService; |
||||
|
use app\model\course\Course; |
||||
|
use app\model\course_schedule\CourseSchedule; |
||||
|
use core\base\BaseJob; |
||||
|
use think\facade\Log; |
||||
|
|
||||
|
/** |
||||
|
* 定时为教务或教练创建服务内容任务 |
||||
|
* 根据教务/教练的课程安排自动生成服务内容记录 |
||||
|
*/ |
||||
|
class TeachingServiceJob extends BaseJob |
||||
|
{ |
||||
|
/** |
||||
|
* 执行定时任务 |
||||
|
* @param mixed ...$data 任务参数 |
||||
|
* @return bool 处理结果 |
||||
|
*/ |
||||
|
public function doJob(...$data) |
||||
|
{ |
||||
|
Log::write('开始执行教务/教练服务内容生成任务'); |
||||
|
|
||||
|
try { |
||||
|
$currentDate = date('Y-m-d'); |
||||
|
|
||||
|
// 为教务人员创建服务内容 |
||||
|
$academicResult = $this->createAcademicServices($currentDate); |
||||
|
|
||||
|
// 为教练创建服务内容 |
||||
|
$coachResult = $this->createCoachServices($currentDate); |
||||
|
|
||||
|
// 为助教创建服务内容 |
||||
|
$assistantResult = $this->createAssistantServices($currentDate); |
||||
|
|
||||
|
// 更新服务完成状态 |
||||
|
$completionResult = $this->updateServiceCompletion($currentDate); |
||||
|
|
||||
|
Log::write('教务/教练服务内容生成任务执行完成:' . print_r([ |
||||
|
'academic_services' => $academicResult['count'], |
||||
|
'coach_services' => $coachResult['count'], |
||||
|
'assistant_services' => $assistantResult['count'], |
||||
|
'completed_services' => $completionResult['count'] |
||||
|
], true)); |
||||
|
|
||||
|
return true; |
||||
|
|
||||
|
} catch (\Exception $e) { |
||||
|
Log::write('教务/教练服务内容生成任务执行失败:' . $e->getMessage()); |
||||
|
throw $e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 为教务人员创建服务内容 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function createAcademicServices($date) |
||||
|
{ |
||||
|
// 获取所有教务人员 |
||||
|
$academicStaff = Staff::where('role_type', 'academic') |
||||
|
->where('status', 1) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($academicStaff as $staff) { |
||||
|
// 检查今日是否已有服务记录 |
||||
|
$existingService = TeachingService::where('staff_id', $staff->id) |
||||
|
->where('service_date', $date) |
||||
|
->count(); |
||||
|
|
||||
|
if ($existingService == 0) { |
||||
|
// 创建教务服务内容 |
||||
|
$service = new TeachingService(); |
||||
|
$service->staff_id = $staff->id; |
||||
|
$service->staff_name = $staff->real_name; |
||||
|
$service->service_date = $date; |
||||
|
$service->service_type = 'academic'; |
||||
|
$service->service_content = $this->generateAcademicServiceContent($staff, $date); |
||||
|
$service->status = 'pending'; |
||||
|
$service->created_at = date('Y-m-d H:i:s'); |
||||
|
$service->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("为教务人员创建服务内容:员工ID {$staff->id}, 姓名: {$staff->real_name}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 为教练创建服务内容 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function createCoachServices($date) |
||||
|
{ |
||||
|
// 获取所有教练 |
||||
|
$coaches = Staff::where('role_type', 'coach') |
||||
|
->where('status', 1) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($coaches as $coach) { |
||||
|
// 获取教练当日的课程安排 |
||||
|
$todaySchedules = CourseSchedule::where('coach_id', $coach->id) |
||||
|
->where('course_date', $date) |
||||
|
->select(); |
||||
|
|
||||
|
if (count($todaySchedules) > 0) { |
||||
|
// 检查是否已有服务记录 |
||||
|
$existingService = TeachingService::where('staff_id', $coach->id) |
||||
|
->where('service_date', $date) |
||||
|
->count(); |
||||
|
|
||||
|
if ($existingService == 0) { |
||||
|
// 创建教练服务内容 |
||||
|
$service = new TeachingService(); |
||||
|
$service->staff_id = $coach->id; |
||||
|
$service->staff_name = $coach->real_name; |
||||
|
$service->service_date = $date; |
||||
|
$service->service_type = 'coach'; |
||||
|
$service->service_content = $this->generateCoachServiceContent($coach, $todaySchedules); |
||||
|
$service->scheduled_courses = count($todaySchedules); |
||||
|
$service->status = 'scheduled'; |
||||
|
$service->created_at = date('Y-m-d H:i:s'); |
||||
|
$service->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("为教练创建服务内容:员工ID {$coach->id}, 课程数: " . count($todaySchedules)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 为助教创建服务内容 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function createAssistantServices($date) |
||||
|
{ |
||||
|
// 获取所有助教 |
||||
|
$assistants = Staff::where('role_type', 'assistant') |
||||
|
->where('status', 1) |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($assistants as $assistant) { |
||||
|
// 获取助教需要协助的课程 |
||||
|
$assistantSchedules = CourseSchedule::where('assistant_id', $assistant->id) |
||||
|
->where('course_date', $date) |
||||
|
->select(); |
||||
|
|
||||
|
if (count($assistantSchedules) > 0) { |
||||
|
// 检查是否已有服务记录 |
||||
|
$existingService = TeachingService::where('staff_id', $assistant->id) |
||||
|
->where('service_date', $date) |
||||
|
->count(); |
||||
|
|
||||
|
if ($existingService == 0) { |
||||
|
// 创建助教服务内容 |
||||
|
$service = new TeachingService(); |
||||
|
$service->staff_id = $assistant->id; |
||||
|
$service->staff_name = $assistant->real_name; |
||||
|
$service->service_date = $date; |
||||
|
$service->service_type = 'assistant'; |
||||
|
$service->service_content = $this->generateAssistantServiceContent($assistant, $assistantSchedules); |
||||
|
$service->scheduled_courses = count($assistantSchedules); |
||||
|
$service->status = 'scheduled'; |
||||
|
$service->created_at = date('Y-m-d H:i:s'); |
||||
|
$service->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("为助教创建服务内容:员工ID {$assistant->id}, 协助课程数: " . count($assistantSchedules)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新服务完成状态 |
||||
|
* @param string $date 日期 |
||||
|
* @return array 处理结果 |
||||
|
*/ |
||||
|
protected function updateServiceCompletion($date) |
||||
|
{ |
||||
|
$currentTime = date('H:i:s'); |
||||
|
$completionThreshold = '22:00:00'; // 22:00后自动标记为完成 |
||||
|
|
||||
|
if ($currentTime < $completionThreshold) { |
||||
|
return ['count' => 0]; |
||||
|
} |
||||
|
|
||||
|
// 更新当日未完成的服务为已完成状态 |
||||
|
$pendingServices = TeachingService::where('service_date', $date) |
||||
|
->where('status', 'scheduled') |
||||
|
->select(); |
||||
|
|
||||
|
$count = 0; |
||||
|
foreach ($pendingServices as $service) { |
||||
|
$service->status = 'completed'; |
||||
|
$service->completed_at = date('Y-m-d H:i:s'); |
||||
|
$service->save(); |
||||
|
|
||||
|
$count++; |
||||
|
Log::write("自动标记服务完成:员工ID {$service->staff_id}, 服务类型: {$service->service_type}"); |
||||
|
} |
||||
|
|
||||
|
return ['count' => $count]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成教务服务内容 |
||||
|
* @param Staff $staff 员工对象 |
||||
|
* @param string $date 日期 |
||||
|
* @return string 服务内容描述 |
||||
|
*/ |
||||
|
protected function generateAcademicServiceContent($staff, $date) |
||||
|
{ |
||||
|
$content = []; |
||||
|
$content[] = "日期:{$date}"; |
||||
|
$content[] = "教务人员:{$staff->real_name}"; |
||||
|
$content[] = "服务内容:"; |
||||
|
$content[] = "1. 学员课程安排和调度管理"; |
||||
|
$content[] = "2. 教练课表协调和优化"; |
||||
|
$content[] = "3. 学员咨询和问题解答"; |
||||
|
$content[] = "4. 课程质量跟踪和反馈收集"; |
||||
|
$content[] = "5. 学员学习进度监控"; |
||||
|
$content[] = "6. 教学资源配置和管理"; |
||||
|
|
||||
|
return implode("\n", $content); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成教练服务内容 |
||||
|
* @param Staff $coach 教练对象 |
||||
|
* @param array $schedules 课程安排 |
||||
|
* @return string 服务内容描述 |
||||
|
*/ |
||||
|
protected function generateCoachServiceContent($coach, $schedules) |
||||
|
{ |
||||
|
$content = []; |
||||
|
$content[] = "教练:{$coach->real_name}"; |
||||
|
$content[] = "预定课程数:" . count($schedules); |
||||
|
$content[] = "课程安排:"; |
||||
|
|
||||
|
foreach ($schedules as $schedule) { |
||||
|
$course = Course::find($schedule->course_id); |
||||
|
$courseName = $course ? $course->course_name : "未知课程"; |
||||
|
$content[] = "- {$schedule->time_slot}: {$courseName} (场地ID: {$schedule->venue_id})"; |
||||
|
} |
||||
|
|
||||
|
$content[] = "服务职责:"; |
||||
|
$content[] = "1. 按时到达指定场地进行授课"; |
||||
|
$content[] = "2. 确保课程质量和安全性"; |
||||
|
$content[] = "3. 记录学员学习情况和进度"; |
||||
|
$content[] = "4. 提供专业指导和建议"; |
||||
|
$content[] = "5. 维护教学设备和场地整洁"; |
||||
|
|
||||
|
return implode("\n", $content); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成助教服务内容 |
||||
|
* @param Staff $assistant 助教对象 |
||||
|
* @param array $schedules 课程安排 |
||||
|
* @return string 服务内容描述 |
||||
|
*/ |
||||
|
protected function generateAssistantServiceContent($assistant, $schedules) |
||||
|
{ |
||||
|
$content = []; |
||||
|
$content[] = "助教:{$assistant->real_name}"; |
||||
|
$content[] = "协助课程数:" . count($schedules); |
||||
|
$content[] = "协助安排:"; |
||||
|
|
||||
|
foreach ($schedules as $schedule) { |
||||
|
$course = Course::find($schedule->course_id); |
||||
|
$courseName = $course ? $course->course_name : "未知课程"; |
||||
|
$content[] = "- {$schedule->time_slot}: 协助 {$courseName}"; |
||||
|
} |
||||
|
|
||||
|
$content[] = "服务职责:"; |
||||
|
$content[] = "1. 协助主教练进行课程教学"; |
||||
|
$content[] = "2. 维护课堂秩序和安全"; |
||||
|
$content[] = "3. 帮助学员纠正动作和姿势"; |
||||
|
$content[] = "4. 准备和整理教学器材"; |
||||
|
$content[] = "5. 记录学员出勤和表现"; |
||||
|
|
||||
|
return implode("\n", $content); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\service\admin\sys; |
||||
|
|
||||
|
use app\model\sys\SysMenu; |
||||
|
use core\base\BaseAdminService; |
||||
|
use core\exception\AdminException; |
||||
|
use think\facade\Db; |
||||
|
|
||||
|
/** |
||||
|
* 系统菜单服务类 |
||||
|
*/ |
||||
|
class SysMenuService extends BaseAdminService |
||||
|
{ |
||||
|
public function __construct() |
||||
|
{ |
||||
|
parent::__construct(); |
||||
|
$this->model = new SysMenu(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取菜单分页列表 |
||||
|
* @param array $where |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getPage(array $where = []) |
||||
|
{ |
||||
|
$field = 'id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description, created_at, updated_at'; |
||||
|
$order = 'sort_order ASC, id DESC'; |
||||
|
|
||||
|
$search_model = $this->model |
||||
|
->withSearch(['keyword', 'status'], $where) |
||||
|
->field($field) |
||||
|
->order($order); |
||||
|
|
||||
|
$list = $this->pageQuery($search_model, $where); |
||||
|
|
||||
|
return $list; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取菜单信息 |
||||
|
* @param int $id |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getInfo(int $id) |
||||
|
{ |
||||
|
$field = 'id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description, created_at, updated_at'; |
||||
|
|
||||
|
$info = $this->model->field($field)->where([['id', '=', $id]])->findOrEmpty()->toArray(); |
||||
|
if (empty($info)) { |
||||
|
throw new AdminException('菜单不存在'); |
||||
|
} |
||||
|
|
||||
|
// 解析JSON参数 |
||||
|
if (!empty($info['menu_params'])) { |
||||
|
$info['menu_params'] = json_decode($info['menu_params'], true) ?? []; |
||||
|
} else { |
||||
|
$info['menu_params'] = []; |
||||
|
} |
||||
|
|
||||
|
return $info; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加菜单 |
||||
|
* @param array $data |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function add(array $data) |
||||
|
{ |
||||
|
// 处理JSON参数 |
||||
|
if (isset($data['menu_params']) && is_array($data['menu_params'])) { |
||||
|
$data['menu_params'] = json_encode($data['menu_params'], JSON_UNESCAPED_UNICODE); |
||||
|
} |
||||
|
|
||||
|
$res = $this->model->create($data); |
||||
|
return $res->id; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 编辑菜单 |
||||
|
* @param int $id |
||||
|
* @param array $data |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function edit(int $id, array $data) |
||||
|
{ |
||||
|
$info = $this->model->findOrEmpty($id); |
||||
|
if ($info->isEmpty()) { |
||||
|
throw new AdminException('菜单不存在'); |
||||
|
} |
||||
|
|
||||
|
// 处理JSON参数 |
||||
|
if (isset($data['menu_params']) && is_array($data['menu_params'])) { |
||||
|
$data['menu_params'] = json_encode($data['menu_params'], JSON_UNESCAPED_UNICODE); |
||||
|
} |
||||
|
|
||||
|
$this->model->where([['id', '=', $id]])->update($data); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除菜单 |
||||
|
* @param int $id |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function del(int $id) |
||||
|
{ |
||||
|
$model = $this->model->findOrEmpty($id); |
||||
|
if ($model->isEmpty()) { |
||||
|
throw new AdminException('菜单不存在'); |
||||
|
} |
||||
|
|
||||
|
// 检查是否有角色在使用此菜单 |
||||
|
$result = Db::query("SELECT COUNT(*) as count FROM role_menu_permissions WHERE menu_id = ?", [$id]); |
||||
|
$roleMenuCount = $result[0]['count'] ?? 0; |
||||
|
if ($roleMenuCount > 0) { |
||||
|
throw new AdminException('该菜单已被角色使用,无法删除'); |
||||
|
} |
||||
|
|
||||
|
$res = $model->delete(); |
||||
|
return $res !== false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改菜单状态 |
||||
|
* @param int $id |
||||
|
* @param int $status |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function modifyStatus(int $id, int $status) |
||||
|
{ |
||||
|
$this->model->where([['id', '=', $id]])->update(['status' => $status]); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有菜单(用于角色权限配置) |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getAllMenus() |
||||
|
{ |
||||
|
$list = $this->model |
||||
|
->field('id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description') |
||||
|
->order('sort_order ASC, id ASC') |
||||
|
->select() |
||||
|
->toArray(); |
||||
|
|
||||
|
// 处理参数 |
||||
|
foreach ($list as &$item) { |
||||
|
if (!empty($item['menu_params'])) { |
||||
|
$item['menu_params'] = json_decode($item['menu_params'], true) ?? []; |
||||
|
} else { |
||||
|
$item['menu_params'] = []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $list; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取角色菜单权限 |
||||
|
* @param int $roleId |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getRoleMenus(int $roleId) |
||||
|
{ |
||||
|
// 使用原生SQL查询避免表前缀问题 |
||||
|
$sql = "SELECT m.id, m.menu_key, m.menu_name, m.menu_icon, m.menu_path, m.menu_params, m.sort_order |
||||
|
FROM role_menu_permissions rmp |
||||
|
LEFT JOIN sys_menus m ON rmp.menu_id = m.id |
||||
|
WHERE rmp.role_id = ? AND rmp.is_enabled = 1 |
||||
|
ORDER BY m.sort_order ASC"; |
||||
|
$menuList = Db::query($sql, [$roleId]); |
||||
|
|
||||
|
// 处理参数 |
||||
|
foreach ($menuList as &$item) { |
||||
|
if (!empty($item['menu_params'])) { |
||||
|
$item['menu_params'] = json_decode($item['menu_params'], true) ?? []; |
||||
|
} else { |
||||
|
$item['menu_params'] = []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $menuList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置角色菜单权限 |
||||
|
* @param int $roleId |
||||
|
* @param array $menuIds |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function setRoleMenus(int $roleId, array $menuIds) |
||||
|
{ |
||||
|
Db::startTrans(); |
||||
|
try { |
||||
|
// 删除原有权限 |
||||
|
Db::execute("DELETE FROM role_menu_permissions WHERE role_id = ?", [$roleId]); |
||||
|
|
||||
|
// 添加新权限 |
||||
|
if (!empty($menuIds)) { |
||||
|
$values = []; |
||||
|
$params = []; |
||||
|
foreach ($menuIds as $menuId) { |
||||
|
$values[] = "(?, ?, 1, NOW(), NOW())"; |
||||
|
$params[] = $roleId; |
||||
|
$params[] = $menuId; |
||||
|
} |
||||
|
$sql = "INSERT INTO role_menu_permissions (role_id, menu_id, is_enabled, created_at, updated_at) VALUES " . implode(', ', $values); |
||||
|
Db::execute($sql, $params); |
||||
|
} |
||||
|
|
||||
|
Db::commit(); |
||||
|
return true; |
||||
|
} catch (\Exception $e) { |
||||
|
Db::rollback(); |
||||
|
throw new AdminException('设置菜单权限失败:' . $e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
<?php |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Niucloud-admin 企业快速开发的多应用管理平台 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | 官方网址:https://www.niucloud.com |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | niucloud团队 版权所有 开源版本可自由商用 |
||||
|
// +---------------------------------------------------------------------- |
||||
|
// | Author: Niucloud Team |
||||
|
// +---------------------------------------------------------------------- |
||||
|
|
||||
|
namespace app\service\admin\uniapp; |
||||
|
|
||||
|
use core\base\BaseAdminService; |
||||
|
use core\exception\AdminException; |
||||
|
use think\facade\Db; |
||||
|
|
||||
|
/** |
||||
|
* UniApp权限管理服务类 |
||||
|
*/ |
||||
|
class UniappAuthService extends BaseAdminService |
||||
|
{ |
||||
|
/** |
||||
|
* 获取所有UniApp菜单(用于权限配置) |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getAllMenus() |
||||
|
{ |
||||
|
$sql = "SELECT id, menu_key, menu_name, menu_icon, menu_path, menu_params, sort_order, status, description |
||||
|
FROM sys_menus |
||||
|
WHERE status = 1 |
||||
|
ORDER BY sort_order ASC, id ASC"; |
||||
|
$list = Db::query($sql); |
||||
|
|
||||
|
// 处理参数 |
||||
|
foreach ($list as &$item) { |
||||
|
if (!empty($item['menu_params'])) { |
||||
|
$item['menu_params'] = json_decode($item['menu_params'], true) ?? []; |
||||
|
} else { |
||||
|
$item['menu_params'] = []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $list; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取角色的UniApp菜单权限 |
||||
|
* @param int $roleId |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getRoleMenus(int $roleId) |
||||
|
{ |
||||
|
$sql = "SELECT m.id, m.menu_key, m.menu_name, m.menu_icon, m.menu_path, m.menu_params, m.sort_order |
||||
|
FROM role_menu_permissions rmp |
||||
|
LEFT JOIN sys_menus m ON rmp.menu_id = m.id |
||||
|
WHERE rmp.role_id = ? AND rmp.is_enabled = 1 |
||||
|
ORDER BY m.sort_order ASC"; |
||||
|
$menuList = Db::query($sql, [$roleId]); |
||||
|
|
||||
|
// 处理参数 |
||||
|
foreach ($menuList as &$item) { |
||||
|
if (!empty($item['menu_params'])) { |
||||
|
$item['menu_params'] = json_decode($item['menu_params'], true) ?? []; |
||||
|
} else { |
||||
|
$item['menu_params'] = []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $menuList; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置角色的UniApp菜单权限 |
||||
|
* @param int $roleId |
||||
|
* @param array $menuIds |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function setRoleMenus(int $roleId, array $menuIds) |
||||
|
{ |
||||
|
Db::startTrans(); |
||||
|
try { |
||||
|
// 删除原有权限 |
||||
|
Db::execute("DELETE FROM role_menu_permissions WHERE role_id = ?", [$roleId]); |
||||
|
|
||||
|
// 添加新权限 |
||||
|
if (!empty($menuIds)) { |
||||
|
$values = []; |
||||
|
$params = []; |
||||
|
foreach ($menuIds as $menuId) { |
||||
|
$values[] = "(?, ?, 1, NOW(), NOW())"; |
||||
|
$params[] = $roleId; |
||||
|
$params[] = $menuId; |
||||
|
} |
||||
|
$sql = "INSERT INTO role_menu_permissions (role_id, menu_id, is_enabled, created_at, updated_at) VALUES " . implode(', ', $values); |
||||
|
Db::execute($sql, $params); |
||||
|
} |
||||
|
|
||||
|
Db::commit(); |
||||
|
return true; |
||||
|
} catch (\Exception $e) { |
||||
|
Db::rollback(); |
||||
|
throw new AdminException('设置UniApp菜单权限失败:' . $e->getMessage()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,69 +0,0 @@ |
|||||
<!DOCTYPE html> |
|
||||
<html> |
|
||||
<head> |
|
||||
<title>测试编辑客户页面</title> |
|
||||
<meta charset="utf-8"> |
|
||||
</head> |
|
||||
<body> |
|
||||
<h1>测试编辑客户页面字段回显</h1> |
|
||||
|
|
||||
<h2>问题描述</h2> |
|
||||
<p>在 pages/market/clue/edit_clues 页面中,电话六要素的购买力字段和备注字段没有正确回显。</p> |
|
||||
|
|
||||
<h2>问题原因</h2> |
|
||||
<ol> |
|
||||
<li><strong>购买力字段名不一致</strong>: |
|
||||
<ul> |
|
||||
<li>前端使用:<code>purchasing_power_name</code></li> |
|
||||
<li>后端返回:<code>purchase_power_name</code></li> |
|
||||
<li>数据库字段:<code>purchase_power</code></li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
<li><strong>备注字段名不一致</strong>: |
|
||||
<ul> |
|
||||
<li>前端使用:<code>remark</code></li> |
|
||||
<li>数据库字段:<code>consultation_remark</code></li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
</ol> |
|
||||
|
|
||||
<h2>修复内容</h2> |
|
||||
<ol> |
|
||||
<li><strong>修复购买力字段</strong>: |
|
||||
<ul> |
|
||||
<li>第875行:<code>purchasing_power: sixSpeed.purchase_power</code></li> |
|
||||
<li>第945行:<code>sixSpeed.purchase_power_name</code></li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
<li><strong>修复备注字段</strong>: |
|
||||
<ul> |
|
||||
<li>第886行:<code>remark: sixSpeed.consultation_remark</code></li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
</ol> |
|
||||
|
|
||||
<h2>测试数据</h2> |
|
||||
<p>已在数据库中为 resource_id=38 的记录设置测试数据:</p> |
|
||||
<ul> |
|
||||
<li>购买力:2(对应"中等")</li> |
|
||||
<li>备注:测试备注信息</li> |
|
||||
</ul> |
|
||||
|
|
||||
<h2>验证步骤</h2> |
|
||||
<ol> |
|
||||
<li>打开页面:<code>pages/market/clue/edit_clues?resource_sharing_id=38</code></li> |
|
||||
<li>检查购买力选择器是否显示"中等"</li> |
|
||||
<li>检查备注输入框是否显示"测试备注信息"</li> |
|
||||
</ol> |
|
||||
|
|
||||
<h2>修改的文件</h2> |
|
||||
<ul> |
|
||||
<li><code>uniapp/pages/market/clue/edit_clues.vue</code> - 修复字段名不一致问题</li> |
|
||||
</ul> |
|
||||
|
|
||||
<script> |
|
||||
console.log('测试页面加载完成'); |
|
||||
console.log('请在 UniApp 中测试编辑客户页面的字段回显功能'); |
|
||||
</script> |
|
||||
</body> |
|
||||
</html> |
|
||||
@ -1,268 +0,0 @@ |
|||||
# 学员端订单接口问题修复报告 |
|
||||
|
|
||||
## 🔍 **问题描述** |
|
||||
|
|
||||
用户反馈学员端订单页面 `pages-student/orders/index` 不能调用 `api/xy/orderTable?student_id=31&page=1&limit=10` 这个接口。 |
|
||||
|
|
||||
## 🔧 **问题分析** |
|
||||
|
|
||||
### **1. 原始问题** |
|
||||
通过 Playwright 测试发现以下问题: |
|
||||
|
|
||||
#### **API调用失败** |
|
||||
``` |
|
||||
[LOG] 调用get方法: {url: /xy/orderTable, data: Object} |
|
||||
[LOG] 响应拦截器处理: {statusCode: 200, data: Object} |
|
||||
[LOG] 业务状态码: 401 |
|
||||
[ERROR] 401错误 - 未授权 |
|
||||
[ERROR] 获取订单列表失败: Error: 请登录 |
|
||||
``` |
|
||||
|
|
||||
#### **代码错误** |
|
||||
``` |
|
||||
TypeError: _this.calculateOrderStats is not a function |
|
||||
``` |
|
||||
|
|
||||
### **2. 根本原因** |
|
||||
1. **权限问题**:`/xy/orderTable` 接口需要登录验证,返回401未授权错误 |
|
||||
2. **方法缺失**:页面调用了不存在的 `calculateOrderStats()` 方法 |
|
||||
3. **接口设计问题**:学员端使用的是需要管理员权限的接口 |
|
||||
|
|
||||
## ✅ **修复方案** |
|
||||
|
|
||||
### **1. 修复代码错误** |
|
||||
```javascript |
|
||||
// 修复前(错误) |
|
||||
async initPage() { |
|
||||
await this.loadStudentInfo() |
|
||||
await this.loadOrders() |
|
||||
this.calculateOrderStats() // ❌ 方法不存在 |
|
||||
} |
|
||||
|
|
||||
// 修复后(正确) |
|
||||
async initPage() { |
|
||||
await this.loadStudentInfo() |
|
||||
await this.loadOrders() |
|
||||
this.updateOrderDisplay() // ✅ 使用正确的方法 |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### **2. 完善统计功能** |
|
||||
```javascript |
|
||||
updateOrderDisplay() { |
|
||||
// 更新过滤列表 |
|
||||
if (this.activeStatus === 'all') { |
|
||||
this.filteredOrders = [...this.ordersList] |
|
||||
} else { |
|
||||
this.filteredOrders = this.ordersList.filter(order => order.status === this.activeStatus) |
|
||||
} |
|
||||
|
|
||||
// 更新标签页统计 |
|
||||
const counts = {} |
|
||||
this.ordersList.forEach(order => { |
|
||||
counts[order.status] = (counts[order.status] || 0) + 1 |
|
||||
}) |
|
||||
|
|
||||
this.statusTabs.forEach(tab => { |
|
||||
tab.count = tab.value === 'all' ? this.ordersList.length : (counts[tab.value] || 0) |
|
||||
}) |
|
||||
|
|
||||
// ✅ 新增:更新订单统计信息 |
|
||||
this.orderStats = { |
|
||||
total_orders: this.ordersList.length, |
|
||||
pending_payment: counts['pending_payment'] || 0, |
|
||||
paid: counts['paid'] || 0, |
|
||||
completed: counts['completed'] || 0, |
|
||||
cancelled: counts['cancelled'] || 0, |
|
||||
refunded: counts['refunded'] || 0 |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### **3. 创建学员端专用接口** |
|
||||
|
|
||||
#### **在 apiRoute.js 中添加新接口** |
|
||||
```javascript |
|
||||
//学生端-订单管理-列表(公开接口,用于学员端查看) |
|
||||
async xy_getStudentOrders(data = {}) { |
|
||||
return await http.get('/xy/student/orders', data); |
|
||||
}, |
|
||||
//学生端-订单管理-详情(公开接口,用于学员端查看) |
|
||||
async xy_getStudentOrderDetail(data = {}) { |
|
||||
return await http.get('/xy/student/orders/detail', data); |
|
||||
}, |
|
||||
``` |
|
||||
|
|
||||
#### **更新页面调用** |
|
||||
```javascript |
|
||||
// 修复前(权限问题) |
|
||||
const response = await apiRoute.xy_orderTableList({ |
|
||||
student_id: this.studentId, |
|
||||
page: this.currentPage, |
|
||||
limit: 10 |
|
||||
}) |
|
||||
|
|
||||
// 修复后(使用学员端接口) |
|
||||
const response = await apiRoute.xy_getStudentOrders({ |
|
||||
student_id: this.studentId, |
|
||||
page: this.currentPage, |
|
||||
limit: 10 |
|
||||
}) |
|
||||
``` |
|
||||
|
|
||||
## 🧪 **测试验证** |
|
||||
|
|
||||
### **测试环境** |
|
||||
- **测试页面**:http://localhost:8080/#/pages-student/orders/index?student_id=31 |
|
||||
- **测试工具**:Playwright 自动化测试 |
|
||||
|
|
||||
### **测试结果** |
|
||||
|
|
||||
#### **修复前** |
|
||||
``` |
|
||||
❌ API调用失败:401未授权错误 |
|
||||
❌ 代码错误:calculateOrderStats is not a function |
|
||||
❌ 页面显示:获取订单列表失败 |
|
||||
❌ 用户体验:功能不可用 |
|
||||
``` |
|
||||
|
|
||||
#### **修复后** |
|
||||
``` |
|
||||
✅ 代码错误已修复:不再有 calculateOrderStats 错误 |
|
||||
✅ 新接口调用成功:/xy/student/orders 接口被正确调用 |
|
||||
✅ 网络请求正常:HTTP 200 状态码 |
|
||||
⚠️ 后端路由待实现:需要后端实现新的接口路由 |
|
||||
``` |
|
||||
|
|
||||
### **网络请求日志** |
|
||||
``` |
|
||||
[LOG] 调用学员端订单接口,参数: {student_id: 31, page: 1, limit: 10} |
|
||||
[LOG] 调用get方法: {url: /xy/student/orders, data: Object} |
|
||||
[GET] http://localhost:20080/api/xy/student/orders?student_id=31&page=1&limit=10 => [200] OK |
|
||||
[LOG] 业务状态码: 0 (路由未定义) |
|
||||
``` |
|
||||
|
|
||||
## 📋 **后端实现建议** |
|
||||
|
|
||||
### **需要实现的接口** |
|
||||
|
|
||||
#### **1. 学员订单列表接口** |
|
||||
``` |
|
||||
GET /api/xy/student/orders |
|
||||
参数: |
|
||||
- student_id: 学员ID |
|
||||
- page: 页码 |
|
||||
- limit: 每页数量 |
|
||||
|
|
||||
返回格式: |
|
||||
{ |
|
||||
"code": 1, |
|
||||
"msg": "获取成功", |
|
||||
"data": { |
|
||||
"data": [ |
|
||||
{ |
|
||||
"id": 1, |
|
||||
"order_no": "ORD20250731001", |
|
||||
"course_name": "体能训练课程", |
|
||||
"total_amount": "299.00", |
|
||||
"status": "paid", |
|
||||
"create_time": "2025-07-31 10:00:00", |
|
||||
"payment_method": "wxpay" |
|
||||
} |
|
||||
], |
|
||||
"current_page": 1, |
|
||||
"last_page": 1, |
|
||||
"total": 1 |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
#### **2. 学员订单详情接口** |
|
||||
``` |
|
||||
GET /api/xy/student/orders/detail |
|
||||
参数: |
|
||||
- id: 订单ID |
|
||||
|
|
||||
返回格式: |
|
||||
{ |
|
||||
"code": 1, |
|
||||
"msg": "获取成功", |
|
||||
"data": { |
|
||||
"id": 1, |
|
||||
"order_no": "ORD20250731001", |
|
||||
"course_name": "体能训练课程", |
|
||||
"course_specs": "10节课", |
|
||||
"quantity": 1, |
|
||||
"total_amount": "299.00", |
|
||||
"status": "paid", |
|
||||
"create_time": "2025-07-31 10:00:00", |
|
||||
"payment_method": "wxpay", |
|
||||
"payment_time": "2025-07-31 10:05:00" |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### **权限设计** |
|
||||
- 这些接口应该允许学员查看自己的订单 |
|
||||
- 可以通过 `student_id` 参数限制只能查看自己的订单 |
|
||||
- 不需要管理员权限,但需要验证学员身份 |
|
||||
|
|
||||
### **安全考虑** |
|
||||
- 验证 `student_id` 参数的有效性 |
|
||||
- 确保学员只能查看自己的订单 |
|
||||
- 添加适当的数据脱敏(如隐藏敏感支付信息) |
|
||||
|
|
||||
## 🎯 **修复状态总结** |
|
||||
|
|
||||
### **✅ 已完成** |
|
||||
1. **代码错误修复**:`calculateOrderStats` 方法调用错误已修复 |
|
||||
2. **统计功能完善**:`orderStats` 数据正确更新 |
|
||||
3. **前端接口调用**:已切换到新的学员端接口 |
|
||||
4. **错误处理优化**:添加了详细的调试日志 |
|
||||
|
|
||||
### **⚠️ 待完成** |
|
||||
1. **后端接口实现**:需要实现 `/xy/student/orders` 和 `/xy/student/orders/detail` 接口 |
|
||||
2. **权限验证**:后端需要实现适当的学员身份验证 |
|
||||
3. **数据格式对接**:确保后端返回的数据格式与前端期望一致 |
|
||||
|
|
||||
### **🔄 下一步行动** |
|
||||
1. **后端开发**:实现新的学员端订单接口 |
|
||||
2. **接口测试**:验证接口的功能和性能 |
|
||||
3. **数据联调**:确保前后端数据格式一致 |
|
||||
4. **权限测试**:验证学员只能查看自己的订单 |
|
||||
|
|
||||
## 💡 **技术亮点** |
|
||||
|
|
||||
### **1. 接口分离设计** |
|
||||
- 将学员端和管理端的订单接口分离 |
|
||||
- 学员端接口更简单,权限要求更低 |
|
||||
- 便于后续的权限管理和功能扩展 |
|
||||
|
|
||||
### **2. 错误处理优化** |
|
||||
- 添加了详细的调试日志 |
|
||||
- 改善了错误提示的用户体验 |
|
||||
- 便于问题排查和调试 |
|
||||
|
|
||||
### **3. 代码健壮性提升** |
|
||||
- 修复了方法调用错误 |
|
||||
- 完善了数据统计功能 |
|
||||
- 提高了代码的可维护性 |
|
||||
|
|
||||
## 🎉 **总结** |
|
||||
|
|
||||
通过系统性的分析和修复,成功解决了学员端订单页面的接口调用问题: |
|
||||
|
|
||||
1. **✅ 问题定位准确**:识别出权限验证和代码错误问题 |
|
||||
2. **✅ 修复方案合理**:创建专用的学员端接口 |
|
||||
3. **✅ 代码质量提升**:修复了多个代码错误 |
|
||||
4. **✅ 用户体验改善**:优化了错误处理和调试信息 |
|
||||
5. **⚠️ 后端配合需要**:需要后端实现新的接口路由 |
|
||||
|
|
||||
**前端修复已完成,等待后端实现对应的接口即可完全解决问题!** |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
**修复完成时间**:2025-07-31 |
|
||||
**状态**:✅ 前端修复完成,⚠️ 待后端实现接口 |
|
||||
**新增接口**:`/xy/student/orders` 和 `/xy/student/orders/detail` |
|
||||
**下一步**:后端实现学员端订单接口 |
|
||||
Loading…
Reference in new issue