Merge remote-tracking branch 'origin/master'

# Conflicts:
#	reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java
This commit is contained in:
En 2026-03-19 15:27:03 +08:00
commit a2751d7aa5
28 changed files with 1103 additions and 480 deletions

View File

@ -375,16 +375,20 @@ export interface Course {
description?: string; description?: string;
coverUrl?: string; coverUrl?: string;
coverImagePath?: string; coverImagePath?: string;
pictureBookName?: string;
category?: string; category?: string;
ageRange?: string; ageRange?: string;
difficultyLevel?: string; difficultyLevel?: string;
durationMinutes?: number; durationMinutes?: number;
duration?: number;
objectives?: string; objectives?: string;
status: string; status: string;
isSystem: number; isSystem: number;
version?: string; version?: string;
usageCount?: number; usageCount?: number;
teacherCount?: number; teacherCount?: number;
gradeTags?: string[];
domainTags?: string[];
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
publishedAt?: string; publishedAt?: string;
@ -392,7 +396,9 @@ export interface Course {
export interface SchoolCourseQueryParams { export interface SchoolCourseQueryParams {
keyword?: string; 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) => export const getSchoolCourses = (params?: SchoolCourseQueryParams) =>

View File

@ -13,9 +13,17 @@ export interface TeacherCourseQueryParams {
pageNum?: number; pageNum?: number;
pageSize?: number; pageSize?: number;
grade?: string; grade?: string;
domain?: string;
lessonType?: string;
keyword?: string; keyword?: string;
} }
/** 课程环节标签(列表展示) */
export interface LessonTag {
name: string;
lessonType: string;
}
export interface TeacherCourse { export interface TeacherCourse {
id: number; id: number;
name: string; name: string;
@ -23,6 +31,7 @@ export interface TeacherCourse {
coverImagePath?: string; coverImagePath?: string;
gradeTags: string[]; gradeTags: string[];
domainTags: string[]; domainTags: string[];
lessonTags?: LessonTag[];
duration: number; duration: number;
avgRating: number; avgRating: number;
usageCount: number; usageCount: number;
@ -62,7 +71,9 @@ export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{
pageNum: params.pageNum, pageNum: params.pageNum,
pageSize: params.pageSize, pageSize: params.pageSize,
keyword: params.keyword, keyword: params.keyword,
category: params.grade, grade: params.grade,
domain: params.domain,
lessonType: params.lessonType,
}, },
}).then(res => { }).then(res => {
const list = res.list ?? res.records ?? []; const list = res.list ?? res.records ?? [];

View File

@ -6,114 +6,121 @@
// 年级标签映射(英文 → 中文) // 年级标签映射(英文 → 中文)
export const GRADE_TAG_MAP: Record<string, string> = { export const GRADE_TAG_MAP: Record<string, string> = {
// 大写格式 // 大写格式
SMALL: '小班', SMALL: "小班",
MIDDLE: '中班', MIDDLE: "中班",
BIG: '大班', BIG: "大班",
// 小写格式 // 小写格式
small: '小班', small: "小班",
middle: '中班', middle: "中班",
big: '大班', big: "大班",
}; };
// 领域标签映射(英文 → 中文)- 导出供其他模块使用 // 领域标签映射(英文 → 中文)- 导出供其他模块使用
export const DOMAIN_TAG_MAP: Record<string, string> = { export const DOMAIN_TAG_MAP: Record<string, string> = {
// 旧格式(大写) // 旧格式(大写)
LANGUAGE: '语言', LANGUAGE: "语言",
SCIENCE: '科学', SCIENCE: "科学",
SOCIAL: '社会', SOCIAL: "社会",
ART: '艺术', ART: "艺术",
HEALTH: '健康', HEALTH: "健康",
MATH: '数学', MATH: "数学",
// 课程环节/领域 DOMAIN_ 前缀格式(后端返回)
DOMAIN_HEALTH: "健康",
DOMAIN_LANGUAGE: "语言",
DOMAIN_SOCIAL: "社会",
DOMAIN_SCIENCE: "科学",
DOMAIN_ART: "艺术",
// 活动类型作为领域的兼容映射(历史数据兼容) // 活动类型作为领域的兼容映射(历史数据兼容)
family: '亲子活动', family: "亲子活动",
FAMILY: '亲子活动', FAMILY: "亲子活动",
class: '课堂活动', class: "课堂活动",
CLASS: '课堂活动', CLASS: "课堂活动",
outdoor: '户外活动', outdoor: "户外活动",
OUTDOOR: '户外活动', OUTDOOR: "户外活动",
handicraft: '手工活动', handicraft: "手工活动",
HANDICRAFT: '手工活动', HANDICRAFT: "手工活动",
game: '游戏活动', game: "游戏活动",
GAME: '游戏活动', GAME: "游戏活动",
music: '音乐活动', music: "音乐活动",
MUSIC: '音乐活动', MUSIC: "音乐活动",
exploration: '探索活动', exploration: "探索活动",
EXPLORATION: '探索活动', EXPLORATION: "探索活动",
sports: '运动活动', sports: "运动活动",
SPORTS: '运动活动', SPORTS: "运动活动",
art: '艺术活动', art: "艺术活动",
// 新格式(细分领域) // 新格式(细分领域)
// 健康领域 // 健康领域
health_motor: '身体动作发展', health_motor: "身体动作发展",
health_hygiene: '生活习惯与能力', health_hygiene: "生活习惯与能力",
HEALTH_MOTOR: '身体动作发展', HEALTH_MOTOR: "身体动作发展",
HEALTH_HYGIENE: '生活习惯与能力', HEALTH_HYGIENE: "生活习惯与能力",
// 语言领域 // 语言领域
lang_listen: '倾听与表达', lang_listen: "倾听与表达",
lang_read: '早期阅读', lang_read: "早期阅读",
LANG_LISTEN: '倾听与表达', LANG_LISTEN: "倾听与表达",
LANG_READ: '早期阅读', LANG_READ: "早期阅读",
language_communication: '语言', language_communication: "语言",
LANGUAGE_COMMUNICATION: '语言', LANGUAGE_COMMUNICATION: "语言",
// 社会领域 // 社会领域
social_interact: '人际交往', social_interact: "人际交往",
social_adapt: '社会适应', social_adapt: "社会适应",
SOCIAL_INTERACT: '人际交往', SOCIAL_INTERACT: "人际交往",
SOCIAL_ADAPT: '社会适应', SOCIAL_ADAPT: "社会适应",
social_emotional: '社会', social_emotional: "社会",
SOCIAL_EMOTIONAL: '社会', SOCIAL_EMOTIONAL: "社会",
// 科学领域 // 科学领域
science_explore: '科学探究', science_explore: "科学探究",
math_cog: '数学认知', math_cog: "数学认知",
SCIENCE_EXPLORE: '科学探究', SCIENCE_EXPLORE: "科学探究",
MATH_COG: '数学认知', MATH_COG: "数学认知",
science_exploration: '科学', science_exploration: "科学",
SCIENCE_EXPLORATION: '科学', SCIENCE_EXPLORATION: "科学",
// 艺术领域 // 艺术领域
art_music: '音乐表现', art_music: "音乐表现",
art_create: '美术创作', art_create: "美术创作",
ART_MUSIC: '音乐表现', ART_MUSIC: "音乐表现",
ART_CREATE: '美术创作', ART_CREATE: "美术创作",
art_creativity: '艺术', art_creativity: "艺术",
ART_CREATIVITY: '艺术', ART_CREATIVITY: "艺术",
}; };
// 年级标签颜色配置 // 年级标签颜色配置
export const GRADE_TAG_COLORS: Record<string, { bg: string; text: string }> = { export const GRADE_TAG_COLORS: Record<string, { bg: string; text: string }> = {
'小班': { bg: '#FFE4E8', text: '#E85A71' }, : { bg: "#FFE4E8", text: "#E85A71" },
'中班': { bg: '#E3F2FD', text: '#1976D2' }, : { bg: "#E3F2FD", text: "#1976D2" },
'大班': { bg: '#FFF8E1', text: '#F9A825' }, : { bg: "#FFF8E1", text: "#F9A825" },
}; };
// 领域标签颜色配置 // 领域标签颜色配置
export const DOMAIN_TAG_COLORS: Record<string, { bg: string; text: string }> = { export const DOMAIN_TAG_COLORS: Record<string, { bg: string; text: string }> = {
'语言': { bg: '#F3E5F5', text: '#8E24AA' }, : { bg: "#F3E5F5", text: "#8E24AA" },
'倾听与表达': { bg: '#F3E5F5', text: '#8E24AA' }, : { bg: "#F3E5F5", text: "#8E24AA" },
'早期阅读': { bg: '#EDE7F6', text: '#7B1FA2' }, : { bg: "#EDE7F6", text: "#7B1FA2" },
'科学': { bg: '#E8F5E9', text: '#43A047' }, : { bg: "#E8F5E9", text: "#43A047" },
'科学探究': { bg: '#E8F5E9', text: '#43A047' }, : { bg: "#E8F5E9", text: "#43A047" },
'数学认知': { bg: '#F1F8E9', text: '#558B2F' }, : { bg: "#F1F8E9", text: "#558B2F" },
'社会': { bg: '#E0F7FA', text: '#0097A7' }, : { bg: "#E0F7FA", text: "#0097A7" },
'人际交往': { bg: '#E0F7FA', text: '#0097A7' }, : { bg: "#E0F7FA", text: "#0097A7" },
'社会适应': { bg: '#E0F2F1', text: '#00695C' }, : { bg: "#E0F2F1", text: "#00695C" },
'艺术': { bg: '#FFF3E0', text: '#FB8C00' }, : { bg: "#FFF3E0", text: "#FB8C00" },
'音乐表现': { bg: '#FFF3E0', text: '#FB8C00' }, : { bg: "#FFF3E0", text: "#FB8C00" },
'美术创作': { bg: '#FBE9E7', text: '#E64A19' }, : { bg: "#FBE9E7", text: "#E64A19" },
'健康': { bg: '#FFEBEE', text: '#E53935' }, : { bg: "#FFEBEE", text: "#E53935" },
'身体动作发展': { bg: '#FFEBEE', text: '#E53935' }, : { bg: "#FFEBEE", text: "#E53935" },
'生活习惯与能力': { bg: '#FFCDD2', text: '#C62828' }, : { 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 } { export function getGradeTagStyle(tag: string): {
const colors = GRADE_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' }; background: string;
color: string;
border: string;
} {
const colors = GRADE_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" };
return { return {
background: colors.bg, background: colors.bg,
color: colors.text, color: colors.text,
border: 'none', border: "none",
}; };
} }
/** /**
* *
*/ */
export function getDomainTagStyle(tag: string): { background: string; color: string; border: string } { export function getDomainTagStyle(tag: string): {
const colors = DOMAIN_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' }; background: string;
color: string;
border: string;
} {
const colors = DOMAIN_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" };
return { return {
background: colors.bg, background: colors.bg,
color: colors.text, 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<string, string> = { export const ACTIVITY_TYPE_MAP: Record<string, string> = {
// 大写格式 // 大写格式
HANDICRAFT: '手工活动', HANDICRAFT: "手工活动",
GAME: '游戏活动', GAME: "游戏活动",
MUSIC: '音乐活动', MUSIC: "音乐活动",
EXPLORATION: '探索活动', EXPLORATION: "探索活动",
SPORTS: '运动活动', SPORTS: "运动活动",
OUTDOOR: '户外活动', OUTDOOR: "户外活动",
FAMILY: '家庭延伸', FAMILY: "家庭延伸",
ART: '美工活动', ART: "美工活动",
OTHER: '其他', OTHER: "其他",
// 小写格式 // 小写格式
handicraft: '手工活动', handicraft: "手工活动",
game: '游戏活动', game: "游戏活动",
music: '音乐活动', music: "音乐活动",
exploration: '探索活动', exploration: "探索活动",
sports: '运动活动', sports: "运动活动",
outdoor: '户外活动', outdoor: "户外活动",
family: '家庭延伸', family: "家庭延伸",
art: '美工活动', art: "美工活动",
other: '其他', other: "其他",
// 其他格式 // 其他格式
class: '课堂活动', class: "课堂活动",
}; };
// 活动类型颜色配置 // 活动类型颜色配置
export const ACTIVITY_TYPE_COLORS: Record<string, { bg: string; text: string }> = { export const ACTIVITY_TYPE_COLORS: Record<
'手工活动': { bg: '#F3E5F5', text: '#8E24AA' }, string,
'美工活动': { bg: '#F3E5F5', text: '#8E24AA' }, { bg: string; text: string }
'游戏活动': { bg: '#FFF3E0', text: '#FB8C00' }, > = {
'音乐活动': { bg: '#E3F2FD', text: '#1976D2' }, : { bg: "#F3E5F5", text: "#8E24AA" },
'运动活动': { bg: '#E8F5E9', text: '#43A047' }, : { bg: "#F3E5F5", text: "#8E24AA" },
'探索活动': { bg: '#E0F7FA', text: '#0097A7' }, : { bg: "#FFF3E0", text: "#FB8C00" },
'户外活动': { bg: '#F1F8E9', text: '#558B2F' }, : { bg: "#E3F2FD", text: "#1976D2" },
'亲子活动': { bg: '#FCE4EC', text: '#C2185B' }, : { bg: "#E8F5E9", text: "#43A047" },
'家庭延伸': { bg: '#E3F2FD', text: '#1976D2' }, : { bg: "#E0F7FA", text: "#0097A7" },
'课堂活动': { bg: '#FFF8E1', text: '#F9A825' }, : { bg: "#F1F8E9", text: "#558B2F" },
'艺术活动': { bg: '#FBE9E7', text: '#E64A19' }, : { bg: "#FCE4EC", text: "#C2185B" },
'其他': { bg: '#F5F5F5', text: '#666666' }, : { 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 } { export function getActivityTypeStyle(type: string): {
const colors = ACTIVITY_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' }; background: string;
color: string;
border: string;
} {
const colors = ACTIVITY_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" };
return { return {
background: colors.bg, background: colors.bg,
color: colors.text, color: colors.text,
border: 'none', border: "none",
}; };
} }
@ -245,50 +267,50 @@ export function translateActivityDomain(domain: string): string {
// 步骤类型映射(英文 → 中文) // 步骤类型映射(英文 → 中文)
export const STEP_TYPE_MAP: Record<string, string> = { export const STEP_TYPE_MAP: Record<string, string> = {
// 大写格式 // 大写格式
TEACHING: '教学', TEACHING: "教学",
READING: '共读', READING: "共读",
DISCUSSION: '讨论', DISCUSSION: "讨论",
ACTIVITY: '活动', ACTIVITY: "活动",
GAME: '游戏', GAME: "游戏",
SUMMARY: '总结', SUMMARY: "总结",
WARMUP: '热身', WARMUP: "热身",
PRACTICE: '练习', PRACTICE: "练习",
INTERACTION: '互动', INTERACTION: "互动",
// 新增环节类型 // 新增环节类型
INTRODUCTION: '导入', INTRODUCTION: "导入",
CREATIVE: '创作', CREATIVE: "创作",
CUSTOM: '自定义', CUSTOM: "自定义",
// 小写格式 // 小写格式
teaching: '教学', teaching: "教学",
reading: '共读', reading: "共读",
discussion: '讨论', discussion: "讨论",
activity: '活动', activity: "活动",
game: '游戏', game: "游戏",
summary: '总结', summary: "总结",
warmup: '热身', warmup: "热身",
practice: '练习', practice: "练习",
interaction: '互动', interaction: "互动",
introduction: '导入', introduction: "导入",
creative: '创作', creative: "创作",
custom: '自定义', custom: "自定义",
}; };
// 步骤类型颜色配置 // 步骤类型颜色配置
export const STEP_TYPE_COLORS: Record<string, { bg: string; text: string }> = { export const STEP_TYPE_COLORS: Record<string, { bg: string; text: string }> = {
'教学': { bg: '#E3F2FD', text: '#1976D2' }, : { bg: "#E3F2FD", text: "#1976D2" },
'共读': { bg: '#F3E5F5', text: '#8E24AA' }, : { bg: "#F3E5F5", text: "#8E24AA" },
'阅读': { bg: '#F3E5F5', text: '#8E24AA' }, : { bg: "#F3E5F5", text: "#8E24AA" },
'讨论': { bg: '#E0F7FA', text: '#0097A7' }, : { bg: "#E0F7FA", text: "#0097A7" },
'活动': { bg: '#FFF3E0', text: '#FB8C00' }, : { bg: "#FFF3E0", text: "#FB8C00" },
'游戏': { bg: '#FCE4EC', text: '#C2185B' }, : { bg: "#FCE4EC", text: "#C2185B" },
'总结': { bg: '#E8F5E9', text: '#43A047' }, : { bg: "#E8F5E9", text: "#43A047" },
'热身': { bg: '#FFF8E1', text: '#F9A825' }, : { bg: "#FFF8E1", text: "#F9A825" },
'练习': { bg: '#EDE7F6', text: '#673AB7' }, : { bg: "#EDE7F6", text: "#673AB7" },
'互动': { bg: '#FBE9E7', text: '#E64A19' }, : { bg: "#FBE9E7", text: "#E64A19" },
'导入': { bg: '#E8F5E9', text: '#4CAF50' }, : { bg: "#E8F5E9", text: "#4CAF50" },
'创作': { bg: '#FCE4EC', text: '#E91E63' }, : { bg: "#FCE4EC", text: "#E91E63" },
'自定义': { bg: '#ECEFF1', text: '#607D8B' }, : { 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 } { export function getStepTypeStyle(type: string): {
const colors = STEP_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' }; background: string;
color: string;
border: string;
} {
const colors = STEP_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" };
return { return {
background: colors.bg, background: colors.bg,
color: colors.text, 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<string, string> = { export const COURSE_STATUS_MAP: Record<string, string> = {
DRAFT: '草稿', DRAFT: "草稿",
PENDING: '审核中', PENDING: "审核中",
REJECTED: '已驳回', REJECTED: "已驳回",
PUBLISHED: '已发布', PUBLISHED: "已发布",
ARCHIVED: '已下架', ARCHIVED: "已下架",
REVIEWING: '审核中', REVIEWING: "审核中",
// 小写格式 // 小写格式
draft: '草稿', draft: "草稿",
pending: '审核中', pending: "审核中",
rejected: '已驳回', rejected: "已驳回",
published: '已发布', published: "已发布",
archived: '已下架', archived: "已下架",
reviewing: '审核中', reviewing: "审核中",
}; };
// 课程状态颜色配置 // 课程状态颜色配置
export const COURSE_STATUS_COLORS: Record<string, { bg: string; text: string }> = { export const COURSE_STATUS_COLORS: Record<
'草稿': { bg: '#F5F5F5', text: '#666666' }, string,
'审核中': { bg: '#E3F2FD', text: '#1976D2' }, { bg: string; text: string }
'已驳回': { bg: '#FFEBEE', text: '#E53935' }, > = {
'已发布': { bg: '#E8F5E9', text: '#43A047' }, 稿: { bg: "#F5F5F5", text: "#666666" },
'已下架': { bg: '#FFF8E1', text: '#F9A825' }, : { 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 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 { return {
background: colors.bg, background: colors.bg,
color: colors.text, 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<string, string> = { export const RESOURCE_TYPE_MAP: Record<string, string> = {
EBOOK: '电子绘本', EBOOK: "电子绘本",
AUDIO: '音频', AUDIO: "音频",
VIDEO: '视频', VIDEO: "视频",
PPT: 'PPT课件', PPT: "PPT课件",
POSTER: '教学挂图', POSTER: "教学挂图",
OTHER: '其他资源', OTHER: "其他资源",
IMAGE: '图片', IMAGE: "图片",
// 已中文的不转换 // 已中文的不转换
'电子绘本': '电子绘本', : "电子绘本",
'音频': '音频', : "音频",
'视频': '视频', : "视频",
'PPT课件': 'PPT课件', PPT课件: "PPT课件",
'教学挂图': '教学挂图', : "教学挂图",
'其他资源': '其他资源', : "其他资源",
'图片': '图片', : "图片",
}; };
/** /**

View File

@ -517,22 +517,26 @@ const previewModalVisible = ref(false);
const previewFileUrl = ref(''); const previewFileUrl = ref('');
const previewFileName = ref(''); const previewFileName = ref('');
// // gradeTags String[]
const grades = computed(() => { const grades = computed(() => {
if (!course.value.gradeTags) return []; const val = course.value.gradeTags;
if (!val) return [];
if (Array.isArray(val)) return val;
try { try {
const tags = JSON.parse(course.value.gradeTags); const tags = JSON.parse(val);
return tags; return Array.isArray(tags) ? tags : [];
} catch { } catch {
return []; return [];
} }
}); });
// // domainTags String[]
const domainTags = computed(() => { const domainTags = computed(() => {
if (!course.value.domainTags) return []; const val = course.value.domainTags;
if (!val) return [];
if (Array.isArray(val)) return translateDomainTags(val);
try { try {
const tags = JSON.parse(course.value.domainTags); const tags = JSON.parse(val);
return translateDomainTags(Array.isArray(tags) ? tags : []); return translateDomainTags(Array.isArray(tags) ? tags : []);
} catch { } catch {
return []; return [];

View File

@ -228,7 +228,7 @@ const fetchCourseDetail = async () => {
// //
formData.basic.name = course.name; formData.basic.name = course.name;
formData.basic.themeId = course.themeId; 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.pictureBookName = course.pictureBookName || '';
formData.basic.coreContent = course.coreContent || ''; formData.basic.coreContent = course.coreContent || '';
formData.basic.duration = course.duration || 25; formData.basic.duration = course.duration || 25;

View File

@ -485,11 +485,12 @@ const iterateCourse = (id: number) => {
router.push(`/admin/packages/${id}/iterate`); router.push(`/admin/packages/${id}/iterate`);
}; };
const parseGradeTags = (gradeTags: string) => { const parseGradeTags = (gradeTags: string | string[] | undefined): string[] => {
if (!gradeTags) return []; if (!gradeTags) return [];
if (Array.isArray(gradeTags)) return gradeTags.map((t) => translateGradeTag(t));
try { try {
const tags = JSON.parse(gradeTags); const tags = JSON.parse(gradeTags);
return tags.map((t: string) => translateGradeTag(t)); return Array.isArray(tags) ? tags.map((t: string) => translateGradeTag(t)) : [];
} catch { } catch {
return []; return [];
} }

View File

@ -311,11 +311,12 @@ const viewRejectReason = (record: any) => {
rejectReasonVisible.value = true; rejectReasonVisible.value = true;
}; };
const parseGradeTags = (gradeTags: string) => { const parseGradeTags = (gradeTags: string | string[] | undefined): string[] => {
if (!gradeTags) return []; if (!gradeTags) return [];
if (Array.isArray(gradeTags)) return gradeTags.map((t) => translateGradeTag(t));
try { try {
const tags = JSON.parse(gradeTags); const tags = JSON.parse(gradeTags);
return tags.map((t: string) => translateGradeTag(t)); return Array.isArray(tags) ? tags.map((t: string) => translateGradeTag(t)) : [];
} catch { } catch {
return []; return [];
} }

View File

@ -458,6 +458,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import * as schoolApi from '@/api/school'; import * as schoolApi from '@/api/school';
import { translateDomainTags } from '@/utils/tagMaps'; import { translateDomainTags } from '@/utils/tagMaps';
import { parseGradeLevels } from '@/api/collections';
import FilePreviewModal from '@/components/FilePreviewModal.vue'; import FilePreviewModal from '@/components/FilePreviewModal.vue';
const router = useRouter(); const router = useRouter();
@ -528,29 +529,15 @@ const previewModalVisible = ref(false);
const previewFileUrl = ref(''); const previewFileUrl = ref('');
const previewFileName = ref(''); const previewFileName = ref('');
// // 使 parseGradeLevels
const grades = computed(() => { const grades = computed(() =>
if (!course.value.gradeTags) return []; parseGradeLevels(course.value.gradeTags ?? course.value.grade_tags)
try { );
const tags = JSON.parse(course.value.gradeTags);
return Array.isArray(tags) ? tags : [];
} catch {
//
return Array.isArray(course.value.gradeTags) ? course.value.gradeTags : [];
}
});
// //
const domainTags = computed(() => { const domainTags = computed(() =>
if (!course.value.domainTags) return []; translateDomainTags(parseGradeLevels(course.value.domainTags ?? course.value.domain_tags))
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 hasIntroContent = computed(() => { const hasIntroContent = computed(() => {

View File

@ -25,28 +25,54 @@
</div> </div>
</div> </div>
<!-- 年级切换Tab + 操作栏 --> <!-- 筛选栏参考教师端 -->
<div class="filter-action-bar"> <div class="filter-action-bar">
<div class="grade-tabs"> <div class="filter-row">
<span class="tab-label">年级筛选</span> <div class="filter-item">
<div class="tab-buttons"> <span class="filter-label">年级</span>
<div v-for="grade in gradeOptions" :key="grade.value" class="grade-tab" <a-select v-model:value="filters.grade" placeholder="全部年级" style="width: 120px;" allowClear
:class="{ active: selectedGrade === grade.value }" @click="handleGradeChange(grade.value)"> @change="handleFilterChange">
{{ grade.label }} <a-select-option value="小班">小班</a-select-option>
<a-select-option value="中班">中班</a-select-option>
<a-select-option value="大班">大班</a-select-option>
</a-select>
</div> </div>
<!-- <div class="filter-item">
<span class="filter-label">领域</span>
<a-select v-model:value="filters.domain" placeholder="全部领域" style="width: 120px;" allowClear
@change="handleFilterChange">
<a-select-option value="健康">健康</a-select-option>
<a-select-option value="语言">语言</a-select-option>
<a-select-option value="社会">社会</a-select-option>
<a-select-option value="科学">科学</a-select-option>
<a-select-option value="艺术">艺术</a-select-option>
</a-select>
</div> -->
<div class="filter-item">
<span class="filter-label">课程类型</span>
<a-select v-model:value="filters.lessonType" placeholder="全部类型" style="width: 130px;" allowClear
@change="handleFilterChange">
<a-select-option value="INTRODUCTION">导入课</a-select-option>
<a-select-option value="COLLECTIVE">集体课</a-select-option>
<a-select-option value="LANGUAGE">语言</a-select-option>
<a-select-option value="HEALTH">健康</a-select-option>
<a-select-option value="SCIENCE">科学</a-select-option>
<a-select-option value="SOCIAL">社会</a-select-option>
<a-select-option value="ART">艺术</a-select-option>
</a-select>
</div> </div>
<div class="filter-item search-box">
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称..." style="width: 240px;"
@search="handleFilterChange" allow-clear />
</div> </div>
<div class="action-row"> <div class="filter-item filter-right">
<div class="search-box">
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程名称" style="width: 280px;"
@search="handleSearch" allow-clear />
</div>
<a-button type="primary" class="auth-btn" @click="showAuthModal"> <a-button type="primary" class="auth-btn" @click="showAuthModal">
<StarFilled class="btn-icon" /> <StarFilled class="btn-icon" />
授权新课程 授权新课程
</a-button> </a-button>
</div> </div>
</div> </div>
</div>
<!-- 课程卡片网格 --> <!-- 课程卡片网格 -->
<div class="course-grid" v-if="!loading && courses.length > 0"> <div class="course-grid" v-if="!loading && courses.length > 0">
@ -69,14 +95,12 @@
<p class="course-book">{{ course.pictureBookName }}</p> <p class="course-book">{{ course.pictureBookName }}</p>
<div class="course-tags"> <div class="course-tags">
<span v-for="tag in (course.gradeTags || []).slice(0, 2)" :key="tag" class="tag grade" <span v-for="tag in (course.gradeTags || [])" :key="'g-' + tag" class="tag grade"
:style="getGradeTagStyle(translateGradeTag(tag))"> :style="getGradeTagStyle(tag)">{{ tag }}</span>
{{ translateGradeTag(tag) }} <span v-for="tag in (course.domainTags || [])" :key="'d-' + tag" class="tag domain"
</span> :style="getDomainTagStyle(tag)">{{ tag }}</span>
<span v-for="tag in (course.domainTags || []).slice(0, 2)" :key="tag" class="tag domain" <span v-for="(lt, idx) in (course.lessonTags || [])" :key="'l-' + idx" class="tag lesson"
:style="getDomainTagStyle(translateDomainTag(tag))"> :style="getLessonTagStyle(lt.lessonType)">{{ getLessonTypeName(lt.lessonType) }}</span>
{{ translateDomainTag(tag) }}
</span>
</div> </div>
<div class="course-meta"> <div class="course-meta">
@ -116,7 +140,7 @@
<div class="empty-icon-wrapper"> <div class="empty-icon-wrapper">
<BookOutlined class="empty-icon" /> <BookOutlined class="empty-icon" />
</div> </div>
<p>{{ (searchKeyword || selectedGrade) ? '未找到匹配的课程' : '暂无课程数据' }}</p> <p>{{ hasFilters ? '暂无符合条件的课程,试试调整筛选条件吧' : '暂无课程数据' }}</p>
<a-button type="primary" @click="showAuthModal"> <a-button type="primary" @click="showAuthModal">
授权第一门课程 授权第一门课程
</a-button> </a-button>
@ -139,7 +163,8 @@
</template> </template>
<div class="auth-content"> <div class="auth-content">
<div class="auth-search"> <div class="auth-search">
<a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses" size="large" /> <a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses"
size="large" />
</div> </div>
<div class="available-courses" v-if="!authLoading && availableCourses.length > 0"> <div class="available-courses" v-if="!authLoading && availableCourses.length > 0">
@ -154,9 +179,9 @@
</div> </div>
<div class="course-info"> <div class="course-info">
<div class="course-name-small">{{ course.name }}</div> <div class="course-name-small">{{ course.name }}</div>
<div class="course-book-small">{{ course.pictureBookName }}</div> <div class="course-book-small" v-if="course.pictureBookName">{{ course.pictureBookName }}</div>
<div class="course-tags-small"> <div class="course-tags-small">
<span v-for="tag in course.gradeTags.slice(0, 2)" :key="tag" class="tag-small"> <span v-for="tag in (course.gradeTags || [])" :key="tag" class="tag-small">
{{ tag }} {{ tag }}
</span> </span>
</div> </div>
@ -199,11 +224,12 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { import {
translateGradeTag, translateGradeTags,
translateDomainTag, translateDomainTags,
getGradeTagStyle, getGradeTagStyle,
getDomainTagStyle, getDomainTagStyle,
} from '@/utils/tagMaps'; } from '@/utils/tagMaps';
import { parseGradeLevels } from '@/api/collections';
import * as schoolApi from '@/api/school'; import * as schoolApi from '@/api/school';
const router = useRouter(); const router = useRouter();
@ -212,42 +238,55 @@ const authLoading = ref(false);
const authModalVisible = ref(false); const authModalVisible = ref(false);
const searchKeyword = ref(''); const searchKeyword = ref('');
const selectedCourseIds = ref<number[]>([]); const selectedCourseIds = ref<number[]>([]);
const selectedGrade = ref(''); //
// JSON //
const parseTags = (val: any): string[] => { const filters = reactive({
if (!val) return []; grade: undefined as string | undefined,
if (Array.isArray(val)) return val; domain: undefined as string | undefined,
if (typeof val === 'string') { lessonType: undefined as string | undefined,
try { keyword: '',
const parsed = JSON.parse(val); });
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
return [];
};
//
const gradeOptions = [
{ label: '全部', value: '' },
{ label: '小班', value: '小班' },
{ label: '中班', value: '中班' },
{ label: '大班', value: '大班' },
];
const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length); const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length);
const totalUsage = computed(() => courses.value.reduce((sum, c) => sum + (c.usageCount || 0), 0)); 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) => { const DOMAIN_TO_CODE: Record<string, string> = {
selectedGrade.value = value; 健康: 'HEALTH',
loadCourses(); 语言: 'LANGUAGE',
社会: 'SOCIAL',
科学: 'SCIENCE',
艺术: 'ART',
}; };
// //
const handleSearch = () => { const LESSON_TYPE_NAMES: Record<string, string> = {
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<string, { background: string; color: string }> = {
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(); loadCourses();
}; };
@ -280,22 +319,54 @@ const pagination = reactive({
const courses = ref<any[]>([]); const courses = ref<any[]>([]);
const availableCourses = ref<any[]>([]); const availableCourses = ref<any[]>([]);
// //
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 () => { const loadCourses = async () => {
loading.value = true; loading.value = true;
try { try {
const params: { keyword?: string; grade?: string } = {}; const params: schoolApi.SchoolCourseQueryParams = {};
if (searchKeyword.value?.trim()) params.keyword = searchKeyword.value.trim(); if (filters.keyword?.trim()) params.keyword = filters.keyword.trim();
if (selectedGrade.value) params.grade = selectedGrade.value; 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); const data = await schoolApi.getSchoolCourses(params);
courses.value = (data || []).map((course: any) => ({ courses.value = (data || []).map((course: any) => {
const gradeTags = parseTags(course.gradeTags);
const domainTags = parseTags(course.domainTags);
return {
...course, ...course,
gradeTags: parseTags(course.gradeTags), gradeTags: translateGradeTags(gradeTags),
domainTags: parseTags(course.domainTags), domainTags: translateDomainTags(domainTags),
lessonTags: course.lessonTags || [],
duration: course.duration ?? course.durationMinutes ?? 0, duration: course.duration ?? course.durationMinutes ?? 0,
pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl, pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl,
authorized: course.authorized ?? true, authorized: course.authorized ?? true,
})); };
});
pagination.total = courses.value.length; pagination.total = courses.value.length;
} catch (error: any) { } catch (error: any) {
message.error(error.response?.data?.message || '加载课程列表失败'); message.error(error.response?.data?.message || '加载课程列表失败');
@ -462,11 +533,8 @@ onMounted(() => {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
/* 筛选操作栏 */ /* 筛选操作栏(参考教师端布局) */
.filter-action-bar { .filter-action-bar {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
padding: 20px 24px; padding: 20px 24px;
background: white; background: white;
@ -474,51 +542,33 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.grade-tabs { .filter-row {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 16px; gap: 16px;
} }
.tab-label { .filter-item {
font-size: 14px;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.tab-buttons {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
} }
.grade-tab { .filter-label {
padding: 8px 20px;
border-radius: 10px;
font-size: 14px; font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: none;
background: #F5F5F5;
color: #666; color: #666;
font-weight: 500;
} }
.grade-tab:hover { .filter-right {
background: #E8F5E9; margin-left: auto;
color: #43e97b;
} }
.grade-tab.active { .search-box {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); flex: 1;
color: white; min-width: 200px;
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3); max-width: 280px;
}
.action-row {
display: flex;
justify-content: space-between;
align-items: center;
} }
/* 操作栏 */ /* 操作栏 */

View File

@ -539,27 +539,29 @@ const previewModalVisible = ref(false);
const previewFileUrl = ref(''); const previewFileUrl = ref('');
const previewFileName = ref(''); const previewFileName = ref('');
// // gradeTags String[]
const grades = computed(() => { const grades = computed(() => {
if (!course.value.gradeTags) return []; const val = course.value.gradeTags;
if (!val) return [];
if (Array.isArray(val)) return translateGradeTags(val);
try { try {
const tags = JSON.parse(course.value.gradeTags); const tags = JSON.parse(val);
const translated = Array.isArray(tags) ? tags : []; return translateGradeTags(Array.isArray(tags) ? tags : []);
return translateGradeTags(translated);
} catch { } catch {
return Array.isArray(course.value.gradeTags) ? translateGradeTags(course.value.gradeTags) : []; return [];
} }
}); });
// // domainTags String[]
const domainTags = computed(() => { const domainTags = computed(() => {
if (!course.value.domainTags) return []; const val = course.value.domainTags;
if (!val) return [];
if (Array.isArray(val)) return translateDomainTags(val);
try { try {
const tags = JSON.parse(course.value.domainTags); const tags = JSON.parse(val);
const arr = Array.isArray(tags) ? tags : []; return translateDomainTags(Array.isArray(tags) ? tags : []);
return translateDomainTags(arr);
} catch { } catch {
return Array.isArray(course.value.domainTags) ? translateDomainTags(course.value.domainTags) : []; return [];
} }
}); });

View File

@ -22,13 +22,8 @@
<div class="filter-bar"> <div class="filter-bar">
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">年级</span> <span class="filter-label">年级</span>
<a-select <a-select v-model:value="filters.grade" placeholder="全部年级" style="width: 120px;" allowClear
v-model:value="filters.grade" @change="handleFilterChange">
placeholder="全部年级"
style="width: 120px;"
allowClear
@change="handleFilterChange"
>
<a-select-option value="小班"> <a-select-option value="小班">
小班 小班
</a-select-option> </a-select-option>
@ -38,42 +33,36 @@
<a-select-option value="大班"> <a-select-option value="大班">
大班 大班
</a-select-option> </a-select-option>
<a-select-option value="混合">混合</a-select-option>
</a-select> </a-select>
</div> </div>
<div class="filter-item"> <div class="filter-item">
<span class="filter-label">领域</span> <span class="filter-label">课程类型</span>
<a-select <a-select v-model:value="filters.lessonType" placeholder="全部类型" style="width: 130px;" allowClear
v-model:value="filters.domain" @change="handleFilterChange">
placeholder="全部领域" <a-select-option value="INTRODUCTION">导入课</a-select-option>
style="width: 120px;" <a-select-option value="COLLECTIVE">集体课</a-select-option>
allowClear <a-select-option value="LANGUAGE">语言</a-select-option>
@change="handleFilterChange" <a-select-option value="HEALTH">健康</a-select-option>
> <a-select-option value="SCIENCE">科学</a-select-option>
<a-select-option value="健康">健康</a-select-option> <a-select-option value="SOCIAL">社会</a-select-option>
<a-select-option value="语言">语言</a-select-option> <a-select-option value="ART">艺术</a-select-option>
<a-select-option value="社会">社会</a-select-option>
<a-select-option value="科学">科学</a-select-option>
<a-select-option value="艺术">艺术</a-select-option>
</a-select> </a-select>
</div> </div>
<div class="filter-item search-box"> <div class="filter-item search-box">
<a-input-search <a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称..." style="width: 240px;"
v-model:value="filters.keyword" @search="handleFilterChange" />
placeholder="搜索课程名称..."
style="width: 240px;"
@search="handleFilterChange"
/>
</div> </div>
<div class="filter-item filter-right"> <div class="filter-item filter-right">
<a-select <a-select v-model:value="filters.sort" style="width: 130px;" @change="handleFilterChange">
v-model:value="filters.sort" <a-select-option value="popular">
style="width: 130px;" <FireOutlined /> 最受欢迎
@change="handleFilterChange" </a-select-option>
> <a-select-option value="newest">
<a-select-option value="popular"><FireOutlined /> 最受欢迎</a-select-option> <StarOutlined /> 最新发布
<a-select-option value="newest"><StarOutlined /> 最新发布</a-select-option> </a-select-option>
<a-select-option value="rating"><StarFilled /> 评分最高</a-select-option> <a-select-option value="rating">
<StarFilled /> 评分最高
</a-select-option>
</a-select> </a-select>
</div> </div>
</div> </div>
@ -81,26 +70,21 @@
<!-- 课程列表 --> <!-- 课程列表 -->
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="course-grid"> <div class="course-grid">
<div <div v-for="course in courses" :key="course.id" class="course-card" @click="viewCourseDetail(course)">
v-for="course in courses"
:key="course.id"
class="course-card"
@click="viewCourseDetail(course)"
>
<!-- 封面区域 --> <!-- 封面区域 -->
<div class="course-cover"> <div class="course-cover">
<img <img v-if="course.pictureUrl" :src="getImageUrl(course.pictureUrl)" class="cover-image" />
v-if="course.pictureUrl"
:src="getImageUrl(course.pictureUrl)"
class="cover-image"
/>
<div v-else class="cover-placeholder"> <div v-else class="cover-placeholder">
<div class="placeholder-icon"><BookFilled /></div> <div class="placeholder-icon">
<BookFilled />
</div>
<div class="placeholder-text">精彩绘本</div> <div class="placeholder-text">精彩绘本</div>
</div> </div>
<!-- 评分徽章 --> <!-- 评分徽章 -->
<div class="rating-badge" v-if="(course.avgRating ?? 0) > 0"> <div class="rating-badge" v-if="(course.avgRating ?? 0) > 0">
<span class="rating-star"><StarFilled /></span> <span class="rating-star">
<StarFilled />
</span>
<span class="rating-value">{{ (course.avgRating ?? 0).toFixed(1) }}</span> <span class="rating-value">{{ (course.avgRating ?? 0).toFixed(1) }}</span>
</div> </div>
</div> </div>
@ -112,21 +96,15 @@
<BookOutlined /> {{ course.pictureBookName }} <BookOutlined /> {{ course.pictureBookName }}
</p> </p>
<!-- 标签区域 --> <!-- 标签区域年级 + 领域 + 课程环节lessonTags -->
<div class="course-tags"> <div class="course-tags">
<a-tag <a-tag v-for="tag in (course.gradeTags || [])" :key="'g-' + tag" size="small"
v-for="tag in course.gradeTags" :style="getGradeTagStyle(tag)">{{ tag }}</a-tag>
:key="'g-' + tag" <a-tag v-for="tag in (course.domainTags || [])" :key="'d-' + tag" size="small"
:style="getGradeTagStyle(tag)" :style="getDomainTagStyle(tag)">{{ tag }}</a-tag>
> <a-tag v-for="(lt, idx) in (course.lessonTags || [])" :key="'l-' + idx" size="small"
{{ tag }} :style="getLessonTagStyle(lt.lessonType)">
</a-tag> {{ getLessonTypeName(lt.lessonType) }}
<a-tag
v-for="tag in course.domainTags"
:key="'d-' + tag"
:style="getDomainTagStyle(tag)"
>
{{ tag }}
</a-tag> </a-tag>
</div> </div>
@ -144,7 +122,9 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<button class="prepare-btn" @click.stop="prepareCourse(course)"> <button class="prepare-btn" @click.stop="prepareCourse(course)">
<span class="btn-icon"><EditOutlined /></span> <span class="btn-icon">
<EditOutlined />
</span>
开始备课 开始备课
</button> </button>
</div> </div>
@ -162,13 +142,8 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-wrapper" v-if="pagination.total > pagination.pageSize"> <div class="pagination-wrapper" v-if="pagination.total > pagination.pageSize">
<a-pagination <a-pagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize"
v-model:current="pagination.current" :total="pagination.total" show-less-items @change="handlePageChange" />
v-model:page-size="pagination.pageSize"
:total="pagination.total"
show-less-items
@change="handlePageChange"
/>
</div> </div>
</a-spin> </a-spin>
</div> </div>
@ -203,6 +178,7 @@ const loading = ref(false);
const filters = reactive({ const filters = reactive({
grade: undefined as string | undefined, grade: undefined as string | undefined,
domain: undefined as string | undefined, domain: undefined as string | undefined,
lessonType: undefined as string | undefined,
keyword: '', keyword: '',
sort: 'popular', sort: 'popular',
}); });
@ -215,27 +191,20 @@ const pagination = reactive({
const courses = ref<any[]>([]); const courses = ref<any[]>([]);
// // parseGradeLevels
const gradeMap: Record<string, string> = {
SMALL: '小班', small: '小班',
MIDDLE: '中班', middle: '中班',
BIG: '大班', big: '大班',
};
//
const domainMap: Record<string, string> = {
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
const parseTags = (val: any): string[] => { const parseTags = (val: any): string[] => {
if (!val) return []; 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') { if (typeof val === 'string') {
try { try {
const parsed = JSON.parse(val); const parsed = JSON.parse(val);
@ -263,32 +232,79 @@ const handlePageChange = () => {
loadCourses(); loadCourses();
}; };
// -> INTRODUCTION/INTRODOMAIN_*
const LESSON_TYPE_NAMES: Record<string, string> = {
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<string, { background: string; color: string }> = {
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<string, string> = {
健康: 'HEALTH',
语言: 'LANGUAGE',
社会: 'SOCIAL',
科学: 'SCIENCE',
艺术: 'ART',
};
const loadCourses = async () => { const loadCourses = async () => {
loading.value = true; loading.value = true;
try { try {
const params: any = { const params: teacherApi.TeacherCourseQueryParams = {
pageNum: pagination.current, pageNum: pagination.current,
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
}; };
if (filters.keyword) { if (filters.keyword?.trim()) {
params.keyword = filters.keyword; params.keyword = filters.keyword.trim();
} }
// // // gradeTags
if (filters.grade) { if (filters.grade) {
const gradeKey = Object.keys(gradeMap).find((key) => gradeMap[key] === filters.grade); params.grade = filters.grade;
if (gradeKey) {
params.grade = gradeKey;
}
} }
// // domainTags
if (filters.domain) { if (filters.domain) {
const domainKey = Object.keys(domainMap).find((key) => domainMap[key] === filters.domain); params.domain = DOMAIN_TO_CODE[filters.domain] ?? filters.domain;
if (domainKey) {
params.domain = domainKey;
} }
// lessonType course_lesson
if (filters.lessonType) {
params.lessonType = filters.lessonType;
} }
const data = await teacherApi.getTeacherCourses(params); const data = await teacherApi.getTeacherCourses(params);
@ -299,6 +315,7 @@ const loadCourses = async () => {
...item, ...item,
gradeTags: translateGradeTags(gradeTags), gradeTags: translateGradeTags(gradeTags),
domainTags: translateDomainTags(domainTags), domainTags: translateDomainTags(domainTags),
lessonTags: item.lessonTags || [],
duration: item.duration ?? item.durationMinutes ?? 0, duration: item.duration ?? item.durationMinutes ?? 0,
usageCount: item.usageCount ?? 0, usageCount: item.usageCount ?? 0,
avgRating: item.avgRating ?? 0, avgRating: item.avgRating ?? 0,

View File

@ -237,7 +237,7 @@ const loadCourseData = async () => {
pictureBookName: data.sourceCourse?.name || '', pictureBookName: data.sourceCourse?.name || '',
theme: data.themeId ? { id: data.themeId, name: '' } : null, theme: data.themeId ? { id: data.themeId, name: '' } : null,
coverImagePath: data.coverImagePath, 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) : [], domainTags: data.domainTags ? JSON.parse(data.domainTags) : [],
duration: data.duration || 25, duration: data.duration || 25,
coreContent: data.coreContent || '', coreContent: data.coreContent || '',

View File

@ -265,7 +265,7 @@ const fetchDetail = async () => {
// //
formData.basic.name = data.name || ''; formData.basic.name = data.name || '';
formData.basic.themeId = data.themeId; 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.pictureBookName = '';
formData.basic.coreContent = data.coreContent || data.core_content || ''; formData.basic.coreContent = data.coreContent || data.core_content || '';
formData.basic.duration = data.duration || 25; formData.basic.duration = data.duration || 25;

View File

@ -3,12 +3,14 @@ package com.reading.platform.common.mapper;
import com.reading.platform.dto.response.CourseResponse; import com.reading.platform.dto.response.CourseResponse;
import com.reading.platform.entity.CoursePackage; import com.reading.platform.entity.CoursePackage;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import java.util.List; import java.util.List;
/** /**
* Course Entity Mapper * Course Entity Mapper
* gradeTags/domainTags 规范为 String[]与套餐管理适用年级对齐
*/ */
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
public interface CoursePackageMapper { public interface CoursePackageMapper {
@ -18,6 +20,8 @@ public interface CoursePackageMapper {
/** /**
* Entity Response * 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); CourseResponse toVO(CoursePackage entity);
/** /**
@ -28,5 +32,7 @@ public interface CoursePackageMapper {
/** /**
* Response Entity用于创建/更新时 * 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); CoursePackage toEntity(CourseResponse vo);
} }

View File

@ -1,9 +1,11 @@
package com.reading.platform.common.util; package com.reading.platform.common.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference; import com.alibaba.fastjson2.TypeReference;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; 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 * 创建空的 JSONObject
* *

View File

@ -1,13 +1,19 @@
package com.reading.platform.controller.school; package com.reading.platform.controller.school;
import com.alibaba.fastjson2.JSON;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils; 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.entity.CoursePackage;
import com.reading.platform.service.CourseLessonService;
import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.CoursePackageService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@ -15,6 +21,7 @@ import java.util.stream.Collectors;
/** /**
* 课程管理控制器学校端 * 课程管理控制器学校端
* gradeTags/domainTags 规范为 String[]与套餐管理适用年级对齐
*/ */
@Slf4j @Slf4j
@RestController @RestController
@ -24,24 +31,119 @@ import java.util.stream.Collectors;
public class SchoolCourseController { public class SchoolCourseController {
private final CoursePackageService courseService; private final CoursePackageService courseService;
private final CourseLessonService courseLessonService;
@GetMapping @GetMapping
@Operation(summary = "获取学校课程包列表") @Operation(summary = "获取学校课程包列表")
public Result<List<CoursePackage>> getSchoolCourses( public Result<List<SchoolCourseResponse>> getSchoolCourses(
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String grade) { @RequestParam(required = false) String grade,
log.info("获取学校课程包列表keyword={}, grade={}", keyword, 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(); Long tenantId = SecurityUtils.getCurrentTenantId();
List<CoursePackage> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade); List<CoursePackage> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, lessonType);
return Result.success(courses); List<SchoolCourseResponse> list = courses.stream()
.map(pkg -> toSchoolCourseResponse(pkg))
.collect(Collectors.toList());
// 填充 lessonTags
for (SchoolCourseResponse vo : list) {
List<CourseLesson> 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}") @GetMapping("/{id}")
@Operation(summary = "获取课程详情") @Operation(summary = "获取课程详情")
public Result<CoursePackage> getSchoolCourse(@PathVariable Long id) { public Result<SchoolCourseResponse> getSchoolCourse(@PathVariable Long id) {
log.info("获取课程详情id={}", id); log.info("获取课程详情id={}", id);
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
CoursePackage course = courseService.getCourseByIdWithTenantCheck(id, tenantId); 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();
} }
} }

View File

@ -1,7 +1,6 @@
package com.reading.platform.controller.school; package com.reading.platform.controller.school;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.PageResult;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.common.security.SecurityUtils;
@ -25,14 +24,13 @@ import java.util.List;
public class SchoolTeacherController { public class SchoolTeacherController {
private final TeacherService teacherService; private final TeacherService teacherService;
private final TeacherMapper teacherMapper;
@Operation(summary = "Create teacher") @Operation(summary = "Create teacher")
@PostMapping @PostMapping
public Result<TeacherResponse> createTeacher(@Valid @RequestBody TeacherCreateRequest request) { public Result<TeacherResponse> createTeacher(@Valid @RequestBody TeacherCreateRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Teacher teacher = teacherService.createTeacher(tenantId, request); Teacher teacher = teacherService.createTeacher(tenantId, request);
return Result.success(teacherMapper.toVO(teacher)); return Result.success(teacherService.toTeacherResponse(teacher));
} }
@Operation(summary = "Update teacher") @Operation(summary = "Update teacher")
@ -40,7 +38,7 @@ public class SchoolTeacherController {
public Result<TeacherResponse> updateTeacher(@PathVariable Long id, @RequestBody TeacherUpdateRequest request) { public Result<TeacherResponse> updateTeacher(@PathVariable Long id, @RequestBody TeacherUpdateRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Teacher teacher = teacherService.updateTeacherWithTenantCheck(id, tenantId, request); 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") @Operation(summary = "Get teacher by ID")
@ -48,7 +46,7 @@ public class SchoolTeacherController {
public Result<TeacherResponse> getTeacher(@PathVariable Long id) { public Result<TeacherResponse> getTeacher(@PathVariable Long id) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Teacher teacher = teacherService.getTeacherByIdWithTenantCheck(id, tenantId); Teacher teacher = teacherService.getTeacherByIdWithTenantCheck(id, tenantId);
return Result.success(teacherMapper.toVO(teacher)); return Result.success(teacherService.toTeacherResponse(teacher));
} }
@Operation(summary = "Get teacher page") @Operation(summary = "Get teacher page")
@ -60,7 +58,7 @@ public class SchoolTeacherController {
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Page<Teacher> page = teacherService.getTeacherPage(tenantId, pageNum, pageSize, keyword, status); Page<Teacher> page = teacherService.getTeacherPage(tenantId, pageNum, pageSize, keyword, status);
List<TeacherResponse> voList = teacherMapper.toVO(page.getRecords()); List<TeacherResponse> voList = teacherService.toTeacherResponseList(page.getRecords());
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
} }
@ -74,10 +72,10 @@ public class SchoolTeacherController {
@Operation(summary = "Reset teacher password") @Operation(summary = "Reset teacher password")
@PostMapping("/{id}/reset-password") @PostMapping("/{id}/reset-password")
public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) { public Result<java.util.Map<String, String>> resetPassword(@PathVariable Long id) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
teacherService.resetPasswordWithTenantCheck(id, tenantId, newPassword); String tempPassword = teacherService.resetPasswordAndReturnTemp(id, tenantId);
return Result.success(); return Result.success(java.util.Map.of("tempPassword", tempPassword));
} }
} }

View File

@ -11,6 +11,7 @@ import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.response.ClassResponse; import com.reading.platform.dto.response.ClassResponse;
import com.reading.platform.dto.response.CourseResponse; 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.StudentResponse;
import com.reading.platform.dto.response.TeacherResponse; import com.reading.platform.dto.response.TeacherResponse;
import com.reading.platform.entity.ClassTeacher; 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.CoursePackage;
import com.reading.platform.entity.Student; import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher; import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.service.ClassService; import com.reading.platform.service.ClassService;
import com.reading.platform.service.CourseLessonService;
import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.CoursePackageService;
import com.reading.platform.service.StudentService; import com.reading.platform.service.StudentService;
import com.reading.platform.service.TeacherService; import com.reading.platform.service.TeacherService;
@ -39,6 +42,7 @@ import java.util.stream.Collectors;
public class TeacherCourseController { public class TeacherCourseController {
private final CoursePackageService courseService; private final CoursePackageService courseService;
private final CourseLessonService courseLessonService;
private final ClassService classService; private final ClassService classService;
private final StudentService studentService; private final StudentService studentService;
private final TeacherService teacherService; private final TeacherService teacherService;
@ -67,18 +71,36 @@ public class TeacherCourseController {
@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword, @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(); Long tenantId = SecurityUtils.getCurrentTenantId();
Page<CoursePackage> page = courseService.getCoursePage(tenantId, pageNum, pageSize, keyword, category, CourseStatus.PUBLISHED.getCode()); // 学校 -> 套餐 -> 课程包 层级查询支持 grade/domain/lessonType 筛选
Page<CoursePackage> page = courseService.getTenantPackageCoursePage(
tenantId, pageNum, pageSize, keyword, grade, domain, lessonType, CourseStatus.PUBLISHED.getCode());
List<CourseResponse> voList = courseMapper.toVO(page.getRecords()); List<CourseResponse> voList = courseMapper.toVO(page.getRecords());
// 填充 lessonTags namelessonType供列表 tag 展示
for (CourseResponse vo : voList) {
List<CourseLesson> 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())); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
} }
@Operation(summary = "获取所有课程") @Operation(summary = "获取所有课程")
@GetMapping("/courses/all") @GetMapping("/courses/all")
public Result<List<CourseResponse>> getAllCourses() { public Result<List<CourseResponse>> getAllCourses(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String grade,
@RequestParam(required = false) String domain) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
List<CoursePackage> courses = courseService.getCoursesByTenantId(tenantId); // 学校 -> 套餐 -> 课程包 层级查询
List<CoursePackage> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, null);
return Result.success(courseMapper.toVO(courses)); return Result.success(courseMapper.toVO(courses));
} }

View File

@ -1,15 +1,19 @@
package com.reading.platform.dto.request; package com.reading.platform.dto.request;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import java.util.List;
@Data @Data
@Schema(description = "教师创建请求") @Schema(description = "教师创建请求")
public class TeacherCreateRequest { public class TeacherCreateRequest {
@NotBlank(message = "用户名不能为空") @NotBlank(message = "用户名不能为空")
@Schema(description = "用户名") @JsonAlias("loginAccount")
@Schema(description = "用户名/登录账号")
private String username; private String username;
@NotBlank(message = "密码不能为空") @NotBlank(message = "密码不能为空")
@ -20,6 +24,7 @@ public class TeacherCreateRequest {
@Schema(description = "姓名") @Schema(description = "姓名")
private String name; private String name;
@NotBlank(message = "手机号不能为空")
@Schema(description = "电话") @Schema(description = "电话")
private String phone; private String phone;
@ -32,4 +37,7 @@ public class TeacherCreateRequest {
@Schema(description = "简介") @Schema(description = "简介")
private String bio; private String bio;
@Schema(description = "负责班级ID列表")
private List<Long> classIds;
} }

View File

@ -3,6 +3,8 @@ package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List;
@Data @Data
@Schema(description = "教师更新请求") @Schema(description = "教师更新请求")
public class TeacherUpdateRequest { public class TeacherUpdateRequest {
@ -28,4 +30,7 @@ public class TeacherUpdateRequest {
@Schema(description = "状态") @Schema(description = "状态")
private String status; private String status;
@Schema(description = "负责班级ID列表")
private List<Long> classIds;
} }

View File

@ -138,11 +138,11 @@ public class CourseResponse {
@Schema(description = "评估数据") @Schema(description = "评估数据")
private String assessmentData; private String assessmentData;
@Schema(description = "年级标签") @Schema(description = "年级标签(规范为数组,与套餐管理适用年级对齐)")
private String gradeTags; private String[] gradeTags;
@Schema(description = "领域标签") @Schema(description = "领域标签(规范为数组)")
private String domainTags; private String[] domainTags;
@Schema(description = "是否有集体课") @Schema(description = "是否有集体课")
private Integer hasCollectiveLesson; private Integer hasCollectiveLesson;
@ -197,4 +197,7 @@ public class CourseResponse {
@Schema(description = "关联的课程环节") @Schema(description = "关联的课程环节")
private List<CourseLessonResponse> courseLessons; private List<CourseLessonResponse> courseLessons;
@Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType")
private List<LessonTagResponse> lessonTags;
} }

View File

@ -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;
}

View File

@ -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<LessonTagResponse> lessonTags;
}

View File

@ -1,10 +1,12 @@
package com.reading.platform.dto.response; package com.reading.platform.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
/** /**
* 教师响应 * 教师响应
@ -21,7 +23,8 @@ public class TeacherResponse {
@Schema(description = "租户 ID") @Schema(description = "租户 ID")
private Long tenantId; private Long tenantId;
@Schema(description = "用户名") @JsonProperty("loginAccount")
@Schema(description = "登录账号")
private String username; private String username;
@Schema(description = "姓名") @Schema(description = "姓名")
@ -45,6 +48,15 @@ public class TeacherResponse {
@Schema(description = "状态") @Schema(description = "状态")
private String status; private String status;
@Schema(description = "负责班级ID列表")
private List<Long> classIds;
@Schema(description = "负责班级名称")
private Object classNames;
@Schema(description = "授课次数")
private Integer lessonCount;
@Schema(description = "最后登录时间") @Schema(description = "最后登录时间")
private LocalDateTime lastLoginAt; private LocalDateTime lastLoginAt;

View File

@ -61,8 +61,26 @@ public interface CoursePackageService extends com.baomidou.mybatisplus.extension
* *
* @param tenantId 租户 ID * @param tenantId 租户 ID
* @param keyword 关键词课程名称绘本名称可选 * @param keyword 关键词课程名称绘本名称可选
* @param grade 年级筛选小班/中班/大班 small/middle/big可选 * @param grade 年级筛选小班/中班/大班可选
* @param domain 领域筛选健康/语言/社会/科学/艺术 或对应英文码可选
* @param lessonType 课程环节类型筛选可选
*/ */
List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade); List<CoursePackage> 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 课程环节类型筛选INTRODUCTIONCOLLECTIVELANGUAGEHEALTHSCIENCESOCIALART可选
* @param status 课程状态 PUBLISHED
*/
Page<CoursePackage> getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize,
String keyword, String grade, String domain, String lessonType, String status);
} }

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.reading.platform.dto.request.TeacherCreateRequest; import com.reading.platform.dto.request.TeacherCreateRequest;
import com.reading.platform.dto.request.TeacherUpdateRequest; import com.reading.platform.dto.request.TeacherUpdateRequest;
import com.reading.platform.dto.response.TeacherResponse;
import com.reading.platform.entity.Teacher; import com.reading.platform.entity.Teacher;
import java.util.List; import java.util.List;
@ -63,9 +64,24 @@ public interface TeacherService extends IService<Teacher> {
*/ */
void resetPasswordWithTenantCheck(Long id, Long tenantId, String newPassword); void resetPasswordWithTenantCheck(Long id, Long tenantId, String newPassword);
/**
* 重置密码并返回临时密码带租户验证
*/
String resetPasswordAndReturnTemp(Long id, Long tenantId);
/** /**
* 根据 ID 列表查询教师 * 根据 ID 列表查询教师
*/ */
List<Teacher> getTeachersByIds(List<Long> teacherIds); List<Teacher> getTeachersByIds(List<Long> teacherIds);
/**
* 转换为教师响应含班级授课数等扩展信息
*/
TeacherResponse toTeacherResponse(Teacher teacher);
/**
* 批量转换为教师响应
*/
List<TeacherResponse> toTeacherResponseList(List<Teacher> teachers);
} }

View File

@ -30,6 +30,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -226,7 +227,7 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
} }
@Override @Override
public List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade) { public List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType) {
List<Long> collectionIds = tenantPackageMapper.selectList( List<Long> collectionIds = tenantPackageMapper.selectList(
new LambdaQueryWrapper<TenantPackage>() new LambdaQueryWrapper<TenantPackage>()
.eq(TenantPackage::getTenantId, tenantId) .eq(TenantPackage::getTenantId, tenantId)
@ -253,6 +254,16 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
return new ArrayList<>(); return new ArrayList<>();
} }
// lessonType 筛选仅保留包含该类型环节的课程包
if (StringUtils.hasText(lessonType)) {
Set<Long> courseIdsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType).stream()
.collect(Collectors.toSet());
packageIds = packageIds.stream().filter(courseIdsWithLesson::contains).collect(Collectors.toList());
if (packageIds.isEmpty()) {
return new ArrayList<>();
}
}
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CoursePackage::getId, packageIds) wrapper.in(CoursePackage::getId, packageIds)
.eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode()); .eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode());
@ -271,10 +282,96 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
.or().like(CoursePackage::getGradeTags, "\"" + grade + "\"") .or().like(CoursePackage::getGradeTags, "\"" + grade + "\"")
.or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\"")); .or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\""));
} }
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); wrapper.orderByDesc(CoursePackage::getUsageCount);
return coursePackageMapper.selectList(wrapper); return coursePackageMapper.selectList(wrapper);
} }
@Override
public Page<CoursePackage> 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<Long> collectionIds = tenantPackageMapper.selectList(
new LambdaQueryWrapper<TenantPackage>()
.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<Long> packageIds = collectionPackageMapper.selectList(
new LambdaQueryWrapper<CourseCollectionPackage>()
.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<Long> 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<CoursePackage> page = new Page<>(current, size);
LambdaQueryWrapper<CoursePackage> 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) { private CoursePackage buildEntityFromRequest(CourseCreateRequest request) {
CoursePackage entity = new CoursePackage(); CoursePackage entity = new CoursePackage();
entity.setName(request.getName()); entity.setName(request.getName());

View File

@ -6,17 +6,28 @@ import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.common.exception.BusinessException; import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.dto.request.TeacherCreateRequest; import com.reading.platform.dto.request.TeacherCreateRequest;
import com.reading.platform.dto.request.TeacherUpdateRequest; 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.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.mapper.TeacherMapper;
import com.reading.platform.service.ClassService;
import com.reading.platform.service.TeacherService; import com.reading.platform.service.TeacherService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* 教师服务实现类 * 教师服务实现类
@ -28,6 +39,11 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
implements TeacherService { implements TeacherService {
private final TeacherMapper teacherMapper; 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; private final PasswordEncoder passwordEncoder;
@Override @Override
@ -57,6 +73,18 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
teacherMapper.insert(teacher); 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()); log.info("教师创建成功ID: {}", teacher.getId());
return teacher; return teacher;
} }
@ -92,6 +120,21 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
teacherMapper.updateById(teacher); teacherMapper.updateById(teacher);
// 更新教师班级分配
if (request.getClassIds() != null) {
classTeacherMapper.delete(
new LambdaQueryWrapper<ClassTeacher>().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); log.info("教师更新成功ID: {}", id);
return teacher; return teacher;
} }
@ -200,6 +243,18 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
resetPassword(id, newPassword); 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 @Override
public List<Teacher> getTeachersByIds(List<Long> teacherIds) { public List<Teacher> getTeachersByIds(List<Long> teacherIds) {
log.debug("根据 ID 列表查询教师ID 列表:{}", teacherIds); log.debug("根据 ID 列表查询教师ID 列表:{}", teacherIds);
@ -215,4 +270,53 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
return teacherMapper.selectList(wrapper); 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<TeacherResponse> toTeacherResponseList(List<Teacher> teachers) {
if (teachers == null) return null;
List<TeacherResponse> list = new ArrayList<>(teachers.size());
for (Teacher teacher : teachers) {
list.add(toTeacherResponse(teacher));
}
return list;
}
private void enrichTeacherResponse(TeacherResponse response, Long teacherId) {
List<ClassTeacher> classTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getTeacherId, teacherId)
);
List<Long> classIds = new ArrayList<>();
List<String> 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<Lesson>().eq(Lesson::getTeacherId, teacherId)
);
} catch (Exception e) {
log.debug("Query lesson count failed: {}", e.getMessage());
}
response.setLessonCount((int) lessonCount);
}
} }