From e926cd5c8960757e89ef7bbe364a0268c8a18f33 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Wed, 6 Aug 2025 10:08:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/views/auth/components/edit-role.vue | 256 +++++++++--- .../components/campus-person-role-edit.vue | 17 +- .../app/adminapi/controller/sys/SysMenu.php | 172 ++++++++ .../adminapi/controller/uniapp/UniappAuth.php | 55 +++ niucloud/app/adminapi/route/sys_menu.php | 50 +++ niucloud/app/adminapi/route/uniapp_auth.php | 32 ++ .../transfer/schedule/CourseValidityJob.php | 272 +++++++++++++ .../transfer/schedule/StaffAttendanceJob.php | 277 +++++++++++++ .../schedule/StudentCourseReminderJob.php | 372 ++++++++++++++++++ .../transfer/schedule/TeachingServiceJob.php | 310 +++++++++++++++ niucloud/app/model/sys/SysMenu.php | 14 +- .../CampusPersonRoleService.php | 26 +- .../app/service/admin/sys/SysMenuService.php | 231 +++++++++++ .../admin/uniapp/UniappAuthService.php | 106 +++++ .../api/apiService/ResourceSharingService.php | 31 +- .../service/api/login/UnifiedLoginService.php | 89 ++++- .../pages-market/clue/class_arrangement.vue | 2 +- uniapp/pages-market/clue/index.vue | 4 +- uniapp/pages/common/home/index.vue | 69 +--- uniapp/test-edit-clues.html | 69 ---- ...学员端订单接口问题修复报告.md | 268 ------------- 21 files changed, 2253 insertions(+), 469 deletions(-) create mode 100644 niucloud/app/adminapi/controller/sys/SysMenu.php create mode 100644 niucloud/app/adminapi/controller/uniapp/UniappAuth.php create mode 100644 niucloud/app/adminapi/route/sys_menu.php create mode 100644 niucloud/app/adminapi/route/uniapp_auth.php create mode 100644 niucloud/app/job/transfer/schedule/CourseValidityJob.php create mode 100644 niucloud/app/job/transfer/schedule/StaffAttendanceJob.php create mode 100644 niucloud/app/job/transfer/schedule/StudentCourseReminderJob.php create mode 100644 niucloud/app/job/transfer/schedule/TeachingServiceJob.php create mode 100644 niucloud/app/service/admin/sys/SysMenuService.php create mode 100644 niucloud/app/service/admin/uniapp/UniappAuthService.php delete mode 100644 uniapp/test-edit-clues.html delete mode 100644 uniapp/学员端订单接口问题修复报告.md diff --git a/admin/src/app/views/auth/components/edit-role.vue b/admin/src/app/views/auth/components/edit-role.vue index 1e4ff3b6..f43e6e80 100644 --- a/admin/src/app/views/auth/components/edit-role.vue +++ b/admin/src/app/views/auth/components/edit-role.vue @@ -62,29 +62,61 @@ -
-
- - -
- {{ - t('foldText') - }} - -
- - - + + + +
+
+ + +
+ {{ + t('foldText') + }} + +
+ + + +
+ + + +
+
+ + +
+ {{ + t('foldText') + }} + +
+ + + +
+
@@ -121,19 +153,64 @@ let popTitle: string = '' // 获取权限数据 const menus = ref[]>([]) +const mobileMenus = ref[]>([]) + +// 获取PC端菜单 getAuthMenus({is_button: 0}).then((res) => { menus.value = res.data }) -// 全选 -const selectAll = ref(false) -const checkStrictly = ref(false) -const treeRef: Record | null = ref(null) -watch(selectAll, () => { - if (selectAll.value) { - treeRef.value.setCheckedNodes(toRaw(menus.value)) +// 获取移动端菜单(UniApp菜单权限) +const getMobileMenus = async () => { + try { + // 使用专门的UniApp权限接口 + const response = await fetch('http://localhost:20080/adminapi/uniapp_auth/menus', { + headers: { + 'Content-Type': 'application/json', + 'token': localStorage.getItem('token') || '' + } + }) + const result = await response.json() + if (result.code === 1) { + mobileMenus.value = result.data.map(item => ({ + menu_key: item.menu_key, + menu_name: item.menu_name, + id: item.id, + children: [] + })) + } + } catch (error) { + console.error('获取移动端菜单失败:', error) + // 如果获取失败,设置一个空数组 + mobileMenus.value = [] + } +} +getMobileMenus() + +// 当前活动的权限标签页 +const activePermissionTab = ref('pc') + +// PC端权限相关 +const selectAllPC = ref(false) +const checkStrictlyPC = ref(false) +const treeRefPC: Record | null = ref(null) +watch(selectAllPC, () => { + if (selectAllPC.value) { + treeRefPC.value.setCheckedNodes(toRaw(menus.value)) + } else { + treeRefPC.value.setCheckedNodes([]) + } +}) + +// 移动端权限相关 +const selectAllMobile = ref(false) +const checkStrictlyMobile = ref(false) +const treeRefMobile: Record | null = ref(null) +watch(selectAllMobile, () => { + if (selectAllMobile.value) { + treeRefMobile.value.setCheckedNodes(toRaw(mobileMenus.value)) } else { - treeRef.value.setCheckedNodes([]) + treeRefMobile.value.setCheckedNodes([]) } }) @@ -151,34 +228,67 @@ const getRolekeyDictList = async () => { } getRolekeyDictList() -const handleCheckChange = debounce((e) => { - formData.rules = treeRef.value.getCheckedKeys() +// PC端权限处理函数 +const handleCheckChangePC = debounce((e) => { + formData.rules = treeRefPC.value.getCheckedKeys() }) -const menuAction = () => { +const menuActionPC = () => { if (isOpen.value) { - collapseAll(menus.value) + collapseAllPC(menus.value) isOpen.value = false } else { - unFoldAll(menus.value) + unFoldAllPC(menus.value) isOpen.value = true } } -// 全部展开 -const unFoldAll = (data: any) => { +// 移动端权限处理函数 +const handleCheckChangeMobile = debounce((e) => { + formData.mobile_rules = treeRefMobile.value.getCheckedKeys() +}) + +const menuActionMobile = () => { + if (isOpen.value) { + collapseAllMobile(mobileMenus.value) + isOpen.value = false + } else { + unFoldAllMobile(mobileMenus.value) + isOpen.value = true + } +} + +// PC端全部展开 +const unFoldAllPC = (data: any) => { Object.keys(data).forEach((key: string | any) => { - treeRef.value.store.nodesMap[data[key].menu_key].expanded = true + treeRefPC.value.store.nodesMap[data[key].menu_key].expanded = true if (data[key].children && data[key].children.length > 0) - collapseAll(data[key].children) + unFoldAllPC(data[key].children) }) } -// 全部折叠 -const collapseAll = (data: any) => { +// PC端全部折叠 +const collapseAllPC = (data: any) => { Object.keys(data).forEach((key: string | any) => { - treeRef.value.store.nodesMap[data[key].menu_key].expanded = false + treeRefPC.value.store.nodesMap[data[key].menu_key].expanded = false if (data[key].children && data[key].children.length > 0) - collapseAll(data[key].children) + collapseAllPC(data[key].children) + }) +} + +// 移动端全部展开 +const unFoldAllMobile = (data: any) => { + Object.keys(data).forEach((key: string | any) => { + treeRefMobile.value.store.nodesMap[data[key].menu_key].expanded = true + if (data[key].children && data[key].children.length > 0) + unFoldAllMobile(data[key].children) + }) +} +// 移动端全部折叠 +const collapseAllMobile = (data: any) => { + Object.keys(data).forEach((key: string | any) => { + treeRefMobile.value.store.nodesMap[data[key].menu_key].expanded = false + if (data[key].children && data[key].children.length > 0) + collapseAllMobile(data[key].children) }) } /** @@ -191,6 +301,7 @@ const initialFormData = { role_key: '', dept_id:'', rules: [], + mobile_rules: [], // 新增移动端权限字段 } const formData: Record = reactive({...initialFormData}) @@ -229,7 +340,36 @@ const confirm = async (formEl: FormInstance | undefined) => { loading.value = true const data = Object.assign({}, formData) - data.rules = data.rules.concat(treeRef.value.getHalfCheckedKeys()) + + // PC端权限处理 + data.rules = data.rules.concat(treeRefPC.value.getHalfCheckedKeys()) + + // 移动端权限处理 + if (treeRefMobile.value) { + data.mobile_rules = data.mobile_rules.concat(treeRefMobile.value.getHalfCheckedKeys()) + + // 调用UniApp权限设置API + if (data.role_id && data.mobile_rules.length >= 0) { + try { + const mobileResponse = await fetch(`http://localhost:20080/adminapi/uniapp_auth/set_role_menus/${data.role_id}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'token': localStorage.getItem('token') || '' + }, + body: JSON.stringify({ + menu_ids: data.mobile_rules + }) + }) + const mobileResult = await mobileResponse.json() + if (mobileResult.code !== 1) { + console.error('移动端权限设置失败:', mobileResult.msg) + } + } catch (error) { + console.error('设置移动端权限失败:', error) + } + } + } save(data) .then((res) => { @@ -248,12 +388,31 @@ const confirm = async (formEl: FormInstance | undefined) => { const setFormData = async (row: any = null) => { loading.value = true - selectAll.value = false + selectAllPC.value = false + selectAllMobile.value = false Object.assign(formData, initialFormData) popTitle = t('addRole') if (row) { popTitle = t('updateRole') const data = await (await getRoleInfo(row.role_id)).data + + // 获取移动端权限数据 + let mobileRulesData = [] + try { + const mobileResponse = await fetch(`http://localhost:20080/adminapi/uniapp_auth/role_menus/${row.role_id}`, { + headers: { + 'Content-Type': 'application/json', + 'token': localStorage.getItem('token') || '' + } + }) + const mobileResult = await mobileResponse.json() + if (mobileResult.code === 1) { + mobileRulesData = mobileResult.data.map(item => item.menu_key) + } + } catch (error) { + console.error('获取移动端权限失败:', error) + } + Object.keys(formData).forEach((key: string) => { if (data[key] != undefined) { if (key == 'rules') { @@ -264,11 +423,16 @@ const setFormData = async (row: any = null) => { checked(data.rules[i], menus.value, newArr) }) formData[key] = newArr + } else if (key == 'mobile_rules') { + formData[key] = mobileRulesData } else { formData[key] = data[key] } } }) + + // 设置移动端权限数据 + formData.mobile_rules = mobileRulesData } loading.value = false } diff --git a/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue b/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue index 2879691c..93617a07 100644 --- a/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue +++ b/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue @@ -234,22 +234,31 @@ const setFormData = async (row: any = null) => { person_id.value = ''; console.log(person_id.value) Object.assign(formData, initialFormData) - if (row.dept_id) { + if (row && row.dept_id) { formData.dept_id = parseInt(row.dept_id) } - if (row.person_id) { + if (row && row.person_id) { formData.person_id = parseInt(row.person_id) person_id.value = parseInt(row.person_id) } loading.value = true - if (row.id) { + if (row && row.id) { const data = await (await getCampusPersonRoleInfo(row.id)).data if (data) Object.keys(formData).forEach((key: string) => { - if (data[key] != undefined) formData[key] = data[key] + if (data[key] != undefined) { + // 校区字段如果是0,设置为空字符串以显示"请选择" + if (key === 'campus_id' && data[key] === 0) { + formData[key] = '' + } else { + formData[key] = data[key] + } + } }) } + // 确保角色列表根据部门重新加载 + await setRoleIdList() loading.value = false } diff --git a/niucloud/app/adminapi/controller/sys/SysMenu.php b/niucloud/app/adminapi/controller/sys/SysMenu.php new file mode 100644 index 00000000..e5480763 --- /dev/null +++ b/niucloud/app/adminapi/controller/sys/SysMenu.php @@ -0,0 +1,172 @@ +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([], '设置成功'); + } +} \ No newline at end of file diff --git a/niucloud/app/adminapi/controller/uniapp/UniappAuth.php b/niucloud/app/adminapi/controller/uniapp/UniappAuth.php new file mode 100644 index 00000000..b2c14a98 --- /dev/null +++ b/niucloud/app/adminapi/controller/uniapp/UniappAuth.php @@ -0,0 +1,55 @@ +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([], '设置成功'); + } +} \ No newline at end of file diff --git a/niucloud/app/adminapi/route/sys_menu.php b/niucloud/app/adminapi/route/sys_menu.php new file mode 100644 index 00000000..264d8d55 --- /dev/null +++ b/niucloud/app/adminapi/route/sys_menu.php @@ -0,0 +1,50 @@ +middleware([ + app\adminapi\middleware\AdminCheckToken::class, + app\adminapi\middleware\AdminCheckRole::class, + app\adminapi\middleware\AdminLog::class +]); \ No newline at end of file diff --git a/niucloud/app/adminapi/route/uniapp_auth.php b/niucloud/app/adminapi/route/uniapp_auth.php new file mode 100644 index 00000000..113794d0 --- /dev/null +++ b/niucloud/app/adminapi/route/uniapp_auth.php @@ -0,0 +1,32 @@ +middleware([ + app\adminapi\middleware\AdminCheckToken::class, + app\adminapi\middleware\AdminCheckRole::class, + app\adminapi\middleware\AdminLog::class +]); \ No newline at end of file diff --git a/niucloud/app/job/transfer/schedule/CourseValidityJob.php b/niucloud/app/job/transfer/schedule/CourseValidityJob.php new file mode 100644 index 00000000..e5159654 --- /dev/null +++ b/niucloud/app/job/transfer/schedule/CourseValidityJob.php @@ -0,0 +1,272 @@ +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}过期" + // ]); + } +} \ No newline at end of file diff --git a/niucloud/app/job/transfer/schedule/StaffAttendanceJob.php b/niucloud/app/job/transfer/schedule/StaffAttendanceJob.php new file mode 100644 index 00000000..c6d08924 --- /dev/null +++ b/niucloud/app/job/transfer/schedule/StaffAttendanceJob.php @@ -0,0 +1,277 @@ +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; + } +} \ No newline at end of file diff --git a/niucloud/app/job/transfer/schedule/StudentCourseReminderJob.php b/niucloud/app/job/transfer/schedule/StudentCourseReminderJob.php new file mode 100644 index 00000000..8985c996 --- /dev/null +++ b/niucloud/app/job/transfer/schedule/StudentCourseReminderJob.php @@ -0,0 +1,372 @@ +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}"); + } +} \ No newline at end of file diff --git a/niucloud/app/job/transfer/schedule/TeachingServiceJob.php b/niucloud/app/job/transfer/schedule/TeachingServiceJob.php new file mode 100644 index 00000000..572196c9 --- /dev/null +++ b/niucloud/app/job/transfer/schedule/TeachingServiceJob.php @@ -0,0 +1,310 @@ +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); + } +} \ No newline at end of file diff --git a/niucloud/app/model/sys/SysMenu.php b/niucloud/app/model/sys/SysMenu.php index 372b830f..147b9177 100644 --- a/niucloud/app/model/sys/SysMenu.php +++ b/niucloud/app/model/sys/SysMenu.php @@ -23,7 +23,7 @@ use think\model\concern\SoftDelete; */ class SysMenu extends BaseModel { - use SoftDelete; + // use SoftDelete; // 暂时禁用软删除 /** * 数据表主键 @@ -35,7 +35,13 @@ class SysMenu extends BaseModel * 模型名称 * @var string */ - protected $name = 'sys_menu'; + protected $name = 'sys_menus'; + + /** + * 表名(不使用前缀) + * @var string + */ + protected $table = 'sys_menus'; /** * 追加字段 * @var array @@ -46,12 +52,12 @@ class SysMenu extends BaseModel * 定义软删除标记字段 * @var string */ - protected $deleteTime = 'delete_time'; + // protected $deleteTime = 'delete_time'; /** * 定义软删除字段的默认值 * @var int */ - protected $defaultSoftDelete = 0; + // protected $defaultSoftDelete = 0; /** * 菜单类型 diff --git a/niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php b/niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php index c02226dd..d8ada696 100644 --- a/niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php +++ b/niucloud/app/service/admin/campus_person_role/CampusPersonRoleService.php @@ -100,8 +100,8 @@ class CampusPersonRoleService extends BaseAdminService if($this->model->where(['person_id' => $data['person_id']])->find()){ return fail("重复操作"); } - $role = new SysRole(); - $data['dept_id'] = $role->where(['role_id' => $data['role_id']])->value("dept_id"); + $db = app()->db; + $data['dept_id'] = $db->table('school_sys_role')->where('role_id', $data['role_id'])->value('dept_id'); $res = $this->model->create($data); return success("操作成功"); @@ -125,8 +125,8 @@ class CampusPersonRoleService extends BaseAdminService $personnel_summary->where(['id' => $tasks['id']])->update(['task_num' => $tasks['task_num']]); } - $role = new SysRole(); - $data['dept_id'] = $role->where(['role_id' => $data['role_id']])->value("dept_id"); + $db = app()->db; + $data['dept_id'] = $db->table('school_sys_role')->where('role_id', $data['role_id'])->value('dept_id'); $this->model->where([['id', '=', $id]])->update($data); return success("操作成功"); @@ -159,8 +159,22 @@ class CampusPersonRoleService extends BaseAdminService } public function getSysRoleAll($data){ - $sysRoleModel = new SysRole(); - return $sysRoleModel->where(['dept_id' => $data['dept_id']])->select()->toArray(); + // 直接查询school_sys_role表,过滤掉超级管理员(role_key为空或role_name为超级管理员) + $db = app()->db; + $query = $db->table('school_sys_role') + ->where('status', 1) + ->where(function ($query) { + $query->whereNotNull('role_key') + ->where('role_key', '<>', '') + ->where('role_name', '<>', '超级管理员'); + }); + + // 如果dept_id不为0,则按部门过滤;如果为0则显示所有可用角色 + if (isset($data['dept_id']) && $data['dept_id'] > 0) { + $query->where('dept_id', $data['dept_id']); + } + + return $query->select()->toArray(); } public function getDepartmentsAll(){ diff --git a/niucloud/app/service/admin/sys/SysMenuService.php b/niucloud/app/service/admin/sys/SysMenuService.php new file mode 100644 index 00000000..f103af56 --- /dev/null +++ b/niucloud/app/service/admin/sys/SysMenuService.php @@ -0,0 +1,231 @@ +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()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/service/admin/uniapp/UniappAuthService.php b/niucloud/app/service/admin/uniapp/UniappAuthService.php new file mode 100644 index 00000000..85ae5191 --- /dev/null +++ b/niucloud/app/service/admin/uniapp/UniappAuthService.php @@ -0,0 +1,106 @@ +getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/ResourceSharingService.php b/niucloud/app/service/api/apiService/ResourceSharingService.php index 1ef17d58..bfb0b0f4 100644 --- a/niucloud/app/service/api/apiService/ResourceSharingService.php +++ b/niucloud/app/service/api/apiService/ResourceSharingService.php @@ -357,6 +357,18 @@ class ResourceSharingService extends BaseApiService // 查询开单状态 $order_status = []; if (!empty($resource_ids)) { + // 首先查询是否有付费订单 + $order_model = new OrderTable(); + $paid_orders = $order_model + ->whereIn('resource_id', $resource_ids) + ->where('order_status', 'paid') + ->field('resource_id') + ->select() + ->toArray(); + + $paid_resource_ids = array_column($paid_orders, 'resource_id'); + + // 查询六速表的开单状态 $six_speed_model = new \app\model\six_speed\SixSpeed(); $six_speed_records = $six_speed_model ->whereIn('resource_id', $resource_ids) @@ -364,8 +376,23 @@ class ResourceSharingService extends BaseApiService ->select() ->toArray(); - foreach ($six_speed_records as $record) { - $order_status[$record['resource_id']] = $record['is_closed'] ? '已开单' : '未开单'; + // 设置每个资源的订单状态 + foreach ($resource_ids as $resource_id) { + if (in_array($resource_id, $paid_resource_ids)) { + $order_status[$resource_id] = '已报名'; + } else { + // 查找对应的六速记录 + $six_speed_record = array_filter($six_speed_records, function($record) use ($resource_id) { + return $record['resource_id'] == $resource_id; + }); + + if (!empty($six_speed_record)) { + $six_speed_record = array_shift($six_speed_record); + $order_status[$resource_id] = $six_speed_record['is_closed'] ? '已开单' : '未开单'; + } else { + $order_status[$resource_id] = '未报名'; + } + } } } diff --git a/niucloud/app/service/api/login/UnifiedLoginService.php b/niucloud/app/service/api/login/UnifiedLoginService.php index 701efe09..f1fd725b 100644 --- a/niucloud/app/service/api/login/UnifiedLoginService.php +++ b/niucloud/app/service/api/login/UnifiedLoginService.php @@ -18,6 +18,7 @@ use app\model\site\Site; use app\model\customer_resources\CustomerResources; use app\service\core\menu\CoreMenuService; use core\util\TokenAuth; +use think\facade\Db; use core\base\BaseService; use core\exception\CommonException; use think\facade\Cache; @@ -437,46 +438,96 @@ class UnifiedLoginService extends BaseService } /** - * 获取员工菜单列表 + * 获取员工菜单列表(动态查询数据库) * @param int $roleType * @return array */ private function getStaffMenuList(int $roleType) { - // 根据角色类型返回对应的菜单权限 + try { + // 查询角色对应的菜单权限 + $menuList = Db::table('sys_menus') + ->alias('m') + ->leftJoin('role_menu_permissions rmp', 'm.id = rmp.menu_id') + ->where('rmp.role_id', $roleType) + ->where('rmp.is_enabled', 1) + ->where('m.status', 1) + ->field('m.menu_key, m.menu_name, m.menu_icon, m.menu_path, m.menu_params, m.sort_order') + ->order('m.sort_order ASC') + ->select() + ->toArray(); + + // 转换为前端需要的格式 + $result = []; + foreach ($menuList as $menu) { + $menuItem = [ + 'key' => $menu['menu_key'], + 'title' => $menu['menu_name'], + 'icon' => $menu['menu_icon'], + 'path' => $menu['menu_path'], + ]; + + // 如果有参数,解析JSON参数 + if (!empty($menu['menu_params'])) { + $params = json_decode($menu['menu_params'], true); + if ($params) { + $menuItem['params'] = $params; + } + } + + $result[] = $menuItem; + } + + return $result; + + } catch (\Exception $e) { + // 如果数据库查询失败,返回默认菜单(兼容处理) + return $this->getDefaultStaffMenuList($roleType); + } + } + + /** + * 获取默认员工菜单列表(兼容处理) + * @param int $roleType + * @return array + */ + private function getDefaultStaffMenuList(int $roleType) + { + // 根据角色类型返回对应的默认菜单权限 switch ($roleType) { case self::STAFF_ROLE_MARKET: return [ - ['path' => '/pages/market/home/index', 'name' => '首页', 'icon' => 'home'], - ['path' => '/pages/market/clue/index', 'name' => '线索管理', 'icon' => 'clue'], - ['path' => '/pages/market/clue/add_clues', 'name' => '添加客户', 'icon' => 'add'], - ['path' => '/pages/market/data/statistics', 'name' => '数据统计', 'icon' => 'data'], - ['path' => '/pages/market/my/index', 'name' => '个人中心', 'icon' => 'user'], + ['key' => 'customer_resource', 'title' => '客户资源', 'icon' => 'person-filled', 'path' => '/pages-market/clue/index'], + ['key' => 'add_customer', 'title' => '添加资源', 'icon' => 'plus-filled', 'path' => '/pages-market/clue/add_clues'], + ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']], + ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'], + ['key' => 'reimbursement', 'title' => '报销管理', 'icon' => 'wallet-filled', 'path' => '/pages-market/reimbursement/list'], ]; case self::STAFF_ROLE_COACH: case self::STAFF_ROLE_TEACHER: return [ - ['path' => '/pages/coach/home/index', 'name' => '首页', 'icon' => 'home'], - ['path' => '/pages/coach/course/list', 'name' => '课表管理', 'icon' => 'course'], - ['path' => '/pages/coach/student/student_list', 'name' => '我的学员', 'icon' => 'student'], - ['path' => '/pages/coach/job/list', 'name' => '作业管理', 'icon' => 'job'], - ['path' => '/pages/coach/my/index', 'name' => '个人中心', 'icon' => 'user'], + ['key' => 'course_query', 'title' => '课程查询', 'icon' => 'search', 'path' => '/pages-coach/coach/schedule/schedule_table'], + ['key' => 'student_management', 'title' => '学员管理', 'icon' => 'contact-filled', 'path' => '/pages-coach/coach/student/student_list'], + ['key' => 'course_arrangement', 'title' => '课程安排', 'icon' => 'calendar-filled', 'path' => '/pages-market/clue/class_arrangement'], + ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']], + ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'], + ['key' => 'resource_library', 'title' => '资料库', 'icon' => 'folder-add-filled', 'path' => '/pages-coach/coach/my/teaching_management'], ]; case self::STAFF_ROLE_SALES: return [ - ['path' => '/pages/market/index/index', 'name' => '首页', 'icon' => 'home'], - ['path' => '/pages/market/clue/index', 'name' => '线索管理', 'icon' => 'clue'], - ['path' => '/pages/market/clue/add_clues', 'name' => '添加客户', 'icon' => 'add'], - ['path' => '/pages/market/clue/clue_table', 'name' => '数据统计', 'icon' => 'data'], - ['path' => '/pages/market/my/index', 'name' => '个人中心', 'icon' => 'user'], + ['key' => 'customer_resource', 'title' => '客户资源', 'icon' => 'person-filled', 'path' => '/pages-market/clue/index'], + ['key' => 'add_customer', 'title' => '添加资源', 'icon' => 'plus-filled', 'path' => '/pages-market/clue/add_clues'], + ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']], + ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'], + ['key' => 'reimbursement', 'title' => '报销管理', 'icon' => 'wallet-filled', 'path' => '/pages-market/reimbursement/list'], ]; default: return [ - ['path' => '/pages/coach/home/index', 'name' => '首页', 'icon' => 'home'], - ['path' => '/pages/coach/my/index', 'name' => '个人中心', 'icon' => 'user'], + ['key' => 'my_data', 'title' => '我的数据', 'icon' => 'bars', 'path' => '/pages/common/dashboard/webview', 'params' => ['type' => 'my_data']], + ['key' => 'my_message', 'title' => '我的消息', 'icon' => 'chat-filled', 'path' => '/pages-common/my_message'], ]; } } diff --git a/uniapp/pages-market/clue/class_arrangement.vue b/uniapp/pages-market/clue/class_arrangement.vue index 871dcc6f..c8d17829 100644 --- a/uniapp/pages-market/clue/class_arrangement.vue +++ b/uniapp/pages-market/clue/class_arrangement.vue @@ -91,7 +91,7 @@ {{ getStatusText(course.status) }} - 时间:{{ course.course_date || '未设置' }} + 时间:{{ course.course_date || '未设置' }} {{ course.time_slot || '' }} 校区:{{ course.campus_name || '未设置' }} 教室:{{ course.venue ? course.venue.venue_name : '未设置' }} 课程:{{ course.course ? course.course.course_name : '未设置' }} diff --git a/uniapp/pages-market/clue/index.vue b/uniapp/pages-market/clue/index.vue index 3914829f..ebe5d2f8 100644 --- a/uniapp/pages-market/clue/index.vue +++ b/uniapp/pages-market/clue/index.vue @@ -45,8 +45,8 @@ {{ v.customerResource.order_status || '未报名' }} - - + + 一访{{ v.customerResource.first_visit_status || '未到' }} diff --git a/uniapp/pages/common/home/index.vue b/uniapp/pages/common/home/index.vue index f15ad2de..dd2d0332 100644 --- a/uniapp/pages/common/home/index.vue +++ b/uniapp/pages/common/home/index.vue @@ -45,70 +45,29 @@ data() { return { userInfo: {}, - gridItems: [ - { - title: '客户资源', - icon: 'person-filled', - path: '/pages-market/clue/index' - }, - { - title: '添加资源', - icon: 'plus-filled', - path: '/pages-market/clue/add_clues' - }, - { - title: '课程安排', - icon: 'calendar-filled', - path: '/pages-market/clue/class_arrangement' - }, - { - title: '课程查询', - icon: 'search', - path: '/pages-coach/coach/schedule/schedule_table' - }, - { - title: '学员管理', - icon: 'contact-filled', - path: '/pages-coach/coach/student/student_list' - }, + gridItems: [], + defaultGridItems: [ { title: '我的数据', icon: 'bars', path: '/pages/common/dashboard/webview', params: { type: 'my_data' } }, - { - title: '部门数据', - icon: 'staff', - path: '/pages/common/dashboard/webview', - params: { type: 'dept_data' } - }, - { - title: '校区数据', - icon: 'location-filled', - path: '/pages/common/dashboard/webview', - params: { type: 'campus_data' } - }, { title: '我的消息', icon: 'chat-filled', path: '/pages-common/my_message' - }, - { - title: '报销管理', - icon: 'wallet-filled', - path: '/pages-market/reimbursement/list' - }, - { - title: '资料库', - icon: 'folder-add-filled', - path: '/pages-coach/coach/my/teaching_management' } ] } }, onLoad() { this.loadUserInfo(); + this.loadMenuList(); + }, + onShow() { + // 每次显示页面时重新加载,确保菜单数据最新 + this.loadMenuList(); }, methods: { loadUserInfo() { @@ -118,6 +77,20 @@ this.userInfo = userInfo; } }, + loadMenuList() { + // 从本地存储获取菜单权限数据 + const menuList = uni.getStorageSync('menuList'); + console.log('从本地存储获取的菜单列表:', menuList); + + if (menuList && Array.isArray(menuList) && menuList.length > 0) { + // 使用登录时返回的动态菜单 + this.gridItems = menuList; + } else { + // 使用默认菜单作为兜底 + this.gridItems = this.defaultGridItems; + console.log('使用默认菜单'); + } + }, handleGridClick(item) { console.log('点击功能按钮:', item.title, item.path); diff --git a/uniapp/test-edit-clues.html b/uniapp/test-edit-clues.html deleted file mode 100644 index 8cb338f7..00000000 --- a/uniapp/test-edit-clues.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - 测试编辑客户页面 - - - -

测试编辑客户页面字段回显

- -

问题描述

-

在 pages/market/clue/edit_clues 页面中,电话六要素的购买力字段和备注字段没有正确回显。

- -

问题原因

-
    -
  1. 购买力字段名不一致: -
      -
    • 前端使用:purchasing_power_name
    • -
    • 后端返回:purchase_power_name
    • -
    • 数据库字段:purchase_power
    • -
    -
  2. -
  3. 备注字段名不一致: -
      -
    • 前端使用:remark
    • -
    • 数据库字段:consultation_remark
    • -
    -
  4. -
- -

修复内容

-
    -
  1. 修复购买力字段: -
      -
    • 第875行:purchasing_power: sixSpeed.purchase_power
    • -
    • 第945行:sixSpeed.purchase_power_name
    • -
    -
  2. -
  3. 修复备注字段: -
      -
    • 第886行:remark: sixSpeed.consultation_remark
    • -
    -
  4. -
- -

测试数据

-

已在数据库中为 resource_id=38 的记录设置测试数据:

-
    -
  • 购买力:2(对应"中等")
  • -
  • 备注:测试备注信息
  • -
- -

验证步骤

-
    -
  1. 打开页面:pages/market/clue/edit_clues?resource_sharing_id=38
  2. -
  3. 检查购买力选择器是否显示"中等"
  4. -
  5. 检查备注输入框是否显示"测试备注信息"
  6. -
- -

修改的文件

-
    -
  • uniapp/pages/market/clue/edit_clues.vue - 修复字段名不一致问题
  • -
- - - - diff --git a/uniapp/学员端订单接口问题修复报告.md b/uniapp/学员端订单接口问题修复报告.md deleted file mode 100644 index 45367f85..00000000 --- a/uniapp/学员端订单接口问题修复报告.md +++ /dev/null @@ -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` -**下一步**:后端实现学员端订单接口