11 changed files with 4478 additions and 504 deletions
File diff suppressed because it is too large
@ -0,0 +1,233 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | 学员端知识库控制器 |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\api\controller\student; |
|||
|
|||
use app\service\api\student\KnowledgeService; |
|||
use core\base\BaseController; |
|||
use think\Response; |
|||
|
|||
/** |
|||
* 学员知识库管理控制器 |
|||
* 提供知识库相关功能接口 |
|||
*/ |
|||
class KnowledgeController extends BaseController |
|||
{ |
|||
/** |
|||
* 获取知识文章列表 |
|||
* @param int $student_id 学员ID |
|||
* @return Response |
|||
*/ |
|||
public function getKnowledgeList($student_id) |
|||
{ |
|||
$data = $this->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()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,208 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | 学员端消息管理控制器 |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
namespace app\api\controller\student; |
|||
|
|||
use app\service\api\student\MessageService; |
|||
use core\base\BaseController; |
|||
|
|||
/** |
|||
* 学员端消息管理控制器 |
|||
*/ |
|||
class MessageController extends BaseController |
|||
{ |
|||
private MessageService $messageService; |
|||
|
|||
public function __construct() |
|||
{ |
|||
$this->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()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,450 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | 学员端知识库服务类 |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\service\api\student; |
|||
|
|||
use app\model\student\Student; |
|||
use think\facade\Db; |
|||
use core\base\BaseService; |
|||
|
|||
/** |
|||
* 学员知识库服务类 |
|||
*/ |
|||
class KnowledgeService extends BaseService |
|||
{ |
|||
/** |
|||
* table_type 枚举值映射 |
|||
*/ |
|||
private $tableTypeMap = [ |
|||
'1' => ['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); |
|||
} |
|||
} |
|||
@ -0,0 +1,417 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | 学员端消息管理服务层 |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
namespace app\service\api\student; |
|||
|
|||
use core\base\BaseApiService; |
|||
use think\facade\Db; |
|||
|
|||
/** |
|||
* 学员端消息管理服务层 |
|||
*/ |
|||
class MessageService extends BaseApiService |
|||
{ |
|||
/** |
|||
* 获取学员消息列表 |
|||
* @param array $data |
|||
* @return array |
|||
*/ |
|||
public function getMessageList(array $data): array |
|||
{ |
|||
$student_id = $data['student_id']; |
|||
$message_type = $data['message_type'] ?? ''; |
|||
$page = max(1, (int)($data['page'] ?? 1)); |
|||
$limit = max(1, min(100, (int)($data['limit'] ?? 10))); |
|||
$keyword = trim($data['keyword'] ?? ''); |
|||
$is_read = $data['is_read'] ?? ''; |
|||
|
|||
// 构建查询条件 |
|||
$where = [ |
|||
['to_id', '=', $student_id], |
|||
['delete_time', '=', 0] |
|||
]; |
|||
|
|||
// 消息类型筛选 |
|||
if (!empty($message_type) && $message_type !== 'all') { |
|||
$where[] = ['message_type', '=', $message_type]; |
|||
} |
|||
|
|||
// 已读状态筛选 |
|||
if ($is_read !== '') { |
|||
$where[] = ['is_read', '=', (int)$is_read]; |
|||
} |
|||
|
|||
// 关键词搜索 |
|||
if (!empty($keyword)) { |
|||
$where[] = ['title|content', 'like', "%{$keyword}%"]; |
|||
} |
|||
|
|||
try { |
|||
// 获取消息列表 |
|||
$list = Db::name('chat_messages') |
|||
->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 |
|||
]; |
|||
} |
|||
} |
|||
@ -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 |
|||
<!-- 搜索框 --> |
|||
<view class="search_section"> |
|||
<view class="search_box"> |
|||
<input |
|||
type="text" |
|||
placeholder="搜索消息内容..." |
|||
v-model="searchKeyword" |
|||
@input="performSearch" |
|||
class="search_input" |
|||
/> |
|||
<view class="search_icon">🔍</view> |
|||
</view> |
|||
</view> |
|||
``` |
|||
|
|||
#### **后端搜索接口** |
|||
```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 |
|||
<!-- 在消息类型筛选后添加搜索框 --> |
|||
<view class="search_section"> |
|||
<view class="search_box"> |
|||
<input |
|||
type="text" |
|||
placeholder="搜索消息标题或内容..." |
|||
v-model="searchKeyword" |
|||
@input="performSearch" |
|||
class="search_input" |
|||
/> |
|||
<view class="search_icon" @click="performSearch">🔍</view> |
|||
</view> |
|||
</view> |
|||
``` |
|||
|
|||
### **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. **低优先级**:添加消息统计字段和触发器 |
|||
|
|||
### **建议** |
|||
建议先修改数据库结构,然后完善前端功能,最后实现后端接口,确保学员端消息管理功能完整可用。 |
|||
@ -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": "<p>富文本内容...</p>", |
|||
"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, '<p><br><strong><em><u><img><a>'); |
|||
|
|||
// 处理图片路径(如果需要) |
|||
$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); |
|||
}); |
|||
``` |
|||
|
|||
这个详细的开发任务文档涵盖了知识库模块的完整开发流程,包括后端接口开发、数据库设计、前后端联调和验收标准,可以作为开发团队的具体执行指南。 |
|||
Loading…
Reference in new issue