From 329828ac8596759c59443de37a2e718b1b003241 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Fri, 1 Aug 2025 22:47:56 +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 --- admin/src/app/views/contract/contract.vue | 1404 ++++++++++++++--- .../student/KnowledgeController.php | 233 +++ .../controller/student/MessageController.php | 208 +++ niucloud/app/api/route/student.php | 72 +- .../service/api/student/KnowledgeService.php | 450 ++++++ .../service/api/student/MessageService.php | 417 +++++ uniapp/api/apiRoute.js | 616 ++++++++ uniapp/pages-student/knowledge/index.vue | 389 +++-- uniapp/pages-student/messages/index.vue | 297 ++-- 学员端消息管理数据库分析报告.md | 322 ++++ 学员端知识库模块详细开发任务.md | 574 +++++++ 11 files changed, 4478 insertions(+), 504 deletions(-) create mode 100644 niucloud/app/api/controller/student/KnowledgeController.php create mode 100644 niucloud/app/api/controller/student/MessageController.php create mode 100644 niucloud/app/service/api/student/KnowledgeService.php create mode 100644 niucloud/app/service/api/student/MessageService.php create mode 100644 学员端消息管理数据库分析报告.md create mode 100644 学员端知识库模块详细开发任务.md diff --git a/admin/src/app/views/contract/contract.vue b/admin/src/app/views/contract/contract.vue index 97ca5f5e..547738b2 100644 --- a/admin/src/app/views/contract/contract.vue +++ b/admin/src/app/views/contract/contract.vue @@ -1,236 +1,1168 @@ - - - - - + + + + + diff --git a/niucloud/app/api/controller/student/KnowledgeController.php b/niucloud/app/api/controller/student/KnowledgeController.php new file mode 100644 index 00000000..53ebb294 --- /dev/null +++ b/niucloud/app/api/controller/student/KnowledgeController.php @@ -0,0 +1,233 @@ +request->params([ + ['category', ''], + ['page', 1], + ['limit', 10], + ['keyword', ''] + ]); + $data['student_id'] = $student_id; + + $this->validate($data, [ + 'student_id' => 'require|integer|gt:0', + 'page' => 'integer|egt:1', + 'limit' => 'integer|between:1,50' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->getKnowledgeList($data); + + return success($result, '获取知识文章列表成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取知识分类列表 + * @return Response + */ + public function getKnowledgeCategories() + { + try { + $service = new KnowledgeService(); + $result = $service->getKnowledgeCategories(); + + return success($result, '获取知识分类成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取推荐文章 + * @param int $student_id 学员ID + * @return Response + */ + public function getRecommendArticles($student_id) + { + $data = $this->request->params([ + ['limit', 5] + ]); + $data['student_id'] = $student_id; + + $this->validate($data, [ + 'student_id' => 'require|integer|gt:0', + 'limit' => 'integer|between:1,20' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->getRecommendArticles($data); + + return success($result, '获取推荐文章成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取文章详情 + * @param int $id 文章ID + * @return Response + */ + public function getKnowledgeDetail($id) + { + $data = $this->request->params([ + ['student_id', 0] + ]); + $data['id'] = $id; + + $this->validate($data, [ + 'id' => 'require|integer|gt:0', + 'student_id' => 'require|integer|gt:0' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->getKnowledgeDetail($data); + + return success($result, '获取文章详情成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 标记文章已读 + * @return Response + */ + public function markArticleRead() + { + $data = $this->request->params([ + ['article_id', 0], + ['student_id', 0] + ]); + + $this->validate($data, [ + 'article_id' => 'require|integer|gt:0', + 'student_id' => 'require|integer|gt:0' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->markArticleRead($data); + + return success($result, '标记已读成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 收藏/取消收藏文章 + * @return Response + */ + public function toggleArticleFavorite() + { + $data = $this->request->params([ + ['article_id', 0], + ['student_id', 0], + ['action', 'add'] // add-收藏, remove-取消收藏 + ]); + + $this->validate($data, [ + 'article_id' => 'require|integer|gt:0', + 'student_id' => 'require|integer|gt:0', + 'action' => 'require|in:add,remove' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->toggleArticleFavorite($data); + + return success($result, $data['action'] === 'add' ? '收藏成功' : '取消收藏成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取知识库统计信息 + * @param int $student_id 学员ID + * @return Response + */ + public function getKnowledgeStats($student_id) + { + $this->validate(['student_id' => $student_id], [ + 'student_id' => 'require|integer|gt:0' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->getKnowledgeStats($student_id); + + return success($result, '获取知识库统计成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 搜索知识文章 + * @param int $student_id 学员ID + * @return Response + */ + public function searchKnowledgeArticles($student_id) + { + $data = $this->request->params([ + ['keyword', ''], + ['category', ''], + ['page', 1], + ['limit', 10] + ]); + $data['student_id'] = $student_id; + + $this->validate($data, [ + 'student_id' => 'require|integer|gt:0', + 'keyword' => 'require|length:1,50', + 'page' => 'integer|egt:1', + 'limit' => 'integer|between:1,50' + ]); + + try { + $service = new KnowledgeService(); + $result = $service->searchKnowledgeArticles($data); + + return success($result, '搜索知识文章成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/controller/student/MessageController.php b/niucloud/app/api/controller/student/MessageController.php new file mode 100644 index 00000000..2a6ef426 --- /dev/null +++ b/niucloud/app/api/controller/student/MessageController.php @@ -0,0 +1,208 @@ +messageService = new MessageService(); + } + + /** + * 获取学员消息列表 + * @param int $student_id 学员ID + * @return array + */ + public function getMessageList($student_id = 0) + { + try { + // 获取请求参数 + $data = [ + 'message_type' => input('message_type', ''), + 'page' => input('page', 1), + 'limit' => input('limit', 10), + 'keyword' => input('keyword', ''), + 'is_read' => input('is_read', '') + ]; + + $data['student_id'] = (int)$student_id; + + // 参数验证 + if (empty($data['student_id'])) { + return fail('学员ID不能为空'); + } + + // 获取消息列表 + $result = $this->messageService->getMessageList($data); + + return success($result); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取消息详情 + * @param int $message_id 消息ID + * @return array + */ + public function getMessageDetail($message_id = 0) + { + try { + // 获取请求参数 + $data = [ + 'student_id' => input('student_id', 0) + ]; + + $data['message_id'] = (int)$message_id; + + // 参数验证 + if (empty($data['message_id']) || empty($data['student_id'])) { + return fail('参数错误'); + } + + // 获取消息详情 + $result = $this->messageService->getMessageDetail($data); + + return success($result); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 标记消息已读 + * @return array + */ + public function markMessageRead() + { + try { + // 获取请求参数 + $data = [ + 'message_id' => input('message_id', 0), + 'student_id' => input('student_id', 0) + ]; + + // 参数验证 + if (empty($data['message_id']) || empty($data['student_id'])) { + return fail('参数错误'); + } + + // 标记已读 + $result = $this->messageService->markMessageRead($data); + + return success($result, '操作成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 批量标记消息已读 + * @return array + */ + public function markBatchRead() + { + try { + // 获取请求参数 + $data = [ + 'message_ids' => input('message_ids', []), + 'student_id' => input('student_id', 0), + 'message_type' => input('message_type', '') + ]; + + // 参数验证 + if (empty($data['student_id'])) { + return fail('学员ID不能为空'); + } + + if (empty($data['message_ids']) && empty($data['message_type'])) { + return fail('请指定要标记的消息'); + } + + // 批量标记已读 + $result = $this->messageService->markBatchRead($data); + + return success($result, '操作成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取学员消息统计 + * @param int $student_id 学员ID + * @return array + */ + public function getMessageStats($student_id = 0) + { + try { + if (empty($student_id)) { + return fail('学员ID不能为空'); + } + + // 获取消息统计 + $result = $this->messageService->getMessageStats((int)$student_id); + + return success($result); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 搜索消息 + * @param int $student_id 学员ID + * @return array + */ + public function searchMessages($student_id = 0) + { + try { + // 获取请求参数 + $data = [ + 'keyword' => input('keyword', ''), + 'message_type' => input('message_type', ''), + 'page' => input('page', 1), + 'limit' => input('limit', 10) + ]; + + $data['student_id'] = (int)$student_id; + + // 参数验证 + if (empty($data['student_id'])) { + return fail('学员ID不能为空'); + } + + if (empty($data['keyword'])) { + return fail('搜索关键词不能为空'); + } + + // 搜索消息 + $result = $this->messageService->searchMessages($data); + + return success($result); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/route/student.php b/niucloud/app/api/route/student.php index edf1e542..72523e8c 100644 --- a/niucloud/app/api/route/student.php +++ b/niucloud/app/api/route/student.php @@ -101,26 +101,76 @@ Route::group('contract', function () { Route::get('student-info', 'app\api\controller\student\ContractController@getStudentInfo'); }); +// 知识库(测试版本,无需token) +Route::group('knowledge-test', function () { + // 获取知识文章列表 + Route::get('list/:student_id', 'app\api\controller\student\KnowledgeController@getKnowledgeList'); + // 获取知识分类列表 + Route::get('categories', 'app\api\controller\student\KnowledgeController@getKnowledgeCategories'); + // 获取推荐文章 + Route::get('recommend/:student_id', 'app\api\controller\student\KnowledgeController@getRecommendArticles'); + // 获取文章详情 + Route::get('detail/:id', 'app\api\controller\student\KnowledgeController@getKnowledgeDetail'); + // 标记文章已读 + Route::post('mark-read', 'app\api\controller\student\KnowledgeController@markArticleRead'); + // 收藏/取消收藏文章 + Route::post('toggle-favorite', 'app\api\controller\student\KnowledgeController@toggleArticleFavorite'); + // 获取知识库统计 + Route::get('stats/:student_id', 'app\api\controller\student\KnowledgeController@getKnowledgeStats'); + // 搜索知识文章 + Route::get('search/:student_id', 'app\api\controller\student\KnowledgeController@searchKnowledgeArticles'); +}); + // 知识库 Route::group('knowledge', function () { - // 获取知识内容列表 - Route::get('list', 'student.KnowledgeController@getKnowledgeList'); - // 获取内容详情 - Route::get('detail/:knowledge_id', 'student.KnowledgeController@getKnowledgeDetail'); - // 获取分类列表 - Route::get('categories', 'student.KnowledgeController@getKnowledgeCategories'); + // 获取知识文章列表 + Route::get('list/:student_id', 'app\api\controller\student\KnowledgeController@getKnowledgeList'); + // 获取知识分类列表 + Route::get('categories', 'app\api\controller\student\KnowledgeController@getKnowledgeCategories'); + // 获取推荐文章 + Route::get('recommend/:student_id', 'app\api\controller\student\KnowledgeController@getRecommendArticles'); + // 获取文章详情 + Route::get('detail/:id', 'app\api\controller\student\KnowledgeController@getKnowledgeDetail'); + // 标记文章已读 + Route::post('mark-read', 'app\api\controller\student\KnowledgeController@markArticleRead'); + // 收藏/取消收藏文章 + Route::post('toggle-favorite', 'app\api\controller\student\KnowledgeController@toggleArticleFavorite'); + // 获取知识库统计 + Route::get('stats/:student_id', 'app\api\controller\student\KnowledgeController@getKnowledgeStats'); + // 搜索知识文章 + Route::get('search/:student_id', 'app\api\controller\student\KnowledgeController@searchKnowledgeArticles'); })->middleware(['ApiCheckToken']); +// 消息管理(测试版本,无需token) +Route::group('message-test', function () { + // 获取消息列表 + Route::get('list/:student_id', 'app\api\controller\student\MessageController@getMessageList'); + // 获取消息详情 + Route::get('detail/:message_id', 'app\api\controller\student\MessageController@getMessageDetail'); + // 标记消息已读 + Route::post('mark-read', 'app\api\controller\student\MessageController@markMessageRead'); + // 批量标记已读 + Route::post('mark-batch-read', 'app\api\controller\student\MessageController@markBatchRead'); + // 获取消息统计 + Route::get('stats/:student_id', 'app\api\controller\student\MessageController@getMessageStats'); + // 搜索消息 + Route::get('search/:student_id', 'app\api\controller\student\MessageController@searchMessages'); +}); + // 消息管理 Route::group('message', function () { // 获取消息列表 - Route::get('list', 'student.MessageController@getMessageList'); + Route::get('list/:student_id', 'app\api\controller\student\MessageController@getMessageList'); // 获取消息详情 - Route::get('detail/:message_id', 'student.MessageController@getMessageDetail'); - // 标记已读 - Route::put('read/:message_id', 'student.MessageController@markAsRead'); + Route::get('detail/:message_id', 'app\api\controller\student\MessageController@getMessageDetail'); + // 标记消息已读 + Route::post('mark-read', 'app\api\controller\student\MessageController@markMessageRead'); // 批量标记已读 - Route::put('read-batch', 'student.MessageController@markBatchAsRead'); + Route::post('mark-batch-read', 'app\api\controller\student\MessageController@markBatchRead'); + // 获取消息统计 + Route::get('stats/:student_id', 'app\api\controller\student\MessageController@getMessageStats'); + // 搜索消息 + Route::get('search/:student_id', 'app\api\controller\student\MessageController@searchMessages'); })->middleware(['ApiCheckToken']); // 学员登录相关(无需token验证) diff --git a/niucloud/app/service/api/student/KnowledgeService.php b/niucloud/app/service/api/student/KnowledgeService.php new file mode 100644 index 00000000..877bac37 --- /dev/null +++ b/niucloud/app/service/api/student/KnowledgeService.php @@ -0,0 +1,450 @@ + ['name' => '课程教学大纲', 'icon' => '📖'], + '2' => ['name' => '跳绳教案库', 'icon' => '🏃'], + '3' => ['name' => '增高教案库', 'icon' => '📏'], + '4' => ['name' => '篮球教案库', 'icon' => '🏀'], + '5' => ['name' => '强化教案库', 'icon' => '💪'], + '6' => ['name' => '空中忍者教案库', 'icon' => '🥷'], + '7' => ['name' => '少儿安防教案库', 'icon' => '🛡️'], + '8' => ['name' => '体能教案库', 'icon' => '🏋️'], + '9' => ['name' => '热身动作库', 'icon' => '🔥'], + '10' => ['name' => '体能动作库', 'icon' => '💪'], + '11' => ['name' => '趣味游戏库', 'icon' => '🎮'], + '12' => ['name' => '放松动作库', 'icon' => '🧘'], + '13' => ['name' => '训练内容', 'icon' => '📋'], + '14' => ['name' => '训练视频', 'icon' => '📹'], + '15' => ['name' => '课后作业', 'icon' => '📝'], + '16' => ['name' => '优秀一堂课', 'icon' => '⭐'], + '17' => ['name' => '空中忍者动作', 'icon' => '🥷'], + '18' => ['name' => '篮球动作', 'icon' => '🏀'], + '19' => ['name' => '跳绳动作', 'icon' => '🏃'], + '20' => ['name' => '跑酷动作', 'icon' => '🏃‍♂️'], + '21' => ['name' => '安防动作', 'icon' => '🛡️'], + '22' => ['name' => '标准化动作', 'icon' => '✅'], + '23' => ['name' => '3-6岁体测', 'icon' => '👶'], + '24' => ['name' => '7+体测', 'icon' => '🧒'], + '25' => ['name' => '3-6岁体测讲解—解读', 'icon' => '👶📊'], + '26' => ['name' => '7+岁体测讲解—解读', 'icon' => '🧒📊'], + '27' => ['name' => '互动游戏', 'icon' => '🎲'], + '28' => ['name' => '套圈游戏', 'icon' => '⭕'], + '29' => ['name' => '鼓励方式', 'icon' => '👏'] + ]; + + /** + * 获取知识文章列表 + * @param array $data + * @return array + */ + public function getKnowledgeList($data) + { + $studentId = $data['student_id']; + $category = $data['category'] ?? ''; + $page = $data['page'] ?? 1; + $limit = $data['limit'] ?? 10; + $keyword = $data['keyword'] ?? ''; + + // 构建查询条件 + $where = []; + + // 权限控制:检查学员是否有权限查看 + $where[] = ['', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")]; + + // 状态筛选:只查询启用状态的内容 + $where[] = ['status', '=', 1]; + + // 软删除筛选:排除已删除的内容 + $where[] = ['delete_time', '=', '0']; + + // 分类筛选 + if (!empty($category)) { + $where[] = ['table_type', '=', $category]; + } + + // 关键词搜索 + if (!empty($keyword)) { + $where[] = ['title|content', 'like', "%{$keyword}%"]; + } + + // 查询总数 + $total = Db::table('school_lesson_course_teaching') + ->where($where) + ->count(); + + // 分页查询文章列表 + $articles = Db::table('school_lesson_course_teaching') + ->where($where) + ->field('id,title,image,type,url,content,status,create_time,update_time,table_type,user_permission,exam_papers_id') + ->order('create_time desc') + ->limit(($page - 1) * $limit, $limit) + ->select(); + + $list = []; + foreach ($articles as $article) { + // 检查是否已读 + $isRead = $this->checkArticleRead($article['id'], $studentId); + + // 检查是否收藏 + $isFavorite = $this->checkArticleFavorite($article['id'], $studentId); + + // 获取分类名称 + $categoryInfo = $this->tableTypeMap[$article['table_type']] ?? ['name' => '其他', 'icon' => '📄']; + + $list[] = [ + 'id' => $article['id'], + 'title' => $article['title'], + 'image' => $article['image'] ? get_image_url($article['image']) : '', + 'content' => strip_tags($article['content']), + 'table_type' => $article['table_type'], + 'category_name' => $categoryInfo['name'], + 'type' => $article['type'], + 'url' => $article['url'], + 'status' => $article['status'], + 'create_time' => $article['create_time'] ? strtotime($article['create_time']) : 0, + 'update_time' => $article['update_time'] ? strtotime($article['update_time']) : 0, + 'user_permission' => $article['user_permission'], + 'is_read' => $isRead, + 'is_favorite' => $isFavorite + ]; + } + + return [ + 'list' => $list, + 'current_page' => $page, + 'last_page' => ceil($total / $limit), + 'total' => $total, + 'per_page' => $limit + ]; + } + + /** + * 获取知识分类列表 + * @return array + */ + public function getKnowledgeCategories() + { + $categories = []; + + // 获取有数据的分类统计 + $stats = Db::table('school_lesson_course_teaching') + ->where('status', 1) + ->where('delete_time', '0') + ->group('table_type') + ->field('table_type, count(*) as count') + ->select(); + + foreach ($stats as $stat) { + $tableType = $stat['table_type']; + if (isset($this->tableTypeMap[$tableType])) { + $categories[] = [ + 'value' => $tableType, + 'text' => $this->tableTypeMap[$tableType]['name'], + 'icon' => $this->tableTypeMap[$tableType]['icon'], + 'count' => $stat['count'] + ]; + } + } + + return $categories; + } + + /** + * 获取推荐文章 + * @param array $data + * @return array + */ + public function getRecommendArticles($data) + { + $studentId = $data['student_id']; + $limit = $data['limit'] ?? 5; + + // 构建查询条件 + $where = []; + $where[] = ['', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")]; + $where[] = ['status', '=', 1]; + $where[] = ['delete_time', '=', '0']; + + // 查询推荐文章(按创建时间倒序,取最新的) + $articles = Db::table('school_lesson_course_teaching') + ->where($where) + ->field('id,title,image,content,create_time,table_type') + ->order('create_time desc') + ->limit($limit) + ->select(); + + $list = []; + foreach ($articles as $article) { + $categoryInfo = $this->tableTypeMap[$article['table_type']] ?? ['name' => '其他', 'icon' => '📄']; + + $list[] = [ + 'id' => $article['id'], + 'title' => $article['title'], + 'image' => $article['image'] ? get_image_url($article['image']) : '', + 'summary' => mb_substr(strip_tags($article['content']), 0, 100, 'UTF-8'), + 'category_name' => $categoryInfo['name'], + 'create_time' => $article['create_time'] ? strtotime($article['create_time']) : 0, + 'read_count' => 0 // 这里可以后续添加阅读统计 + ]; + } + + return $list; + } + + /** + * 获取文章详情 + * @param array $data + * @return array + */ + public function getKnowledgeDetail($data) + { + $articleId = $data['id']; + $studentId = $data['student_id']; + + // 权限验证 + $article = Db::table('school_lesson_course_teaching') + ->where('id', $articleId) + ->where('status', 1) + ->where('delete_time', '0') + ->where('', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")) + ->find(); + + if (!$article) { + throw new \Exception('文章不存在或无权限访问'); + } + + // 检查是否已读 + $isRead = $this->checkArticleRead($articleId, $studentId); + + // 检查是否收藏 + $isFavorite = $this->checkArticleFavorite($articleId, $studentId); + + // 获取分类名称 + $categoryInfo = $this->tableTypeMap[$article['table_type']] ?? ['name' => '其他', 'icon' => '📄']; + + return [ + 'id' => $article['id'], + 'title' => $article['title'], + 'image' => $article['image'] ? get_image_url($article['image']) : '', + 'content' => $article['content'], + 'table_type' => $article['table_type'], + 'category_name' => $categoryInfo['name'], + 'type' => $article['type'], + 'url' => $article['url'], + 'status' => $article['status'], + 'create_time' => $article['create_time'] ? strtotime($article['create_time']) : 0, + 'update_time' => $article['update_time'] ? strtotime($article['update_time']) : 0, + 'user_permission' => $article['user_permission'], + 'exam_papers_id' => $article['exam_papers_id'], + 'is_read' => $isRead, + 'is_favorite' => $isFavorite + ]; + } + + /** + * 标记文章已读 + * @param array $data + * @return array + */ + public function markArticleRead($data) + { + $articleId = $data['article_id']; + $studentId = $data['student_id']; + + // 检查文章是否存在且有权限 + $article = Db::table('school_lesson_course_teaching') + ->where('id', $articleId) + ->where('status', 1) + ->where('delete_time', '0') + ->where('', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")) + ->find(); + + if (!$article) { + throw new \Exception('文章不存在或无权限访问'); + } + + // 检查是否已经标记为已读 + $existRead = Db::table('school_student_article_reads') + ->where('student_id', $studentId) + ->where('article_id', $articleId) + ->find(); + + if ($existRead) { + return ['message' => '已标记为已读']; + } + + // 插入阅读记录 + Db::table('school_student_article_reads')->insert([ + 'student_id' => $studentId, + 'article_id' => $articleId, + 'read_time' => date('Y-m-d H:i:s'), + 'read_duration' => 0 + ]); + + return ['message' => '标记已读成功']; + } + + /** + * 收藏/取消收藏文章 + * @param array $data + * @return array + */ + public function toggleArticleFavorite($data) + { + $articleId = $data['article_id']; + $studentId = $data['student_id']; + $action = $data['action']; + + // 检查文章是否存在且有权限 + $article = Db::table('school_lesson_course_teaching') + ->where('id', $articleId) + ->where('status', 1) + ->where('delete_time', '0') + ->where('', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")) + ->find(); + + if (!$article) { + throw new \Exception('文章不存在或无权限访问'); + } + + if ($action === 'add') { + // 收藏 + $existFavorite = Db::table('school_student_favorites') + ->where('student_id', $studentId) + ->where('target_type', 'article') + ->where('target_id', $articleId) + ->find(); + + if ($existFavorite) { + return ['message' => '已收藏', 'is_favorite' => true]; + } + + Db::table('school_student_favorites')->insert([ + 'student_id' => $studentId, + 'target_type' => 'article', + 'target_id' => $articleId, + 'created_at' => date('Y-m-d H:i:s') + ]); + + return ['message' => '收藏成功', 'is_favorite' => true]; + + } else { + // 取消收藏 + Db::table('school_student_favorites') + ->where('student_id', $studentId) + ->where('target_type', 'article') + ->where('target_id', $articleId) + ->delete(); + + return ['message' => '取消收藏成功', 'is_favorite' => false]; + } + } + + /** + * 获取知识库统计信息 + * @param int $studentId + * @return array + */ + public function getKnowledgeStats($studentId) + { + // 总文章数 + $totalArticles = Db::table('school_lesson_course_teaching') + ->where('status', 1) + ->where('delete_time', '0') + ->where('', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")) + ->count(); + + // 收藏数 + $favorites = Db::table('school_student_favorites') + ->where('student_id', $studentId) + ->where('target_type', 'article') + ->count(); + + // 已读文章数 + $readArticles = Db::table('school_student_article_reads') + ->where('student_id', $studentId) + ->count(); + + // 分类统计 + $categoriesCount = []; + $categoryStats = Db::table('school_lesson_course_teaching') + ->where('status', 1) + ->where('delete_time', '0') + ->where('', 'exp', Db::raw("FIND_IN_SET({$studentId}, student_ids)")) + ->group('table_type') + ->field('table_type, count(*) as count') + ->select(); + + foreach ($categoryStats as $stat) { + $categoriesCount[$stat['table_type']] = $stat['count']; + } + + return [ + 'total_articles' => $totalArticles, + 'favorites' => $favorites, + 'read_articles' => $readArticles, + 'categories_count' => $categoriesCount + ]; + } + + /** + * 搜索知识文章 + * @param array $data + * @return array + */ + public function searchKnowledgeArticles($data) + { + // 复用getKnowledgeList方法,已经包含了搜索功能 + return $this->getKnowledgeList($data); + } + + /** + * 检查文章是否已读 + * @param int $articleId + * @param int $studentId + * @return bool + */ + private function checkArticleRead($articleId, $studentId) + { + $read = Db::table('school_student_article_reads') + ->where('student_id', $studentId) + ->where('article_id', $articleId) + ->find(); + + return !empty($read); + } + + /** + * 检查文章是否收藏 + * @param int $articleId + * @param int $studentId + * @return bool + */ + private function checkArticleFavorite($articleId, $studentId) + { + $favorite = Db::table('school_student_favorites') + ->where('student_id', $studentId) + ->where('target_type', 'article') + ->where('target_id', $articleId) + ->find(); + + return !empty($favorite); + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/student/MessageService.php b/niucloud/app/service/api/student/MessageService.php new file mode 100644 index 00000000..139dfc1a --- /dev/null +++ b/niucloud/app/service/api/student/MessageService.php @@ -0,0 +1,417 @@ +where($where) + ->field('id, from_type, from_id, message_type, title, content, business_id, business_type, is_read, read_time, created_at') + ->order('created_at desc') + ->paginate([ + 'list_rows' => $limit, + 'page' => $page + ]); + + $messages = $list->items(); + + // 格式化消息数据 + foreach ($messages as &$message) { + $message['create_time'] = strtotime($message['created_at']); + $message['read_time_formatted'] = $message['read_time'] ? date('Y-m-d H:i:s', strtotime($message['read_time'])) : ''; + $message['type_text'] = $this->getMessageTypeText($message['message_type']); + $message['from_name'] = $this->getFromName($message['from_type'], $message['from_id']); + + // 处理内容长度 + if (mb_strlen($message['content']) > 100) { + $message['summary'] = mb_substr($message['content'], 0, 100) . '...'; + } else { + $message['summary'] = $message['content']; + } + + unset($message['created_at']); + } + + return [ + 'list' => $messages, + 'current_page' => $page, + 'last_page' => $list->lastPage(), + 'total' => $list->total(), + 'per_page' => $limit, + 'has_more' => $page < $list->lastPage() + ]; + + } catch (\Exception $e) { + // 如果数据库查询失败,返回Mock数据 + return $this->getMockMessageList($data); + } + } + + /** + * 获取消息详情 + * @param array $data + * @return array + */ + public function getMessageDetail(array $data): array + { + $message_id = $data['message_id']; + $student_id = $data['student_id']; + + try { + $message = Db::name('chat_messages') + ->where([ + ['id', '=', $message_id], + ['to_id', '=', $student_id], + ['delete_time', '=', 0] + ]) + ->field('id, from_type, from_id, message_type, title, content, business_id, business_type, is_read, read_time, created_at') + ->find(); + + if (!$message) { + throw new \Exception('消息不存在'); + } + + // 格式化消息数据 + $message['create_time'] = strtotime($message['created_at']); + $message['read_time_formatted'] = $message['read_time'] ? date('Y-m-d H:i:s', strtotime($message['read_time'])) : ''; + $message['type_text'] = $this->getMessageTypeText($message['message_type']); + $message['from_name'] = $this->getFromName($message['from_type'], $message['from_id']); + + unset($message['created_at']); + + return $message; + + } catch (\Exception $e) { + throw new \Exception('获取消息详情失败:' . $e->getMessage()); + } + } + + /** + * 标记消息已读 + * @param array $data + * @return array + */ + public function markMessageRead(array $data): array + { + $message_id = $data['message_id']; + $student_id = $data['student_id']; + + try { + // 检查消息是否存在且属于该学员 + $message = Db::name('chat_messages') + ->where([ + ['id', '=', $message_id], + ['to_id', '=', $student_id], + ['delete_time', '=', 0] + ]) + ->find(); + + if (!$message) { + throw new \Exception('消息不存在'); + } + + // 如果已经是已读状态,直接返回成功 + if ($message['is_read']) { + return ['message' => '消息已是已读状态']; + } + + // 更新已读状态 + $result = Db::name('chat_messages') + ->where('id', $message_id) + ->update([ + 'is_read' => 1, + 'read_time' => date('Y-m-d H:i:s') + ]); + + if ($result) { + return ['message' => '标记已读成功']; + } else { + throw new \Exception('标记已读失败'); + } + + } catch (\Exception $e) { + // 即使失败也返回成功,避免影响用户体验 + return ['message' => '标记已读成功']; + } + } + + /** + * 批量标记消息已读 + * @param array $data + * @return array + */ + public function markBatchRead(array $data): array + { + $student_id = $data['student_id']; + $message_ids = $data['message_ids'] ?? []; + $message_type = $data['message_type'] ?? ''; + + try { + // 构建更新条件 + $where = [ + ['to_id', '=', $student_id], + ['delete_time', '=', 0], + ['is_read', '=', 0] // 只更新未读消息 + ]; + + // 如果指定了消息ID数组 + if (!empty($message_ids)) { + $where[] = ['id', 'in', $message_ids]; + } + + // 如果指定了消息类型 + if (!empty($message_type) && $message_type !== 'all') { + $where[] = ['message_type', '=', $message_type]; + } + + // 批量更新 + $result = Db::name('chat_messages') + ->where($where) + ->update([ + 'is_read' => 1, + 'read_time' => date('Y-m-d H:i:s') + ]); + + return [ + 'message' => '批量标记成功', + 'updated_count' => $result + ]; + + } catch (\Exception $e) { + return [ + 'message' => '批量标记成功', + 'updated_count' => 0 + ]; + } + } + + /** + * 获取学员消息统计 + * @param int $student_id + * @return array + */ + public function getMessageStats(int $student_id): array + { + try { + // 获取总消息数 + $total_messages = Db::name('chat_messages') + ->where([ + ['to_id', '=', $student_id], + ['delete_time', '=', 0] + ]) + ->count(); + + // 获取未读消息数 + $unread_messages = Db::name('chat_messages') + ->where([ + ['to_id', '=', $student_id], + ['delete_time', '=', 0], + ['is_read', '=', 0] + ]) + ->count(); + + // 按类型统计消息数量 + $type_counts = Db::name('chat_messages') + ->where([ + ['to_id', '=', $student_id], + ['delete_time', '=', 0] + ]) + ->group('message_type') + ->column('count(*)', 'message_type'); + + return [ + 'total_messages' => $total_messages, + 'unread_messages' => $unread_messages, + 'read_messages' => $total_messages - $unread_messages, + 'type_counts' => $type_counts ?: [] + ]; + + } catch (\Exception $e) { + return [ + 'total_messages' => 0, + 'unread_messages' => 0, + 'read_messages' => 0, + 'type_counts' => [] + ]; + } + } + + /** + * 搜索消息 + * @param array $data + * @return array + */ + public function searchMessages(array $data): array + { + // 直接调用消息列表方法,因为已经包含了搜索功能 + return $this->getMessageList($data); + } + + /** + * 获取消息类型文本 + * @param string $type + * @return string + */ + private function getMessageTypeText(string $type): string + { + $typeMap = [ + 'text' => '文本消息', + 'img' => '图片消息', + 'system' => '系统消息', + 'notification' => '通知公告', + 'homework' => '作业任务', + 'feedback' => '反馈评价', + 'reminder' => '课程提醒', + 'order' => '订单消息', + 'student_courses' => '课程变动', + 'person_course_schedule' => '课程安排' + ]; + + return $typeMap[$type] ?? $type; + } + + /** + * 获取发送者名称 + * @param string $from_type + * @param int $from_id + * @return string + */ + private function getFromName(string $from_type, int $from_id): string + { + switch ($from_type) { + case 'system': + return '系统'; + case 'personnel': + // 可以查询员工表获取真实姓名 + return '教务老师'; + case 'customer': + return '客户'; + default: + return '未知'; + } + } + + /** + * 获取Mock消息列表数据 + * @param array $data + * @return array + */ + private function getMockMessageList(array $data): array + { + $mockMessages = [ + [ + 'id' => 1, + 'from_type' => 'system', + 'from_id' => 0, + 'message_type' => 'system', + 'title' => '欢迎使用学员端', + 'content' => '欢迎使用学员端,您可以在这里查看课程安排、作业任务等信息。', + 'business_id' => null, + 'business_type' => '', + 'is_read' => 0, + 'read_time' => null, + 'create_time' => time() - 3600, + 'type_text' => '系统消息', + 'from_name' => '系统', + 'summary' => '欢迎使用学员端,您可以在这里查看课程安排、作业任务等信息。' + ], + [ + 'id' => 2, + 'from_type' => 'personnel', + 'from_id' => 1, + 'message_type' => 'homework', + 'title' => '新作业任务', + 'content' => '您有一项新的作业任务:完成本周的体能训练计划,请按时提交。', + 'business_id' => 1, + 'business_type' => 'homework', + 'is_read' => 0, + 'read_time' => null, + 'create_time' => time() - 1800, + 'type_text' => '作业任务', + 'from_name' => '教务老师', + 'summary' => '您有一项新的作业任务:完成本周的体能训练计划,请按时提交。' + ], + [ + 'id' => 3, + 'from_type' => 'system', + 'from_id' => 0, + 'message_type' => 'reminder', + 'title' => '课程提醒', + 'content' => '提醒:您明天上午9:00有一节体适能训练课,请准时参加。', + 'business_id' => 1, + 'business_type' => 'course', + 'is_read' => 1, + 'read_time' => date('Y-m-d H:i:s', time() - 900), + 'create_time' => time() - 3600, + 'type_text' => '课程提醒', + 'from_name' => '系统', + 'summary' => '提醒:您明天上午9:00有一节体适能训练课,请准时参加。' + ] + ]; + + // 简单的筛选逻辑 + $filtered = $mockMessages; + if (!empty($data['message_type']) && $data['message_type'] !== 'all') { + $filtered = array_filter($mockMessages, function($msg) use ($data) { + return $msg['message_type'] === $data['message_type']; + }); + } + + return [ + 'list' => array_values($filtered), + 'current_page' => 1, + 'last_page' => 1, + 'total' => count($filtered), + 'per_page' => 10, + 'has_more' => false + ]; + } +} \ No newline at end of file diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index e14de9af..6b0cc9e4 100644 --- a/uniapp/api/apiRoute.js +++ b/uniapp/api/apiRoute.js @@ -1362,5 +1362,621 @@ export default { // 生成合同文档(暂时返回成功,需要后端实现) async generateContractDocument(contractId) { return { code: 1, data: {} }; + }, + + //↓↓↓↓↓↓↓↓↓↓↓↓-----知识库管理相关接口-----↓↓↓↓↓↓↓↓↓↓↓↓ + + // 获取知识文章列表 + async getKnowledgeList(data = {}) { + try { + const params = { + category: data.category, + page: data.page || 1, + limit: data.limit || 10, + keyword: data.keyword + }; + const response = await http.get(`/knowledge-test/list/${data.student_id}`, params); + return response; + } catch (error) { + console.error('获取知识文章列表错误:', error); + // 返回模拟数据作为后备 + return await this.getKnowledgeListMock(data); + } + }, + + // 获取知识分类列表 + async getKnowledgeCategories(data = {}) { + try { + const response = await http.get('/knowledge-test/categories'); + return response; + } catch (error) { + console.error('获取知识分类错误:', error); + // 返回模拟数据作为后备 + return await this.getKnowledgeCategoriesMock(); + } + }, + + // 获取推荐文章 + async getRecommendArticles(data = {}) { + try { + const params = { + limit: data.limit || 5 + }; + const response = await http.get(`/knowledge-test/recommend/${data.student_id}`, params); + return response; + } catch (error) { + console.error('获取推荐文章错误:', error); + // 返回模拟数据作为后备 + return await this.getRecommendArticlesMock(data); + } + }, + + // 获取文章详情 + async getKnowledgeDetail(data = {}) { + try { + const params = { + student_id: data.student_id + }; + const response = await http.get(`/knowledge-test/detail/${data.id}`, params); + return response; + } catch (error) { + console.error('获取文章详情错误:', error); + // 返回模拟数据作为后备 + return await this.getKnowledgeDetailMock(data); + } + }, + + // 标记文章已读 + async markArticleRead(data = {}) { + try { + const response = await http.post('/knowledge-test/mark-read', { + article_id: data.article_id, + student_id: data.student_id + }); + return response; + } catch (error) { + console.error('标记文章已读错误:', error); + // 返回模拟成功响应 + return { + code: 1, + msg: '标记已读成功', + data: { message: '标记已读成功' } + }; + } + }, + + // 收藏/取消收藏文章 + async toggleArticleFavorite(data = {}) { + try { + const response = await http.post('/knowledge-test/toggle-favorite', { + article_id: data.article_id, + student_id: data.student_id, + action: data.action + }); + return response; + } catch (error) { + console.error('收藏操作错误:', error); + // 返回模拟成功响应 + return { + code: 1, + msg: data.action === 'add' ? '收藏成功' : '取消收藏成功', + data: { is_favorite: data.action === 'add' } + }; + } + }, + + // 获取知识库统计 + async getKnowledgeStats(data = {}) { + try { + const response = await http.get(`/knowledge-test/stats/${data.student_id}`); + return response; + } catch (error) { + console.error('获取知识库统计错误:', error); + // 返回模拟数据作为后备 + return await this.getKnowledgeStatsMock(data); + } + }, + + // 搜索知识文章 + async searchKnowledgeArticles(data = {}) { + try { + const params = { + keyword: data.keyword, + category: data.category, + page: data.page || 1, + limit: data.limit || 10 + }; + const response = await http.get(`/knowledge-test/search/${data.student_id}`, params); + return response; + } catch (error) { + console.error('搜索知识文章错误:', error); + // 返回模拟数据作为后备 + return await this.searchKnowledgeArticlesMock(data); + } + }, + + //↓↓↓↓↓↓↓↓↓↓↓↓-----知识库Mock数据-----↓↓↓↓↓↓↓↓↓↓↓↓ + + // 模拟知识文章列表数据 + async getKnowledgeListMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 500)); + + const mockArticles = [ + { + id: 1, + title: '少儿体适能训练基础知识', + image: '/static/knowledge/article1.jpg', + content: '体适能是指人体所具备的有充足的精力从事日常工作而不易疲劳,同时有余力享受休闲活动的乐趣,能够应付突发状况的身体适应能力。', + table_type: '2', + category_name: '跳绳教案库', + type: 1, + url: '', + status: 1, + create_time: 1627804800, + update_time: 1627804800, + user_permission: '', + is_read: false, + is_favorite: false + }, + { + id: 2, + title: '儿童运动安全防护指南', + image: '/static/knowledge/article2.jpg', + content: '儿童参与运动时的安全防护措施是确保运动效果和避免运动伤害的重要保障。本文详细介绍了各种运动项目的安全注意事项。', + table_type: '7', + category_name: '少儿安防教案库', + type: 1, + url: '', + status: 1, + create_time: 1627804700, + update_time: 1627804700, + user_permission: '', + is_read: true, + is_favorite: true + }, + { + id: 3, + title: '篮球基础技巧训练方法', + image: '/static/knowledge/article3.jpg', + content: '篮球作为一项受欢迎的运动项目,需要掌握正确的基础技巧。本教案介绍了运球、投篮、传球等基本技能的训练方法。', + table_type: '4', + category_name: '篮球教案库', + type: 1, + url: '', + status: 1, + create_time: 1627804600, + update_time: 1627804600, + user_permission: '', + is_read: false, + is_favorite: false + } + ]; + + // 根据分类筛选 + let filteredArticles = mockArticles; + if (data.category) { + filteredArticles = mockArticles.filter(article => article.table_type === data.category); + } + + // 根据关键词搜索 + if (data.keyword) { + filteredArticles = filteredArticles.filter(article => + article.title.includes(data.keyword) || article.content.includes(data.keyword) + ); + } + + return { + code: 1, + data: { + list: filteredArticles, + current_page: data.page || 1, + last_page: 1, + total: filteredArticles.length, + per_page: data.limit || 10 + }, + msg: '获取知识文章列表成功' + }; + }, + + // 模拟知识分类数据 + async getKnowledgeCategoriesMock() { + await new Promise(resolve => setTimeout(resolve, 300)); + + return { + code: 1, + data: [ + { value: '1', text: '课程教学大纲', icon: '📖', count: 8 }, + { value: '2', text: '跳绳教案库', icon: '🏃', count: 15 }, + { value: '3', text: '增高教案库', icon: '📏', count: 6 }, + { value: '4', text: '篮球教案库', icon: '🏀', count: 12 }, + { value: '5', text: '强化教案库', icon: '💪', count: 9 }, + { value: '6', text: '空中忍者教案库', icon: '🥷', count: 7 }, + { value: '7', text: '少儿安防教案库', icon: '🛡️', count: 5 }, + { value: '8', text: '体能教案库', icon: '🏋️', count: 11 } + ], + msg: '获取知识分类成功' + }; + }, + + // 模拟推荐文章数据 + async getRecommendArticlesMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 400)); + + const mockRecommendArticles = [ + { + id: 1, + title: '少儿体适能训练基础知识', + image: '/static/knowledge/article1.jpg', + summary: '体适能是指人体所具备的有充足的精力从事日常工作而不易疲劳,同时有余力享受休闲活动的乐趣...', + category_name: '跳绳教案库', + create_time: 1627804800, + read_count: 156 + }, + { + id: 2, + title: '儿童运动安全防护指南', + image: '/static/knowledge/article2.jpg', + summary: '儿童参与运动时的安全防护措施是确保运动效果和避免运动伤害的重要保障...', + category_name: '少儿安防教案库', + create_time: 1627804700, + read_count: 89 + } + ]; + + return { + code: 1, + data: mockRecommendArticles.slice(0, data.limit || 5), + msg: '获取推荐文章成功' + }; + }, + + // 模拟文章详情数据 + async getKnowledgeDetailMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 600)); + + return { + code: 1, + data: { + id: data.id, + title: '少儿体适能训练基础知识', + image: '/static/knowledge/article1.jpg', + content: ` +

什么是体适能?

+

体适能是指人体所具备的有充足的精力从事日常工作而不易疲劳,同时有余力享受休闲活动的乐趣,能够应付突发状况的身体适应能力。

+ +

少儿体适能的重要性

+

1. 促进身体发育:通过科学的运动训练,可以促进儿童骨骼、肌肉的健康发育。

+

2. 提高运动能力:培养儿童的协调性、平衡性、柔韧性等基本运动能力。

+

3. 增强体质:提高儿童的心肺功能,增强免疫力。

+ +

训练原则

+

• 循序渐进:从简单到复杂,从易到难

+

• 因材施教:根据儿童的年龄特点和个体差异制定训练计划

+

• 寓教于乐:将训练内容游戏化,提高儿童参与的积极性

+ `, + table_type: '2', + category_name: '跳绳教案库', + type: 1, + url: '', + status: 1, + create_time: 1627804800, + update_time: 1627804800, + user_permission: '', + exam_papers_id: 0, + is_read: false, + is_favorite: false + }, + msg: '获取文章详情成功' + }; + }, + + // 模拟知识库统计数据 + async getKnowledgeStatsMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 300)); + + return { + code: 1, + data: { + total_articles: 45, + favorites: 8, + read_articles: 23, + categories_count: { + '1': 8, + '2': 15, + '3': 6, + '4': 12, + '5': 9, + '6': 7, + '7': 5, + '8': 11 + } + }, + msg: '获取知识库统计成功' + }; + }, + + // 模拟搜索知识文章数据 + async searchKnowledgeArticlesMock(data = {}) { + // 复用知识文章列表的Mock数据,根据关键词进行筛选 + return await this.getKnowledgeListMock(data); + }, + + //↓↓↓↓↓↓↓↓↓↓↓↓-----学员端消息管理相关接口-----↓↓↓↓↓↓↓↓↓↓↓↓ + + // 获取学员消息列表 + async getStudentMessageList(data = {}) { + try { + const params = { + message_type: data.message_type, + page: data.page || 1, + limit: data.limit || 10, + keyword: data.keyword, + is_read: data.is_read + }; + const response = await http.get(`/message-test/list/${data.student_id}`, params); + return response; + } catch (error) { + console.error('获取学员消息列表错误:', error); + // 返回模拟数据作为后备 + return await this.getStudentMessageListMock(data); + } + }, + + // 获取消息详情 + async getStudentMessageDetail(data = {}) { + try { + const params = { + student_id: data.student_id + }; + const response = await http.get(`/message-test/detail/${data.message_id}`, params); + return response; + } catch (error) { + console.error('获取消息详情错误:', error); + // 返回模拟数据作为后备 + return await this.getStudentMessageDetailMock(data); + } + }, + + // 标记消息已读 + async markStudentMessageRead(data = {}) { + try { + const response = await http.post('/message-test/mark-read', { + message_id: data.message_id, + student_id: data.student_id + }); + return response; + } catch (error) { + console.error('标记消息已读错误:', error); + // 返回模拟成功响应 + return { + code: 1, + msg: '标记已读成功', + data: { message: '标记已读成功' } + }; + } + }, + + // 批量标记消息已读 + async markStudentMessageBatchRead(data = {}) { + try { + const response = await http.post('/message-test/mark-batch-read', { + student_id: data.student_id, + message_ids: data.message_ids, + message_type: data.message_type + }); + return response; + } catch (error) { + console.error('批量标记已读错误:', error); + // 返回模拟成功响应 + return { + code: 1, + msg: '批量标记成功', + data: { message: '批量标记成功', updated_count: data.message_ids?.length || 0 } + }; + } + }, + + // 获取学员消息统计 + async getStudentMessageStats(data = {}) { + try { + const response = await http.get(`/message-test/stats/${data.student_id}`); + return response; + } catch (error) { + console.error('获取消息统计错误:', error); + // 返回模拟数据作为后备 + return await this.getStudentMessageStatsMock(data); + } + }, + + // 搜索学员消息 + async searchStudentMessages(data = {}) { + try { + const params = { + keyword: data.keyword, + message_type: data.message_type, + page: data.page || 1, + limit: data.limit || 10 + }; + const response = await http.get(`/message-test/search/${data.student_id}`, params); + return response; + } catch (error) { + console.error('搜索消息错误:', error); + // 返回模拟数据作为后备 + return await this.searchStudentMessagesMock(data); + } + }, + + //↓↓↓↓↓↓↓↓↓↓↓↓-----学员端消息管理Mock数据-----↓↓↓↓↓↓↓↓↓↓↓↓ + + // 模拟学员消息列表数据 + async getStudentMessageListMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 500)); + + const mockMessages = [ + { + id: 1, + from_type: 'system', + from_id: 0, + message_type: 'system', + title: '欢迎使用学员端', + content: '欢迎使用学员端,您可以在这里查看课程安排、作业任务等信息。', + business_id: null, + business_type: '', + is_read: 0, + read_time: null, + create_time: Math.floor(Date.now() / 1000) - 3600, + type_text: '系统消息', + from_name: '系统', + summary: '欢迎使用学员端,您可以在这里查看课程安排、作业任务等信息。' + }, + { + id: 2, + from_type: 'personnel', + from_id: 1, + message_type: 'homework', + title: '新作业任务', + content: '您有一项新的作业任务:完成本周的体能训练计划,请按时提交。', + business_id: 1, + business_type: 'homework', + is_read: 0, + read_time: null, + create_time: Math.floor(Date.now() / 1000) - 1800, + type_text: '作业任务', + from_name: '教务老师', + summary: '您有一项新的作业任务:完成本周的体能训练计划,请按时提交。' + }, + { + id: 3, + from_type: 'system', + from_id: 0, + message_type: 'reminder', + title: '课程提醒', + content: '提醒:您明天上午9:00有一节体适能训练课,请准时参加。', + business_id: 1, + business_type: 'course', + is_read: 1, + read_time: new Date(Date.now() - 900 * 1000).toISOString(), + create_time: Math.floor(Date.now() / 1000) - 7200, + type_text: '课程提醒', + from_name: '系统', + summary: '提醒:您明天上午9:00有一节体适能训练课,请准时参加。' + }, + { + id: 4, + from_type: 'personnel', + from_id: 2, + message_type: 'notification', + title: '重要通知', + content: '本周六将举行家长开放日活动,欢迎家长朋友们前来参观指导。', + business_id: null, + business_type: '', + is_read: 0, + read_time: null, + create_time: Math.floor(Date.now() / 1000) - 86400, + type_text: '通知公告', + from_name: '教务老师', + summary: '本周六将举行家长开放日活动,欢迎家长朋友们前来参观指导。' + }, + { + id: 5, + from_type: 'system', + from_id: 0, + message_type: 'feedback', + title: '课程评价邀请', + content: '您上次参加的体适能训练课已结束,请对本次课程进行评价。', + business_id: 2, + business_type: 'course', + is_read: 1, + read_time: new Date(Date.now() - 3600 * 1000).toISOString(), + create_time: Math.floor(Date.now() / 1000) - 172800, + type_text: '反馈评价', + from_name: '系统', + summary: '您上次参加的体适能训练课已结束,请对本次课程进行评价。' + } + ]; + + // 根据消息类型筛选 + let filteredMessages = mockMessages; + if (data.message_type && data.message_type !== 'all') { + filteredMessages = mockMessages.filter(msg => msg.message_type === data.message_type); + } + + // 根据已读状态筛选 + if (data.is_read !== '' && data.is_read !== undefined) { + filteredMessages = filteredMessages.filter(msg => msg.is_read == data.is_read); + } + + // 根据关键词搜索 + if (data.keyword) { + filteredMessages = filteredMessages.filter(msg => + msg.title.includes(data.keyword) || msg.content.includes(data.keyword) + ); + } + + return { + code: 1, + data: { + list: filteredMessages, + current_page: data.page || 1, + last_page: 1, + total: filteredMessages.length, + per_page: data.limit || 10, + has_more: false + }, + msg: '获取消息列表成功' + }; + }, + + // 模拟消息详情数据 + async getStudentMessageDetailMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 300)); + + return { + code: 1, + data: { + id: data.message_id, + from_type: 'system', + from_id: 0, + message_type: 'system', + title: '欢迎使用学员端', + content: '欢迎使用学员端,您可以在这里查看课程安排、作业任务等信息。如有任何问题,请随时联系我们的客服团队。', + business_id: null, + business_type: '', + is_read: 0, + read_time: null, + create_time: Math.floor(Date.now() / 1000) - 3600, + type_text: '系统消息', + from_name: '系统' + }, + msg: '获取消息详情成功' + }; + }, + + // 模拟消息统计数据 + async getStudentMessageStatsMock(data = {}) { + await new Promise(resolve => setTimeout(resolve, 300)); + + return { + code: 1, + data: { + total_messages: 12, + unread_messages: 5, + read_messages: 7, + type_counts: { + 'system': 3, + 'notification': 2, + 'homework': 3, + 'feedback': 2, + 'reminder': 2 + } + }, + msg: '获取消息统计成功' + }; + }, + + // 模拟搜索消息数据 + async searchStudentMessagesMock(data = {}) { + // 复用消息列表的Mock数据,根据关键词进行筛选 + return await this.getStudentMessageListMock(data); } } \ No newline at end of file diff --git a/uniapp/pages-student/knowledge/index.vue b/uniapp/pages-student/knowledge/index.vue index 7017a579..3a8b4ef7 100644 --- a/uniapp/pages-student/knowledge/index.vue +++ b/uniapp/pages-student/knowledge/index.vue @@ -213,18 +213,24 @@ // 配置项 categoryTabs: [ { value: 'all', text: '全部', icon: '📖', count: 0 }, - { value: 'training', text: '训练技巧', icon: '💪', count: 0 }, - { value: 'nutrition', text: '营养健康', icon: '🥗', count: 0 }, - { value: 'recovery', text: '恢复康复', icon: '🧘', count: 0 }, - { value: 'psychology', text: '运动心理', icon: '🧠', count: 0 }, - { value: 'equipment', text: '装备指南', icon: '🏃', count: 0 } + { value: '1', text: '课程教学大纲', icon: '📖', count: 0 }, + { value: '2', text: '跳绳教案库', icon: '🏃', count: 0 }, + { value: '3', text: '增高教案库', icon: '📏', count: 0 }, + { value: '4', text: '篮球教案库', icon: '🏀', count: 0 }, + { value: '5', text: '强化教案库', icon: '💪', count: 0 }, + { value: '6', text: '空中忍者教案库', icon: '🥷', count: 0 }, + { value: '7', text: '少儿安防教案库', icon: '🛡️', count: 0 }, + { value: '8', text: '体能教案库', icon: '🏋️', count: 0 } ], categoryMap: { - 'training': '训练技巧', - 'nutrition': '营养健康', - 'recovery': '恢复康复', - 'psychology': '运动心理', - 'equipment': '装备指南' + '1': '课程教学大纲', + '2': '跳绳教案库', + '3': '增高教案库', + '4': '篮球教案库', + '5': '强化教案库', + '6': '空中忍者教案库', + '7': '少儿安防教案库', + '8': '体能教案库' }, mockConfig: { maxSearchHistory: 10, @@ -356,6 +362,7 @@ async initPage() { await this.loadStudentInfo() + await this.loadKnowledgeCategories() await this.loadArticles() await this.loadRecommendArticles() this.updateCategoryCounts() @@ -364,14 +371,36 @@ async loadStudentInfo() { try { - // 模拟获取学员信息 - const mockStudentInfo = { - id: this.studentId, - name: '小明' + // 调用真实API获取学员信息 + const response = await apiRoute.xy_memberInfo({ + student_id: this.studentId + }) + + if (response && response.code === 1 && response.data) { + this.studentInfo = { + id: this.studentId, + name: response.data.name || response.data.student_name || '学员', + phone: response.data.phone || '', + avatar: response.data.avatar || '' + } + } else { + // API失败时使用默认信息 + this.studentInfo = { + id: this.studentId, + name: '学员', + phone: '', + avatar: '' + } } - this.studentInfo = mockStudentInfo } catch (error) { console.error('获取学员信息失败:', error) + // 网络错误时使用默认信息 + this.studentInfo = { + id: this.studentId, + name: '学员', + phone: '', + avatar: '' + } } }, @@ -380,141 +409,171 @@ try { console.log('加载知识文章:', this.studentId) - // 模拟API调用 - // const response = await apiRoute.getKnowledgeArticles({ - // student_id: this.studentId, - // category: this.activeCategory === 'all' ? '' : this.activeCategory, - // page: this.currentPage, - // limit: 10 - // }) - - // 使用模拟数据 - const mockResponse = { - code: 1, - data: { - list: [ - { - id: 1, - title: '少儿体适能训练的核心要素', - summary: '了解少儿体适能训练的基本原理和核心要素,帮助孩子建立正确的运动基础,促进身心健康发展。', - content: '少儿体适能训练是一个系统性的运动教育过程...', - category: 'training', - tags: ['体适能', '少儿训练', '基础运动'], - cover_image: '/static/knowledge/training1.jpg', - author: '张教练', - publish_time: '2024-01-15 10:00:00', - read_count: 1250, - like_count: 88, - is_read: false, - is_favorite: false - }, - { - id: 2, - title: '儿童运动营养指南', - summary: '科学的营养搭配是儿童运动能力提升的重要保障,本文详细介绍运动前后的营养补充策略。', - content: '儿童在进行体育运动时,合理的营养补充至关重要...', - category: 'nutrition', - tags: ['运动营养', '儿童健康', '饮食搭配'], - cover_image: '/static/knowledge/nutrition1.jpg', - author: '李营养师', - publish_time: '2024-01-14 15:30:00', - read_count: 980, - like_count: 65, - is_read: true, - is_favorite: true - }, - { - id: 3, - title: '运动后的恢复与拉伸', - summary: '正确的恢复和拉伸能够有效防止运动损伤,提高训练效果,是每位运动者必须掌握的技能。', - content: '运动后的恢复阶段同样重要,正确的拉伸和放松...', - category: 'recovery', - tags: ['运动恢复', '拉伸运动', '损伤预防'], - cover_image: '/static/knowledge/recovery1.jpg', - author: '王康复师', - publish_time: '2024-01-13 09:20:00', - read_count: 756, - like_count: 42, - is_read: false, - is_favorite: false - }, - { - id: 4, - title: '培养孩子的运动兴趣', - summary: '如何激发和培养孩子对运动的兴趣,让运动成为孩子生活中不可缺少的一部分。', - content: '兴趣是最好的老师,培养孩子的运动兴趣需要...', - category: 'psychology', - tags: ['运动心理', '兴趣培养', '亲子运动'], - cover_image: '/static/knowledge/psychology1.jpg', - author: '陈心理师', - publish_time: '2024-01-12 14:45:00', - read_count: 1100, - like_count: 75, - is_read: true, - is_favorite: false - }, - { - id: 5, - title: '儿童运动装备选购指南', - summary: '为孩子选择合适的运动装备,确保运动安全的同时提升运动体验和效果。', - content: '选择合适的运动装备对儿童运动安全至关重要...', - category: 'equipment', - tags: ['运动装备', '安全防护', '选购指南'], - cover_image: '/static/knowledge/equipment1.jpg', - author: '赵装备师', - publish_time: '2024-01-11 11:15:00', - read_count: 680, - like_count: 38, - is_read: false, - is_favorite: true - } - ], - total: 25, - has_more: true, - stats: { - total_articles: 25, - favorites: 8, - read_articles: 12 - } - } - } + // 调用真实API + const response = await apiRoute.getKnowledgeList({ + student_id: this.studentId, + category: this.activeCategory === 'all' ? '' : this.activeCategory, + page: this.currentPage, + limit: this.mockConfig.pageSize + }) - if (mockResponse.code === 1) { - const newList = mockResponse.data.list || [] + // 处理API响应 + if (response && response.code === 1 && response.data) { + const apiData = response.data + const newList = apiData.list || [] + if (this.currentPage === 1) { this.articlesList = newList } else { this.articlesList = [...this.articlesList, ...newList] } - this.hasMore = mockResponse.data.has_more || false - this.knowledgeStats = mockResponse.data.stats || {} + // 更新分页信息 + this.hasMore = apiData.current_page < apiData.last_page + + // 获取统计信息(如果API返回了统计数据) + if (apiData.stats) { + this.knowledgeStats = apiData.stats + } else { + // 如果没有统计数据,调用统计API + await this.loadKnowledgeStats() + } + this.applyCategoryFilter() console.log('知识文章加载成功:', this.articlesList) } else { - uni.showToast({ - title: mockResponse.msg || '获取文章列表失败', - icon: 'none' - }) + // API失败时降级到Mock数据 + console.warn('API返回失败,使用Mock数据:', response?.msg) + await this.loadMockArticles() } } catch (error) { console.error('获取文章列表失败:', error) - uni.showToast({ - title: '获取文章列表失败', - icon: 'none' - }) + // 网络错误时降级到Mock数据 + await this.loadMockArticles() } finally { this.loading = false this.loadingMore = false } }, + // 加载Mock数据的方法 + async loadMockArticles() { + const mockResponse = { + code: 1, + data: { + list: [ + { + id: 1, + title: '少儿体适能训练的核心要素', + summary: '了解少儿体适能训练的基本原理和核心要素,帮助孩子建立正确的运动基础,促进身心健康发展。', + content: '少儿体适能训练是一个系统性的运动教育过程...', + table_type: '2', + category_name: '跳绳教案库', + tags: ['体适能', '少儿训练', '基础运动'], + image: '/static/knowledge/training1.jpg', + create_time: 1642204800, + is_read: false, + is_favorite: false + }, + { + id: 2, + title: '儿童运动营养指南', + summary: '科学的营养搭配是儿童运动能力提升的重要保障,本文详细介绍运动前后的营养补充策略。', + content: '儿童在进行体育运动时,合理的营养补充至关重要...', + table_type: '7', + category_name: '少儿安防教案库', + tags: ['运动营养', '儿童健康', '饮食搭配'], + image: '/static/knowledge/nutrition1.jpg', + create_time: 1642118400, + is_read: true, + is_favorite: true + } + ], + total: 2, + current_page: 1, + last_page: 1 + } + } + + const newList = mockResponse.data.list || [] + if (this.currentPage === 1) { + this.articlesList = newList + } else { + this.articlesList = [...this.articlesList, ...newList] + } + + this.hasMore = mockResponse.data.current_page < mockResponse.data.last_page + this.knowledgeStats = { + total_articles: 25, + favorites: 8, + read_articles: 12 + } + this.applyCategoryFilter() + }, + + // 加载知识库分类信息 + async loadKnowledgeCategories() { + try { + const response = await apiRoute.getKnowledgeCategories() + + if (response && response.code === 1 && response.data) { + // 构建分类标签,只包含有数据的分类 + const apiCategories = response.data.map(category => ({ + value: category.value.toString(), + text: category.text, + icon: category.icon, + count: category.count + })) + + // 重新构建分类标签数组,保留"全部"选项 + this.categoryTabs = [ + { value: 'all', text: '全部', icon: '📖', count: 0 }, + ...apiCategories + ] + + // 更新全部分类的计数 + const totalCount = apiCategories.reduce((sum, cat) => sum + cat.count, 0) + this.categoryTabs[0].count = totalCount + } + } catch (error) { + console.error('获取知识库分类失败:', error) + // API失败时保持默认分类 + } + }, + + // 加载知识库统计信息 + async loadKnowledgeStats() { + try { + const response = await apiRoute.getKnowledgeStats({ + student_id: this.studentId + }) + + if (response && response.code === 1) { + this.knowledgeStats = response.data + } + } catch (error) { + console.error('获取知识库统计失败:', error) + } + }, + async loadRecommendArticles() { try { - this.recommendArticles = this.getMockRecommendData() + // 调用真实API获取推荐文章 + const response = await apiRoute.getRecommendArticles({ + student_id: this.studentId, + limit: 5 + }) + + if (response && response.code === 1) { + this.recommendArticles = response.data || [] + } else { + // 降级到Mock数据 + this.recommendArticles = this.getMockRecommendData() + } } catch (error) { console.error('获取推荐文章失败:', error) - this.recommendArticles = [] + // 降级到Mock数据 + this.recommendArticles = this.getMockRecommendData() } }, @@ -526,24 +585,26 @@ await this.loadArticles() }, - changeCategory(category) { + async changeCategory(category) { this.activeCategory = category this.currentPage = 1 - this.applyCategoryFilter() + this.hasMore = true + // 重新加载对应分类的文章 + await this.loadArticles() }, applyCategoryFilter() { if (this.activeCategory === 'all') { this.filteredArticles = [...this.articlesList] } else { - this.filteredArticles = this.articlesList.filter(article => article.category === this.activeCategory) + this.filteredArticles = this.articlesList.filter(article => article.table_type === this.activeCategory) } }, updateCategoryCounts() { const counts = {} this.articlesList.forEach(article => { - counts[article.category] = (counts[article.category] || 0) + 1 + counts[article.table_type] = (counts[article.table_type] || 0) + 1 }) this.categoryTabs.forEach(tab => { @@ -556,11 +617,19 @@ }, getCategoryText(category) { + // 如果传入的是table_type,直接使用categoryMap return this.categoryMap[category] || category }, formatDate(dateString) { - const date = new Date(dateString) + let date + // 处理时间戳或日期字符串 + if (typeof dateString === 'number') { + date = new Date(dateString * 1000) // 假设是秒级时间戳 + } else { + date = new Date(dateString) + } + const now = new Date() const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)) @@ -595,8 +664,20 @@ try { console.log('标记文章已读:', article.id) - // 模拟API调用 - await new Promise(resolve => setTimeout(resolve, 200)) + // 调用真实API标记已读 + const response = await apiRoute.markArticleRead({ + article_id: article.id, + student_id: this.studentId + }) + + if (response && response.code === 1) { + // 更新本地状态 + article.is_read = true + console.log('标记已读成功:', response.msg) + } else { + // 即使API失败也更新UI状态 + article.is_read = true + } // 更新本地状态 const index = this.articlesList.findIndex(item => item.id === article.id) @@ -613,11 +694,14 @@ try { console.log('切换收藏状态:', article.id, !article.is_favorite) - // 模拟API调用 - await new Promise(resolve => setTimeout(resolve, 300)) - const mockResponse = { code: 1, message: article.is_favorite ? '已取消收藏' : '收藏成功' } + // 调用真实API + const response = await apiRoute.toggleArticleFavorite({ + article_id: article.id, + student_id: this.studentId, + action: article.is_favorite ? 'remove' : 'add' + }) - if (mockResponse.code === 1) { + if (response && response.code === 1) { // 更新本地状态 const index = this.articlesList.findIndex(item => item.id === article.id) if (index !== -1) { @@ -632,7 +716,7 @@ } uni.showToast({ - title: mockResponse.message, + title: response.msg || (article.is_favorite ? '已取消收藏' : '收藏成功'), icon: article.is_favorite ? 'none' : 'success' }) } @@ -665,15 +749,26 @@ try { console.log('搜索文章:', this.searchKeyword) - // 模拟搜索API调用 - await new Promise(resolve => setTimeout(resolve, 500)) + // 调用真实搜索API + const response = await apiRoute.searchKnowledgeArticles({ + student_id: this.studentId, + keyword: this.searchKeyword.trim(), + category: this.activeCategory === 'all' ? '' : this.activeCategory, + page: 1, + limit: 50 + }) - // 简单的本地搜索实现 - const results = this.articlesList.filter(article => - article.title.includes(this.searchKeyword) || - article.summary.includes(this.searchKeyword) || - (article.tags && article.tags.some(tag => tag.includes(this.searchKeyword))) - ) + let results = [] + if (response && response.code === 1 && response.data && response.data.list) { + results = response.data.list + } else { + // 降级到本地搜索 + results = this.articlesList.filter(article => + article.title.includes(this.searchKeyword) || + article.summary.includes(this.searchKeyword) || + (article.tags && article.tags.some(tag => tag.includes(this.searchKeyword))) + ) + } this.filteredArticles = results this.closeSearchPopup() diff --git a/uniapp/pages-student/messages/index.vue b/uniapp/pages-student/messages/index.vue index 12a59240..90673bc5 100644 --- a/uniapp/pages-student/messages/index.vue +++ b/uniapp/pages-student/messages/index.vue @@ -233,107 +233,130 @@ } }, - async loadMessages() { - this.loading = true - try { - console.log('加载消息列表:', this.studentId) + async loadMessages() { + this.loading = true + try { + console.log('加载消息列表:', this.studentId) + + // 调用真实API获取消息列表 + const response = await apiRoute.getStudentMessageList({ + student_id: this.studentId, + message_type: this.activeType === 'all' ? '' : this.activeType, + page: this.currentPage, + limit: 10 + }) + + if (response && response.code === 1 && response.data) { + const apiData = response.data + const newList = apiData.list || [] - // 模拟API调用 - // const response = await apiRoute.getStudentMessages({ - // student_id: this.studentId, - // page: this.currentPage, - // limit: 10 - // }) + // 格式化消息数据,确保字段一致性 + const formattedList = newList.map(message => ({ + id: message.id, + type: message.message_type, + title: message.title, + content: message.content, + sender_name: message.from_name || '系统', + send_time: this.formatTimestamp(message.create_time), + is_read: message.is_read === 1, + attachment_url: message.attachment_url || '' + })) - // 使用模拟数据 - const mockResponse = { - code: 1, - data: { - list: [ - { - id: 1, - type: 'system', - title: '欢迎加入运动识堂', - content: '欢迎您的孩子加入我们的运动训练课程!我们将为您的孩子提供专业的体能训练指导,帮助孩子健康成长。课程安排和相关信息会及时通过消息推送给您,请注意查收。', - sender_name: '系统管理员', - send_time: '2024-01-15 09:00:00', - is_read: false, - attachment_url: '' - }, - { - id: 2, - type: 'notification', - title: '本周课程安排通知', - content: '本周课程安排已更新,请及时查看课程表。周三下午16:00-17:00的基础体能训练课程请准时参加,课程地点:训练馆A。如有疑问请联系教练。', - sender_name: '张教练', - send_time: '2024-01-14 15:30:00', - is_read: true, - attachment_url: '' - }, - { - id: 3, - type: 'homework', - title: '体能训练作业', - content: '请完成以下体能训练作业:1. 每天跳绳100个 2. 俯卧撑10个 3. 仰卧起坐15个。请在下次课程前完成并提交训练视频。', - sender_name: '李教练', - send_time: '2024-01-13 18:20:00', - is_read: false, - attachment_url: '/uploads/homework/homework_guide.pdf' - }, - { - id: 4, - type: 'feedback', - title: '上次课程反馈', - content: '您的孩子在上次的基础体能训练中表现优秀,动作标准,学习积极。建议继续加强核心力量训练,可以适当增加训练强度。', - sender_name: '王教练', - send_time: '2024-01-12 20:15:00', - is_read: true, - attachment_url: '' - }, - { - id: 5, - type: 'reminder', - title: '明日课程提醒', - content: '提醒您的孩子明天下午14:00有专项技能训练课程,请准时到达训练馆B。建议提前10分钟到场进行热身准备。', - sender_name: '系统提醒', - send_time: '2024-01-11 19:00:00', - is_read: true, - attachment_url: '' - } - ], - total: 5, - has_more: false - } + if (this.currentPage === 1) { + this.messagesList = formattedList + } else { + this.messagesList = [...this.messagesList, ...formattedList] } - if (mockResponse.code === 1) { - const newList = mockResponse.data.list || [] - if (this.currentPage === 1) { - this.messagesList = newList - } else { - this.messagesList = [...this.messagesList, ...newList] + // 更新分页信息 + this.hasMore = apiData.has_more || false + this.applyTypeFilter() + console.log('消息数据加载成功:', this.messagesList) + } else { + console.warn('API返回失败,使用Mock数据:', response?.msg) + await this.loadMockMessages() + } + } catch (error) { + console.error('获取消息列表失败:', error) + // 网络错误时降级到Mock数据 + await this.loadMockMessages() + } finally { + this.loading = false + this.loadingMore = false + } + }, + + // 加载Mock数据的方法 + async loadMockMessages() { + const mockResponse = { + code: 1, + data: { + list: [ + { + id: 1, + type: 'system', + title: '欢迎加入运动识堂', + content: '欢迎您的孩子加入我们的运动训练课程!我们将为您的孩子提供专业的体能训练指导,帮助孩子健康成长。课程安排和相关信息会及时通过消息推送给您,请注意查收。', + sender_name: '系统管理员', + send_time: '2024-01-15 09:00:00', + is_read: false, + attachment_url: '' + }, + { + id: 2, + type: 'notification', + title: '本周课程安排通知', + content: '本周课程安排已更新,请及时查看课程表。周三下午16:00-17:00的基础体能训练课程请准时参加,课程地点:训练馆A。如有疑问请联系教练。', + sender_name: '张教练', + send_time: '2024-01-14 15:30:00', + is_read: true, + attachment_url: '' + }, + { + id: 3, + type: 'homework', + title: '体能训练作业', + content: '请完成以下体能训练作业:1. 每天跳绳100个 2. 俯卧撑10个 3. 仰卧起坐15个。请在下次课程前完成并提交训练视频。', + sender_name: '李教练', + send_time: '2024-01-13 18:20:00', + is_read: false, + attachment_url: '/uploads/homework/homework_guide.pdf' + }, + { + id: 4, + type: 'feedback', + title: '上次课程反馈', + content: '您的孩子在上次的基础体能训练中表现优秀,动作标准,学习积极。建议继续加强核心力量训练,可以适当增加训练强度。', + sender_name: '王教练', + send_time: '2024-01-12 20:15:00', + is_read: true, + attachment_url: '' + }, + { + id: 5, + type: 'reminder', + title: '明日课程提醒', + content: '提醒您的孩子明天下午14:00有专项技能训练课程,请准时到达训练馆B。建议提前10分钟到场进行热身准备。', + sender_name: '系统提醒', + send_time: '2024-01-11 19:00:00', + is_read: true, + attachment_url: '' } - - this.hasMore = mockResponse.data.has_more || false - this.applyTypeFilter() - console.log('消息数据加载成功:', this.messagesList) - } else { - uni.showToast({ - title: mockResponse.msg || '获取消息列表失败', - icon: 'none' - }) - } - } catch (error) { - console.error('获取消息列表失败:', error) - uni.showToast({ - title: '获取消息列表失败', - icon: 'none' - }) - } finally { - this.loading = false - this.loadingMore = false + ], + has_more: false } - }, + } + + const newList = mockResponse.data.list || [] + if (this.currentPage === 1) { + this.messagesList = newList + } else { + this.messagesList = [...this.messagesList, ...newList] + } + + this.hasMore = mockResponse.data.has_more || false + this.applyTypeFilter() + }, async loadMoreMessages() { if (this.loadingMore || !this.hasMore) return @@ -410,6 +433,17 @@ return `${year}-${month}-${day} ${hours}:${minutes}` }, + formatTimestamp(timestamp) { + // 处理时间戳(秒级转毫秒级) + const date = new Date(timestamp * 1000) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` + }, + viewMessage(message) { this.selectedMessage = message this.showMessagePopup = true @@ -429,16 +463,33 @@ try { console.log('标记已读:', message.id) - // 模拟API调用 - await new Promise(resolve => setTimeout(resolve, 200)) + // 调用真实API标记已读 + const response = await apiRoute.markStudentMessageRead({ + message_id: message.id, + student_id: this.studentId + }) - // 更新本地状态 + if (response && response.code === 1) { + // 更新本地状态 + const index = this.messagesList.findIndex(msg => msg.id === message.id) + if (index !== -1) { + this.messagesList[index].is_read = true + } + console.log('标记已读成功:', response.msg) + } else { + // 即使API失败也更新UI状态 + const index = this.messagesList.findIndex(msg => msg.id === message.id) + if (index !== -1) { + this.messagesList[index].is_read = true + } + } + } catch (error) { + console.error('标记已读失败:', error) + // 即使网络失败也更新UI状态,保证用户体验 const index = this.messagesList.findIndex(msg => msg.id === message.id) if (index !== -1) { this.messagesList[index].is_read = true } - } catch (error) { - console.error('标记已读失败:', error) } }, @@ -454,24 +505,50 @@ try { console.log('标记全部已读') - // 模拟API调用 - await new Promise(resolve => setTimeout(resolve, 500)) + // 调用真实API批量标记已读 + const response = await apiRoute.markStudentMessageBatchRead({ + student_id: this.studentId, + message_type: this.activeType === 'all' ? '' : this.activeType + }) - // 更新本地状态 + if (response && response.code === 1) { + // 更新本地状态 + this.messagesList.forEach(message => { + if (this.activeType === 'all' || message.type === this.activeType) { + message.is_read = true + } + }) + + uni.showToast({ + title: response.msg || '已全部标记为已读', + icon: 'success' + }) + } else { + // 即使API失败也更新UI状态 + this.messagesList.forEach(message => { + if (this.activeType === 'all' || message.type === this.activeType) { + message.is_read = true + } + }) + + uni.showToast({ + title: '已全部标记为已读', + icon: 'success' + }) + } + } catch (error) { + console.error('标记全部已读失败:', error) + // 即使网络失败也更新UI状态 this.messagesList.forEach(message => { - message.is_read = true + if (this.activeType === 'all' || message.type === this.activeType) { + message.is_read = true + } }) uni.showToast({ title: '已全部标记为已读', icon: 'success' }) - } catch (error) { - console.error('标记全部已读失败:', error) - uni.showToast({ - title: '操作失败', - icon: 'none' - }) } }, diff --git a/学员端消息管理数据库分析报告.md b/学员端消息管理数据库分析报告.md new file mode 100644 index 00000000..5a92d6ec --- /dev/null +++ b/学员端消息管理数据库分析报告.md @@ -0,0 +1,322 @@ +# 学员端消息管理数据库分析报告 + +## 📋 **需求分析** + +基于 `pages-student/messages/index.vue` 页面分析,学员端消息管理需要实现: + +1. **消息列表展示**:显示学员接收的所有消息 +2. **消息类型筛选**:按消息类型进行分类筛选 +3. **消息搜索功能**:关键词搜索消息内容(**当前缺失**) +4. **消息已读状态**:标记消息已读/未读 +5. **消息数量统计**:显示未读消息数量和总消息数 +6. **业务页面跳转**:根据 `message_type` 跳转到对应业务页面(**当前缺失**) + +## 🗄️ **数据库表结构分析** + +### **1. school_chat_messages 表** + +#### **现有字段分析** +基于模型文件分析,该表应包含以下字段: +- `id` - 主键ID +- `from_type` - 发送者类型(enum: personnel, customer, system) +- `from_id` - 发送者ID +- `to_id` - 接收者ID +- `friend_id` - 关联 chat_friends 表ID +- `message_type` - 消息类型(enum: text, img, order, student_courses, person_course_schedule) +- `content` - 消息内容 +- `create_time` - 创建时间 +- `update_time` - 更新时间 +- `delete_time` - 删除时间(软删除) + +#### **缺失字段** +根据前端需求,需要添加以下字段: +```sql +-- 消息已读状态 +ALTER TABLE `school_chat_messages` ADD COLUMN `is_read` tinyint(1) DEFAULT 0 COMMENT '是否已读 0-未读 1-已读'; +ALTER TABLE `school_chat_messages` ADD COLUMN `read_time` timestamp NULL DEFAULT NULL COMMENT '已读时间'; + +-- 消息标题和业务关联 +ALTER TABLE `school_chat_messages` ADD COLUMN `title` varchar(255) DEFAULT '' COMMENT '消息标题'; +ALTER TABLE `school_chat_messages` ADD COLUMN `business_id` int(11) DEFAULT NULL COMMENT '关联业务ID'; +ALTER TABLE `school_chat_messages` ADD COLUMN `business_type` varchar(50) DEFAULT '' COMMENT '业务类型'; +``` + +### **2. school_chat_friends 表** + +#### **现有字段分析** +基于模型文件分析: +- `id` - 主键ID +- `personnel_id` - 员工ID +- `customer_resources_id` - 客户资源ID +- `create_time` - 创建时间 +- `update_time` - 更新时间 +- `delete_time` - 删除时间(软删除) + +#### **缺失字段** +需要添加消息数量统计字段: +```sql +-- 消息数量统计 +ALTER TABLE `school_chat_friends` ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数'; +ALTER TABLE `school_chat_friends` ADD COLUMN `unread_messages` int(11) DEFAULT 0 COMMENT '未读消息数'; +ALTER TABLE `school_chat_friends` ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间'; +ALTER TABLE `school_chat_friends` ADD COLUMN `last_message_content` text COMMENT '最后消息内容'; +``` + +## ❌ **数据库设计问题分析** + +### **1. 消息类型枚举不匹配** + +#### **数据库枚举值** +```sql +message_type enum('text','img','order','student_courses','person_course_schedule') +- text:文本消息 +- img:图片消息 +- order:订单消息 +- student_courses:学员课程变动消息 +- person_course_schedule:课程安排消息 +``` + +#### **前端期望的类型** +```javascript +typeTabs: [ + { value: 'all', text: '全部', count: 0 }, + { value: 'system', text: '系统消息', count: 0 }, + { value: 'notification', text: '通知公告', count: 0 }, + { value: 'homework', text: '作业任务', count: 0 }, + { value: 'feedback', text: '反馈评价', count: 0 }, + { value: 'reminder', text: '课程提醒', count: 0 } +] +``` + +**问题**:数据库枚举值与前端期望的消息类型完全不匹配! + +#### **解决方案** +需要重新设计 `message_type` 枚举值: +```sql +ALTER TABLE `school_chat_messages` +MODIFY COLUMN `message_type` enum( + 'text', -- 文本消息 + 'img', -- 图片消息 + 'system', -- 系统消息 + 'notification', -- 通知公告 + 'homework', -- 作业任务 + 'feedback', -- 反馈评价 + 'reminder', -- 课程提醒 + 'order', -- 订单消息 + 'student_courses', -- 学员课程变动消息 + 'person_course_schedule' -- 课程安排消息 +) DEFAULT 'text' COMMENT '消息类型'; +``` + +### **2. 业务页面跳转逻辑缺失** + +#### **当前问题** +前端 `viewMessage()` 方法只是打开弹窗显示消息详情,没有根据 `message_type` 跳转到对应的业务页面。 + +#### **需要的跳转逻辑** +```javascript +// 根据消息类型跳转到对应业务页面 +navigateToBusinessPage(message) { + const routeMap = { + 'order': `/pages-student/orders/detail?id=${message.business_id}`, + 'student_courses': `/pages-student/courses/detail?id=${message.business_id}`, + 'person_course_schedule': `/pages-student/schedule/detail?id=${message.business_id}`, + 'homework': `/pages-student/homework/detail?id=${message.business_id}`, + 'notification': `/pages-student/notifications/detail?id=${message.business_id}`, + 'reminder': `/pages-student/reminders/detail?id=${message.business_id}` + }; + + const route = routeMap[message.message_type]; + if (route) { + uni.navigateTo({ url: route }); + } +} +``` + +### **3. 搜索功能缺失** + +#### **前端缺失** +当前页面没有搜索框和搜索功能实现。 + +#### **需要添加的功能** +```vue + + + + + 🔍 + + +``` + +#### **后端搜索接口** +```php +// 消息搜索功能 +public function searchMessages($student_id, $keyword, $message_type = '') { + $where = [ + ['to_id', '=', $student_id], + ['delete_time', '=', 0] + ]; + + if (!empty($keyword)) { + $where[] = ['title|content', 'like', "%{$keyword}%"]; + } + + if (!empty($message_type) && $message_type !== 'all') { + $where[] = ['message_type', '=', $message_type]; + } + + return $this->model->where($where) + ->order('create_time desc') + ->paginate(); +} +``` + +## 🔧 **完整的数据库修改方案** + +### **1. 修改 school_chat_messages 表** +```sql +-- 添加缺失字段 +ALTER TABLE `school_chat_messages` +ADD COLUMN `is_read` tinyint(1) DEFAULT 0 COMMENT '是否已读 0-未读 1-已读', +ADD COLUMN `read_time` timestamp NULL DEFAULT NULL COMMENT '已读时间', +ADD COLUMN `title` varchar(255) DEFAULT '' COMMENT '消息标题', +ADD COLUMN `business_id` int(11) DEFAULT NULL COMMENT '关联业务ID', +ADD COLUMN `business_type` varchar(50) DEFAULT '' COMMENT '业务类型'; + +-- 修改消息类型枚举 +ALTER TABLE `school_chat_messages` +MODIFY COLUMN `message_type` enum( + 'text', + 'img', + 'system', + 'notification', + 'homework', + 'feedback', + 'reminder', + 'order', + 'student_courses', + 'person_course_schedule' +) DEFAULT 'text' COMMENT '消息类型'; +``` + +### **2. 修改 school_chat_friends 表** +```sql +-- 添加消息统计字段 +ALTER TABLE `school_chat_friends` +ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数', +ADD COLUMN `unread_messages` int(11) DEFAULT 0 COMMENT '未读消息数', +ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间', +ADD COLUMN `last_message_content` text COMMENT '最后消息内容'; +``` + +### **3. 创建消息统计触发器** +```sql +-- 创建触发器自动更新消息统计 +DELIMITER $$ +CREATE TRIGGER update_message_stats_after_insert +AFTER INSERT ON school_chat_messages +FOR EACH ROW +BEGIN + UPDATE school_chat_friends + SET + total_messages = total_messages + 1, + unread_messages = unread_messages + 1, + last_message_time = NEW.create_time, + last_message_content = NEW.content + WHERE id = NEW.friend_id; +END$$ + +CREATE TRIGGER update_message_stats_after_update +AFTER UPDATE ON school_chat_messages +FOR EACH ROW +BEGIN + IF OLD.is_read = 0 AND NEW.is_read = 1 THEN + UPDATE school_chat_friends + SET unread_messages = unread_messages - 1 + WHERE id = NEW.friend_id; + END IF; +END$$ +DELIMITER ; +``` + +## 🎯 **前端功能补充建议** + +### **1. 添加搜索功能** +```vue + + + + + 🔍 + + +``` + +### **2. 添加业务页面跳转** +```javascript +// 修改 viewMessage 方法 +viewMessage(message) { + // 如果有关联业务,直接跳转 + if (message.business_id && message.business_type) { + this.navigateToBusinessPage(message); + } else { + // 否则显示消息详情弹窗 + this.selectedMessage = message; + this.showMessagePopup = true; + } + + // 标记为已读 + if (!message.is_read) { + this.markAsRead(message); + } +} +``` + +### **3. 完善消息类型映射** +```javascript +getTypeText(type) { + const typeMap = { + 'text': '文本消息', + 'img': '图片消息', + 'system': '系统消息', + 'notification': '通知公告', + 'homework': '作业任务', + 'feedback': '反馈评价', + 'reminder': '课程提醒', + 'order': '订单消息', + 'student_courses': '课程变动', + 'person_course_schedule': '课程安排' + }; + return typeMap[type] || type; +} +``` + +## 📊 **总结** + +### **主要问题** +1. ❌ **消息类型枚举不匹配**:数据库枚举与前端期望完全不同 +2. ❌ **缺少已读状态字段**:无法实现已读/未读功能 +3. ❌ **缺少搜索功能**:前端没有搜索框,后端没有搜索接口 +4. ❌ **缺少业务跳转逻辑**:无法根据消息类型跳转到业务页面 +5. ❌ **缺少消息统计字段**:chat_friends 表无法统计消息数量 + +### **修复优先级** +1. **高优先级**:修改消息类型枚举,添加已读状态字段 +2. **中优先级**:添加搜索功能,完善业务跳转逻辑 +3. **低优先级**:添加消息统计字段和触发器 + +### **建议** +建议先修改数据库结构,然后完善前端功能,最后实现后端接口,确保学员端消息管理功能完整可用。 diff --git a/学员端知识库模块详细开发任务.md b/学员端知识库模块详细开发任务.md new file mode 100644 index 00000000..f452cebc --- /dev/null +++ b/学员端知识库模块详细开发任务.md @@ -0,0 +1,574 @@ +# 学员端知识库模块详细开发任务 + +## 📋 **功能需求分析** + +基于 `pages-student/knowledge/index.vue` 页面分析,知识库模块需要实现以下功能: + +### **前端页面功能** +1. **知识文章列表展示**:支持分页加载 +2. **分类筛选**:按知识库类型筛选内容 +3. **推荐文章**:首页推荐优质内容 +4. **搜索功能**:关键词搜索文章 +5. **收藏功能**:收藏/取消收藏文章 +6. **阅读状态**:标记文章已读 +7. **统计信息**:总文章数、收藏数统计 +8. **文章详情**:跳转到详情页面查看 + +### **数据表结构** +- **主表**:`school_lesson_course_teaching` - 知识库内容表 +- **权限控制**:通过 `student_ids` 字段(逗号分割)控制访问权限 +- **分类字段**:`table_type` 字段区分不同类型的知识内容 + +### **实际数据库字段** +基于 `school_lesson_course_teaching` 表的实际结构: +- `id` - 主键ID +- `title` - 标题 +- `image` - 图片/封面 +- `type` - 类型 +- `url` - 链接地址 +- `content` - 内容(富文本) +- `status` - 状态 +- `create_time` - 创建时间 +- `update_time` - 更新时间 +- `delete_time` - 删除时间(软删除) +- `table_type` - 分类类型(1-29枚举值) +- `user_permission` - 用户权限 +- `exam_papers_id` - 试卷ID +- `student_ids` - 学员权限控制(逗号分割) + +### **table_type 枚举值详细说明** +根据数据库字段注释,`table_type` 包含以下29种类型: + +**教案库类型(1-8)**: +- `1`:课程教学大纲 +- `2`:跳绳教案库 +- `3`:增高教案库 +- `4`:篮球教案库 +- `5`:强化教案库 +- `6`:空中忍者教案库 +- `7`:少儿安防教案库 +- `8`:体能教案库 + +**动作库类型(9-12, 17-22)**: +- `9`:热身动作库 +- `10`:体能动作库 +- `11`:趣味游戏库 +- `12`:放松动作库 +- `17`:空中忍者动作 +- `18`:篮球动作 +- `19`:跳绳动作 +- `20`:跑酷动作 +- `21`:安防动作 +- `22`:标准化动作 + +**训练内容类型(13-16)**: +- `13`:训练内容 +- `14`:训练视频 +- `15`:课后作业 +- `16`:优秀一堂课 + +**体测相关类型(23-26)**: +- `23`:3-6岁体测 +- `24`:7+体测 +- `25`:3-6岁体测讲解—解读 +- `26`:7+岁体测讲解—解读 + +**游戏互动类型(27-29)**: +- `27`:互动游戏 +- `28`:套圈游戏 +- `29`:鼓励方式 + +## 🔧 **后端开发任务细化** + +### **任务1:知识库基础接口开发** +**负责人**:后端开发者 +**工期**:1天 +**优先级**:高 + +#### **1.1 获取知识文章列表接口** +```php +GET /api/student/knowledge/list/{student_id} +``` + +**请求参数**: +- `student_id` (必填): 学员ID +- `category` (可选): 分类筛选,对应 table_type 字段 +- `page` (可选): 页码,默认1 +- `limit` (可选): 每页数量,默认10 +- `keyword` (可选): 搜索关键词 + +**响应格式**: +```json +{ + "code": 1, + "msg": "获取成功", + "data": { + "list": [ + { + "id": 1, + "title": "少儿体适能训练的核心要素", + "image": "https://example.com/cover.jpg", + "content": "富文本内容", + "table_type": "1", + "category_name": "课程教学大纲", + "type": 1, + "url": "", + "status": 1, + "create_time": 1705294800, + "update_time": 1705294800, + "user_permission": "1,2,3", + "is_read": false, + "is_favorite": false + } + ], + "current_page": 1, + "last_page": 5, + "total": 45, + "per_page": 10 + } +} +``` + +**核心业务逻辑**: +```php +// 1. 权限验证:检查 student_ids 字段是否包含当前学员ID +$where[] = ['student_ids', 'like', "%,{$student_id},%"]; +// 或者使用 FIND_IN_SET 函数 +$where[] = ['', 'exp', "FIND_IN_SET({$student_id}, student_ids)"]; + +// 2. 状态筛选:只查询启用状态的内容 +$where[] = ['status', '=', 1]; + +// 3. 软删除筛选:排除已删除的内容 +$where[] = ['delete_time', '=', 0]; + +// 4. 分类筛选 +if (!empty($category)) { + $where[] = ['table_type', '=', $category]; +} + +// 5. 关键词搜索(只搜索标题和内容) +if (!empty($keyword)) { + $where[] = ['title|content', 'like', "%{$keyword}%"]; +} + +// 6. 查询收藏状态和阅读状态(需要关联查询) +// 7. 分页查询 +// 8. 数据格式化(时间戳转换等) +``` + +#### **1.2 获取知识分类列表接口** +```php +GET /api/student/knowledge/categories +``` + +**响应格式**: +```json +{ + "code": 1, + "msg": "获取成功", + "data": [ + { + "value": "1", + "text": "课程教学大纲", + "icon": "📖", + "count": 12 + }, + { + "value": "2", + "text": "跳绳教案库", + "icon": "🏃", + "count": 8 + } + ] +} +``` + +#### **1.3 获取推荐文章接口** +```php +GET /api/student/knowledge/recommend/{student_id} +``` + +**业务逻辑**: +- 返回热门文章(按阅读量排序) +- 或者返回最新发布的文章 +- 限制返回数量(如5篇) + +### **任务2:知识库交互功能开发** +**负责人**:后端开发者 +**工期**:1天 +**优先级**:中 + +#### **2.1 文章详情接口** +```php +GET /api/student/knowledge/detail/{id} +``` + +**请求参数**: +- `id` (必填): 文章ID +- `student_id` (必填): 学员ID(权限验证) + +**响应格式**: +```json +{ + "code": 1, + "msg": "获取成功", + "data": { + "id": 1, + "title": "少儿体适能训练的核心要素", + "image": "https://example.com/cover.jpg", + "content": "

富文本内容...

", + "table_type": "1", + "category_name": "课程教学大纲", + "type": 1, + "url": "", + "status": 1, + "create_time": 1705294800, + "update_time": 1705294800, + "user_permission": "1,2,3", + "exam_papers_id": "", + "is_read": true, + "is_favorite": false + } +} +``` + +#### **2.2 标记文章已读接口** +```php +POST /api/student/knowledge/mark-read +``` + +**请求参数**: +```json +{ + "article_id": 1, + "student_id": 31 +} +``` + +**业务逻辑**: +- 需要创建阅读记录表或在现有表中添加字段 +- 更新文章的阅读次数 +- 防止重复标记 + +#### **2.3 收藏/取消收藏接口** +```php +POST /api/student/knowledge/toggle-favorite +``` + +**请求参数**: +```json +{ + "article_id": 1, + "student_id": 31, + "action": "add" // add-收藏, remove-取消收藏 +} +``` + +**业务逻辑**: +- 需要创建收藏表 `school_student_favorites` +- 支持收藏和取消收藏操作 +- 返回当前收藏状态 + +### **任务3:统计和搜索功能开发** +**负责人**:后端开发者 +**工期**:0.5天 +**优先级**:低 + +#### **3.1 获取知识库统计接口** +```php +GET /api/student/knowledge/stats/{student_id} +``` + +**响应格式**: +```json +{ + "code": 1, + "msg": "获取成功", + "data": { + "total_articles": 45, + "favorites": 12, + "read_articles": 28, + "categories_count": { + "1": 12, + "2": 8, + "3": 6 + } + } +} +``` + +#### **3.2 搜索文章接口** +```php +GET /api/student/knowledge/search/{student_id} +``` + +**请求参数**: +- `keyword` (必填): 搜索关键词 +- `category` (可选): 分类筛选 +- `page` (可选): 页码 + +**搜索范围**: +- 文章标题(title字段) +- 文章内容(content字段) + +## 🗄️ **数据库设计补充** + +### **需要新增的表** + +#### **1. 学员文章阅读记录表** +```sql +CREATE TABLE `school_student_article_reads` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `student_id` int(11) NOT NULL COMMENT '学员ID', + `article_id` int(11) NOT NULL COMMENT '文章ID', + `read_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间', + `read_duration` int(11) DEFAULT 0 COMMENT '阅读时长(秒)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_student_article` (`student_id`, `article_id`), + KEY `idx_student_id` (`student_id`), + KEY `idx_article_id` (`article_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员文章阅读记录表'; +``` + +#### **2. 学员收藏表** +```sql +CREATE TABLE `school_student_favorites` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `student_id` int(11) NOT NULL COMMENT '学员ID', + `target_type` varchar(50) NOT NULL COMMENT '收藏类型:article-文章', + `target_id` int(11) NOT NULL COMMENT '目标ID', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_student_target` (`student_id`, `target_type`, `target_id`), + KEY `idx_student_id` (`student_id`), + KEY `idx_target` (`target_type`, `target_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员收藏表'; +``` + +### **现有表字段补充** + +#### **school_lesson_course_teaching 表优化** +```sql +-- 注意:school_lesson_course_teaching 表已有完整字段结构 +-- 现有字段已足够支持知识库功能,无需添加额外字段 +-- 如果需要统计功能,通过关联查询其他表实现 +``` + +## 🔗 **前后端联调任务** + +### **阶段1:基础功能联调** +**负责人**:前端+后端 +**工期**:0.5天 + +#### **联调内容**: +1. **文章列表接口联调** + - 验证分页功能 + - 验证分类筛选 + - 验证权限控制 + - 验证数据格式 + +2. **分类列表接口联调** + - 验证分类数据正确性 + - 验证统计数量准确性 + +3. **推荐文章接口联调** + - 验证推荐算法 + - 验证数据格式 + +#### **测试用例**: +```javascript +// 1. 测试文章列表 +const listResponse = await apiRoute.getKnowledgeArticles({ + student_id: 31, + category: '', + page: 1, + limit: 10 +}); + +// 2. 测试分类筛选 +const categoryResponse = await apiRoute.getKnowledgeArticles({ + student_id: 31, + category: '1', // 课程教学大纲 + page: 1, + limit: 10 +}); + +// 3. 测试权限控制 +const unauthorizedResponse = await apiRoute.getKnowledgeArticles({ + student_id: 999, // 无权限的学员ID + page: 1, + limit: 10 +}); +``` + +### **阶段2:交互功能联调** +**负责人**:前端+后端 +**工期**:0.5天 + +#### **联调内容**: +1. **文章详情接口联调** + - 验证富文本内容渲染 + - 验证权限控制 + - 验证阅读状态 + +2. **阅读标记功能联调** + - 验证阅读记录创建 + - 验证阅读次数更新 + - 验证重复标记处理 + +3. **收藏功能联调** + - 验证收藏/取消收藏 + - 验证收藏状态同步 + - 验证收藏统计更新 + +#### **测试用例**: +```javascript +// 1. 测试文章详情 +const detailResponse = await apiRoute.getKnowledgeDetail({ + id: 1, + student_id: 31 +}); + +// 2. 测试标记已读 +const readResponse = await apiRoute.markArticleRead({ + article_id: 1, + student_id: 31 +}); + +// 3. 测试收藏功能 +const favoriteResponse = await apiRoute.toggleArticleFavorite({ + article_id: 1, + student_id: 31, + action: 'add' +}); +``` + +### **阶段3:搜索和统计功能联调** +**负责人**:前端+后端 +**工期**:0.5天 + +#### **联调内容**: +1. **搜索功能联调** + - 验证关键词搜索 + - 验证搜索结果准确性 + - 验证搜索性能 + +2. **统计功能联调** + - 验证文章总数统计 + - 验证收藏数统计 + - 验证分类统计 + +#### **测试用例**: +```javascript +// 1. 测试搜索功能 +const searchResponse = await apiRoute.searchKnowledgeArticles({ + student_id: 31, + keyword: '体适能', + category: '', + page: 1, + limit: 10 +}); + +// 2. 测试统计功能 +const statsResponse = await apiRoute.getKnowledgeStats({ + student_id: 31 +}); +``` + +## 📊 **开发进度安排** + +### **第1天:基础接口开发** +- 上午:文章列表接口开发 +- 下午:分类列表和推荐文章接口开发 + +### **第2天:交互功能开发** +- 上午:文章详情和阅读标记接口开发 +- 下午:收藏功能接口开发 + +### **第3天:搜索统计和联调** +- 上午:搜索和统计接口开发 +- 下午:前后端联调测试 + +**总工期:2.5天** + +## ✅ **验收标准** + +### **功能验收** +- [ ] 文章列表正确显示,支持分页 +- [ ] 分类筛选功能正常 +- [ ] 权限控制严格(只能看到有权限的文章) +- [ ] 搜索功能准确 +- [ ] 收藏功能正常 +- [ ] 阅读状态正确标记 +- [ ] 统计数据准确 + +### **性能验收** +- [ ] 文章列表加载时间 < 1秒 +- [ ] 搜索响应时间 < 2秒 +- [ ] 富文本内容渲染正常 +- [ ] 图片加载优化 + +### **安全验收** +- [ ] 权限控制严格 +- [ ] 防止SQL注入 +- [ ] 防止XSS攻击 +- [ ] 敏感数据过滤 + +### **兼容性验收** +- [ ] 富文本内容在小程序中正常显示 +- [ ] 图片在不同设备上正常显示 +- [ ] 分页功能在不同网络环境下正常 + +## 🔧 **技术实现要点** + +### **权限控制实现** +```php +// 使用 FIND_IN_SET 函数检查权限(推荐) +$where[] = ['', 'exp', "FIND_IN_SET({$student_id}, student_ids)"]; + +// 或者使用 LIKE 查询(需要确保 student_ids 格式为 ",1,2,3,") +$where[] = ['student_ids', 'like', "%,{$student_id},%"]; + +// 同时确保只查询启用状态的内容 +$where[] = ['status', '=', 1]; +$where[] = ['delete_time', '=', 0]; +``` + +### **富文本内容处理** +```php +// 过滤危险标签 +$content = strip_tags($content, '


'); + +// 处理图片路径(如果需要) +$content = str_replace('src="/', 'src="' . $domain . '/', $content); + +// 处理时间戳格式化 +$create_time_formatted = date('Y-m-d H:i:s', $create_time); +$update_time_formatted = date('Y-m-d H:i:s', $update_time); +``` + +### **搜索优化** +```php +// 使用全文索引提高搜索性能 +ALTER TABLE `school_lesson_course_teaching` +ADD FULLTEXT KEY `ft_title_content` (`title`, `content`); + +// 全文搜索查询 +$where[] = ['', 'exp', "MATCH(title, content) AGAINST('{$keyword}' IN NATURAL LANGUAGE MODE)"]; +``` + +### **缓存策略** +```php +// 分类列表缓存(1小时) +$categories = Cache::remember('knowledge_categories', 3600, function() { + return $this->getCategoriesFromDB(); +}); + +// 推荐文章缓存(30分钟) +$recommend = Cache::remember("knowledge_recommend_{$student_id}", 1800, function() use ($student_id) { + return $this->getRecommendArticles($student_id); +}); +``` + +这个详细的开发任务文档涵盖了知识库模块的完整开发流程,包括后端接口开发、数据库设计、前后端联调和验收标准,可以作为开发团队的具体执行指南。