From 81dd74662e9e92a36cb984b068e36c72d7fffb3c Mon Sep 17 00:00:00 2001 From: zhonghua Date: Thu, 19 Mar 2026 14:05:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20/api/v1/school/cour?= =?UTF-8?q?ses=20=E6=8E=A5=E5=8F=A3=20gradeTags=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=95=B0=E6=8D=AE=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: 增强 SchoolCourseController.parseJsonArray 兼容多种 JSON 格式 - 后端: 新增 SchoolCourseResponse,gradeTags/domainTags 规范为 String[] - 前端: 学校端课程列表/详情统一使用 parseGradeLevels 解析 gradeTags - 前端: 兼容 grade_tags/domain_tags snake_case 字段 Made-with: Cursor --- reading-platform-frontend/src/api/school.ts | 4 + .../views/admin/courses/CourseDetailView.vue | 18 ++-- .../views/admin/courses/CourseEditView.vue | 2 +- .../views/admin/courses/CourseListView.vue | 5 +- .../views/admin/courses/CourseReviewView.vue | 5 +- .../views/school/courses/CourseDetailView.vue | 29 ++---- .../views/school/courses/CourseListView.vue | 29 ++---- .../teacher/courses/CourseDetailView.vue | 26 ++--- .../views/teacher/courses/CourseListView.vue | 14 ++- .../views/teacher/courses/PrepareModeView.vue | 2 +- .../school-courses/SchoolCourseEditView.vue | 2 +- .../common/mapper/CoursePackageMapper.java | 6 ++ .../platform/common/util/JsonUtils.java | 21 +++++ .../school/SchoolCourseController.java | 94 ++++++++++++++++++- .../platform/dto/response/CourseResponse.java | 8 +- .../dto/response/SchoolCourseResponse.java | 68 ++++++++++++++ 16 files changed, 254 insertions(+), 79 deletions(-) create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts index f17f3ff..a5b0cd7 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; 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..5bc287b 100644 --- a/reading-platform-frontend/src/views/school/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/school/courses/CourseListView.vue @@ -69,11 +69,11 @@

《{{ course.pictureBookName }}》

- {{ translateGradeTag(tag) }} - {{ translateDomainTag(tag) }} @@ -139,7 +139,8 @@
@@ -154,9 +155,9 @@
{{ course.name }}
-
《{{ course.pictureBookName }}》
+
《{{ course.pictureBookName }}》
- + {{ tag }}
@@ -204,6 +205,7 @@ import { getGradeTagStyle, getDomainTagStyle, } from '@/utils/tagMaps'; +import { parseGradeLevels } from '@/api/collections'; import * as schoolApi from '@/api/school'; const router = useRouter(); @@ -214,21 +216,6 @@ 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: '' }, @@ -290,8 +277,6 @@ const loadCourses = async () => { 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, 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..c108e9f 100644 --- a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue @@ -232,10 +232,20 @@ const domainMap: Record = { 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); 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..d0042fd 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,16 @@ 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.SchoolCourseResponse; import com.reading.platform.entity.CoursePackage; 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 +18,7 @@ import java.util.stream.Collectors; /** * 课程管理控制器(学校端) + * gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐 */ @Slf4j @RestController @@ -27,21 +31,103 @@ public class SchoolCourseController { @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); Long tenantId = SecurityUtils.getCurrentTenantId(); List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade); - return Result.success(courses); + List list = courses.stream() + .map(this::toSchoolCourseResponse) + .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/dto/response/CourseResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java index bbb3fb6..2c856f6 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; 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..9bf0b10 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java @@ -0,0 +1,68 @@ +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; + +/** + * 学校端课程响应(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; +}