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:
parent
b3b04c8ea3
commit
1c1321bddd
@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
327
reading-platform-frontend/src/api/productBundle.ts
Normal file
327
reading-platform-frontend/src/api/productBundle.ts
Normal 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`);
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 + " 条记录");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user