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