# 套餐数据显示问题修复测试

## 问题描述

数据库中有很多套餐和课程包数据，但超管端、学校端页面上没有显示数据。接口有返回数据，但前端无法正确解析显示。

## 问题原因

1. **字段格式不匹配**：后端 `gradeLevels` 存储的是 JSON 字符串或逗号分隔字符串，前端期望数组格式
2. **数据结构不匹配**：学校端期望的数据结构与后端返回不一致
3. **缺少字段**：前端需要 `courses` 字段（包含的课程列表），但后端未返回

## 修复方案

### 后端修改

#### 1. CoursePackageResponse.java

修改字段类型和添加新字段：

```java
// gradeLevels: String → String[]
@Schema(description = "年级水平（数组）")
private String[] gradeLevels;

// 新增字段
@Schema(description = "包含的课程")
private List<CoursePackageCourseItem> courses;

@Schema(description = "开始日期（租户套餐）")
private LocalDate startDate;

@Schema(description = "结束日期（租户套餐）")
private LocalDate endDate;
```

#### 2. CoursePackageService.java

添加 `gradeLevels` 转换逻辑和 `courses` 填充逻辑：

```java
// 解析 gradeLevels，数据库中存储的是 JSON 数组字符串
String[] gradeLevelsArray = null;
if (pkg.getGradeLevels() != null && !pkg.getGradeLevels().isEmpty()) {
    try {
        // 尝试解析 JSON 数组格式：["小班","中班"]
        gradeLevelsArray = JSON.parseArray(pkg.getGradeLevels(), String.class).toArray(new String[0]);
    } catch (Exception e) {
        // 如果不是 JSON 格式，尝试按逗号分隔处理
        gradeLevelsArray = pkg.getGradeLevels().split(",");
    }
}

// 查询套餐包含的课程
List<CoursePackageCourse> packageCourses = packageCourseMapper.selectList(
    new LambdaQueryWrapper<CoursePackageCourse>()
        .eq(CoursePackageCourse::getPackageId, pkg.getId())
        .orderByAsc(CoursePackageCourse::getSortOrder)
);
```

#### 3. CoursePackageMapper.java

添加 MapStruct 类型转换方法：

```java
@Named("stringToArray")
default String[] stringToArray(String value) {
    if (value == null || value.isEmpty()) {
        return new String[0];
    }
    return value.split(",");
}

@Named("arrayToString")
default String arrayToString(String[] value) {
    if (value == null || value.length == 0) {
        return null;
    }
    return String.join(",", value);
}
```

#### 4. SchoolPackageController.java

修改返回类型：

```java
// 导入
import com.reading.platform.dto.response.CoursePackageResponse;

// 修改返回类型
public Result<List<CoursePackageResponse>> findTenantPackages() {
    Long tenantId = SecurityUtils.getCurrentTenantId();
    return Result.success(packageService.findTenantPackages(tenantId));
}
```

### 前端修改

#### 1. PackageView.vue

修改数据访问路径：

```vue
<!-- 修改前 -->
<div class="package-name">{{ item.package.name }}</div>
<span class="info-value">{{ item.package.courseCount }} 个</span>

<!-- 修改后 -->
<div class="package-name">{{ item.name }}</div>
<span class="info-value">{{ item.courseCount }} 个</span>
```

#### 2. school.ts

更新 `CoursePackage` 接口定义：

```typescript
export interface CoursePackage {
  id: number;
  name: string;
  description?: string;
  price: number;
  discountPrice?: number;
  discountType?: string;
  gradeLevels: string[];
  status: string;
  courseCount: number;
  tenantCount: number;
  createdAt: string;
  startDate?: string;
  endDate?: string;
  courses?: Array<{
    id: number;
    name: string;
    gradeLevel: string;
    sortOrder: number;
  }>;
}
```

## 测试验证

### 超管端套餐列表接口测试

```bash
curl -X GET "http://localhost:8080/api/v1/admin/packages?page=1&pageSize=10" \
  -H "Authorization: Bearer <admin_token>"
```

**返回结果**：

```json
{
  "code": 200,
  "data": {
    "list": [
      {
        "id": 3,
        "name": "幼儿园阅读启蒙套餐",
        "gradeLevels": ["小班", "中班"],
        "courseCount": 5,
        "tenantCount": 1,
        "courses": [
          {"id": 6, "name": "小猪佩奇绘本阅读", "gradeLevel": "小班", "sortOrder": 1},
          {"id": 7, "name": "彩虹色的花", "gradeLevel": "小班", "sortOrder": 2},
          {"id": 8, "name": "牙齿大街的新鲜事", "gradeLevel": "中班", "sortOrder": 3},
          {"id": 9, "name": "猜猜我有多爱你", "gradeLevel": "小班", "sortOrder": 4},
          {"id": 10, "name": "逃家小兔", "gradeLevel": "小班", "sortOrder": 5}
        ]
      }
    ]
  }
}
```

### 学校端套餐列表接口测试

```bash
curl -X GET "http://localhost:8080/api/v1/school/packages" \
  -H "Authorization: Bearer <school_token>"
```

**返回结果**：

```json
{
  "code": 200,
  "data": [
    {
      "id": 3,
      "name": "幼儿园阅读启蒙套餐",
      "gradeLevels": ["小班", "中班"],
      "courseCount": 5,
      "startDate": "2026-01-01",
      "endDate": "2026-12-31",
      "courses": [
        {"id": 6, "name": "小猪佩奇绘本阅读", "gradeLevel": "小班", "sortOrder": 1},
        {"id": 7, "name": "彩虹色的花", "gradeLevel": "小班", "sortOrder": 2},
        ...
      ]
    },
    {
      "id": 4,
      "name": "亲子共读成长套餐",
      "gradeLevels": ["小班", "中班", "大班"],
      "courseCount": 5,
      "startDate": "2026-02-01",
      "endDate": "2027-01-31",
      "courses": [...]
    }
  ]
}
```

## 测试结果

| 接口 | 测试项 | 结果 |
|------|--------|------|
| 超管端套餐列表 | 返回数据格式 | ✅ 通过 |
| 超管端套餐列表 | gradeLevels 数组格式 | ✅ 通过 |
| 超管端套餐列表 | courses 字段填充 | ✅ 通过 |
| 超管端套餐列表 | tenantCount 统计 | ✅ 通过 |
| 学校端套餐列表 | 返回数据格式 | ✅ 通过 |
| 学校端套餐列表 | startDate/endDate 字段 | ✅ 通过 |
| 学校端套餐列表 | gradeLevels 数组格式 | ✅ 通过 |
| 学校端套餐列表 | courses 字段填充 | ✅ 通过 |

## 修改的文件

### 后端文件

| 文件 | 修改内容 |
|------|----------|
| `CoursePackageResponse.java` | gradeLevels 改为 String[]，添加 courses、startDate、endDate 字段 |
| `CoursePackageService.java` | 添加 gradeLevels 转换和 courses 填充逻辑 |
| `CoursePackageMapper.java` | 添加 MapStruct 类型转换方法 |
| `SchoolPackageController.java` | 修改返回类型为 CoursePackageResponse |
| `V8__add_tenant_package_test_data.sql` | 添加租户套餐测试数据 |

### 前端文件

| 文件 | 修改内容 |
|------|----------|
| `PackageView.vue` | 修改数据访问路径 |
| `school.ts` | 更新 CoursePackage 接口定义 |

## 测试数据

V8 迁移脚本添加的测试数据：

```sql
INSERT INTO tenant_package (id, tenant_id, package_id, start_date, end_date, price_paid, status) VALUES
(1, 1, 3, '2026-01-01', '2026-12-31', 7999, 'ACTIVE'),
(2, 1, 4, '2026-02-01', '2027-01-31', 15999, 'ACTIVE');
```

## 总结

本次修复解决了前后端数据结构不匹配的问题：

1. 后端正确返回 `gradeLevels` 数组格式
2. 后端补充了前端需要的 `courses` 字段
3. 学校端套餐接口返回包含 `startDate` 和 `endDate` 字段
4. 前端正确解析并显示数据

修复后，超管端和学校端的套餐页面都能正确显示数据。

## 测试日期

2026-03-14
