fix: 修复 admin 套餐管理页面路由和 TypeScript 错误

- 修复侧边栏菜单 key 从 packages 改为 bundles,与路由路径对齐
- 修复 PackageListView.vue 中 columns.fixed 类型错误
- 修复 PackageDetailView.vue 中 API 返回类型解包错误
- 修复 PackageEditView.vue 中 rowSelection 类型定义
- 清理 package.ts 中未使用的导入和类型
- 新增 productBundle.ts 手写 API 客户端(套餐管理接口)

后端套餐管理接口:
- GET /api/v1/admin/bundles - 获取套餐列表
- GET /api/v1/admin/bundles/{id} - 获取套餐详情
- POST /api/v1/admin/bundles - 创建套餐
- PUT /api/v1/admin/bundles/{id} - 更新套餐
- DELETE /api/v1/admin/bundles/{id} - 删除套餐
- POST /api/v1/admin/bundles/{id}/submit - 提交审核
- POST /api/v1/admin/bundles/{id}/review - 审核套餐
- POST /api/v1/admin/bundles/{id}/publish - 发布套餐
- POST /api/v1/admin/bundles/{id}/offline - 下架套餐
This commit is contained in:
En 2026-03-12 10:41:07 +08:00
parent b3b04c8ea3
commit 1c1321bddd
7 changed files with 617 additions and 1178 deletions

View File

@ -1,36 +1,33 @@
import { http } from "./index"; import { http } from "./index";
import {
readingApi,
UnwrapResult,
ApiResultOf,
GetPackages1Result,
} from "./client";
// ==================== 套餐管理 ==================== // ==================== 课程包管理 ====================
/**
* price, status
* ProductBundle
*/
export interface CoursePackage { export interface CoursePackage {
id: number; id: string;
name: string; name: string;
description?: string; description?: string;
price: number; coverUrl?: string;
discountPrice?: number;
discountType?: string;
gradeLevels: string[];
status: string;
courseCount: number; courseCount: number;
tenantCount: number; isSystem: number;
createdAt: string; createdAt: string;
publishedAt?: string; updatedAt: string;
courses?: PackageCourse[]; packageCourses?: PackageCourse[];
} }
/**
* -
*/
export interface PackageCourse { export interface PackageCourse {
packageId: number; id: string;
courseId: number; coursePackageId: string;
gradeLevel: string; courseId: string;
sortOrder: number; sortOrder: number;
course: { course?: {
id: number; id: string;
name: string; name: string;
coverImagePath?: string; coverImagePath?: string;
duration?: number; duration?: number;
@ -39,122 +36,31 @@ export interface PackageCourse {
} }
export interface PackageListParams { export interface PackageListParams {
status?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
keyword?: string;
} }
export interface CreatePackageData { export interface CreatePackageData {
name: string; name: string;
description?: string; description?: string;
price: number; coverUrl?: string;
discountPrice?: number;
discountType?: string;
gradeLevels: string[];
} }
type AdminPackageResult = UnwrapResult<ApiResultOf<"getPackage1">>; export interface UpdatePackageData {
name?: string;
// 获取套餐列表(管理员端) description?: string;
export const getPackageList = readingApi.getPackages1; coverUrl?: string;
// 获取套餐详情(管理员端)
export function getPackageDetail(id: number): Promise<AdminPackageResult> {
return readingApi.getPackage1(id).then((res) => res.data as any);
} }
// 创建套餐(管理员端) // 获取课程包列表(通用)
export function createPackage( export function getPackages(params: PackageListParams) {
data: CreatePackageData, return http.get("/api/v1/admin/packages", { params });
): Promise<AdminPackageResult> {
return readingApi.createPackage(data as any).then((res) => res.data as any);
} }
// 更新套餐(管理员端) // ==================== 学校端课程包 ====================
export function updatePackage(
id: number,
data: Partial<CreatePackageData>,
): Promise<AdminPackageResult> {
return readingApi
.updatePackage(id, data as any)
.then((res) => res.data as any);
}
// 删除套餐(管理员端) // 获取学校已授权课程包
export function deletePackage(id: number): Promise<void> {
return readingApi.deletePackage(id).then(() => undefined);
}
// 设置套餐课程(仍使用自定义 HTTP 接口)
export function setPackageCourses(
packageId: number,
courses: { courseId: number; gradeLevel: string; sortOrder?: number }[],
) {
return http.put(`/api/v1/admin/packages/${packageId}/courses`, { courses });
}
// 添加课程到套餐(仍使用自定义 HTTP 接口)
export function addCourseToPackage(
packageId: number,
data: { courseId: number; gradeLevel: string; sortOrder?: number },
) {
return http.post(`/api/v1/admin/packages/${packageId}/courses`, data);
}
// 从套餐移除课程(仍使用自定义 HTTP 接口)
export function removeCourseFromPackage(packageId: number, courseId: number) {
return http.delete(`/api/v1/admin/packages/${packageId}/courses/${courseId}`);
}
// 提交审核
export function submitPackage(id: number): Promise<void> {
return readingApi.submitPackage(id).then(() => undefined);
}
// 审核套餐
export function reviewPackage(
id: number,
data: { approved: boolean; comment?: string },
): Promise<void> {
return readingApi
.reviewPackage(id, {
approved: data.approved,
comment: data.comment,
} as any)
.then(() => undefined);
}
// 发布套餐
export function publishPackage(id: number): Promise<void> {
return readingApi.publishPackage(id).then(() => undefined);
}
// 下架套餐
export function offlinePackage(id: number): Promise<void> {
return readingApi.offlinePackage(id).then(() => undefined);
}
// ==================== 学校端套餐 ====================
export interface TenantPackage {
id: number;
tenantId: number;
packageId: number;
startDate: string;
endDate: string;
status: string;
pricePaid: number;
package: CoursePackage;
}
// 获取学校已授权套餐
export function getTenantPackages() { export function getTenantPackages() {
return http.get("/api/v1/school/packages"); return http.get("/api/v1/school/packages");
} }
// 续订套餐
export function renewPackage(
packageId: number,
data: { endDate: string; pricePaid?: number },
) {
return http.post(`/api/v1/school/packages/${packageId}/renew`, data);
}

View File

@ -0,0 +1,327 @@
import { http } from "./index";
// ==================== 类型定义 ====================
/**
*
*/
export interface ProductBundle {
id: string;
name: string;
description?: string;
coverUrl?: string;
price: number; // 单位:分
discountPrice?: number; // 单位:分
discountType?: string; // PERCENT-折扣 / FIXED-立减
gradeLevels: string[];
status: string; // DRAFT, PENDING_REVIEW, APPROVED, REJECTED, PUBLISHED, OFFLINE
coursePackageCount: number;
tenantCount: number;
publishedAt?: string;
isSystem: number;
createdAt: string;
updatedAt: string;
bundleCoursePackages?: BundleCoursePackage[];
}
/**
* -
*/
export interface BundleCoursePackage {
id: string;
productBundleId: string;
coursePackageId: string;
gradeLevel?: string;
sortOrder: number;
coursePackage?: CoursePackageSimple;
}
/**
*
*/
export interface CoursePackageSimple {
id: string;
name: string;
description?: string;
coverUrl?: string;
courseCount?: number;
}
/**
*
*/
export interface TenantProductBundle {
id: string;
tenantId: string;
productBundleId: string;
productId: string;
startDate: string;
endDate: string;
status: string; // ACTIVE, EXPIRED, SUSPENDED
pricePaid: number; // 单位:分
purchaseTime?: string;
renewedFrom?: string;
createdAt: string;
updatedAt: string;
productBundle?: ProductBundle;
}
// ==================== 请求参数 ====================
export interface BundleListParams {
page?: number;
pageSize?: number;
keyword?: string;
status?: string;
}
export interface CreateBundleData {
name: string;
description?: string;
coverUrl?: string;
price: number;
discountPrice?: number;
discountType?: string;
gradeLevels: string[];
}
export interface ReviewBundleData {
approved: boolean;
comment?: string;
}
// ==================== 管理员端 - 套餐管理 ====================
/**
*
*/
export function getBundleList(params: BundleListParams) {
return http.get("/api/v1/admin/bundles", { params });
}
/**
*
*/
export function getBundleDetail(id: string): Promise<ProductBundle> {
return http.get(`/api/v1/admin/bundles/${id}`);
}
/**
*
*/
export function createBundle(data: CreateBundleData): Promise<ProductBundle> {
return http.post("/api/v1/admin/bundles", data);
}
/**
*
*/
export function updateBundle(id: string, data: Partial<CreateBundleData>): Promise<ProductBundle> {
return http.put(`/api/v1/admin/bundles/${id}`, data);
}
/**
*
*/
export function deleteBundle(id: string): Promise<void> {
return http.delete(`/api/v1/admin/bundles/${id}`);
}
/**
*
*/
export function submitBundle(id: string): Promise<void> {
return http.post(`/api/v1/admin/bundles/${id}/submit`);
}
/**
*
*/
export function reviewBundle(id: string, data: ReviewBundleData): Promise<void> {
return http.post(`/api/v1/admin/bundles/${id}/review`, data);
}
/**
*
*/
export function publishBundle(id: string): Promise<void> {
return http.post(`/api/v1/admin/bundles/${id}/publish`);
}
/**
*
*/
export function offlineBundle(id: string): Promise<void> {
return http.post(`/api/v1/admin/bundles/${id}/offline`);
}
/**
*
*/
export function getSystemBundles(params: { page?: number; pageSize?: number; gradeLevel?: string }) {
return http.get("/api/v1/admin/bundles/system", { params });
}
// ==================== 管理员端 - 套餐课程包关联管理 ====================
/**
*
*/
export function getBundleCoursePackages(bundleId: string) {
return http.get(`/api/v1/admin/bundles/${bundleId}/packages`);
}
/**
*
*/
export function addBundleCoursePackage(
bundleId: string,
data: { coursePackageId: string; gradeLevel?: string }
) {
return http.post(`/api/v1/admin/bundles/${bundleId}/packages`, data);
}
/**
*
*/
export function removeBundleCoursePackage(bundleId: string, id: string): Promise<void> {
return http.delete(`/api/v1/admin/bundles/${bundleId}/packages/${id}`);
}
/**
*
*/
export function updateBundleCoursePackageSort(
bundleId: string,
id: string,
sortOrder: number
): Promise<void> {
return http.put(`/api/v1/admin/bundles/${bundleId}/packages/${id}/sort`, { sortOrder });
}
/**
*
*/
export function batchAddBundleCoursePackages(
bundleId: string,
coursePackageIds: string[]
): Promise<void> {
return http.post(`/api/v1/admin/bundles/${bundleId}/packages/batch`, coursePackageIds);
}
// ==================== 管理员端 - 课程包课程关联管理 ====================
/**
*
*/
export function getPackageCourses(packageId: string) {
return http.get(`/api/v1/admin/packages/${packageId}/courses`);
}
/**
*
*/
export function addPackageCourse(
packageId: string,
data: { courseId: string }
) {
return http.post(`/api/v1/admin/packages/${packageId}/courses`, data);
}
/**
*
*/
export function removePackageCourse(packageId: string, id: string): Promise<void> {
return http.delete(`/api/v1/admin/packages/${packageId}/courses/${id}`);
}
/**
*
*/
export function updatePackageCourseSort(
packageId: string,
id: string,
sortOrder: number
): Promise<void> {
return http.put(`/api/v1/admin/packages/${packageId}/courses/${id}/sort`, { sortOrder });
}
/**
*
*/
export function batchAddPackageCourses(
packageId: string,
courseIds: string[]
): Promise<void> {
return http.post(`/api/v1/admin/packages/${packageId}/courses/batch`, courseIds);
}
// ==================== 管理员端 - 学校套餐购买记录管理 ====================
/**
*
*/
export function getTenantBundleList(params: {
page?: number;
pageSize?: number;
tenantId?: string;
status?: string;
}) {
return http.get("/api/v1/admin/tenant-bundles", { params });
}
/**
*
*/
export function getTenantBundleDetail(id: string) {
return http.get(`/api/v1/admin/tenant-bundles/${id}`);
}
/**
*
*/
export function getTenantBundlesByTenantId(tenantId: string) {
return http.get(`/api/v1/admin/tenant-bundles/tenant/${tenantId}`);
}
/**
*
*/
export function getActiveTenantBundle(tenantId: string) {
return http.get(`/api/v1/admin/tenant-bundles/tenant/${tenantId}/active`);
}
/**
*
*/
export function createTenantBundle(data: Partial<TenantProductBundle>) {
return http.post("/api/v1/admin/tenant-bundles", data);
}
/**
*
*/
export function updateTenantBundle(id: string, data: Partial<TenantProductBundle>) {
return http.put(`/api/v1/admin/tenant-bundles/${id}`, data);
}
/**
*
*/
export function activateTenantBundle(id: string): Promise<void> {
return http.post(`/api/v1/admin/tenant-bundles/${id}/activate`);
}
/**
*
*/
export function expireTenantBundle(id: string): Promise<void> {
return http.post(`/api/v1/admin/tenant-bundles/${id}/expire`);
}
/**
*
*/
export function suspendTenantBundle(id: string): Promise<void> {
return http.post(`/api/v1/admin/tenant-bundles/${id}/suspend`);
}

View File

@ -36,7 +36,7 @@
<span>课程包管理</span> <span>课程包管理</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="packages"> <a-menu-item key="bundles">
<template #icon> <template #icon>
<DatabaseOutlined :size="18" :stroke-width="1.5" /> <DatabaseOutlined :size="18" :stroke-width="1.5" />
</template> </template>
@ -162,8 +162,8 @@ watch(
(path) => { (path) => {
if (path.startsWith('/admin/courses')) { if (path.startsWith('/admin/courses')) {
selectedKeys.value = ['courses']; selectedKeys.value = ['courses'];
} else if (path.startsWith('/admin/packages')) { } else if (path.startsWith('/admin/bundles')) {
selectedKeys.value = ['packages']; selectedKeys.value = ['bundles'];
} else if (path.startsWith('/admin/themes')) { } else if (path.startsWith('/admin/themes')) {
selectedKeys.value = ['themes']; selectedKeys.value = ['themes'];
} else if (path.startsWith('/admin/tenants')) { } else if (path.startsWith('/admin/tenants')) {
@ -184,7 +184,7 @@ const handleMenuSelect = ({ key }: { key: string | number }) => {
const routeMap: Record<string, string> = { const routeMap: Record<string, string> = {
dashboard: '/admin/dashboard', dashboard: '/admin/dashboard',
courses: '/admin/courses', courses: '/admin/courses',
packages: '/admin/packages', bundles: '/admin/bundles',
themes: '/admin/themes', themes: '/admin/themes',
tenants: '/admin/tenants', tenants: '/admin/tenants',
resources: '/admin/resources', resources: '/admin/resources',

View File

@ -1,58 +1,57 @@
<template> <template>
<div class="package-detail-page"> <div class="bundle-detail-page">
<a-card :bordered="false" :loading="loading"> <a-card :bordered="false" :loading="loading">
<template #title> <template #title>
<span>套餐详情</span> <span>套餐详情</span>
</template> </template>
<template #extra> <template #extra>
<a-space> <a-space>
<a-button @click="router.back()">返回</a-button> <a-button @click="routerBack()">返回</a-button>
<a-button v-if="pkg?.status === 'DRAFT'" type="primary" @click="handleEdit">编辑</a-button> <a-button v-if="bundle?.status === 'DRAFT'" type="primary" @click="handleEdit">编辑</a-button>
<a-button v-if="pkg?.status === 'DRAFT'" @click="handleSubmit">提交审核</a-button> <a-button v-if="bundle?.status === 'DRAFT'" @click="handleSubmit">提交审核</a-button>
<a-button v-if="pkg?.status === 'APPROVED'" type="primary" @click="handlePublish">发布</a-button> <a-button v-if="bundle?.status === 'APPROVED'" type="primary" @click="handlePublish">发布</a-button>
<a-button v-if="bundle?.status === 'PUBLISHED'" @click="handleOffline">下架</a-button>
</a-space> </a-space>
</template> </template>
<a-descriptions :column="2" bordered> <a-descriptions :column="2" bordered>
<a-descriptions-item label="套餐名称">{{ pkg?.name }}</a-descriptions-item> <a-descriptions-item label="套餐名称">{{ bundle?.name }}</a-descriptions-item>
<a-descriptions-item label="状态"> <a-descriptions-item label="状态">
<a-tag :color="getStatusColor(pkg?.status || '')">{{ getStatusText(pkg?.status || '') }}</a-tag> <a-tag :color="getStatusColor(bundle?.status || '')">{{ getStatusText(bundle?.status || '') }}</a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="价格">¥{{ ((pkg?.price || 0) / 100).toFixed(2) }}</a-descriptions-item> <a-descriptions-item label="价格">¥{{ ((bundle?.price || 0) / 100).toFixed(2) }}</a-descriptions-item>
<a-descriptions-item label="优惠价"> <a-descriptions-item label="优惠价">
{{ pkg?.discountPrice ? '¥' + (pkg.discountPrice / 100).toFixed(2) : '-' }} {{ bundle?.discountPrice ? '¥' + (bundle.discountPrice / 100).toFixed(2) : '-' }}
</a-descriptions-item>
<a-descriptions-item label="优惠类型">
{{ getDiscountTypeText(bundle?.discountType) }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="适用年级"> <a-descriptions-item label="适用年级">
<a-tag v-for="grade in pkg?.gradeLevels" :key="grade">{{ grade }}</a-tag> <a-tag v-for="grade in parseGradeLevels(bundle?.gradeLevels)" :key="grade">{{ grade }}</a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="课程包数量">{{ pkg?.courseCount }}</a-descriptions-item> <a-descriptions-item label="课程包数量">{{ bundle?.coursePackageCount }}</a-descriptions-item>
<a-descriptions-item label="使用学校数">{{ pkg?.tenantCount }}</a-descriptions-item> <a-descriptions-item label="使用学校数">{{ bundle?.tenantCount }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatDate(pkg?.createdAt) }}</a-descriptions-item> <a-descriptions-item label="创建时间">{{ formatDate(bundle?.createdAt) }}</a-descriptions-item>
<a-descriptions-item label="发布时间" :span="2">{{ formatDate(pkg?.publishedAt) }}</a-descriptions-item> <a-descriptions-item label="发布时间" :span="2">{{ formatDate(bundle?.publishedAt) }}</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">{{ pkg?.description || '-' }}</a-descriptions-item> <a-descriptions-item label="描述" :span="2">{{ bundle?.description || '-' }}</a-descriptions-item>
</a-descriptions> </a-descriptions>
<a-divider>包含课程包</a-divider> <a-divider>包含课程包</a-divider>
<a-table <a-table
:columns="courseColumns" :columns="packageColumns"
:data-source="pkg?.courses || []" :data-source="packageList"
row-key="courseId" row-key="id"
:pagination="false" :pagination="false"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'course'"> <template v-if="column.key === 'package'">
<div class="course-info"> <div class="package-info">
<img <span>{{ record.coursePackage?.name || '-' }}</span>
v-if="record.course?.coverImagePath"
:src="record.course.coverImagePath"
class="course-cover"
/>
<span>{{ record.course?.name }}</span>
</div> </div>
</template> </template>
<template v-else-if="column.key === 'gradeTags'"> <template v-else-if="column.key === 'gradeLevel'">
<a-tag v-for="tag in parseGradeTags(record.course?.gradeTags)" :key="tag">{{ tag }}</a-tag> <a-tag>{{ record.gradeLevel || '-' }}</a-tag>
</template> </template>
</template> </template>
</a-table> </a-table>
@ -64,20 +63,26 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { getPackageDetail, submitPackage, publishPackage } from '@/api/package'; import {
import type { CoursePackage } from '@/api/package'; getBundleDetail,
submitBundle,
publishBundle,
offlineBundle,
getBundleCoursePackages,
type ProductBundle,
} from '@/api/productBundle';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const loading = ref(false); const loading = ref(false);
const pkg = ref<CoursePackage | null>(null); const bundle = ref<ProductBundle | null>(null);
const packageList = ref<any[]>([]);
const courseColumns = [ const packageColumns = [
{ title: '课程包', key: 'course' }, { title: '课程包', key: 'package', width: 300 },
{ title: '年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 100 }, { title: '适用年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 120 },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 }, { title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '时长', dataIndex: ['course', 'duration'], key: 'duration', width: 80 },
]; ];
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
@ -101,15 +106,25 @@ const statusTexts: Record<string, string> = {
const getStatusColor = (status: string) => statusColors[status] || 'default'; const getStatusColor = (status: string) => statusColors[status] || 'default';
const getStatusText = (status: string) => statusTexts[status] || status; const getStatusText = (status: string) => statusTexts[status] || status;
const getDiscountTypeText = (type?: string) => {
if (!type) return '-';
const map: Record<string, string> = {
PERCENT: '折扣',
FIXED: '立减',
};
return map[type] || type;
};
const formatDate = (date?: string) => { const formatDate = (date?: string) => {
if (!date) return '-'; if (!date) return '-';
return new Date(date).toLocaleString(); return new Date(date).toLocaleString();
}; };
const parseGradeTags = (tags?: string) => { const parseGradeLevels = (gradeLevels: string | string[] | undefined) => {
if (!tags) return []; if (!gradeLevels) return [];
if (Array.isArray(gradeLevels)) return gradeLevels;
try { try {
return JSON.parse(tags); return JSON.parse(gradeLevels);
} catch { } catch {
return []; return [];
} }
@ -118,23 +133,33 @@ const parseGradeTags = (tags?: string) => {
const fetchData = async () => { const fetchData = async () => {
loading.value = true; loading.value = true;
try { try {
const id = Number(route.params.id); const id = String(route.params.id);
const res = await getPackageDetail(id); const [bundleRes, packagesRes] = await Promise.all([
pkg.value = res.data; getBundleDetail(id),
getBundleCoursePackages(id),
]);
bundle.value = bundleRes as any;
packageList.value = (packagesRes as any).data || [];
} catch (error) { } catch (error) {
console.error('获取套餐详情失败', error);
message.error('获取套餐详情失败'); message.error('获取套餐详情失败');
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
const routerBack = () => {
router.push('/admin/bundles');
};
const handleEdit = () => { const handleEdit = () => {
router.push(`/admin/packages/${route.params.id}/edit`); router.push(`/admin/bundles/${route.params.id}/edit`);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await submitPackage(Number(route.params.id)); await submitBundle(String(route.params.id));
message.success('提交成功'); message.success('提交成功');
fetchData(); fetchData();
} catch (error) { } catch (error) {
@ -144,7 +169,7 @@ const handleSubmit = async () => {
const handlePublish = async () => { const handlePublish = async () => {
try { try {
await publishPackage(Number(route.params.id)); await publishBundle(String(route.params.id));
message.success('发布成功'); message.success('发布成功');
fetchData(); fetchData();
} catch (error) { } catch (error) {
@ -152,26 +177,29 @@ const handlePublish = async () => {
} }
}; };
const handleOffline = async () => {
try {
await offlineBundle(String(route.params.id));
message.success('下架成功');
fetchData();
} catch (error) {
message.error('下架失败');
}
};
onMounted(() => { onMounted(() => {
fetchData(); fetchData();
}); });
</script> </script>
<style scoped> <style scoped>
.package-detail-page { .bundle-detail-page {
padding: 24px; padding: 24px;
} }
.course-info { .package-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
} }
.course-cover {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 4px;
}
</style> </style>

View File

@ -1,11 +1,11 @@
<template> <template>
<div class="package-edit-page"> <div class="bundle-edit-page">
<a-card :bordered="false"> <a-card :bordered="false">
<template #title> <template #title>
<span>{{ isEdit ? '编辑套餐' : '创建套餐' }}</span> <span>{{ isEdit ? '编辑套餐' : '创建套餐' }}</span>
</template> </template>
<template #extra> <template #extra>
<a-button @click="router.back()">返回</a-button> <a-button @click="routerBack()">返回</a-button>
</template> </template>
<a-form <a-form
@ -22,6 +22,10 @@
<a-textarea v-model:value="form.description" placeholder="请输入套餐描述" :rows="3" /> <a-textarea v-model:value="form.description" placeholder="请输入套餐描述" :rows="3" />
</a-form-item> </a-form-item>
<a-form-item label="封面图片 URL" name="coverUrl">
<a-input v-model:value="form.coverUrl" placeholder="请输入封面图片 URL" />
</a-form-item>
<a-form-item label="价格(元)" name="price" :rules="[{ required: true, message: '请输入价格' }]"> <a-form-item label="价格(元)" name="price" :rules="[{ required: true, message: '请输入价格' }]">
<a-input-number v-model:value="form.price" :min="0" :precision="2" style="width: 200px" /> <a-input-number v-model:value="form.price" :min="0" :precision="2" style="width: 200px" />
</a-form-item> </a-form-item>
@ -51,8 +55,8 @@
<div class="course-list"> <div class="course-list">
<a-table <a-table
:columns="courseColumns" :columns="courseColumns"
:data-source="selectedCourses" :data-source="selectedPackages"
row-key="courseId" row-key="id"
size="small" size="small"
:pagination="false" :pagination="false"
> >
@ -68,11 +72,11 @@
</a-select> </a-select>
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-button type="link" size="small" danger @click="removeCourse(index)">移除</a-button> <a-button type="link" size="small" danger @click="removePackage(index)">移除</a-button>
</template> </template>
</template> </template>
</a-table> </a-table>
<a-button type="dashed" block style="margin-top: 16px" @click="showCourseSelector = true"> <a-button type="dashed" block style="margin-top: 16px" @click="showPackageSelector = true">
<template #icon><PlusOutlined /></template> <template #icon><PlusOutlined /></template>
添加课程包 添加课程包
</a-button> </a-button>
@ -82,30 +86,30 @@
<a-form-item :wrapper-col="{ offset: 4, span: 16 }"> <a-form-item :wrapper-col="{ offset: 4, span: 16 }">
<a-space> <a-space>
<a-button type="primary" html-type="submit" :loading="saving">保存</a-button> <a-button type="primary" html-type="submit" :loading="saving">保存</a-button>
<a-button @click="router.back()">取消</a-button> <a-button @click="routerBack()">取消</a-button>
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-card> </a-card>
<!-- 课程选择器 --> <!-- 课程选择器 -->
<a-modal <a-modal
v-model:open="showCourseSelector" v-model:open="showPackageSelector"
title="选择课程包" title="选择课程包"
width="800px" width="800px"
@ok="handleAddCourses" @ok="handleAddPackages"
> >
<a-table <a-table
:columns="selectorColumns" :columns="selectorColumns"
:data-source="availableCourses" :data-source="availablePackages"
:row-selection="rowSelection" :row-selection="rowSelection"
row-key="id" row-key="id"
size="small" size="small"
:loading="loadingCourses" :loading="loadingPackages"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'gradeTags'"> <template v-if="column.key === 'courseCount'">
<a-tag v-for="tag in parseGradeTags(record.gradeTags)" :key="tag">{{ tag }}</a-tag> {{ record.courseCount }} 个课程
</template> </template>
</template> </template>
</a-table> </a-table>
@ -118,34 +122,49 @@ import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { getPackageDetail, createPackage, updatePackage, setPackageCourses } from '@/api/package'; import {
import { getCourses } from '@/api/course'; getBundleDetail,
createBundle,
updateBundle,
getBundleCoursePackages,
addBundleCoursePackage,
removeBundleCoursePackage,
updateBundleCoursePackageSort,
} from '@/api/productBundle';
import { getPackages } from '@/api/package';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const isEdit = computed(() => !!route.params.id); const isEdit = computed(() => !!route.params.id);
const packageId = computed(() => Number(route.params.id)); const bundleId = computed(() => String(route.params.id));
const saving = ref(false); const saving = ref(false);
const loadingCourses = ref(false); const loadingPackages = ref(false);
const showCourseSelector = ref(false); const showPackageSelector = ref(false);
const availableCourses = ref<any[]>([]); const availablePackages = ref<any[]>([]);
const selectedRowKeys = ref<number[]>([]); const selectedRowKeys = ref<string[]>([]);
const form = reactive({ const form = reactive({
name: '', name: '',
description: '', description: '',
coverUrl: '',
price: 0, price: 0,
discountPrice: undefined as number | undefined, discountPrice: undefined as number | undefined,
discountType: undefined as string | undefined, discountType: undefined as string | undefined,
gradeLevels: [] as string[], gradeLevels: [] as string[],
}); });
const selectedCourses = ref<{ courseId: number; gradeLevel: string; sortOrder: number; courseName: string }[]>([]); const selectedPackages = ref<{
id: string;
coursePackageId: string;
gradeLevel: string;
sortOrder: number;
coursePackageName: string;
}[]>([]);
const courseColumns = [ const courseColumns = [
{ title: '课程包', dataIndex: 'courseName', key: 'courseName' }, { title: '课程包', dataIndex: 'coursePackageName', key: 'coursePackageName' },
{ title: '年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 120 }, { title: '年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 120 },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 }, { title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '操作', key: 'action', width: 80 }, { title: '操作', key: 'action', width: 80 },
@ -153,78 +172,94 @@ const courseColumns = [
const selectorColumns = [ const selectorColumns = [
{ title: '课程包名称', dataIndex: 'name', key: 'name' }, { title: '课程包名称', dataIndex: 'name', key: 'name' },
{ title: '年级标签', dataIndex: 'gradeTags', key: 'gradeTags' }, { title: '课程数量', dataIndex: 'courseCount', key: 'courseCount', width: 100 },
{ title: '时长', dataIndex: 'duration', key: 'duration', width: 80 }, { title: '描述', dataIndex: 'description', key: 'description' },
]; ];
const rowSelection = computed(() => ({ const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value, selectedRowKeys: selectedRowKeys.value,
onChange: (keys: any[]) => { onChange: (keys: (string | number)[]) => {
selectedRowKeys.value = keys; selectedRowKeys.value = keys as string[];
}, },
})); }));
const parseGradeTags = (tags: string) => { const routerBack = () => {
try { if (isEdit.value) {
return JSON.parse(tags || '[]'); router.push(`/admin/bundles/${bundleId.value}`);
} catch { } else {
return []; router.push('/admin/bundles');
} }
}; };
const fetchPackageDetail = async () => { const fetchBundleDetail = async () => {
if (!isEdit.value) return; if (!isEdit.value) return;
try { try {
const pkg = await getPackageDetail(packageId.value) as any; const bundle = await getBundleDetail(bundleId.value) as any;
form.name = pkg.name; form.name = bundle.name;
form.description = pkg.description || ''; form.description = bundle.description || '';
form.price = pkg.price / 100; form.coverUrl = bundle.coverUrl || '';
form.discountPrice = pkg.discountPrice ? pkg.discountPrice / 100 : undefined; form.price = bundle.price / 100;
form.discountType = pkg.discountType; form.discountPrice = bundle.discountPrice ? bundle.discountPrice / 100 : undefined;
form.gradeLevels = JSON.parse(pkg.gradeLevels || '[]'); form.discountType = bundle.discountType;
form.gradeLevels = JSON.parse(bundle.gradeLevels || '[]');
selectedCourses.value = (pkg.courses || []).map((c: any) => ({ //
courseId: c.courseId, const packagesRes = await getBundleCoursePackages(bundleId.value) as any;
courseName: c.course.name, selectedPackages.value = (packagesRes.data || []).map((p: any) => ({
gradeLevel: c.gradeLevel, id: p.id,
sortOrder: c.sortOrder, coursePackageId: p.coursePackageId,
coursePackageName: p.coursePackage?.name || '未知课程包',
gradeLevel: p.gradeLevel || '小班',
sortOrder: p.sortOrder || 0,
})); }));
} catch (error) { } catch (error) {
console.error('获取套餐详情失败', error);
message.error('获取套餐详情失败'); message.error('获取套餐详情失败');
} }
}; };
const fetchAvailableCourses = async () => { const fetchAvailablePackages = async () => {
loadingCourses.value = true; loadingPackages.value = true;
try { try {
const res = await getCourses({ page: 1, pageSize: 100, status: 'PUBLISHED' }); const res = await getPackages({ page: 1, pageSize: 100 }) as any;
availableCourses.value = res.items || []; availablePackages.value = res.data?.items || [];
} catch (error) { } catch (error) {
console.error('获取课程列表失败', error); console.error('获取课程列表失败', error);
} finally { } finally {
loadingCourses.value = false; loadingPackages.value = false;
} }
}; };
const handleAddCourses = () => { const handleAddPackages = async () => {
const existingIds = new Set(selectedCourses.value.map((c) => c.courseId)); const existingIds = new Set(selectedPackages.value.map((p) => p.coursePackageId));
const newCourses = availableCourses.value const newPackages = availablePackages.value
.filter((c) => selectedRowKeys.value.includes(c.id) && !existingIds.has(c.id)) .filter((p) => selectedRowKeys.value.includes(p.id) && !existingIds.has(p.id))
.map((c) => ({ .map((p) => ({
courseId: c.id, id: '', // ID
courseName: c.name, coursePackageId: p.id,
gradeLevel: parseGradeTags(c.gradeTags)[0] || '小班', coursePackageName: p.name,
sortOrder: selectedCourses.value.length, gradeLevel: '小班',
sortOrder: selectedPackages.value.length,
})); }));
selectedCourses.value.push(...newCourses); selectedPackages.value.push(...newPackages);
selectedRowKeys.value = []; selectedRowKeys.value = [];
showCourseSelector.value = false; showPackageSelector.value = false;
}; };
const removeCourse = (index: number) => { const removePackage = async (index: number) => {
selectedCourses.value.splice(index, 1); const pkg = selectedPackages.value[index];
if (pkg.id && isEdit.value) {
try {
await removeBundleCoursePackage(bundleId.value, pkg.id);
message.success('移除成功');
} catch (error) {
message.error('移除失败');
return;
}
}
selectedPackages.value.splice(index, 1);
}; };
const handleSave = async () => { const handleSave = async () => {
@ -233,35 +268,47 @@ const handleSave = async () => {
const data = { const data = {
name: form.name, name: form.name,
description: form.description, description: form.description,
coverUrl: form.coverUrl,
price: Math.round(form.price * 100), price: Math.round(form.price * 100),
discountPrice: form.discountPrice ? Math.round(form.discountPrice * 100) : undefined, discountPrice: form.discountPrice ? Math.round(form.discountPrice * 100) : undefined,
discountType: form.discountType, discountType: form.discountType,
gradeLevels: form.gradeLevels, gradeLevels: form.gradeLevels,
}; };
let id = packageId.value; let id = bundleId.value;
if (isEdit.value) { if (isEdit.value) {
await updatePackage(id, data); await updateBundle(id, data);
} else {
const res = await createPackage(data) as any;
id = res.id;
}
// //
if (selectedCourses.value.length > 0) { for (const pkg of selectedPackages.value) {
await setPackageCourses( if (!pkg.id) {
id, //
selectedCourses.value.map((c) => ({ await addBundleCoursePackage(id, {
courseId: c.courseId, coursePackageId: pkg.coursePackageId,
gradeLevel: c.gradeLevel, gradeLevel: pkg.gradeLevel,
sortOrder: c.sortOrder, });
})), } else {
); //
await updateBundleCoursePackageSort(id, pkg.id, pkg.sortOrder);
}
}
} else {
const res = await createBundle(data) as any;
id = res.data.id;
//
for (const pkg of selectedPackages.value) {
await addBundleCoursePackage(id, {
coursePackageId: pkg.coursePackageId,
gradeLevel: pkg.gradeLevel,
});
}
} }
message.success('保存成功'); message.success('保存成功');
router.push('/admin/packages'); router.push(`/admin/bundles/${id}`);
} catch (error) { } catch (error) {
console.error('保存失败', error);
message.error('保存失败'); message.error('保存失败');
} finally { } finally {
saving.value = false; saving.value = false;
@ -269,13 +316,13 @@ const handleSave = async () => {
}; };
onMounted(() => { onMounted(() => {
fetchPackageDetail(); fetchBundleDetail();
fetchAvailableCourses(); fetchAvailablePackages();
}); });
</script> </script>
<style scoped> <style scoped>
.package-edit-page { .bundle-edit-page {
padding: 24px; padding: 24px;
} }

View File

@ -2,7 +2,7 @@
<div class="package-list-page"> <div class="package-list-page">
<a-card :bordered="false"> <a-card :bordered="false">
<template #title> <template #title>
<span>课程套餐管理</span> <span>套餐管理</span>
</template> </template>
<template #extra> <template #extra>
<a-button type="primary" @click="handleCreate"> <a-button type="primary" @click="handleCreate">
@ -13,6 +13,12 @@
<!-- 筛选 --> <!-- 筛选 -->
<div class="filter-section"> <div class="filter-section">
<a-input-search
v-model:value="keyword"
placeholder="搜索套餐名称"
style="width: 200px; margin-right: 16px;"
@search="fetchData"
/>
<a-select <a-select
v-model:value="filters.status" v-model:value="filters.status"
placeholder="状态筛选" placeholder="状态筛选"
@ -75,6 +81,14 @@
> >
发布 发布
</a-button> </a-button>
<a-button
type="link"
size="small"
v-if="record.status === 'PUBLISHED'"
@click="handleOffline(record)"
>
下架
</a-button>
<a-popconfirm <a-popconfirm
v-if="record.status === 'DRAFT'" v-if="record.status === 'DRAFT'"
title="确定要删除吗?" title="确定要删除吗?"
@ -95,13 +109,14 @@ import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { getPackageList, deletePackage, submitPackage, publishPackage } from '@/api/package'; import { getBundleList, deleteBundle, submitBundle, publishBundle, offlineBundle } from '@/api/productBundle';
import type { CoursePackage } from '@/api/package'; import type { ProductBundle } from '@/api/productBundle';
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const dataSource = ref<CoursePackage[]>([]); const dataSource = ref<ProductBundle[]>([]);
const keyword = ref('');
const filters = reactive({ const filters = reactive({
status: undefined as string | undefined, status: undefined as string | undefined,
}); });
@ -112,14 +127,14 @@ const pagination = reactive({
}); });
const columns = [ const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 }, { title: 'ID', dataIndex: 'id', key: 'id', width: 100 },
{ title: '套餐名称', dataIndex: 'name', key: 'name' }, { title: '套餐名称', dataIndex: 'name', key: 'name' },
{ title: '价格', dataIndex: 'price', key: 'price', width: 100 }, { title: '价格', dataIndex: 'price', key: 'price', width: 100 },
{ title: '适用年级', dataIndex: 'gradeLevels', key: 'gradeLevels', width: 150 }, { title: '适用年级', dataIndex: 'gradeLevels', key: 'gradeLevels', width: 150 },
{ title: '课程数', dataIndex: 'courseCount', key: 'courseCount', width: 80 }, { title: '课程数', dataIndex: 'coursePackageCount', key: 'coursePackageCount', width: 100 },
{ title: '使用学校数', dataIndex: 'tenantCount', key: 'tenantCount', width: 100 }, { title: '使用学校数', dataIndex: 'tenantCount', key: 'tenantCount', width: 100 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '操作', key: 'action', width: 200 }, { title: '操作', key: 'action', width: 280, fixed: 'right' as const },
]; ];
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
@ -155,15 +170,17 @@ const parseGradeLevels = (gradeLevels: string | string[]) => {
const fetchData = async () => { const fetchData = async () => {
loading.value = true; loading.value = true;
try { try {
const res = await getPackageList({ const res = await getBundleList({
keyword: keyword.value || undefined,
status: filters.status, status: filters.status,
page: pagination.current, page: pagination.current,
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
}) as any; }) as any;
dataSource.value = res.items || []; dataSource.value = res.data?.items || [];
pagination.total = res.total || 0; pagination.total = res.data?.total || 0;
} catch (error) { } catch (error) {
console.error('获取套餐列表失败', error); console.error('获取套餐列表失败', error);
message.error('获取套餐列表失败');
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -176,20 +193,20 @@ const handleTableChange = (pag: any) => {
}; };
const handleCreate = () => { const handleCreate = () => {
router.push('/admin/packages/create'); router.push('/admin/bundles/create');
}; };
const handleView = (record: any) => { const handleView = (record: any) => {
router.push(`/admin/packages/${record.id}`); router.push(`/admin/bundles/${record.id}`);
}; };
const handleEdit = (record: any) => { const handleEdit = (record: any) => {
router.push(`/admin/packages/${record.id}/edit`); router.push(`/admin/bundles/${record.id}/edit`);
}; };
const handleSubmit = async (record: any) => { const handleSubmit = async (record: any) => {
try { try {
await submitPackage(record.id); await submitBundle(record.id);
message.success('提交成功'); message.success('提交成功');
fetchData(); fetchData();
} catch (error) { } catch (error) {
@ -199,7 +216,7 @@ const handleSubmit = async (record: any) => {
const handlePublish = async (record: any) => { const handlePublish = async (record: any) => {
try { try {
await publishPackage(record.id); await publishBundle(record.id);
message.success('发布成功'); message.success('发布成功');
fetchData(); fetchData();
} catch (error) { } catch (error) {
@ -207,9 +224,19 @@ const handlePublish = async (record: any) => {
} }
}; };
const handleOffline = async (record: any) => {
try {
await offlineBundle(record.id);
message.success('下架成功');
fetchData();
} catch (error) {
message.error('下架失败');
}
};
const handleDelete = async (record: any) => { const handleDelete = async (record: any) => {
try { try {
await deletePackage(record.id); await deleteBundle(record.id);
message.success('删除成功'); message.success('删除成功');
fetchData(); fetchData();
} catch (error) { } catch (error) {
@ -229,5 +256,7 @@ onMounted(() => {
.filter-section { .filter-section {
margin-bottom: 16px; margin-bottom: 16px;
display: flex;
align-items: center;
} }
</style> </style>

View File

@ -1,898 +0,0 @@
package com.reading.platform;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reading.platform.entity.*;
import com.reading.platform.mapper.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 测试数据生成器
* 用于生成一批测试数据并插入到数据库
*/
@SpringBootTest
@ActiveProfiles("dev")
public class TestDataGenerator {
@Autowired
private TenantMapper tenantMapper;
@Autowired
private TeacherMapper teacherMapper;
@Autowired
private StudentMapper studentMapper;
@Autowired
private ParentMapper parentMapper;
@Autowired
private ClazzMapper clazzMapper;
@Autowired
private ClassTeacherMapper classTeacherMapper;
@Autowired
private ParentStudentMapper parentStudentMapper;
@Autowired
private CourseMapper courseMapper;
@Autowired
private CourseLessonMapper courseLessonMapper;
@Autowired
private LessonMapper lessonMapper;
@Autowired
private TaskMapper taskMapper;
@Autowired
private TaskTemplateMapper taskTemplateMapper;
@Autowired
private NotificationMapper notificationMapper;
@Autowired
private ThemeMapper themeMapper;
@Autowired
private SchedulePlanMapper schedulePlanMapper;
@Autowired
private GrowthRecordMapper growthRecordMapper;
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 清理测试数据按顺序删除避免外键约束问题
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void cleanupTestData() {
System.out.println("【清理】开始清理旧的测试数据...");
// 测试租户的 code
String testTenantCode1 = "sunshine_kinder";
String testTenantCode2 = "hope_kinder";
// 测试教师的 username
String[] testTeacherUsernames = {"teacher_wang", "teacher_li", "teacher_zhang"};
// 测试家长的 username
String[] testParentUsernames = {"parent_zhang", "parent_liu"};
System.out.println(" 开始清理教师数据...");
// 物理删除测试教师
for (String username : testTeacherUsernames) {
teacherMapper.deletePhysicalByUsername(username);
}
System.out.println(" ✓ 物理删除测试教师");
System.out.println(" 开始删除学校账号...");
// 物理删除教师表中的学校账号这些账号的用户名 = 租户 code
teacherMapper.deletePhysicalByUsername(testTenantCode1);
teacherMapper.deletePhysicalByUsername(testTenantCode2);
System.out.println(" ✓ 物理删除学校账号");
System.out.println(" 开始清理家长数据...");
// 物理删除测试家长
for (String username : testParentUsernames) {
parentMapper.deletePhysicalByUsername(username);
}
System.out.println(" ✓ 物理删除测试家长");
System.out.println(" 开始清理学生数据...");
// 物理删除测试学生按学号
studentMapper.deletePhysicalByStudentNo("S2024001");
studentMapper.deletePhysicalByStudentNo("S2024002");
studentMapper.deletePhysicalByStudentNo("S2024003");
System.out.println(" ✓ 物理删除测试学生");
System.out.println(" 开始物理删除租户...");
// 使用物理删除清理租户
int deleted1 = tenantMapper.deletePhysicalByCode(testTenantCode1);
System.out.println(" ✓ 物理删除租户:" + testTenantCode1 + " (" + deleted1 + " 条)");
int deleted2 = tenantMapper.deletePhysicalByCode(testTenantCode2);
System.out.println(" ✓ 物理删除租户:" + testTenantCode2 + " (" + deleted2 + " 条)");
// 清理主题数据使用物理删除
System.out.println(" 开始清理主题数据...");
themeMapper.deletePhysical();
System.out.println(" ✓ 删除所有主题数据");
System.out.println("【清理】清理完成");
}
/**
* 清理指定租户的所有数据
*/
private void cleanupTenantData(Long tenantId) {
// 按依赖顺序删除
growthRecordMapper.delete(new LambdaQueryWrapper<GrowthRecord>().eq(GrowthRecord::getTenantId, tenantId));
schedulePlanMapper.delete(new LambdaQueryWrapper<SchedulePlan>().eq(SchedulePlan::getTenantId, tenantId));
notificationMapper.delete(new LambdaQueryWrapper<Notification>().eq(Notification::getTenantId, tenantId));
taskTemplateMapper.delete(new LambdaQueryWrapper<TaskTemplate>().eq(TaskTemplate::getTenantId, tenantId));
taskMapper.delete(new LambdaQueryWrapper<Task>().eq(Task::getTenantId, tenantId));
lessonMapper.delete(new LambdaQueryWrapper<Lesson>().eq(Lesson::getTenantId, tenantId));
courseMapper.delete(new LambdaQueryWrapper<Course>().eq(Course::getTenantId, tenantId));
// 删除家长学生关联
ParentStudentMapper psMapper = parentStudentMapper;
// 先获取该租户的家长和学生
var parents = parentMapper.selectList(new LambdaQueryWrapper<Parent>().eq(Parent::getTenantId, tenantId));
for (Parent parent : parents) {
psMapper.delete(new LambdaQueryWrapper<ParentStudent>().eq(ParentStudent::getParentId, parent.getId()));
}
// 删除学生
studentMapper.delete(new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId));
// 删除家长
parentMapper.delete(new LambdaQueryWrapper<Parent>().eq(Parent::getTenantId, tenantId));
// 删除班级教师关联
var classes = clazzMapper.selectList(new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId));
for (Clazz clazz : classes) {
classTeacherMapper.delete(new LambdaQueryWrapper<ClassTeachers>().eq(ClassTeachers::getClassId, clazz.getId()));
}
// 删除班级
clazzMapper.delete(new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId));
// 删除教师包括学校账号
teacherMapper.delete(new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenantId));
}
/**
* 生成完整的测试数据
* 包括租户教师学生家长班级课程课时任务通知等
*/
@Test
public void generateAllTestData() {
System.out.println("========== 开始生成测试数据 ==========");
// 先清理测试租户的旧数据如果存在
cleanupTestData();
// 1. 创建租户幼儿园
Long tenantId = createTenants();
// 2. 创建主题
createThemes();
// 3. 创建教师
Long teacherId1 = createTeachers(tenantId);
Long teacherId2 = createTeachers2(tenantId);
// 4. 创建学生
Long studentId1 = createStudents(tenantId);
Long studentId2 = createStudents2(tenantId);
Long studentId3 = createStudents3(tenantId);
// 5. 创建家长
Long parentId1 = createParents(tenantId);
Long parentId2 = createParents2(tenantId);
// 6. 创建班级
Long classId1 = createClasses(tenantId);
Long classId2 = createClasses2(tenantId);
// 7. 关联班级和教师
createClassTeachers(classId1, teacherId1);
createClassTeachers2(classId2, teacherId2);
// 8. 关联家长和学生
createParentStudents(parentId1, studentId1);
createParentStudents2(parentId2, studentId2, studentId3);
// 9. 创建课程
Long courseId1 = createCourses(tenantId);
Long courseId2 = createCourses2(tenantId);
// 10. 创建课程课时
createCourseLessons(courseId1);
createCourseLessons2(courseId2);
// 11. 创建课时教学活动
createLessons(tenantId, courseId1, classId1, teacherId1);
// 12. 创建任务
createTasks(tenantId, courseId1, teacherId1);
// 13. 创建任务模板
createTaskTemplates(tenantId);
// 14. 创建通知
createNotifications(tenantId, teacherId1, parentId1);
// 15. 创建课表计划
createSchedulePlans(tenantId, classId1, courseId1, teacherId1);
// 16. 创建成长记录
createGrowthRecords(tenantId, studentId1);
System.out.println("========== 测试数据生成完成 ==========");
}
/**
* 创建租户幼儿园
*/
private Long createTenants() {
System.out.println("【1】创建租户...");
Tenant tenant = new Tenant();
tenant.setName("阳光幼儿园");
tenant.setCode("sunshine_kinder");
tenant.setContactName("张园长");
tenant.setContactPhone("13800138001");
tenant.setContactEmail("sunshine@kinder.com");
tenant.setAddress("北京市朝阳区阳光路 100 号");
tenant.setLogoUrl("/uploads/logos/sunshine.png");
tenant.setStatus("active");
tenant.setExpireAt(LocalDateTime.now().plusYears(1));
tenant.setMaxStudents(500);
tenant.setMaxTeachers(50);
tenantMapper.insert(tenant);
System.out.println(" ✓ 创建租户:阳光幼儿园 (ID=" + tenant.getId() + ")");
// 创建第二个租户
Tenant tenant2 = new Tenant();
tenant2.setName("希望幼儿园");
tenant2.setCode("hope_kinder");
tenant2.setContactName("李园长");
tenant2.setContactPhone("13800138002");
tenant2.setContactEmail("hope@kinder.com");
tenant2.setAddress("北京市海淀区希望路 200 号");
tenant2.setLogoUrl("/uploads/logos/hope.png");
tenant2.setStatus("active");
tenant2.setExpireAt(LocalDateTime.now().plusYears(1));
tenant2.setMaxStudents(300);
tenant2.setMaxTeachers(30);
tenantMapper.insert(tenant2);
System.out.println(" ✓ 创建租户:希望幼儿园 (ID=" + tenant2.getId() + ")");
// 创建学校登录账号教师表中
Teacher schoolAccount = new Teacher();
schoolAccount.setTenantId(tenant.getId());
schoolAccount.setUsername(tenant.getCode());
schoolAccount.setPassword(passwordEncoder.encode("123456"));
schoolAccount.setName("阳光幼儿园 - 学校账号");
schoolAccount.setStatus("active");
teacherMapper.insert(schoolAccount);
System.out.println(" ✓ 创建学校登录账号 (username=" + tenant.getCode() + ", password=123456)");
return tenant.getId();
}
/**
* 创建主题
*/
private void createThemes() {
System.out.println("【2】创建主题...");
String[][] themes = {
{"language", "语言与文字", "#FF6B6B"},
{"math", "数学与逻辑", "#4ECDC4"},
{"science", "科学探索", "#45B7D1"},
{"art", "艺术与创造", "#FFA07A"},
{"social", "社会情感", "#98D8C8"},
{"health", "健康运动", "#F7DC6F"},
{"nature", "自然认知", "#82E0AA"}
};
int sortOrder = 1;
for (String[] theme : themes) {
Theme entity = new Theme();
entity.setName(theme[0]);
entity.setDisplayName(theme[1]);
entity.setColor(theme[2]);
entity.setIcon("icon-" + theme[0]);
entity.setSortOrder(sortOrder++);
entity.setIsEnabled(1);
themeMapper.insert(entity);
System.out.println(" ✓ 创建主题:" + theme[1] + " (" + theme[0] + ")");
}
}
/**
* 创建教师
*/
private Long createTeachers(Long tenantId) {
System.out.println("【3】创建教师...");
Teacher teacher = new Teacher();
teacher.setTenantId(tenantId);
teacher.setUsername("teacher_wang");
teacher.setPassword(passwordEncoder.encode("123456"));
teacher.setName("王老师");
teacher.setPhone("13900139001");
teacher.setEmail("wang@kinder.com");
teacher.setGender("female");
teacher.setBio("资深幼儿教师,擅长语言教学");
teacher.setStatus("active");
teacherMapper.insert(teacher);
System.out.println(" ✓ 创建教师:王老师 (ID=" + teacher.getId() + ", username=teacher_wang)");
return teacher.getId();
}
private Long createTeachers2(Long tenantId) {
Teacher teacher = new Teacher();
teacher.setTenantId(tenantId);
teacher.setUsername("teacher_li");
teacher.setPassword(passwordEncoder.encode("123456"));
teacher.setName("李老师");
teacher.setPhone("13900139002");
teacher.setEmail("li@kinder.com");
teacher.setGender("male");
teacher.setBio("体育教师,擅长运动游戏");
teacher.setStatus("active");
teacherMapper.insert(teacher);
System.out.println(" ✓ 创建教师:李老师 (ID=" + teacher.getId() + ", username=teacher_li)");
return teacher.getId();
}
/**
* 创建学生
*/
private Long createStudents(Long tenantId) {
System.out.println("【4】创建学生...");
Student student = new Student();
student.setTenantId(tenantId);
student.setName("小明");
student.setGender("male");
student.setBirthDate(LocalDate.of(2019, 5, 15));
student.setAvatarUrl("/uploads/avatars/student1.png");
student.setGrade("大班");
student.setStudentNo("S2024001");
student.setReadingLevel("中级");
student.setInterests("阅读、画画、积木");
student.setNotes("活泼好动,喜欢提问");
student.setStatus("active");
studentMapper.insert(student);
System.out.println(" ✓ 创建学生:小明 (ID=" + student.getId() + ")");
return student.getId();
}
private Long createStudents2(Long tenantId) {
Student student = new Student();
student.setTenantId(tenantId);
student.setName("小红");
student.setGender("female");
student.setBirthDate(LocalDate.of(2019, 8, 20));
student.setAvatarUrl("/uploads/avatars/student2.png");
student.setGrade("大班");
student.setStudentNo("S2024002");
student.setReadingLevel("高级");
student.setInterests("阅读、唱歌、跳舞");
student.setNotes("文静乖巧,记忆力好");
student.setStatus("active");
studentMapper.insert(student);
System.out.println(" ✓ 创建学生:小红 (ID=" + student.getId() + ")");
return student.getId();
}
private Long createStudents3(Long tenantId) {
Student student = new Student();
student.setTenantId(tenantId);
student.setName("小强");
student.setGender("male");
student.setBirthDate(LocalDate.of(2020, 2, 10));
student.setAvatarUrl("/uploads/avatars/student3.png");
student.setGrade("中班");
student.setStudentNo("S2024003");
student.setReadingLevel("初级");
student.setInterests("运动、游戏");
student.setNotes("性格开朗,喜欢集体活动");
student.setStatus("active");
studentMapper.insert(student);
System.out.println(" ✓ 创建学生:小强 (ID=" + student.getId() + ")");
return student.getId();
}
/**
* 创建家长
*/
private Long createParents(Long tenantId) {
System.out.println("【5】创建家长...");
Parent parent = new Parent();
parent.setTenantId(tenantId);
parent.setUsername("parent_zhang");
parent.setPassword(passwordEncoder.encode("123456"));
parent.setName("张先生");
parent.setPhone("13700137001");
parent.setEmail("zhang@qq.com");
parent.setGender("male");
parent.setStatus("active");
parentMapper.insert(parent);
System.out.println(" ✓ 创建家长:张先生 (ID=" + parent.getId() + ", username=parent_zhang)");
return parent.getId();
}
private Long createParents2(Long tenantId) {
Parent parent = new Parent();
parent.setTenantId(tenantId);
parent.setUsername("parent_liu");
parent.setPassword(passwordEncoder.encode("123456"));
parent.setName("刘女士");
parent.setPhone("13700137002");
parent.setEmail("liu@qq.com");
parent.setGender("female");
parent.setStatus("active");
parentMapper.insert(parent);
System.out.println(" ✓ 创建家长:刘女士 (ID=" + parent.getId() + ", username=parent_liu)");
return parent.getId();
}
/**
* 创建班级
*/
private Long createClasses(Long tenantId) {
System.out.println("【6】创建班级...");
Clazz clazz = new Clazz();
clazz.setTenantId(tenantId);
clazz.setName("大(一)班");
clazz.setGrade("大班");
clazz.setDescription("2019 年出生的小朋友");
clazz.setCapacity(35);
clazz.setStatus("active");
clazzMapper.insert(clazz);
System.out.println(" ✓ 创建班级:大(一)班 (ID=" + clazz.getId() + ")");
return clazz.getId();
}
private Long createClasses2(Long tenantId) {
Clazz clazz = new Clazz();
clazz.setTenantId(tenantId);
clazz.setName("中(一)班");
clazz.setGrade("中班");
clazz.setDescription("2020 年出生的小朋友");
clazz.setCapacity(30);
clazz.setStatus("active");
clazzMapper.insert(clazz);
System.out.println(" ✓ 创建班级:中(一)班 (ID=" + clazz.getId() + ")");
return clazz.getId();
}
/**
* 创建班级 - 教师关联
*/
private void createClassTeachers(Long classId, Long teacherId) {
System.out.println("【7】创建班级 - 教师关联...");
ClassTeachers ct = new ClassTeachers();
ct.setClassId(classId);
ct.setTeacherId(teacherId);
ct.setRole("head_teacher");
classTeacherMapper.insert(ct);
System.out.println(" ✓ 关联班级 - 教师:大(一)班 - 王老师(班主任)");
}
private void createClassTeachers2(Long classId, Long teacherId) {
ClassTeachers ct = new ClassTeachers();
ct.setClassId(classId);
ct.setTeacherId(teacherId);
ct.setRole("head_teacher");
classTeacherMapper.insert(ct);
System.out.println(" ✓ 关联班级 - 教师:中(一)班 - 李老师(班主任)");
}
/**
* 创建家长 - 学生关联
*/
private void createParentStudents(Long parentId, Long studentId) {
System.out.println("【8】创建家长 - 学生关联...");
ParentStudent ps = new ParentStudent();
ps.setParentId(parentId);
ps.setStudentId(studentId);
ps.setRelationship("father");
ps.setIsPrimary(1);
parentStudentMapper.insert(ps);
System.out.println(" ✓ 关联家长 - 学生:张先生 - 小明(父子)");
}
private void createParentStudents2(Long parentId, Long studentId1, Long studentId2) {
ParentStudent ps1 = new ParentStudent();
ps1.setParentId(parentId);
ps1.setStudentId(studentId1);
ps1.setRelationship("mother");
ps1.setIsPrimary(1);
parentStudentMapper.insert(ps1);
ParentStudent ps2 = new ParentStudent();
ps2.setParentId(parentId);
ps2.setStudentId(studentId2);
ps2.setRelationship("mother");
ps2.setIsPrimary(1);
parentStudentMapper.insert(ps2);
System.out.println(" ✓ 关联家长 - 学生:刘女士 - 小红、小强(母子)");
}
/**
* 创建课程
*/
private Long createCourses(Long tenantId) {
System.out.println("【9】创建课程...");
Course course = new Course();
course.setTenantId(tenantId);
course.setName("绘本阅读入门");
course.setCode("READ001");
course.setDescription("适合大班幼儿的绘本阅读课程,培养孩子的阅读兴趣和基础阅读能力");
course.setCoverUrl("/uploads/courses/reading101.png");
course.setCategory("language");
course.setAgeRange("5-6 岁");
course.setDifficultyLevel("beginner");
course.setDurationMinutes(30);
course.setObjectives("培养阅读兴趣、提升语言表达能力、增强想象力");
course.setStatus("published");
course.setIsSystem(0);
course.setThemeId(1L);
course.setPictureBookName("《猜猜我有多爱你》");
course.setVersion("1.0");
course.setIsLatest(1);
courseMapper.insert(course);
System.out.println(" ✓ 创建课程:绘本阅读入门 (ID=" + course.getId() + ")");
return course.getId();
}
private Long createCourses2(Long tenantId) {
Course course = new Course();
course.setTenantId(tenantId);
course.setName("趣味数学游戏");
course.setCode("MATH001");
course.setDescription("通过游戏的方式学习基础数学概念");
course.setCoverUrl("/uploads/courses/math101.png");
course.setCategory("math");
course.setAgeRange("4-5 岁");
course.setDifficultyLevel("beginner");
course.setDurationMinutes(25);
course.setObjectives("认识数字、学习简单加减法、培养逻辑思维");
course.setStatus("published");
course.setIsSystem(0);
course.setThemeId(2L);
course.setVersion("1.0");
course.setIsLatest(1);
courseMapper.insert(course);
System.out.println(" ✓ 创建课程:趣味数学游戏 (ID=" + course.getId() + ")");
return course.getId();
}
/**
* 创建课程课时
*/
private void createCourseLessons(Long courseId) {
System.out.println("【10】创建课程课时...");
String[][] lessons = {
{"第一讲:认识绘本", "了解绘本的结构和特点"},
{"第二讲:封面故事", "从封面猜测故事内容"},
{"第三讲:角色认知", "认识故事中的主要角色"},
{"第四讲:情节理解", "理解故事的发展脉络"},
{"第五讲:情感体验", "感受故事中的情感表达"},
{"第六讲:创意延伸", "发挥想象,创编故事结局"}
};
for (int i = 0; i < lessons.length; i++) {
CourseLesson lesson = new CourseLesson();
lesson.setCourseId(courseId);
lesson.setTitle(lessons[i][0]);
lesson.setDescription(lessons[i][1]);
lesson.setContent("这里是课时内容详情...");
lesson.setSortOrder(i + 1);
lesson.setDurationMinutes(15);
lesson.setStatus("published");
courseLessonMapper.insert(lesson);
}
System.out.println(" ✓ 创建课程课时:" + lessons.length + " 个课时");
}
private void createCourseLessons2(Long courseId) {
String[][] lessons = {
{"第一讲:数字歌", "学习数字 1-10"},
{"第二讲:比大小", "认识大小概念"},
{"第三讲:数一数", "练习点数"},
{"第四讲:简单加法", "学习 5 以内加法"},
{"第五讲:简单减法", "学习 5 以内减法"}
};
for (int i = 0; i < lessons.length; i++) {
CourseLesson lesson = new CourseLesson();
lesson.setCourseId(courseId);
lesson.setTitle(lessons[i][0]);
lesson.setDescription(lessons[i][1]);
lesson.setContent("这里是课时内容详情...");
lesson.setSortOrder(i + 1);
lesson.setDurationMinutes(12);
lesson.setStatus("published");
courseLessonMapper.insert(lesson);
}
System.out.println(" ✓ 创建课程课时:" + lessons.length + " 个课时");
}
/**
* 创建课时教学活动
*/
private void createLessons(Long tenantId, Long courseId, Long classId, Long teacherId) {
System.out.println("【11】创建课时教学活动...");
LocalDate baseDate = LocalDate.now();
for (int i = 0; i < 4; i++) {
Lesson lesson = new Lesson();
lesson.setTenantId(tenantId);
lesson.setCourseId(courseId);
lesson.setClassId(classId);
lesson.setTeacherId(teacherId);
lesson.setTitle("绘本阅读 - 第" + (i + 1) + "");
lesson.setLessonDate(baseDate.plusWeeks(i));
lesson.setStartTime(LocalTime.of(9, 0));
lesson.setEndTime(LocalTime.of(9, 30));
lesson.setLocation("大一班教室");
lesson.setStatus("scheduled");
lesson.setNotes("请小朋友们提前准备好绘本");
lessonMapper.insert(lesson);
}
System.out.println(" ✓ 创建课时4 个教学活动");
}
/**
* 创建任务
*/
private void createTasks(Long tenantId, Long courseId, Long teacherId) {
System.out.println("【12】创建任务...");
LocalDate baseDate = LocalDate.now();
// 任务 1
Task task1 = new Task();
task1.setTenantId(tenantId);
task1.setTitle("阅读打卡第 1 周");
task1.setDescription("请家长陪同孩子每天阅读 15 分钟,并记录阅读内容");
task1.setType("reading");
task1.setCourseId(courseId);
task1.setCreatorId(teacherId);
task1.setCreatorRole("teacher");
task1.setStartDate(baseDate);
task1.setDueDate(baseDate.plusDays(7));
task1.setStatus("published");
taskMapper.insert(task1);
// 任务 2
Task task2 = new Task();
task2.setTenantId(tenantId);
task2.setTitle("绘画作业:我喜欢的故事角色");
task2.setDescription("画出你最喜欢的故事角色,并说明理由");
task2.setType("homework");
task2.setCourseId(courseId);
task2.setCreatorId(teacherId);
task2.setCreatorRole("teacher");
task2.setStartDate(baseDate);
task2.setDueDate(baseDate.plusDays(5));
task2.setStatus("published");
taskMapper.insert(task2);
// 任务 3
Task task3 = new Task();
task3.setTenantId(tenantId);
task3.setTitle("周末亲子活动");
task3.setDescription("周末和孩子一起去图书馆或书店");
task3.setType("activity");
task3.setCourseId(courseId);
task3.setCreatorId(teacherId);
task3.setCreatorRole("teacher");
task3.setStartDate(baseDate.plusDays(5));
task3.setDueDate(baseDate.plusDays(7));
task3.setStatus("published");
taskMapper.insert(task3);
System.out.println(" ✓ 创建任务3 个任务");
}
/**
* 创建任务模板
*/
private void createTaskTemplates(Long tenantId) {
System.out.println("【13】创建任务模板...");
String[][] templates = {
{"阅读打卡模板", "用于日常阅读打卡记录", "reading", "请记录今天的阅读内容..."},
{"绘画作业模板", "用于美术类作业", "homework", "请上传孩子的绘画作品..."},
{"亲子活动模板", "用于记录亲子活动", "activity", "请分享活动过程和感受..."},
{"观察记录模板", "用于观察类作业", "homework", "请记录观察到的现象..."}
};
for (String[] template : templates) {
TaskTemplate tt = new TaskTemplate();
tt.setTenantId(tenantId);
tt.setName(template[0]);
tt.setDescription(template[1]);
tt.setType(template[2]);
tt.setContent(template[3]);
tt.setIsPublic(1);
taskTemplateMapper.insert(tt);
}
System.out.println(" ✓ 创建任务模板:" + templates.length + " 个模板");
}
/**
* 创建通知
*/
private void createNotifications(Long tenantId, Long teacherId, Long parentId) {
System.out.println("【14】创建通知...");
// 通知 1 - 系统通知
Notification n1 = new Notification();
n1.setTenantId(tenantId);
n1.setTitle("新学期开始通知");
n1.setContent("亲爱的家长们,新学期即将开始,请做好入园准备。");
n1.setType("system");
n1.setRecipientType("all");
n1.setIsRead(0);
notificationMapper.insert(n1);
// 通知 2 - 课程通知
Notification n2 = new Notification();
n2.setTenantId(tenantId);
n2.setTitle("绘本阅读课程更新");
n2.setContent("《猜猜我有多爱你》课程已更新,请查看。");
n2.setType("course");
n2.setSenderId(teacherId);
n2.setSenderRole("teacher");
n2.setRecipientType("all");
n2.setIsRead(0);
notificationMapper.insert(n2);
// 通知 3 - 任务通知
Notification n3 = new Notification();
n3.setTenantId(tenantId);
n3.setTitle("新任务发布");
n3.setContent("本周的阅读打卡任务已发布,请家长陪同完成。");
n3.setType("task");
n3.setSenderId(teacherId);
n3.setSenderRole("teacher");
n3.setRecipientId(parentId);
n3.setRecipientType("parent");
n3.setIsRead(0);
notificationMapper.insert(n3);
System.out.println(" ✓ 创建通知3 条通知");
}
/**
* 创建课表计划
*/
private void createSchedulePlans(Long tenantId, Long classId, Long courseId, Long teacherId) {
System.out.println("【15】创建课表计划...");
int[][] schedules = {
{1, 1}, // 周一第 1
{1, 3}, // 周一第 3
{3, 2}, // 周三第 2
{5, 1} // 周五第 1
};
for (int[] schedule : schedules) {
SchedulePlan plan = new SchedulePlan();
plan.setTenantId(tenantId);
plan.setName("绘本阅读课");
plan.setClassId(classId);
plan.setCourseId(courseId);
plan.setTeacherId(teacherId);
plan.setDayOfWeek(schedule[0]);
plan.setPeriod(schedule[1]);
plan.setStartTime(LocalTime.of(9, 0));
plan.setEndTime(LocalTime.of(9, 30));
plan.setStartDate(LocalDate.now());
plan.setEndDate(LocalDate.now().plusMonths(4));
plan.setLocation("大一班教室");
plan.setStatus("active");
schedulePlanMapper.insert(plan);
}
System.out.println(" ✓ 创建课表计划:" + schedules.length + " 个课程安排");
}
/**
* 创建成长记录
*/
private void createGrowthRecords(Long tenantId, Long studentId) {
System.out.println("【16】创建成长记录...");
String[][] records = {
{"reading", "第一次独立阅读", "今天小明第一次独立读完了一本绘本,非常棒!", "阅读进步"},
{"behavior", "帮助同学", "小明主动帮助摔倒的同学,很有爱心", "品德表现"},
{"achievement", "绘画比赛获奖", "在幼儿园绘画比赛中获得一等奖", "荣誉奖项"}
};
for (String[] record : records) {
GrowthRecord gr = new GrowthRecord();
gr.setTenantId(tenantId);
gr.setStudentId(studentId);
gr.setType(record[0]);
gr.setTitle(record[1]);
gr.setContent(record[2]);
gr.setRecordedBy(1L); // 设置记录人 ID
gr.setRecorderRole("teacher"); // 设置记录人角色
gr.setRecordDate(LocalDate.now());
gr.setTags("[\"" + record[3] + "\"]");
growthRecordMapper.insert(gr);
}
System.out.println(" ✓ 创建成长记录:" + records.length + " 条记录");
}
}