diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts index f17f3ff..2203858 100644 --- a/reading-platform-frontend/src/api/school.ts +++ b/reading-platform-frontend/src/api/school.ts @@ -375,16 +375,20 @@ export interface Course { description?: string; coverUrl?: string; coverImagePath?: string; + pictureBookName?: string; category?: string; ageRange?: string; difficultyLevel?: string; durationMinutes?: number; + duration?: number; objectives?: string; status: string; isSystem: number; version?: string; usageCount?: number; teacherCount?: number; + gradeTags?: string[]; + domainTags?: string[]; createdAt?: string; updatedAt?: string; publishedAt?: string; @@ -392,7 +396,9 @@ export interface Course { export interface SchoolCourseQueryParams { keyword?: string; - grade?: string; // 小班|中班|大班 或 small|middle|big + grade?: string; // 小班|中班|大班 + domain?: string; // 健康|语言|社会|科学|艺术(传英文码) + lessonType?: string; // INTRODUCTION|COLLECTIVE|LANGUAGE|HEALTH|SCIENCE|SOCIAL|ART } export const getSchoolCourses = (params?: SchoolCourseQueryParams) => diff --git a/reading-platform-frontend/src/api/teacher.ts b/reading-platform-frontend/src/api/teacher.ts index fbd3221..110332b 100644 --- a/reading-platform-frontend/src/api/teacher.ts +++ b/reading-platform-frontend/src/api/teacher.ts @@ -13,9 +13,17 @@ export interface TeacherCourseQueryParams { pageNum?: number; pageSize?: number; grade?: string; + domain?: string; + lessonType?: string; keyword?: string; } +/** 课程环节标签(列表展示) */ +export interface LessonTag { + name: string; + lessonType: string; +} + export interface TeacherCourse { id: number; name: string; @@ -23,6 +31,7 @@ export interface TeacherCourse { coverImagePath?: string; gradeTags: string[]; domainTags: string[]; + lessonTags?: LessonTag[]; duration: number; avgRating: number; usageCount: number; @@ -62,7 +71,9 @@ export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{ pageNum: params.pageNum, pageSize: params.pageSize, keyword: params.keyword, - category: params.grade, + grade: params.grade, + domain: params.domain, + lessonType: params.lessonType, }, }).then(res => { const list = res.list ?? res.records ?? []; diff --git a/reading-platform-frontend/src/utils/tagMaps.ts b/reading-platform-frontend/src/utils/tagMaps.ts index af5f394..c9b4512 100644 --- a/reading-platform-frontend/src/utils/tagMaps.ts +++ b/reading-platform-frontend/src/utils/tagMaps.ts @@ -6,114 +6,121 @@ // 年级标签映射(英文 → 中文) export const GRADE_TAG_MAP: Record = { // 大写格式 - SMALL: '小班', - MIDDLE: '中班', - BIG: '大班', + SMALL: "小班", + MIDDLE: "中班", + BIG: "大班", // 小写格式 - small: '小班', - middle: '中班', - big: '大班', + small: "小班", + middle: "中班", + big: "大班", }; // 领域标签映射(英文 → 中文)- 导出供其他模块使用 export const DOMAIN_TAG_MAP: Record = { // 旧格式(大写) - LANGUAGE: '语言', - SCIENCE: '科学', - SOCIAL: '社会', - ART: '艺术', - HEALTH: '健康', - MATH: '数学', + LANGUAGE: "语言", + SCIENCE: "科学", + SOCIAL: "社会", + ART: "艺术", + HEALTH: "健康", + MATH: "数学", + + // 课程环节/领域 DOMAIN_ 前缀格式(后端返回) + DOMAIN_HEALTH: "健康", + DOMAIN_LANGUAGE: "语言", + DOMAIN_SOCIAL: "社会", + DOMAIN_SCIENCE: "科学", + DOMAIN_ART: "艺术", // 活动类型作为领域的兼容映射(历史数据兼容) - family: '亲子活动', - FAMILY: '亲子活动', - class: '课堂活动', - CLASS: '课堂活动', - outdoor: '户外活动', - OUTDOOR: '户外活动', - handicraft: '手工活动', - HANDICRAFT: '手工活动', - game: '游戏活动', - GAME: '游戏活动', - music: '音乐活动', - MUSIC: '音乐活动', - exploration: '探索活动', - EXPLORATION: '探索活动', - sports: '运动活动', - SPORTS: '运动活动', - art: '艺术活动', + family: "亲子活动", + FAMILY: "亲子活动", + class: "课堂活动", + CLASS: "课堂活动", + outdoor: "户外活动", + OUTDOOR: "户外活动", + handicraft: "手工活动", + HANDICRAFT: "手工活动", + game: "游戏活动", + GAME: "游戏活动", + music: "音乐活动", + MUSIC: "音乐活动", + exploration: "探索活动", + EXPLORATION: "探索活动", + sports: "运动活动", + SPORTS: "运动活动", + art: "艺术活动", // 新格式(细分领域) // 健康领域 - health_motor: '身体动作发展', - health_hygiene: '生活习惯与能力', - HEALTH_MOTOR: '身体动作发展', - HEALTH_HYGIENE: '生活习惯与能力', + health_motor: "身体动作发展", + health_hygiene: "生活习惯与能力", + HEALTH_MOTOR: "身体动作发展", + HEALTH_HYGIENE: "生活习惯与能力", // 语言领域 - lang_listen: '倾听与表达', - lang_read: '早期阅读', - LANG_LISTEN: '倾听与表达', - LANG_READ: '早期阅读', - language_communication: '语言', - LANGUAGE_COMMUNICATION: '语言', + lang_listen: "倾听与表达", + lang_read: "早期阅读", + LANG_LISTEN: "倾听与表达", + LANG_READ: "早期阅读", + language_communication: "语言", + LANGUAGE_COMMUNICATION: "语言", // 社会领域 - social_interact: '人际交往', - social_adapt: '社会适应', - SOCIAL_INTERACT: '人际交往', - SOCIAL_ADAPT: '社会适应', - social_emotional: '社会', - SOCIAL_EMOTIONAL: '社会', + social_interact: "人际交往", + social_adapt: "社会适应", + SOCIAL_INTERACT: "人际交往", + SOCIAL_ADAPT: "社会适应", + social_emotional: "社会", + SOCIAL_EMOTIONAL: "社会", // 科学领域 - science_explore: '科学探究', - math_cog: '数学认知', - SCIENCE_EXPLORE: '科学探究', - MATH_COG: '数学认知', - science_exploration: '科学', - SCIENCE_EXPLORATION: '科学', + science_explore: "科学探究", + math_cog: "数学认知", + SCIENCE_EXPLORE: "科学探究", + MATH_COG: "数学认知", + science_exploration: "科学", + SCIENCE_EXPLORATION: "科学", // 艺术领域 - art_music: '音乐表现', - art_create: '美术创作', - ART_MUSIC: '音乐表现', - ART_CREATE: '美术创作', - art_creativity: '艺术', - ART_CREATIVITY: '艺术', + art_music: "音乐表现", + art_create: "美术创作", + ART_MUSIC: "音乐表现", + ART_CREATE: "美术创作", + art_creativity: "艺术", + ART_CREATIVITY: "艺术", }; // 年级标签颜色配置 export const GRADE_TAG_COLORS: Record = { - '小班': { bg: '#FFE4E8', text: '#E85A71' }, - '中班': { bg: '#E3F2FD', text: '#1976D2' }, - '大班': { bg: '#FFF8E1', text: '#F9A825' }, + 小班: { bg: "#FFE4E8", text: "#E85A71" }, + 中班: { bg: "#E3F2FD", text: "#1976D2" }, + 大班: { bg: "#FFF8E1", text: "#F9A825" }, }; // 领域标签颜色配置 export const DOMAIN_TAG_COLORS: Record = { - '语言': { bg: '#F3E5F5', text: '#8E24AA' }, - '倾听与表达': { bg: '#F3E5F5', text: '#8E24AA' }, - '早期阅读': { bg: '#EDE7F6', text: '#7B1FA2' }, + 语言: { bg: "#F3E5F5", text: "#8E24AA" }, + 倾听与表达: { bg: "#F3E5F5", text: "#8E24AA" }, + 早期阅读: { bg: "#EDE7F6", text: "#7B1FA2" }, - '科学': { bg: '#E8F5E9', text: '#43A047' }, - '科学探究': { bg: '#E8F5E9', text: '#43A047' }, - '数学认知': { bg: '#F1F8E9', text: '#558B2F' }, + 科学: { bg: "#E8F5E9", text: "#43A047" }, + 科学探究: { bg: "#E8F5E9", text: "#43A047" }, + 数学认知: { bg: "#F1F8E9", text: "#558B2F" }, - '社会': { bg: '#E0F7FA', text: '#0097A7' }, - '人际交往': { bg: '#E0F7FA', text: '#0097A7' }, - '社会适应': { bg: '#E0F2F1', text: '#00695C' }, + 社会: { bg: "#E0F7FA", text: "#0097A7" }, + 人际交往: { bg: "#E0F7FA", text: "#0097A7" }, + 社会适应: { bg: "#E0F2F1", text: "#00695C" }, - '艺术': { bg: '#FFF3E0', text: '#FB8C00' }, - '音乐表现': { bg: '#FFF3E0', text: '#FB8C00' }, - '美术创作': { bg: '#FBE9E7', text: '#E64A19' }, + 艺术: { bg: "#FFF3E0", text: "#FB8C00" }, + 音乐表现: { bg: "#FFF3E0", text: "#FB8C00" }, + 美术创作: { bg: "#FBE9E7", text: "#E64A19" }, - '健康': { bg: '#FFEBEE', text: '#E53935' }, - '身体动作发展': { bg: '#FFEBEE', text: '#E53935' }, - '生活习惯与能力': { bg: '#FFCDD2', text: '#C62828' }, + 健康: { bg: "#FFEBEE", text: "#E53935" }, + 身体动作发展: { bg: "#FFEBEE", text: "#E53935" }, + 生活习惯与能力: { bg: "#FFCDD2", text: "#C62828" }, - '数学': { bg: '#FFF8E1', text: '#F9A825' }, + 数学: { bg: "#FFF8E1", text: "#F9A825" }, }; /** @@ -147,24 +154,32 @@ export function translateDomainTags(tags: string[]): string[] { /** * 获取年级标签样式 */ -export function getGradeTagStyle(tag: string): { background: string; color: string; border: string } { - const colors = GRADE_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' }; +export function getGradeTagStyle(tag: string): { + background: string; + color: string; + border: string; +} { + const colors = GRADE_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } /** * 获取领域标签样式 */ -export function getDomainTagStyle(tag: string): { background: string; color: string; border: string } { - const colors = DOMAIN_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' }; +export function getDomainTagStyle(tag: string): { + background: string; + color: string; + border: string; +} { + const colors = DOMAIN_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -173,45 +188,48 @@ export function getDomainTagStyle(tag: string): { background: string; color: str // 活动类型映射(英文 → 中文) export const ACTIVITY_TYPE_MAP: Record = { // 大写格式 - HANDICRAFT: '手工活动', - GAME: '游戏活动', - MUSIC: '音乐活动', - EXPLORATION: '探索活动', - SPORTS: '运动活动', - OUTDOOR: '户外活动', - FAMILY: '家庭延伸', - ART: '美工活动', - OTHER: '其他', + HANDICRAFT: "手工活动", + GAME: "游戏活动", + MUSIC: "音乐活动", + EXPLORATION: "探索活动", + SPORTS: "运动活动", + OUTDOOR: "户外活动", + FAMILY: "家庭延伸", + ART: "美工活动", + OTHER: "其他", // 小写格式 - handicraft: '手工活动', - game: '游戏活动', - music: '音乐活动', - exploration: '探索活动', - sports: '运动活动', - outdoor: '户外活动', - family: '家庭延伸', - art: '美工活动', - other: '其他', + handicraft: "手工活动", + game: "游戏活动", + music: "音乐活动", + exploration: "探索活动", + sports: "运动活动", + outdoor: "户外活动", + family: "家庭延伸", + art: "美工活动", + other: "其他", // 其他格式 - class: '课堂活动', + class: "课堂活动", }; // 活动类型颜色配置 -export const ACTIVITY_TYPE_COLORS: Record = { - '手工活动': { bg: '#F3E5F5', text: '#8E24AA' }, - '美工活动': { bg: '#F3E5F5', text: '#8E24AA' }, - '游戏活动': { bg: '#FFF3E0', text: '#FB8C00' }, - '音乐活动': { bg: '#E3F2FD', text: '#1976D2' }, - '运动活动': { bg: '#E8F5E9', text: '#43A047' }, - '探索活动': { bg: '#E0F7FA', text: '#0097A7' }, - '户外活动': { bg: '#F1F8E9', text: '#558B2F' }, - '亲子活动': { bg: '#FCE4EC', text: '#C2185B' }, - '家庭延伸': { bg: '#E3F2FD', text: '#1976D2' }, - '课堂活动': { bg: '#FFF8E1', text: '#F9A825' }, - '艺术活动': { bg: '#FBE9E7', text: '#E64A19' }, - '其他': { bg: '#F5F5F5', text: '#666666' }, +export const ACTIVITY_TYPE_COLORS: Record< + string, + { bg: string; text: string } +> = { + 手工活动: { bg: "#F3E5F5", text: "#8E24AA" }, + 美工活动: { bg: "#F3E5F5", text: "#8E24AA" }, + 游戏活动: { bg: "#FFF3E0", text: "#FB8C00" }, + 音乐活动: { bg: "#E3F2FD", text: "#1976D2" }, + 运动活动: { bg: "#E8F5E9", text: "#43A047" }, + 探索活动: { bg: "#E0F7FA", text: "#0097A7" }, + 户外活动: { bg: "#F1F8E9", text: "#558B2F" }, + 亲子活动: { bg: "#FCE4EC", text: "#C2185B" }, + 家庭延伸: { bg: "#E3F2FD", text: "#1976D2" }, + 课堂活动: { bg: "#FFF8E1", text: "#F9A825" }, + 艺术活动: { bg: "#FBE9E7", text: "#E64A19" }, + 其他: { bg: "#F5F5F5", text: "#666666" }, }; /** @@ -224,12 +242,16 @@ export function translateActivityType(type: string): string { /** * 获取活动类型样式 */ -export function getActivityTypeStyle(type: string): { background: string; color: string; border: string } { - const colors = ACTIVITY_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' }; +export function getActivityTypeStyle(type: string): { + background: string; + color: string; + border: string; +} { + const colors = ACTIVITY_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -245,50 +267,50 @@ export function translateActivityDomain(domain: string): string { // 步骤类型映射(英文 → 中文) export const STEP_TYPE_MAP: Record = { // 大写格式 - TEACHING: '教学', - READING: '共读', - DISCUSSION: '讨论', - ACTIVITY: '活动', - GAME: '游戏', - SUMMARY: '总结', - WARMUP: '热身', - PRACTICE: '练习', - INTERACTION: '互动', + TEACHING: "教学", + READING: "共读", + DISCUSSION: "讨论", + ACTIVITY: "活动", + GAME: "游戏", + SUMMARY: "总结", + WARMUP: "热身", + PRACTICE: "练习", + INTERACTION: "互动", // 新增环节类型 - INTRODUCTION: '导入', - CREATIVE: '创作', - CUSTOM: '自定义', + INTRODUCTION: "导入", + CREATIVE: "创作", + CUSTOM: "自定义", // 小写格式 - teaching: '教学', - reading: '共读', - discussion: '讨论', - activity: '活动', - game: '游戏', - summary: '总结', - warmup: '热身', - practice: '练习', - interaction: '互动', - introduction: '导入', - creative: '创作', - custom: '自定义', + teaching: "教学", + reading: "共读", + discussion: "讨论", + activity: "活动", + game: "游戏", + summary: "总结", + warmup: "热身", + practice: "练习", + interaction: "互动", + introduction: "导入", + creative: "创作", + custom: "自定义", }; // 步骤类型颜色配置 export const STEP_TYPE_COLORS: Record = { - '教学': { bg: '#E3F2FD', text: '#1976D2' }, - '共读': { bg: '#F3E5F5', text: '#8E24AA' }, - '阅读': { bg: '#F3E5F5', text: '#8E24AA' }, - '讨论': { bg: '#E0F7FA', text: '#0097A7' }, - '活动': { bg: '#FFF3E0', text: '#FB8C00' }, - '游戏': { bg: '#FCE4EC', text: '#C2185B' }, - '总结': { bg: '#E8F5E9', text: '#43A047' }, - '热身': { bg: '#FFF8E1', text: '#F9A825' }, - '练习': { bg: '#EDE7F6', text: '#673AB7' }, - '互动': { bg: '#FBE9E7', text: '#E64A19' }, - '导入': { bg: '#E8F5E9', text: '#4CAF50' }, - '创作': { bg: '#FCE4EC', text: '#E91E63' }, - '自定义': { bg: '#ECEFF1', text: '#607D8B' }, + 教学: { bg: "#E3F2FD", text: "#1976D2" }, + 共读: { bg: "#F3E5F5", text: "#8E24AA" }, + 阅读: { bg: "#F3E5F5", text: "#8E24AA" }, + 讨论: { bg: "#E0F7FA", text: "#0097A7" }, + 活动: { bg: "#FFF3E0", text: "#FB8C00" }, + 游戏: { bg: "#FCE4EC", text: "#C2185B" }, + 总结: { bg: "#E8F5E9", text: "#43A047" }, + 热身: { bg: "#FFF8E1", text: "#F9A825" }, + 练习: { bg: "#EDE7F6", text: "#673AB7" }, + 互动: { bg: "#FBE9E7", text: "#E64A19" }, + 导入: { bg: "#E8F5E9", text: "#4CAF50" }, + 创作: { bg: "#FCE4EC", text: "#E91E63" }, + 自定义: { bg: "#ECEFF1", text: "#607D8B" }, }; /** @@ -301,12 +323,16 @@ export function translateStepType(type: string): string { /** * 获取步骤类型样式 */ -export function getStepTypeStyle(type: string): { background: string; color: string; border: string } { - const colors = STEP_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' }; +export function getStepTypeStyle(type: string): { + background: string; + color: string; + border: string; +} { + const colors = STEP_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -314,29 +340,32 @@ export function getStepTypeStyle(type: string): { background: string; color: str // 课程状态映射(英文 → 中文) export const COURSE_STATUS_MAP: Record = { - DRAFT: '草稿', - PENDING: '审核中', - REJECTED: '已驳回', - PUBLISHED: '已发布', - ARCHIVED: '已下架', - REVIEWING: '审核中', + DRAFT: "草稿", + PENDING: "审核中", + REJECTED: "已驳回", + PUBLISHED: "已发布", + ARCHIVED: "已下架", + REVIEWING: "审核中", // 小写格式 - draft: '草稿', - pending: '审核中', - rejected: '已驳回', - published: '已发布', - archived: '已下架', - reviewing: '审核中', + draft: "草稿", + pending: "审核中", + rejected: "已驳回", + published: "已发布", + archived: "已下架", + reviewing: "审核中", }; // 课程状态颜色配置 -export const COURSE_STATUS_COLORS: Record = { - '草稿': { bg: '#F5F5F5', text: '#666666' }, - '审核中': { bg: '#E3F2FD', text: '#1976D2' }, - '已驳回': { bg: '#FFEBEE', text: '#E53935' }, - '已发布': { bg: '#E8F5E9', text: '#43A047' }, - '已下架': { bg: '#FFF8E1', text: '#F9A825' }, +export const COURSE_STATUS_COLORS: Record< + string, + { bg: string; text: string } +> = { + 草稿: { bg: "#F5F5F5", text: "#666666" }, + 审核中: { bg: "#E3F2FD", text: "#1976D2" }, + 已驳回: { bg: "#FFEBEE", text: "#E53935" }, + 已发布: { bg: "#E8F5E9", text: "#43A047" }, + 已下架: { bg: "#FFF8E1", text: "#F9A825" }, }; /** @@ -349,13 +378,20 @@ export function translateCourseStatus(status: string): string { /** * 获取课程状态样式 */ -export function getCourseStatusStyle(status: string): { background: string; color: string; border: string } { +export function getCourseStatusStyle(status: string): { + background: string; + color: string; + border: string; +} { const chineseStatus = COURSE_STATUS_MAP[status] || status; - const colors = COURSE_STATUS_COLORS[chineseStatus] || { bg: '#F0F0F0', text: '#666' }; + const colors = COURSE_STATUS_COLORS[chineseStatus] || { + bg: "#F0F0F0", + text: "#666", + }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -363,22 +399,22 @@ export function getCourseStatusStyle(status: string): { background: string; colo // 资源类型映射 export const RESOURCE_TYPE_MAP: Record = { - EBOOK: '电子绘本', - AUDIO: '音频', - VIDEO: '视频', - PPT: 'PPT课件', - POSTER: '教学挂图', - OTHER: '其他资源', - IMAGE: '图片', + EBOOK: "电子绘本", + AUDIO: "音频", + VIDEO: "视频", + PPT: "PPT课件", + POSTER: "教学挂图", + OTHER: "其他资源", + IMAGE: "图片", // 已中文的不转换 - '电子绘本': '电子绘本', - '音频': '音频', - '视频': '视频', - 'PPT课件': 'PPT课件', - '教学挂图': '教学挂图', - '其他资源': '其他资源', - '图片': '图片', + 电子绘本: "电子绘本", + 音频: "音频", + 视频: "视频", + PPT课件: "PPT课件", + 教学挂图: "教学挂图", + 其他资源: "其他资源", + 图片: "图片", }; /** diff --git a/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue b/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue index f470525..e6ea6e4 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue @@ -517,22 +517,26 @@ const previewModalVisible = ref(false); const previewFileUrl = ref(''); const previewFileName = ref(''); -// 年级 +// 年级(gradeTags 规范为 String[],与套餐管理对齐) const grades = computed(() => { - if (!course.value.gradeTags) return []; + const val = course.value.gradeTags; + if (!val) return []; + if (Array.isArray(val)) return val; try { - const tags = JSON.parse(course.value.gradeTags); - return tags; + const tags = JSON.parse(val); + return Array.isArray(tags) ? tags : []; } catch { return []; } }); -// 领域标签(核心发展目标,翻译为中文) +// 领域标签(domainTags 规范为 String[]) const domainTags = computed(() => { - if (!course.value.domainTags) return []; + const val = course.value.domainTags; + if (!val) return []; + if (Array.isArray(val)) return translateDomainTags(val); try { - const tags = JSON.parse(course.value.domainTags); + const tags = JSON.parse(val); return translateDomainTags(Array.isArray(tags) ? tags : []); } catch { return []; diff --git a/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue b/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue index 49013e4..0905ce1 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue @@ -228,7 +228,7 @@ const fetchCourseDetail = async () => { // 基本信息 formData.basic.name = course.name; formData.basic.themeId = course.themeId; - formData.basic.grades = course.gradeTags ? JSON.parse(course.gradeTags) : []; + formData.basic.grades = Array.isArray(course.gradeTags) ? course.gradeTags : (course.gradeTags ? JSON.parse(course.gradeTags) : []); formData.basic.pictureBookName = course.pictureBookName || ''; formData.basic.coreContent = course.coreContent || ''; formData.basic.duration = course.duration || 25; diff --git a/reading-platform-frontend/src/views/admin/courses/CourseListView.vue b/reading-platform-frontend/src/views/admin/courses/CourseListView.vue index 8a06af1..95567c2 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseListView.vue @@ -485,11 +485,12 @@ const iterateCourse = (id: number) => { router.push(`/admin/packages/${id}/iterate`); }; -const parseGradeTags = (gradeTags: string) => { +const parseGradeTags = (gradeTags: string | string[] | undefined): string[] => { if (!gradeTags) return []; + if (Array.isArray(gradeTags)) return gradeTags.map((t) => translateGradeTag(t)); try { const tags = JSON.parse(gradeTags); - return tags.map((t: string) => translateGradeTag(t)); + return Array.isArray(tags) ? tags.map((t: string) => translateGradeTag(t)) : []; } catch { return []; } diff --git a/reading-platform-frontend/src/views/admin/courses/CourseReviewView.vue b/reading-platform-frontend/src/views/admin/courses/CourseReviewView.vue index 146f376..a37fa50 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseReviewView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseReviewView.vue @@ -311,11 +311,12 @@ const viewRejectReason = (record: any) => { rejectReasonVisible.value = true; }; -const parseGradeTags = (gradeTags: string) => { +const parseGradeTags = (gradeTags: string | string[] | undefined): string[] => { if (!gradeTags) return []; + if (Array.isArray(gradeTags)) return gradeTags.map((t) => translateGradeTag(t)); try { const tags = JSON.parse(gradeTags); - return tags.map((t: string) => translateGradeTag(t)); + return Array.isArray(tags) ? tags.map((t: string) => translateGradeTag(t)) : []; } catch { return []; } diff --git a/reading-platform-frontend/src/views/school/courses/CourseDetailView.vue b/reading-platform-frontend/src/views/school/courses/CourseDetailView.vue index 34f1ce1..1ee8662 100644 --- a/reading-platform-frontend/src/views/school/courses/CourseDetailView.vue +++ b/reading-platform-frontend/src/views/school/courses/CourseDetailView.vue @@ -458,6 +458,7 @@ import { } from '@ant-design/icons-vue'; import * as schoolApi from '@/api/school'; import { translateDomainTags } from '@/utils/tagMaps'; +import { parseGradeLevels } from '@/api/collections'; import FilePreviewModal from '@/components/FilePreviewModal.vue'; const router = useRouter(); @@ -528,29 +529,15 @@ const previewModalVisible = ref(false); const previewFileUrl = ref(''); const previewFileName = ref(''); -// 年级 -const grades = computed(() => { - if (!course.value.gradeTags) return []; - try { - const tags = JSON.parse(course.value.gradeTags); - return Array.isArray(tags) ? tags : []; - } catch { - // 可能已经是数组 - return Array.isArray(course.value.gradeTags) ? course.value.gradeTags : []; - } -}); +// 年级(统一使用 parseGradeLevels 解析,兼容多种格式) +const grades = computed(() => + parseGradeLevels(course.value.gradeTags ?? course.value.grade_tags) +); // 领域标签(核心发展目标,翻译为中文) -const domainTags = computed(() => { - if (!course.value.domainTags) return []; - try { - const tags = JSON.parse(course.value.domainTags); - const arr = Array.isArray(tags) ? tags : []; - return translateDomainTags(arr); - } catch { - return Array.isArray(course.value.domainTags) ? translateDomainTags(course.value.domainTags) : []; - } -}); +const domainTags = computed(() => + translateDomainTags(parseGradeLevels(course.value.domainTags ?? course.value.domain_tags)) +); // 是否有课程介绍内容 const hasIntroContent = computed(() => { diff --git a/reading-platform-frontend/src/views/school/courses/CourseListView.vue b/reading-platform-frontend/src/views/school/courses/CourseListView.vue index 4aaadce..462132c 100644 --- a/reading-platform-frontend/src/views/school/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/school/courses/CourseListView.vue @@ -25,26 +25,52 @@ - +
-
- 年级筛选 -
-
- {{ grade.label }} -
+
+
+ 年级 + + 小班 + 中班 + 大班 +
-
-
-
@@ -69,14 +95,12 @@

《{{ course.pictureBookName }}》

- - {{ translateGradeTag(tag) }} - - - {{ translateDomainTag(tag) }} - + {{ tag }} + {{ tag }} + {{ getLessonTypeName(lt.lessonType) }}
@@ -116,7 +140,7 @@
-

{{ (searchKeyword || selectedGrade) ? '未找到匹配的课程' : '暂无课程数据' }}

+

{{ hasFilters ? '暂无符合条件的课程,试试调整筛选条件吧' : '暂无课程数据' }}

授权第一门课程 @@ -139,7 +163,8 @@
@@ -154,9 +179,9 @@
{{ course.name }}
-
《{{ course.pictureBookName }}》
+
《{{ course.pictureBookName }}》
- + {{ tag }}
@@ -199,11 +224,12 @@ import { } from '@ant-design/icons-vue'; import { message } from 'ant-design-vue'; import { - translateGradeTag, - translateDomainTag, + translateGradeTags, + translateDomainTags, getGradeTagStyle, getDomainTagStyle, } from '@/utils/tagMaps'; +import { parseGradeLevels } from '@/api/collections'; import * as schoolApi from '@/api/school'; const router = useRouter(); @@ -212,42 +238,55 @@ const authLoading = ref(false); const authModalVisible = ref(false); const searchKeyword = ref(''); const selectedCourseIds = ref([]); -const selectedGrade = ref(''); // 选中的年级 -// 解析标签(后端返回 JSON 字符串,需解析为数组) -const parseTags = (val: any): string[] => { - if (!val) return []; - if (Array.isArray(val)) return val; - if (typeof val === 'string') { - try { - const parsed = JSON.parse(val); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - return []; -}; - -// 年级选项 -const gradeOptions = [ - { label: '全部', value: '' }, - { label: '小班', value: '小班' }, - { label: '中班', value: '中班' }, - { label: '大班', value: '大班' }, -]; +// 筛选条件(参考教师端) +const filters = reactive({ + grade: undefined as string | undefined, + domain: undefined as string | undefined, + lessonType: undefined as string | undefined, + keyword: '', +}); const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length); const totalUsage = computed(() => courses.value.reduce((sum, c) => sum + (c.usageCount || 0), 0)); +const hasFilters = computed(() => + !!(filters.grade || filters.domain || filters.lessonType || filters.keyword?.trim()) +); -// 年级切换:请求后端筛选 -const handleGradeChange = (value: string) => { - selectedGrade.value = value; - loadCourses(); +// 五大领域:中文 -> 后端英文码 +const DOMAIN_TO_CODE: Record = { + 健康: 'HEALTH', + 语言: 'LANGUAGE', + 社会: 'SOCIAL', + 科学: 'SCIENCE', + 艺术: 'ART', }; -// 搜索:请求后端筛选 -const handleSearch = () => { +// 课程环节类型映射 +const LESSON_TYPE_NAMES: Record = { + INTRODUCTION: '导入课', INTRO: '导入课', COLLECTIVE: '集体课', + LANGUAGE: '语言', HEALTH: '健康', SCIENCE: '科学', SOCIAL: '社会', ART: '艺术', + DOMAIN_HEALTH: '健康', DOMAIN_LANGUAGE: '语言', DOMAIN_SOCIAL: '社会', + DOMAIN_SCIENCE: '科学', DOMAIN_ART: '艺术', +}; +const getLessonTypeName = (type: string) => LESSON_TYPE_NAMES[type] || type; + +const getLessonTagStyle = (type: string) => { + const colors: Record = { + INTRODUCTION: { background: '#E8F5E9', color: '#2E7D32' }, INTRO: { background: '#E8F5E9', color: '#2E7D32' }, + COLLECTIVE: { background: '#E3F2FD', color: '#1565C0' }, + LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, HEALTH: { background: '#FFEBEE', color: '#C62828' }, + SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, SOCIAL: { background: '#E0F7FA', color: '#00838F' }, + ART: { background: '#FFF3E0', color: '#E65100' }, + DOMAIN_HEALTH: { background: '#FFEBEE', color: '#C62828' }, DOMAIN_LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, + DOMAIN_SOCIAL: { background: '#E0F7FA', color: '#00838F' }, DOMAIN_SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, + DOMAIN_ART: { background: '#FFF3E0', color: '#E65100' }, + }; + const c = colors[type] || { background: '#F5F5F5', color: '#666' }; + return { background: c.background, color: c.color, border: 'none' }; +}; + +const handleFilterChange = () => { loadCourses(); }; @@ -280,22 +319,54 @@ const pagination = reactive({ const courses = ref([]); const availableCourses = ref([]); -// 加载课程列表(支持后端搜索与年级筛选) +// 解析标签 +const parseTags = (val: any): string[] => { + if (!val) return []; + if (Array.isArray(val)) { + if (val.length === 0) return []; + if (val[0]?.toString().startsWith('[')) { + try { + return JSON.parse(val.join('')); + } catch { + return []; + } + } + return val; + } + if (typeof val === 'string') { + try { + const parsed = JSON.parse(val); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + return []; +}; + +// 加载课程列表(支持年级、领域、课程类型、关键词筛选) const loadCourses = async () => { loading.value = true; try { - const params: { keyword?: string; grade?: string } = {}; - if (searchKeyword.value?.trim()) params.keyword = searchKeyword.value.trim(); - if (selectedGrade.value) params.grade = selectedGrade.value; + const params: schoolApi.SchoolCourseQueryParams = {}; + if (filters.keyword?.trim()) params.keyword = filters.keyword.trim(); + if (filters.grade) params.grade = filters.grade; + if (filters.domain) params.domain = DOMAIN_TO_CODE[filters.domain] ?? filters.domain; + if (filters.lessonType) params.lessonType = filters.lessonType; const data = await schoolApi.getSchoolCourses(params); - courses.value = (data || []).map((course: any) => ({ - ...course, - gradeTags: parseTags(course.gradeTags), - domainTags: parseTags(course.domainTags), - duration: course.duration ?? course.durationMinutes ?? 0, - pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl, - authorized: course.authorized ?? true, - })); + courses.value = (data || []).map((course: any) => { + const gradeTags = parseTags(course.gradeTags); + const domainTags = parseTags(course.domainTags); + return { + ...course, + gradeTags: translateGradeTags(gradeTags), + domainTags: translateDomainTags(domainTags), + lessonTags: course.lessonTags || [], + duration: course.duration ?? course.durationMinutes ?? 0, + pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl, + authorized: course.authorized ?? true, + }; + }); pagination.total = courses.value.length; } catch (error: any) { message.error(error.response?.data?.message || '加载课程列表失败'); @@ -462,11 +533,8 @@ onMounted(() => { color: rgba(255, 255, 255, 0.8); } -/* 筛选操作栏 */ +/* 筛选操作栏(参考教师端布局) */ .filter-action-bar { - display: flex; - flex-direction: column; - gap: 16px; margin-bottom: 24px; padding: 20px 24px; background: white; @@ -474,51 +542,33 @@ onMounted(() => { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); } -.grade-tabs { +.filter-row { display: flex; align-items: center; + flex-wrap: wrap; gap: 16px; } -.tab-label { - font-size: 14px; - font-weight: 600; - color: #333; - white-space: nowrap; -} - -.tab-buttons { +.filter-item { display: flex; + align-items: center; gap: 8px; } -.grade-tab { - padding: 8px 20px; - border-radius: 10px; +.filter-label { font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - border: none; - background: #F5F5F5; color: #666; + font-weight: 500; } -.grade-tab:hover { - background: #E8F5E9; - color: #43e97b; +.filter-right { + margin-left: auto; } -.grade-tab.active { - background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); - color: white; - box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3); -} - -.action-row { - display: flex; - justify-content: space-between; - align-items: center; +.search-box { + flex: 1; + min-width: 200px; + max-width: 280px; } /* 操作栏 */ diff --git a/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue b/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue index 2741b30..9f4311d 100644 --- a/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue @@ -539,27 +539,29 @@ const previewModalVisible = ref(false); const previewFileUrl = ref(''); const previewFileName = ref(''); -// 年级 +// 年级(gradeTags 规范为 String[],与套餐管理对齐) const grades = computed(() => { - if (!course.value.gradeTags) return []; + const val = course.value.gradeTags; + if (!val) return []; + if (Array.isArray(val)) return translateGradeTags(val); try { - const tags = JSON.parse(course.value.gradeTags); - const translated = Array.isArray(tags) ? tags : []; - return translateGradeTags(translated); + const tags = JSON.parse(val); + return translateGradeTags(Array.isArray(tags) ? tags : []); } catch { - return Array.isArray(course.value.gradeTags) ? translateGradeTags(course.value.gradeTags) : []; + return []; } }); -// 领域标签(核心发展目标,翻译为中文) +// 领域标签(domainTags 规范为 String[]) const domainTags = computed(() => { - if (!course.value.domainTags) return []; + const val = course.value.domainTags; + if (!val) return []; + if (Array.isArray(val)) return translateDomainTags(val); try { - const tags = JSON.parse(course.value.domainTags); - const arr = Array.isArray(tags) ? tags : []; - return translateDomainTags(arr); + const tags = JSON.parse(val); + return translateDomainTags(Array.isArray(tags) ? tags : []); } catch { - return Array.isArray(course.value.domainTags) ? translateDomainTags(course.value.domainTags) : []; + return []; } }); diff --git a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue index f8f0bbe..3335f68 100644 --- a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue @@ -22,13 +22,8 @@
年级 - + 小班 @@ -38,42 +33,36 @@ 大班 - 混合
- 领域 - - 健康 - 语言 - 社会 - 科学 - 艺术 + 课程类型 + + 导入课 + 集体课 + 语言 + 健康 + 科学 + 社会 + 艺术
- - 最受欢迎 - 最新发布 - 评分最高 + + + 最受欢迎 + + + 最新发布 + + + 评分最高 +
@@ -81,26 +70,21 @@
-
+
- +
-
+
+ +
精彩绘本
- + + + {{ (course.avgRating ?? 0).toFixed(1) }}
@@ -112,21 +96,15 @@ {{ course.pictureBookName }}

- +
- - {{ tag }} - - - {{ tag }} + {{ tag }} + {{ tag }} + + {{ getLessonTypeName(lt.lessonType) }}
@@ -144,7 +122,9 @@
@@ -162,13 +142,8 @@
- +
@@ -203,6 +178,7 @@ const loading = ref(false); const filters = reactive({ grade: undefined as string | undefined, domain: undefined as string | undefined, + lessonType: undefined as string | undefined, keyword: '', sort: 'popular', }); @@ -215,27 +191,20 @@ const pagination = reactive({ const courses = ref([]); -// 年级映射(用于筛选) -const gradeMap: Record = { - SMALL: '小班', small: '小班', - MIDDLE: '中班', middle: '中班', - BIG: '大班', big: '大班', -}; - -// 领域映射(用于筛选) -const domainMap: Record = { - LANGUAGE: '语言', language: '语言', lang_listen: '语言', lang_read: '语言', - SCIENCE: '科学', science: '科学', science_explore: '科学', math_cog: '数学', - SOCIAL: '社会', social: '社会', social_interact: '社会', social_adapt: '社会', - ART: '艺术', art: '艺术', art_music: '艺术', art_create: '艺术', - HEALTH: '健康', health: '健康', health_motor: '健康', health_hygiene: '健康', - MATH: '数学', math: '数学', -}; - -// 解析标签(后端可能返回 JSON 字符串或数组) +// 解析标签(与套餐管理 parseGradeLevels 对齐,兼容多种格式) const parseTags = (val: any): string[] => { if (!val) return []; - if (Array.isArray(val)) return val; + if (Array.isArray(val)) { + if (val.length === 0) return []; + if (val[0]?.toString().startsWith('[')) { + try { + return JSON.parse(val.join('')); + } catch { + return []; + } + } + return val; + } if (typeof val === 'string') { try { const parsed = JSON.parse(val); @@ -263,32 +232,79 @@ const handlePageChange = () => { loadCourses(); }; +// 课程环节类型:英文码 -> 中文展示名(兼容 INTRODUCTION/INTRO、DOMAIN_* 等后端格式) +const LESSON_TYPE_NAMES: Record = { + INTRODUCTION: '导入课', + INTRO: '导入课', + COLLECTIVE: '集体课', + LANGUAGE: '语言', + HEALTH: '健康', + SCIENCE: '科学', + SOCIAL: '社会', + ART: '艺术', + DOMAIN_HEALTH: '健康', + DOMAIN_LANGUAGE: '语言', + DOMAIN_SOCIAL: '社会', + DOMAIN_SCIENCE: '科学', + DOMAIN_ART: '艺术', +}; +const getLessonTypeName = (type: string) => LESSON_TYPE_NAMES[type] || type; + +// 课程环节标签样式(按类型区分颜色) +const getLessonTagStyle = (type: string) => { + const colors: Record = { + INTRODUCTION: { background: '#E8F5E9', color: '#2E7D32' }, + INTRO: { background: '#E8F5E9', color: '#2E7D32' }, + COLLECTIVE: { background: '#E3F2FD', color: '#1565C0' }, + LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, + HEALTH: { background: '#FFEBEE', color: '#C62828' }, + SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, + SOCIAL: { background: '#E0F7FA', color: '#00838F' }, + ART: { background: '#FFF3E0', color: '#E65100' }, + DOMAIN_HEALTH: { background: '#FFEBEE', color: '#C62828' }, + DOMAIN_LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, + DOMAIN_SOCIAL: { background: '#E0F7FA', color: '#00838F' }, + DOMAIN_SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, + DOMAIN_ART: { background: '#FFF3E0', color: '#E65100' }, + }; + const c = colors[type] || { background: '#F5F5F5', color: '#666' }; + return { background: c.background, color: c.color, border: 'none' }; +}; + +// 五大领域:中文 -> 后端 domainTags 存储的英文码(用于筛选) +const DOMAIN_TO_CODE: Record = { + 健康: 'HEALTH', + 语言: 'LANGUAGE', + 社会: 'SOCIAL', + 科学: 'SCIENCE', + 艺术: 'ART', +}; + const loadCourses = async () => { loading.value = true; try { - const params: any = { + const params: teacherApi.TeacherCourseQueryParams = { pageNum: pagination.current, pageSize: pagination.pageSize, }; - if (filters.keyword) { - params.keyword = filters.keyword; + if (filters.keyword?.trim()) { + params.keyword = filters.keyword.trim(); } - // 年级筛选映射 + // 年级筛选:直接传小班/中班/大班,后端匹配 gradeTags if (filters.grade) { - const gradeKey = Object.keys(gradeMap).find((key) => gradeMap[key] === filters.grade); - if (gradeKey) { - params.grade = gradeKey; - } + params.grade = filters.grade; } - // 领域筛选映射 + // 领域筛选:传英文码,后端匹配 domainTags(五大领域课配置) if (filters.domain) { - const domainKey = Object.keys(domainMap).find((key) => domainMap[key] === filters.domain); - if (domainKey) { - params.domain = domainKey; - } + params.domain = DOMAIN_TO_CODE[filters.domain] ?? filters.domain; + } + + // 课程类型筛选:传 lessonType,后端匹配 course_lesson + if (filters.lessonType) { + params.lessonType = filters.lessonType; } const data = await teacherApi.getTeacherCourses(params); @@ -299,6 +315,7 @@ const loadCourses = async () => { ...item, gradeTags: translateGradeTags(gradeTags), domainTags: translateDomainTags(domainTags), + lessonTags: item.lessonTags || [], duration: item.duration ?? item.durationMinutes ?? 0, usageCount: item.usageCount ?? 0, avgRating: item.avgRating ?? 0, diff --git a/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue b/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue index 987a006..8f26d2c 100644 --- a/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue @@ -237,7 +237,7 @@ const loadCourseData = async () => { pictureBookName: data.sourceCourse?.name || '', theme: data.themeId ? { id: data.themeId, name: '' } : null, coverImagePath: data.coverImagePath, - gradeTags: data.gradeTags ? JSON.parse(data.gradeTags) : [], + gradeTags: Array.isArray(data.gradeTags) ? data.gradeTags : (data.gradeTags ? JSON.parse(data.gradeTags) : []), domainTags: data.domainTags ? JSON.parse(data.domainTags) : [], duration: data.duration || 25, coreContent: data.coreContent || '', diff --git a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue index 147c949..70ced58 100644 --- a/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue +++ b/reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue @@ -265,7 +265,7 @@ const fetchDetail = async () => { // 基本信息 formData.basic.name = data.name || ''; formData.basic.themeId = data.themeId; - formData.basic.grades = data.gradeTags ? JSON.parse(data.gradeTags) : []; + formData.basic.grades = Array.isArray(data.gradeTags) ? data.gradeTags : (data.gradeTags ? JSON.parse(data.gradeTags) : []); formData.basic.pictureBookName = ''; formData.basic.coreContent = data.coreContent || data.core_content || ''; formData.basic.duration = data.duration || 25; diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/mapper/CoursePackageMapper.java b/reading-platform-java/src/main/java/com/reading/platform/common/mapper/CoursePackageMapper.java index 4e4694c..9b509dc 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/mapper/CoursePackageMapper.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/mapper/CoursePackageMapper.java @@ -3,12 +3,14 @@ package com.reading.platform.common.mapper; import com.reading.platform.dto.response.CourseResponse; import com.reading.platform.entity.CoursePackage; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; import java.util.List; /** * Course Entity Mapper + * gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐 */ @Mapper(componentModel = "spring") public interface CoursePackageMapper { @@ -18,6 +20,8 @@ public interface CoursePackageMapper { /** * Entity 转 Response */ + @Mapping(target = "gradeTags", expression = "java(com.reading.platform.common.util.JsonUtils.parseStringArray(entity.getGradeTags()))") + @Mapping(target = "domainTags", expression = "java(com.reading.platform.common.util.JsonUtils.parseStringArray(entity.getDomainTags()))") CourseResponse toVO(CoursePackage entity); /** @@ -28,5 +32,7 @@ public interface CoursePackageMapper { /** * Response 转 Entity(用于创建/更新时) */ + @Mapping(target = "gradeTags", expression = "java(vo.getGradeTags() != null ? com.reading.platform.common.util.JsonUtils.toJson(vo.getGradeTags()) : null)") + @Mapping(target = "domainTags", expression = "java(vo.getDomainTags() != null ? com.reading.platform.common.util.JsonUtils.toJson(vo.getDomainTags()) : null)") CoursePackage toEntity(CourseResponse vo); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/util/JsonUtils.java b/reading-platform-java/src/main/java/com/reading/platform/common/util/JsonUtils.java index d8bdbfb..e4de1b9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/util/JsonUtils.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/util/JsonUtils.java @@ -1,9 +1,11 @@ package com.reading.platform.common.util; +import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.TypeReference; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.HashMap; @@ -223,6 +225,25 @@ public class JsonUtils { } } + /** + * 解析 JSON 数组为 String[](与套餐管理 gradeLevels 对齐) + * 支持 ["小班","中班"] 或 小班,中班 格式 + */ + public static String[] parseStringArray(String json) { + if (!StringUtils.hasText(json)) { + return new String[0]; + } + try { + if (json.trim().startsWith("[")) { + return JSON.parseArray(json, String.class).toArray(new String[0]); + } + return json.split(","); + } catch (Exception e) { + log.warn("解析 JSON 数组失败: {}", json, e); + return new String[0]; + } + } + /** * 创建空的 JSONObject * diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java index a24f7d8..da08a7b 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java @@ -1,13 +1,19 @@ package com.reading.platform.controller.school; +import com.alibaba.fastjson2.JSON; import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; +import com.reading.platform.dto.response.LessonTagResponse; +import com.reading.platform.dto.response.SchoolCourseResponse; +import com.reading.platform.entity.CourseLesson; import com.reading.platform.entity.CoursePackage; +import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CoursePackageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -15,6 +21,7 @@ import java.util.stream.Collectors; /** * 课程管理控制器(学校端) + * gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐 */ @Slf4j @RestController @@ -24,24 +31,119 @@ import java.util.stream.Collectors; public class SchoolCourseController { private final CoursePackageService courseService; + private final CourseLessonService courseLessonService; @GetMapping @Operation(summary = "获取学校课程包列表") - public Result> getSchoolCourses( + public Result> getSchoolCourses( @RequestParam(required = false) String keyword, - @RequestParam(required = false) String grade) { - log.info("获取学校课程包列表,keyword={}, grade={}", keyword, grade); + @RequestParam(required = false) String grade, + @RequestParam(required = false) String domain, + @RequestParam(required = false) String lessonType) { + log.info("获取学校课程包列表,keyword={}, grade={}, domain={}, lessonType={}", keyword, grade, domain, lessonType); Long tenantId = SecurityUtils.getCurrentTenantId(); - List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade); - return Result.success(courses); + List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, lessonType); + List list = courses.stream() + .map(pkg -> toSchoolCourseResponse(pkg)) + .collect(Collectors.toList()); + // 填充 lessonTags + for (SchoolCourseResponse vo : list) { + List lessons = courseLessonService.findByCourseId(vo.getId()); + vo.setLessonTags(lessons.stream() + .map(l -> LessonTagResponse.builder() + .name(l.getName()) + .lessonType(l.getLessonType()) + .build()) + .collect(Collectors.toList())); + } + return Result.success(list); } @GetMapping("/{id}") @Operation(summary = "获取课程详情") - public Result getSchoolCourse(@PathVariable Long id) { + public Result getSchoolCourse(@PathVariable Long id) { log.info("获取课程详情,id={}", id); Long tenantId = SecurityUtils.getCurrentTenantId(); CoursePackage course = courseService.getCourseByIdWithTenantCheck(id, tenantId); - return Result.success(course); + return Result.success(toSchoolCourseResponse(course)); + } + + /** + * 转换为学校端课程响应(gradeTags/domainTags 规范为 String[]) + */ + private SchoolCourseResponse toSchoolCourseResponse(CoursePackage pkg) { + return SchoolCourseResponse.builder() + .id(pkg.getId()) + .tenantId(pkg.getTenantId()) + .name(pkg.getName()) + .code(pkg.getCode()) + .description(pkg.getDescription()) + .pictureBookName(pkg.getPictureBookName()) + .coverImagePath(pkg.getCoverImagePath()) + .coverUrl(pkg.getCoverUrl()) + .gradeTags(parseJsonArray(pkg.getGradeTags())) + .domainTags(parseJsonArray(pkg.getDomainTags())) + .duration(pkg.getDurationMinutes()) + .usageCount(pkg.getUsageCount()) + .teacherCount(pkg.getTeacherCount()) + .avgRating(pkg.getAvgRating()) + .status(pkg.getStatus()) + .createdAt(pkg.getCreatedAt()) + .updatedAt(pkg.getUpdatedAt()) + .build(); + } + + /** + * 解析 JSON 数组为 String[],兼容多种格式: + * - 标准 JSON: ["小班","中班"] + * - 逗号分隔: 小班,中班 + * - 错误格式(split 导致): ["[\"小班\"", " \"中班\""] -> 提取有效值 + */ + private String[] parseJsonArray(String json) { + if (!StringUtils.hasText(json)) { + return new String[0]; + } + String s = json.trim(); + try { + if (s.startsWith("[")) { + var list = JSON.parseArray(s, String.class); + if (list != null && !list.isEmpty()) { + // 检查是否为被错误 split 的格式,如 ["[\"小班\"", " \"中班\""] + String first = list.get(0); + if (first != null && first.startsWith("[\"") && !first.contains(",")) { + return list.stream() + .map(String::trim) + .map(this::extractQuotedValue) + .filter(v -> v != null && !v.isEmpty()) + .toArray(String[]::new); + } + return list.stream() + .map(v -> v != null ? v.trim() : "") + .filter(v -> !v.isEmpty()) + .toArray(String[]::new); + } + return new String[0]; + } + return java.util.Arrays.stream(s.split(",")) + .map(String::trim) + .filter(v -> !v.isEmpty()) + .toArray(String[]::new); + } catch (Exception e) { + log.warn("解析 JSON 数组失败: {}", json, e); + return new String[0]; + } + } + + private String extractQuotedValue(String s) { + if (s == null) return null; + s = s.trim(); + int start = s.indexOf('"'); + if (start >= 0) { + int end = s.indexOf('"', start + 1); + if (end > start) { + return s.substring(start + 1, end).trim(); + } + } + return s.replaceAll("^\\[\"|\"\\]?$", "").trim(); } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTeacherController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTeacherController.java index ece4b0d..01229c7 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTeacherController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTeacherController.java @@ -1,7 +1,6 @@ package com.reading.platform.controller.school; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.mapper.TeacherMapper; import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; @@ -25,14 +24,13 @@ import java.util.List; public class SchoolTeacherController { private final TeacherService teacherService; - private final TeacherMapper teacherMapper; @Operation(summary = "Create teacher") @PostMapping public Result createTeacher(@Valid @RequestBody TeacherCreateRequest request) { Long tenantId = SecurityUtils.getCurrentTenantId(); Teacher teacher = teacherService.createTeacher(tenantId, request); - return Result.success(teacherMapper.toVO(teacher)); + return Result.success(teacherService.toTeacherResponse(teacher)); } @Operation(summary = "Update teacher") @@ -40,7 +38,7 @@ public class SchoolTeacherController { public Result updateTeacher(@PathVariable Long id, @RequestBody TeacherUpdateRequest request) { Long tenantId = SecurityUtils.getCurrentTenantId(); Teacher teacher = teacherService.updateTeacherWithTenantCheck(id, tenantId, request); - return Result.success(teacherMapper.toVO(teacher)); + return Result.success(teacherService.toTeacherResponse(teacher)); } @Operation(summary = "Get teacher by ID") @@ -48,7 +46,7 @@ public class SchoolTeacherController { public Result getTeacher(@PathVariable Long id) { Long tenantId = SecurityUtils.getCurrentTenantId(); Teacher teacher = teacherService.getTeacherByIdWithTenantCheck(id, tenantId); - return Result.success(teacherMapper.toVO(teacher)); + return Result.success(teacherService.toTeacherResponse(teacher)); } @Operation(summary = "Get teacher page") @@ -60,7 +58,7 @@ public class SchoolTeacherController { @RequestParam(required = false) String status) { Long tenantId = SecurityUtils.getCurrentTenantId(); Page page = teacherService.getTeacherPage(tenantId, pageNum, pageSize, keyword, status); - List voList = teacherMapper.toVO(page.getRecords()); + List voList = teacherService.toTeacherResponseList(page.getRecords()); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); } @@ -74,10 +72,10 @@ public class SchoolTeacherController { @Operation(summary = "Reset teacher password") @PostMapping("/{id}/reset-password") - public Result resetPassword(@PathVariable Long id, @RequestParam String newPassword) { + public Result> resetPassword(@PathVariable Long id) { Long tenantId = SecurityUtils.getCurrentTenantId(); - teacherService.resetPasswordWithTenantCheck(id, tenantId, newPassword); - return Result.success(); + String tempPassword = teacherService.resetPasswordAndReturnTemp(id, tenantId); + return Result.success(java.util.Map.of("tempPassword", tempPassword)); } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java index e47687b..5577303 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java @@ -11,6 +11,7 @@ import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.dto.response.ClassResponse; import com.reading.platform.dto.response.CourseResponse; +import com.reading.platform.dto.response.LessonTagResponse; import com.reading.platform.dto.response.StudentResponse; import com.reading.platform.dto.response.TeacherResponse; import com.reading.platform.entity.ClassTeacher; @@ -18,7 +19,9 @@ import com.reading.platform.entity.Clazz; import com.reading.platform.entity.CoursePackage; import com.reading.platform.entity.Student; import com.reading.platform.entity.Teacher; +import com.reading.platform.entity.CourseLesson; import com.reading.platform.service.ClassService; +import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.StudentService; import com.reading.platform.service.TeacherService; @@ -39,6 +42,7 @@ import java.util.stream.Collectors; public class TeacherCourseController { private final CoursePackageService courseService; + private final CourseLessonService courseLessonService; private final ClassService classService; private final StudentService studentService; private final TeacherService teacherService; @@ -67,18 +71,36 @@ public class TeacherCourseController { @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, - @RequestParam(required = false) String category) { + @RequestParam(required = false) String grade, + @RequestParam(required = false) String domain, + @RequestParam(required = false) String lessonType) { Long tenantId = SecurityUtils.getCurrentTenantId(); - Page page = courseService.getCoursePage(tenantId, pageNum, pageSize, keyword, category, CourseStatus.PUBLISHED.getCode()); + // 按 学校 -> 套餐 -> 课程包 层级查询,支持 grade/domain/lessonType 筛选 + Page page = courseService.getTenantPackageCoursePage( + tenantId, pageNum, pageSize, keyword, grade, domain, lessonType, CourseStatus.PUBLISHED.getCode()); List voList = courseMapper.toVO(page.getRecords()); + // 填充 lessonTags(仅 name、lessonType)供列表 tag 展示 + for (CourseResponse vo : voList) { + List lessons = courseLessonService.findByCourseId(vo.getId()); + vo.setLessonTags(lessons.stream() + .map(l -> LessonTagResponse.builder() + .name(l.getName()) + .lessonType(l.getLessonType()) + .build()) + .collect(Collectors.toList())); + } return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); } @Operation(summary = "获取所有课程") @GetMapping("/courses/all") - public Result> getAllCourses() { + public Result> getAllCourses( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String grade, + @RequestParam(required = false) String domain) { Long tenantId = SecurityUtils.getCurrentTenantId(); - List courses = courseService.getCoursesByTenantId(tenantId); + // 按 学校 -> 套餐 -> 课程包 层级查询 + List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, null); return Result.success(courseMapper.toVO(courses)); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherCreateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherCreateRequest.java index d287ea0..b7a35e0 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherCreateRequest.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherCreateRequest.java @@ -1,15 +1,19 @@ package com.reading.platform.dto.request; +import com.fasterxml.jackson.annotation.JsonAlias; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; +import java.util.List; + @Data @Schema(description = "教师创建请求") public class TeacherCreateRequest { @NotBlank(message = "用户名不能为空") - @Schema(description = "用户名") + @JsonAlias("loginAccount") + @Schema(description = "用户名/登录账号") private String username; @NotBlank(message = "密码不能为空") @@ -20,6 +24,7 @@ public class TeacherCreateRequest { @Schema(description = "姓名") private String name; + @NotBlank(message = "手机号不能为空") @Schema(description = "电话") private String phone; @@ -32,4 +37,7 @@ public class TeacherCreateRequest { @Schema(description = "简介") private String bio; + @Schema(description = "负责班级ID列表") + private List classIds; + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherUpdateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherUpdateRequest.java index c81d760..e5943c2 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherUpdateRequest.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherUpdateRequest.java @@ -3,6 +3,8 @@ package com.reading.platform.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.List; + @Data @Schema(description = "教师更新请求") public class TeacherUpdateRequest { @@ -28,4 +30,7 @@ public class TeacherUpdateRequest { @Schema(description = "状态") private String status; + @Schema(description = "负责班级ID列表") + private List classIds; + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java index bbb3fb6..d9f7c95 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java @@ -138,11 +138,11 @@ public class CourseResponse { @Schema(description = "评估数据") private String assessmentData; - @Schema(description = "年级标签") - private String gradeTags; + @Schema(description = "年级标签(规范为数组,与套餐管理适用年级对齐)") + private String[] gradeTags; - @Schema(description = "领域标签") - private String domainTags; + @Schema(description = "领域标签(规范为数组)") + private String[] domainTags; @Schema(description = "是否有集体课") private Integer hasCollectiveLesson; @@ -197,4 +197,7 @@ public class CourseResponse { @Schema(description = "关联的课程环节") private List courseLessons; + + @Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType)") + private List lessonTags; } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java new file mode 100644 index 0000000..9b5d853 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java @@ -0,0 +1,24 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 课程环节标签(列表展示用,仅 name 和 lessonType) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "课程环节标签") +public class LessonTagResponse { + + @Schema(description = "环节名称") + private String name; + + @Schema(description = "环节类型:INTRODUCTION、COLLECTIVE、LANGUAGE、HEALTH、SCIENCE、SOCIAL、ART") + private String lessonType; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java new file mode 100644 index 0000000..a88fcc1 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java @@ -0,0 +1,72 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 学校端课程响应(gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐) + */ +@Data +@Builder +@Schema(description = "学校端课程响应") +public class SchoolCourseResponse { + + @Schema(description = "ID") + private Long id; + + @Schema(description = "租户 ID") + private Long tenantId; + + @Schema(description = "课程名称") + private String name; + + @Schema(description = "课程编码") + private String code; + + @Schema(description = "描述") + private String description; + + @Schema(description = "绘本名称") + private String pictureBookName; + + @Schema(description = "封面图片路径") + private String coverImagePath; + + @Schema(description = "封面 URL") + private String coverUrl; + + @Schema(description = "年级标签(规范为数组)") + private String[] gradeTags; + + @Schema(description = "领域标签(规范为数组)") + private String[] domainTags; + + @Schema(description = "课程时长(分钟)") + private Integer duration; + + @Schema(description = "使用次数") + private Integer usageCount; + + @Schema(description = "教师数量") + private Integer teacherCount; + + @Schema(description = "平均评分") + private BigDecimal avgRating; + + @Schema(description = "状态") + private String status; + + @Schema(description = "创建时间") + private LocalDateTime createdAt; + + @Schema(description = "更新时间") + private LocalDateTime updatedAt; + + @Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType)") + private List lessonTags; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/TeacherResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/TeacherResponse.java index dbb7b03..4e0042f 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/TeacherResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/TeacherResponse.java @@ -1,10 +1,12 @@ package com.reading.platform.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; /** * 教师响应 @@ -21,7 +23,8 @@ public class TeacherResponse { @Schema(description = "租户 ID") private Long tenantId; - @Schema(description = "用户名") + @JsonProperty("loginAccount") + @Schema(description = "登录账号") private String username; @Schema(description = "姓名") @@ -45,6 +48,15 @@ public class TeacherResponse { @Schema(description = "状态") private String status; + @Schema(description = "负责班级ID列表") + private List classIds; + + @Schema(description = "负责班级名称") + private Object classNames; + + @Schema(description = "授课次数") + private Integer lessonCount; + @Schema(description = "最后登录时间") private LocalDateTime lastLoginAt; diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java index f4926bc..86b8dbf 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java @@ -59,10 +59,28 @@ public interface CoursePackageService extends com.baomidou.mybatisplus.extension /** * 查询租户套餐下的课程 * - * @param tenantId 租户 ID - * @param keyword 关键词(课程名称、绘本名称,可选) - * @param grade 年级筛选(小班/中班/大班 或 small/middle/big,可选) + * @param tenantId 租户 ID + * @param keyword 关键词(课程名称、绘本名称,可选) + * @param grade 年级筛选(小班/中班/大班,可选) + * @param domain 领域筛选(健康/语言/社会/科学/艺术 或对应英文码,可选) + * @param lessonType 课程环节类型筛选(可选) */ - List getTenantPackageCourses(Long tenantId, String keyword, String grade); + List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType); + + /** + * 按 学校 -> 套餐 -> 课程包 层级分页查询教师可用课程 + * 教师端课程中心应使用此方法,通过租户已购买的套餐获取课程包 + * + * @param tenantId 租户(学校)ID + * @param pageNum 页码 + * @param pageSize 每页数量 + * @param keyword 关键词(课程名称、绘本名称,可选) + * @param grade 年级筛选(小班/中班/大班 或 SMALL/MIDDLE/BIG,可选) + * @param domain 领域筛选(健康/语言/社会/科学/艺术 或 HEALTH/LANGUAGE/SOCIAL/SCIENCE/ART,可选) + * @param lessonType 课程环节类型筛选(INTRODUCTION、COLLECTIVE、LANGUAGE、HEALTH、SCIENCE、SOCIAL、ART,可选) + * @param status 课程状态(如 PUBLISHED) + */ + Page getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize, + String keyword, String grade, String domain, String lessonType, String status); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/TeacherService.java b/reading-platform-java/src/main/java/com/reading/platform/service/TeacherService.java index 00de796..8220f00 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/TeacherService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/TeacherService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.reading.platform.dto.request.TeacherCreateRequest; import com.reading.platform.dto.request.TeacherUpdateRequest; +import com.reading.platform.dto.response.TeacherResponse; import com.reading.platform.entity.Teacher; import java.util.List; @@ -63,9 +64,24 @@ public interface TeacherService extends IService { */ void resetPasswordWithTenantCheck(Long id, Long tenantId, String newPassword); + /** + * 重置密码并返回临时密码(带租户验证) + */ + String resetPasswordAndReturnTemp(Long id, Long tenantId); + /** * 根据 ID 列表查询教师 */ List getTeachersByIds(List teacherIds); + /** + * 转换为教师响应(含班级、授课数等扩展信息) + */ + TeacherResponse toTeacherResponse(Teacher teacher); + + /** + * 批量转换为教师响应 + */ + List toTeacherResponseList(List teachers); + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java index 018540b..a651828 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java @@ -30,6 +30,7 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** @@ -226,7 +227,7 @@ public class CoursePackageServiceImpl extends ServiceImpl getTenantPackageCourses(Long tenantId, String keyword, String grade) { + public List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType) { List collectionIds = tenantPackageMapper.selectList( new LambdaQueryWrapper() .eq(TenantPackage::getTenantId, tenantId) @@ -253,6 +254,16 @@ public class CoursePackageServiceImpl extends ServiceImpl(); } + // lessonType 筛选:仅保留包含该类型环节的课程包 + if (StringUtils.hasText(lessonType)) { + Set courseIdsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType).stream() + .collect(Collectors.toSet()); + packageIds = packageIds.stream().filter(courseIdsWithLesson::contains).collect(Collectors.toList()); + if (packageIds.isEmpty()) { + return new ArrayList<>(); + } + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(CoursePackage::getId, packageIds) .eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode()); @@ -271,10 +282,96 @@ public class CoursePackageServiceImpl extends ServiceImpl w + .like(CoursePackage::getDomainTags, domain) + .or().like(CoursePackage::getDomainTags, domainLower) + .or().like(CoursePackage::getDomainTags, "\"" + domain + "\"") + .or().like(CoursePackage::getDomainTags, "\"" + domainLower + "\"")); + } wrapper.orderByDesc(CoursePackage::getUsageCount); return coursePackageMapper.selectList(wrapper); } + @Override + public Page getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize, + String keyword, String grade, String domain, String lessonType, String status) { + int current = pageNum != null && pageNum > 0 ? pageNum : 1; + int size = pageSize != null && pageSize > 0 ? pageSize : 10; + + // 1. 学校 -> 套餐:查询租户已购买的套餐(collection) + List collectionIds = tenantPackageMapper.selectList( + new LambdaQueryWrapper() + .eq(TenantPackage::getTenantId, tenantId) + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE.getCode()) + .isNotNull(TenantPackage::getCollectionId)) + .stream() + .map(TenantPackage::getCollectionId) + .distinct() + .collect(Collectors.toList()); + + if (collectionIds.isEmpty()) { + return new Page<>(current, size, 0); + } + + // 2. 套餐 -> 课程包:查询套餐下的课程包 ID + List packageIds = collectionPackageMapper.selectList( + new LambdaQueryWrapper() + .in(CourseCollectionPackage::getCollectionId, collectionIds)) + .stream() + .map(CourseCollectionPackage::getPackageId) + .distinct() + .collect(Collectors.toList()); + + if (packageIds.isEmpty()) { + return new Page<>(current, size, 0); + } + + // 2.5 lessonType 筛选:仅保留包含该类型环节的课程包 + if (StringUtils.hasText(lessonType)) { + Set courseIdsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType).stream() + .collect(Collectors.toSet()); + packageIds = packageIds.stream().filter(courseIdsWithLesson::contains).collect(Collectors.toList()); + if (packageIds.isEmpty()) { + return new Page<>(current, size, 0); + } + } + + // 3. 分页查询课程包(gradeTags 年级筛选,domainTags 领域筛选) + Page page = new Page<>(current, size); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(CoursePackage::getId, packageIds) + .eq(CoursePackage::getStatus, status != null ? status : CourseStatus.PUBLISHED.getCode()); + + if (StringUtils.hasText(keyword)) { + wrapper.and(w -> w + .like(CoursePackage::getName, keyword) + .or().like(CoursePackage::getPictureBookName, keyword) + .or().like(CoursePackage::getCode, keyword)); + } + // 年级筛选:匹配 gradeTags(支持中文小班/中班/大班 或英文 SMALL/MIDDLE/BIG) + if (StringUtils.hasText(grade)) { + String gradeLower = grade.toLowerCase(); + wrapper.and(w -> w + .like(CoursePackage::getGradeTags, grade) + .or().like(CoursePackage::getGradeTags, gradeLower) + .or().like(CoursePackage::getGradeTags, "\"" + grade + "\"") + .or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\"")); + } + // 领域筛选:匹配 domainTags(支持五大领域中文或英文码 HEALTH/LANGUAGE/SOCIAL/SCIENCE/ART) + if (StringUtils.hasText(domain)) { + String domainLower = domain.toLowerCase(); + wrapper.and(w -> w + .like(CoursePackage::getDomainTags, domain) + .or().like(CoursePackage::getDomainTags, domainLower) + .or().like(CoursePackage::getDomainTags, "\"" + domain + "\"") + .or().like(CoursePackage::getDomainTags, "\"" + domainLower + "\"")); + } + wrapper.orderByDesc(CoursePackage::getUsageCount); + return coursePackageMapper.selectPage(page, wrapper); + } + private CoursePackage buildEntityFromRequest(CourseCreateRequest request) { CoursePackage entity = new CoursePackage(); entity.setName(request.getName()); diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java index 18dc0e8..c58ac0e 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java @@ -6,17 +6,28 @@ import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.dto.request.TeacherCreateRequest; import com.reading.platform.dto.request.TeacherUpdateRequest; +import com.reading.platform.dto.response.TeacherResponse; +import com.reading.platform.entity.ClassTeacher; +import com.reading.platform.entity.Clazz; +import com.reading.platform.entity.Lesson; import com.reading.platform.entity.Teacher; +import com.reading.platform.mapper.ClassTeacherMapper; +import com.reading.platform.mapper.ClazzMapper; +import com.reading.platform.mapper.LessonMapper; import com.reading.platform.mapper.TeacherMapper; +import com.reading.platform.service.ClassService; import com.reading.platform.service.TeacherService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * 教师服务实现类 @@ -28,6 +39,11 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi implements TeacherService { private final TeacherMapper teacherMapper; + private final com.reading.platform.common.mapper.TeacherMapper teacherVoMapper; + private final ClassTeacherMapper classTeacherMapper; + private final ClazzMapper clazzMapper; + private final LessonMapper lessonMapper; + private final ClassService classService; private final PasswordEncoder passwordEncoder; @Override @@ -57,6 +73,18 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi teacherMapper.insert(teacher); + // 分配教师到班级 + if (!CollectionUtils.isEmpty(request.getClassIds())) { + for (Long classId : request.getClassIds()) { + classService.getClassByIdWithTenantCheck(classId, tenantId); + ClassTeacher classTeacher = new ClassTeacher(); + classTeacher.setClassId(classId); + classTeacher.setTeacherId(teacher.getId()); + classTeacher.setRole("MAIN"); + classTeacherMapper.insert(classTeacher); + } + } + log.info("教师创建成功,ID: {}", teacher.getId()); return teacher; } @@ -92,6 +120,21 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi teacherMapper.updateById(teacher); + // 更新教师班级分配 + if (request.getClassIds() != null) { + classTeacherMapper.delete( + new LambdaQueryWrapper().eq(ClassTeacher::getTeacherId, id) + ); + for (Long classId : request.getClassIds()) { + classService.getClassByIdWithTenantCheck(classId, teacher.getTenantId()); + ClassTeacher classTeacher = new ClassTeacher(); + classTeacher.setClassId(classId); + classTeacher.setTeacherId(id); + classTeacher.setRole("MAIN"); + classTeacherMapper.insert(classTeacher); + } + } + log.info("教师更新成功,ID: {}", id); return teacher; } @@ -200,6 +243,18 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi resetPassword(id, newPassword); } + @Override + @Transactional + public String resetPasswordAndReturnTemp(Long id, Long tenantId) { + log.info("开始重置密码并返回临时密码,ID: {}, tenantId: {}", id, tenantId); + Teacher teacher = getTeacherByIdWithTenantCheck(id, tenantId); + String tempPassword = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + teacher.setPassword(passwordEncoder.encode(tempPassword)); + teacherMapper.updateById(teacher); + log.info("密码重置成功,ID: {}", id); + return tempPassword; + } + @Override public List getTeachersByIds(List teacherIds) { log.debug("根据 ID 列表查询教师,ID 列表:{}", teacherIds); @@ -215,4 +270,53 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi return teacherMapper.selectList(wrapper); } + @Override + public TeacherResponse toTeacherResponse(Teacher teacher) { + if (teacher == null) return null; + TeacherResponse response = teacherVoMapper.toVO(teacher); + enrichTeacherResponse(response, teacher.getId()); + return response; + } + + @Override + public List toTeacherResponseList(List teachers) { + if (teachers == null) return null; + List list = new ArrayList<>(teachers.size()); + for (Teacher teacher : teachers) { + list.add(toTeacherResponse(teacher)); + } + return list; + } + + private void enrichTeacherResponse(TeacherResponse response, Long teacherId) { + List classTeachers = classTeacherMapper.selectList( + new LambdaQueryWrapper().eq(ClassTeacher::getTeacherId, teacherId) + ); + List classIds = new ArrayList<>(); + List classNames = new ArrayList<>(); + for (ClassTeacher ct : classTeachers) { + classIds.add(ct.getClassId()); + Clazz clazz = clazzMapper.selectById(ct.getClassId()); + if (clazz != null) { + classNames.add(clazz.getName()); + } + } + response.setClassIds(classIds); + response.setClassNames(classNames.isEmpty() ? null : classNames); + + if ("active".equals(response.getStatus())) { + response.setStatus("ACTIVE"); + } + + long lessonCount = 0; + try { + lessonCount = lessonMapper.selectCount( + new LambdaQueryWrapper().eq(Lesson::getTeacherId, teacherId) + ); + } catch (Exception e) { + log.debug("Query lesson count failed: {}", e.getMessage()); + } + response.setLessonCount((int) lessonCount); + } + }