diff --git a/reading-platform-frontend/src/api/admin.ts b/reading-platform-frontend/src/api/admin.ts index 3e53880..c6a5975 100644 --- a/reading-platform-frontend/src/api/admin.ts +++ b/reading-platform-frontend/src/api/admin.ts @@ -71,6 +71,7 @@ export interface CreateTenantDto { contactPerson?: string; contactPhone?: string; packageType?: string; + packageId?: number; teacherQuota?: number; studentQuota?: number; startDate?: string; @@ -129,6 +130,21 @@ export interface PopularCourse { teacherCount: number; } +export interface CoursePackage { + id: number; + name: string; + description: string; + price: number; + discountPrice?: number; + discountType?: string; + gradeLevels?: string[]; + courseCount: number; + status: string; + publishedAt?: string; + createdAt?: string; + updatedAt?: string; +} + export interface AdminSettings { // Basic settings systemName?: string; @@ -207,6 +223,11 @@ export const getActiveTenants = (limit?: number) => export const getPopularCourses = (limit?: number) => http.get('/v1/admin/stats/courses/popular', { params: { limit } }); +// ==================== 课程套餐 ==================== + +export const getPublishedPackages = () => + http.get('/v1/admin/packages/all'); + // ==================== 系统设置 ==================== export const getAdminSettings = () => diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts index a9bcf0d..fb9415d 100644 --- a/reading-platform-frontend/src/api/school.ts +++ b/reading-platform-frontend/src/api/school.ts @@ -334,11 +334,34 @@ export const updateSettings = (data: UpdateSettingsDto) => // ==================== 课程管理 ==================== +export interface Course { + id: number; + tenantId?: number; + name: string; + code?: string; + description?: string; + coverUrl?: string; + coverImagePath?: string; + category?: string; + ageRange?: string; + difficultyLevel?: string; + durationMinutes?: number; + objectives?: string; + status: string; + isSystem: number; + version?: string; + usageCount?: number; + teacherCount?: number; + createdAt?: string; + updatedAt?: string; + publishedAt?: string; +} + export const getSchoolCourses = () => - http.get('/v1/school/courses'); + http.get('/v1/school/courses'); export const getSchoolCourse = (id: number) => - http.get(`/v1/school/courses/${id}`); + http.get(`/v1/school/courses/${id}`); // ==================== 班级教师管理 ==================== diff --git a/reading-platform-frontend/src/components.d.ts b/reading-platform-frontend/src/components.d.ts index 70e729b..5eeb3a9 100644 --- a/reading-platform-frontend/src/components.d.ts +++ b/reading-platform-frontend/src/components.d.ts @@ -34,6 +34,9 @@ declare module 'vue' { ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] + AList: typeof import('ant-design-vue/es')['List'] + AListItem: typeof import('ant-design-vue/es')['ListItem'] + AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta'] AMenu: typeof import('ant-design-vue/es')['Menu'] AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] @@ -48,6 +51,7 @@ declare module 'vue' { ARate: typeof import('ant-design-vue/es')['Rate'] ARow: typeof import('ant-design-vue/es')['Row'] ASelect: typeof import('ant-design-vue/es')['Select'] + ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] ASpace: typeof import('ant-design-vue/es')['Space'] @@ -55,6 +59,8 @@ declare module 'vue' { AStatistic: typeof import('ant-design-vue/es')['Statistic'] AStep: typeof import('ant-design-vue/es')['Step'] ASteps: typeof import('ant-design-vue/es')['Steps'] + ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] + ASwitch: typeof import('ant-design-vue/es')['Switch'] ATable: typeof import('ant-design-vue/es')['Table'] ATabPane: typeof import('ant-design-vue/es')['TabPane'] ATabs: typeof import('ant-design-vue/es')['Tabs'] @@ -62,6 +68,8 @@ declare module 'vue' { ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] + ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] + AUpload: typeof import('ant-design-vue/es')['Upload'] FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default'] FileUploader: typeof import('./components/course/FileUploader.vue')['default'] LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default'] diff --git a/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue b/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue index 5ccdc17..66ab940 100644 --- a/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue +++ b/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue @@ -170,11 +170,15 @@ - - 基础版 - 标准版 - 高级版 - 定制版 + + 请选择套餐 + + {{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}元) + @@ -348,10 +352,12 @@ import { updateTenantStatus, resetTenantPassword, deleteTenant, + getPublishedPackages, type Tenant, type TenantDetail, type CreateTenantDto, type UpdateTenantDto, + type CoursePackage, } from '@/api/admin'; // 搜索表单 @@ -414,7 +420,8 @@ const formData = reactive({ contactPerson: '', contactPhone: '', address: '', - packageType: 'STANDARD', + packageType: '', + packageId: undefined, teacherQuota: 20, studentQuota: 200, startDate: '', @@ -444,6 +451,35 @@ const quotaForm = reactive({ const drawerVisible = ref(false); const detailData = ref(null); +// 套餐列表 +const packageList = ref([]); + +// 格式化价格(分转为元) +const formatPackagePrice = (priceInCents: number) => { + return (priceInCents / 100).toFixed(2); +}; + +// 处理套餐类型变化,自动填充配额等信息 +const handlePackageTypeChange = (value: string) => { + const selectedPackage = packageList.value.find(pkg => pkg.name === value); + if (selectedPackage) { + // 设置选中的套餐 ID + formData.packageId = selectedPackage.id; + + // 根据套餐自动设置配额(这里可以根据实际需求调整) + if (selectedPackage.name.includes('基础')) { + formData.teacherQuota = 10; + formData.studentQuota = 100; + } else if (selectedPackage.name.includes('标准')) { + formData.teacherQuota = 20; + formData.studentQuota = 200; + } else if (selectedPackage.name.includes('高级')) { + formData.teacherQuota = 50; + formData.studentQuota = 500; + } + } +}; + // 加载数据 const loadData = async () => { loading.value = true; @@ -464,6 +500,16 @@ const loadData = async () => { } }; +// 加载套餐列表 +const loadPackageList = async () => { + try { + const res = await getPublishedPackages(); + packageList.value = res; + } catch (error) { + console.error('加载套餐列表失败', error); + } +}; + // 搜索 const handleSearch = () => { pagination.current = 1; @@ -497,7 +543,8 @@ const showAddModal = () => { contactPerson: '', contactPhone: '', address: '', - packageType: 'STANDARD', + packageType: '', + packageId: undefined, teacherQuota: 20, studentQuota: 200, }); @@ -695,6 +742,7 @@ const getStatusText = (status: string) => { // 初始化 onMounted(() => { loadData(); + loadPackageList(); }); diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java index 25cdde8..ff2b0c9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java @@ -34,6 +34,9 @@ public enum ErrorCode { TENANT_EXPIRED(3002, "Tenant has expired"), TENANT_DISABLED(3003, "Tenant is disabled"), + // Package Errors (3100+) + PACKAGE_NOT_FOUND(3101, "Package not found"), + // User Errors (4000+) USER_NOT_FOUND(4001, "User not found"), USER_ALREADY_EXISTS(4002, "User already exists"), diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/TenantPackageStatus.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/TenantPackageStatus.java new file mode 100644 index 0000000..4fb9ba7 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/TenantPackageStatus.java @@ -0,0 +1,34 @@ +package com.reading.platform.common.enums; + +import lombok.Getter; + +/** + * 租户套餐状态枚举 + */ +@Getter +public enum TenantPackageStatus { + + ACTIVE("ACTIVE", "激活"), + EXPIRED("EXPIRED", "已过期"); + + private final String code; + private final String description; + + TenantPackageStatus(String code, String description) { + this.code = code; + this.description = description; + } + + /** + * 根据 code 获取枚举 + */ + public static TenantPackageStatus fromCode(String code) { + for (TenantPackageStatus status : values()) { + if (status.getCode().equals(code)) { + return status; + } + } + return ACTIVE; + } + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminCourseController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminCourseController.java index 7d6d529..5469105 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminCourseController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminCourseController.java @@ -6,6 +6,7 @@ import com.reading.platform.common.enums.UserRole; import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.Result; import com.reading.platform.dto.request.CourseCreateRequest; +import com.reading.platform.dto.request.CoursePageQueryRequest; import com.reading.platform.dto.request.CourseUpdateRequest; import com.reading.platform.dto.response.CourseResponse; import com.reading.platform.entity.Course; @@ -17,18 +18,21 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; -@Tag(name = "Admin - Course", description = "System Course Management APIs for Admin") +/** + * 课程管理控制器(超管端) + */ @RestController @RequestMapping("/api/v1/admin/courses") @RequiredArgsConstructor @Slf4j @RequireRole(UserRole.ADMIN) +@Tag(name = "超管端 - 课程管理") public class AdminCourseController { private final CourseService courseService; - @Operation(summary = "Create system course") @PostMapping + @Operation(summary = "创建课程") public Result createCourse(@Valid @RequestBody CourseCreateRequest request) { log.info("收到课程创建请求,name={}, themeId={}, gradeTags={}", request.getName(), request.getThemeId(), request.getGradeTags()); try { @@ -41,51 +45,57 @@ public class AdminCourseController { } } - @Operation(summary = "Update course") @PutMapping("/{id}") + @Operation(summary = "更新课程") public Result updateCourse(@PathVariable Long id, @RequestBody CourseUpdateRequest request) { return Result.success(courseService.updateCourse(id, request)); } - @Operation(summary = "Get course by ID") @GetMapping("/{id}") + @Operation(summary = "查询课程详情") public Result getCourse(@PathVariable Long id) { return Result.success(courseService.getCourseByIdWithLessons(id)); } - @Operation(summary = "Get system course page") @GetMapping - public Result> getCoursePage( - @RequestParam(required = false, defaultValue = "1") Integer pageNum, - @RequestParam(required = false, defaultValue = "10") Integer pageSize, - @RequestParam(required = false) String keyword, - @RequestParam(required = false) String category, - @RequestParam(required = false) String status, - @RequestParam(required = false, defaultValue = "false") Boolean reviewOnly) { + @Operation(summary = "分页查询课程") + public Result> getCoursePage(CoursePageQueryRequest request) { log.info("查询课程列表,pageNum={}, pageSize={}, keyword={}, category={}, status={}, reviewOnly={}", - pageNum, pageSize, keyword, category, status, reviewOnly); - Page page = courseService.getSystemCoursePage(pageNum, pageSize, keyword, category, status, reviewOnly); + request.getPageNum(), request.getPageSize(), request.getKeyword(), request.getCategory(), request.getStatus(), request.getReviewOnly()); + // 页码 + // 每页数量 + // 关键词 + // 分类 + // 状态 + // 是否仅查询待审核 + Page page = courseService.getSystemCoursePage( + request.getPageNum(), + request.getPageSize(), + request.getKeyword(), + request.getCategory(), + request.getStatus(), + request.getReviewOnly()); PageResult result = PageResult.of(page); log.info("课程列表查询结果,total={}, list={}", result.getTotal(), result.getList().size()); return Result.success(result); } - @Operation(summary = "Delete course") @DeleteMapping("/{id}") + @Operation(summary = "删除课程") public Result deleteCourse(@PathVariable Long id) { courseService.deleteCourse(id); return Result.success(); } - @Operation(summary = "Publish course") @PostMapping("/{id}/publish") + @Operation(summary = "发布课程") public Result publishCourse(@PathVariable Long id) { courseService.publishCourse(id); return Result.success(); } - @Operation(summary = "Archive course") @PostMapping("/{id}/archive") + @Operation(summary = "归档课程") public Result archiveCourse(@PathVariable Long id) { courseService.archiveCourse(id); return Result.success(); diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminPackageController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminPackageController.java index 48d5b22..f1b844e 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminPackageController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminPackageController.java @@ -129,6 +129,12 @@ public class AdminPackageController { return Result.success(); } + @GetMapping("/all") + @Operation(summary = "查询所有已发布的套餐列表") + public Result> getPublishedPackages() { + return Result.success(packageService.findPublishedPackages()); + } + @PostMapping("/{id}/grant") @Operation(summary = "授权套餐给租户") @RequireRole(UserRole.ADMIN) diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminStatsController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminStatsController.java index c705aae..29f1557 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminStatsController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminStatsController.java @@ -3,74 +3,84 @@ package com.reading.platform.controller.admin; import com.reading.platform.common.annotation.RequireRole; import com.reading.platform.common.enums.UserRole; import com.reading.platform.common.response.Result; +import com.reading.platform.dto.request.ActiveTenantsQueryRequest; +import com.reading.platform.dto.request.PopularCoursesQueryRequest; +import com.reading.platform.dto.request.RecentActivitiesQueryRequest; +import com.reading.platform.dto.response.ActiveTenantItemResponse; +import com.reading.platform.dto.response.PopularCourseItemResponse; +import com.reading.platform.dto.response.RecentActivityItemResponse; +import com.reading.platform.dto.response.StatsResponse; +import com.reading.platform.dto.response.StatsTrendResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** - * 超管端 - 统计管理 + * 统计管理控制器(超管端) */ -@Tag(name = "超管 - 统计管理", description = "Admin Stats APIs") @RestController @RequestMapping("/api/v1/admin/stats") @RequiredArgsConstructor @RequireRole(UserRole.ADMIN) +@Tag(name = "超管端 - 统计管理") public class AdminStatsController { - @Operation(summary = "获取统计数据") @GetMapping - public Result> getStats() { + @Operation(summary = "获取统计数据") + public Result getStats() { // TODO: 实现统计数据查询 - Map stats = new HashMap<>(); - stats.put("totalTenants", 0); - stats.put("activeTenants", 0); - stats.put("totalTeachers", 0); - stats.put("totalStudents", 0); - stats.put("totalCourses", 0); - stats.put("totalLessons", 0); - return Result.success(stats); + return Result.success(StatsResponse.builder() + .totalTenants(0L) + .activeTenants(0L) + .totalTeachers(0L) + .totalStudents(0L) + .totalCourses(0L) + .totalLessons(0L) + .build()); } - @Operation(summary = "获取趋势数据") @GetMapping("/trend") - public Result> getTrendData() { + @Operation(summary = "获取趋势数据") + public Result getTrendData() { // TODO: 实现趋势数据查询 - Map trend = new HashMap<>(); - trend.put("dates", new String[]{}); - trend.put("newStudents", new Integer[]{}); - trend.put("newTeachers", new Integer[]{}); - trend.put("newCourses", new Integer[]{}); - return Result.success(trend); + return Result.success(StatsTrendResponse.builder() + .dates(new ArrayList<>()) + .newStudents(new ArrayList<>()) + .newTeachers(new ArrayList<>()) + .newCourses(new ArrayList<>()) + .build()); } - @Operation(summary = "获取活跃租户") @GetMapping("/tenants/active") - public Result>> getActiveTenants(@RequestParam(required = false, defaultValue = "5") Integer limit) { + @Operation(summary = "获取活跃租户") + public Result> getActiveTenants(@ModelAttribute ActiveTenantsQueryRequest request) { + // 返回数量限制 // TODO: 实现活跃租户查询 return Result.success(new ArrayList<>()); } - @Operation(summary = "获取热门课程") @GetMapping("/courses/popular") - public Result>> getPopularCourses(@RequestParam(required = false, defaultValue = "5") Integer limit) { + @Operation(summary = "获取热门课程") + public Result> getPopularCourses(@ModelAttribute PopularCoursesQueryRequest request) { + // 返回数量限制 // TODO: 实现热门课程查询 return Result.success(new ArrayList<>()); } - @Operation(summary = "获取最近活动") @GetMapping("/activities") - public Result>> getRecentActivities(@RequestParam(required = false, defaultValue = "10") Integer limit) { + @Operation(summary = "获取最近活动") + public Result> getRecentActivities(@ModelAttribute RecentActivitiesQueryRequest request) { + // 返回数量限制 // TODO: 实现最近活动查询 return Result.success(new ArrayList<>()); } + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java index 5ea15b7..e0b8ae5 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java @@ -1,39 +1,45 @@ package com.reading.platform.controller.school; import com.reading.platform.common.response.Result; +import com.reading.platform.entity.Course; +import com.reading.platform.service.CourseService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.stream.Collectors; /** * 课程管理控制器(学校端) */ +@Slf4j @RestController @RequestMapping("/api/v1/school/courses") @RequiredArgsConstructor @Tag(name = "学校端 - 课程管理") public class SchoolCourseController { + private final CourseService courseService; + @GetMapping @Operation(summary = "获取学校课程列表") - public Result>> getSchoolCourses() { - List> courses = new ArrayList<>(); - // For now, return empty list - // TODO: Implement tenant course query + public Result> getSchoolCourses() { + log.info("获取学校课程列表"); + // TODO: 从 SecurityContext 获取当前登录用户所属租户 ID + // 临时使用 tenantId = 1 作为测试 + Long tenantId = 1L; + List courses = courseService.getTenantPackageCourses(tenantId); return Result.success(courses); } @GetMapping("/{id}") @Operation(summary = "获取课程详情") - public Result> getSchoolCourse(@PathVariable Long id) { - Map course = new HashMap<>(); - // TODO: Implement course detail query + public Result getSchoolCourse(@PathVariable Long id) { + log.info("获取课程详情,id={}", id); + Course course = courseService.getCourseById(id); return Result.success(course); } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ActiveTenantsQueryRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ActiveTenantsQueryRequest.java new file mode 100644 index 0000000..2b3bf0b --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ActiveTenantsQueryRequest.java @@ -0,0 +1,15 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 活跃租户查询请求 + */ +@Data +@Schema(description = "活跃租户查询请求") +public class ActiveTenantsQueryRequest { + + @Schema(description = "返回数量限制", example = "5") + private Integer limit = 5; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/CoursePageQueryRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/CoursePageQueryRequest.java new file mode 100644 index 0000000..6d6f76b --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/CoursePageQueryRequest.java @@ -0,0 +1,30 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 课程分页查询请求 + */ +@Data +@Schema(description = "课程分页查询请求") +public class CoursePageQueryRequest { + + @Schema(description = "页码", example = "1") + private Integer pageNum = 1; + + @Schema(description = "每页数量", example = "10") + private Integer pageSize = 10; + + @Schema(description = "关键词") + private String keyword; + + @Schema(description = "分类") + private String category; + + @Schema(description = "状态") + private String status; + + @Schema(description = "是否仅查询待审核", example = "false") + private Boolean reviewOnly = false; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/PopularCoursesQueryRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/PopularCoursesQueryRequest.java new file mode 100644 index 0000000..f1f4c77 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/PopularCoursesQueryRequest.java @@ -0,0 +1,15 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 热门课程查询请求 + */ +@Data +@Schema(description = "热门课程查询请求") +public class PopularCoursesQueryRequest { + + @Schema(description = "返回数量限制", example = "5") + private Integer limit = 5; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/RecentActivitiesQueryRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/RecentActivitiesQueryRequest.java new file mode 100644 index 0000000..980533b --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/RecentActivitiesQueryRequest.java @@ -0,0 +1,15 @@ +package com.reading.platform.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 最近活动查询请求 + */ +@Data +@Schema(description = "最近活动查询请求") +public class RecentActivitiesQueryRequest { + + @Schema(description = "返回数量限制", example = "10") + private Integer limit = 10; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TenantCreateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TenantCreateRequest.java index 63fda32..809f165 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/TenantCreateRequest.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/TenantCreateRequest.java @@ -49,6 +49,9 @@ public class TenantCreateRequest { @Schema(description = "结束日期") private LocalDate expireDate; + @Schema(description = "课程套餐 ID(可选)") + private Long packageId; + @Schema(description = "过期时间(兼容旧字段)") @Deprecated private LocalDateTime expireAt; diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/ActiveTenantItemResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/ActiveTenantItemResponse.java new file mode 100644 index 0000000..ca46709 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/ActiveTenantItemResponse.java @@ -0,0 +1,30 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 活跃租户项响应 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "活跃租户项响应") +public class ActiveTenantItemResponse { + + @Schema(description = "租户 ID") + private Long tenantId; + + @Schema(description = "租户名称") + private String tenantName; + + @Schema(description = "活跃用户数") + private Integer activeUsers; + + @Schema(description = "课程使用数") + private Integer courseCount; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/PopularCourseItemResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/PopularCourseItemResponse.java new file mode 100644 index 0000000..69e1c7e --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/PopularCourseItemResponse.java @@ -0,0 +1,30 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 热门课程项响应 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "热门课程项响应") +public class PopularCourseItemResponse { + + @Schema(description = "课程 ID") + private Long courseId; + + @Schema(description = "课程名称") + private String courseName; + + @Schema(description = "使用次数") + private Integer usageCount; + + @Schema(description = "教师数量") + private Integer teacherCount; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/RecentActivityItemResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/RecentActivityItemResponse.java new file mode 100644 index 0000000..5c5d186 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/RecentActivityItemResponse.java @@ -0,0 +1,38 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 最近活动项响应 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "最近活动项响应") +public class RecentActivityItemResponse { + + @Schema(description = "活动 ID") + private Long activityId; + + @Schema(description = "活动类型") + private String activityType; + + @Schema(description = "活动描述") + private String description; + + @Schema(description = "操作人 ID") + private Long operatorId; + + @Schema(description = "操作人名称") + private String operatorName; + + @Schema(description = "操作时间") + private LocalDateTime operationTime; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/StatsResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/StatsResponse.java new file mode 100644 index 0000000..34dc9e1 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/StatsResponse.java @@ -0,0 +1,36 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 统计数据响应 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "统计数据响应") +public class StatsResponse { + + @Schema(description = "租户总数") + private Long totalTenants; + + @Schema(description = "活跃租户数") + private Long activeTenants; + + @Schema(description = "教师总数") + private Long totalTeachers; + + @Schema(description = "学生总数") + private Long totalStudents; + + @Schema(description = "课程总数") + private Long totalCourses; + + @Schema(description = "课时总数") + private Long totalLessons; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/StatsTrendResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/StatsTrendResponse.java new file mode 100644 index 0000000..6b8cad0 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/StatsTrendResponse.java @@ -0,0 +1,32 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; + +/** + * 趋势数据响应 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "趋势数据响应") +public class StatsTrendResponse { + + @Schema(description = "日期列表") + private List dates; + + @Schema(description = "新增学生数列表") + private List newStudents; + + @Schema(description = "新增教师数列表") + private List newTeachers; + + @Schema(description = "新增课程数列表") + private List newCourses; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/entity/TenantPackage.java b/reading-platform-java/src/main/java/com/reading/platform/entity/TenantPackage.java index 2fa8f5f..cca1844 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/entity/TenantPackage.java +++ b/reading-platform-java/src/main/java/com/reading/platform/entity/TenantPackage.java @@ -1,6 +1,7 @@ package com.reading.platform.entity; import com.baomidou.mybatisplus.annotation.TableName; +import com.reading.platform.common.enums.TenantPackageStatus; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -32,6 +33,6 @@ public class TenantPackage extends BaseEntity { private Long pricePaid; @Schema(description = "状态:ACTIVE、EXPIRED") - private String status; + private TenantPackageStatus status; } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java index 2d8858d..ffecd7c 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java @@ -3,6 +3,7 @@ package com.reading.platform.service; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.reading.platform.common.enums.TenantPackageStatus; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.entity.*; import com.reading.platform.mapper.*; @@ -399,7 +400,7 @@ public class CourseLessonService extends ServiceImpl() .eq(TenantPackage::getTenantId, tenantId) - .eq(TenantPackage::getStatus, "ACTIVE") + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE) ); if (count == 0) { diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java index 108f72d..3bad731 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.reading.platform.common.enums.CourseStatus; +import com.reading.platform.common.enums.TenantPackageStatus; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.common.response.PageResult; import com.reading.platform.dto.response.CoursePackageResponse; @@ -111,7 +112,7 @@ public class CoursePackageService extends ServiceImpl() .eq(TenantPackage::getPackageId, pkg.getId()) - .eq(TenantPackage::getStatus, "ACTIVE") + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE) ); response.setTenantCount(tenantCount.intValue()); @@ -229,7 +230,7 @@ public class CoursePackageService extends ServiceImpl() .eq(TenantPackage::getPackageId, id) - .eq(TenantPackage::getStatus, "ACTIVE") + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE) ); if (tenantCount > 0) { @@ -371,7 +372,7 @@ public class CoursePackageService extends ServiceImpl tenantPackages = tenantPackageMapper.selectList( new LambdaQueryWrapper() .eq(TenantPackage::getTenantId, tenantId) - .eq(TenantPackage::getStatus, "ACTIVE") + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE) .orderByDesc(TenantPackage::getCreatedAt) ); @@ -409,7 +410,7 @@ public class CoursePackageService extends ServiceImpl findPublishedPackages() { + log.info("查询所有已发布的套餐列表"); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode()) + .orderByDesc(CoursePackage::getCreatedAt); + + List packages = packageMapper.selectList(wrapper); + log.info("查询所有已发布的套餐列表成功,count={}", packages.size()); + return packages; + } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CourseService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CourseService.java index 33251c5..93ecdea 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CourseService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CourseService.java @@ -43,4 +43,9 @@ public interface CourseService extends com.baomidou.mybatisplus.extension.servic List getCoursesByTenantId(Long tenantId); + /** + * 查询租户套餐下的课程 + */ + List getTenantPackageCourses(Long tenantId); + } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseServiceImpl.java index 12393b2..c5b47b7 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseServiceImpl.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.reading.platform.common.enums.CourseStatus; import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.enums.TenantPackageStatus; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.dto.request.CourseCreateRequest; import com.reading.platform.dto.request.CourseUpdateRequest; @@ -13,8 +14,12 @@ import com.reading.platform.dto.response.CourseResponse; import com.reading.platform.dto.response.LessonStepResponse; import com.reading.platform.entity.Course; import com.reading.platform.entity.CourseLesson; +import com.reading.platform.entity.CoursePackageCourse; import com.reading.platform.entity.LessonStep; +import com.reading.platform.entity.TenantPackage; import com.reading.platform.mapper.CourseMapper; +import com.reading.platform.mapper.CoursePackageCourseMapper; +import com.reading.platform.mapper.TenantPackageMapper; import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CourseService; import lombok.RequiredArgsConstructor; @@ -36,6 +41,8 @@ public class CourseServiceImpl extends ServiceImpl private final CourseMapper courseMapper; private final CourseLessonService courseLessonService; + private final TenantPackageMapper tenantPackageMapper; + private final CoursePackageCourseMapper packageCourseMapper; @Override @Transactional @@ -453,6 +460,51 @@ public class CourseServiceImpl extends ServiceImpl ); } + @Override + public List getTenantPackageCourses(Long tenantId) { + log.info("查询租户套餐下的课程,tenantId={}", tenantId); + + // 1. 查询租户的套餐关联 + List tenantPackages = tenantPackageMapper.selectList( + new LambdaQueryWrapper() + .eq(TenantPackage::getTenantId, tenantId) + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE) + ); + + if (tenantPackages.isEmpty()) { + log.info("租户套餐下的课程查询结果为空,tenantId={}", tenantId); + return List.of(); + } + + // 2. 获取套餐 ID 列表 + List packageIds = tenantPackages.stream() + .map(TenantPackage::getPackageId) + .collect(Collectors.toList()); + + // 3. 查询套餐包含的课程 ID + List packageCourses = packageCourseMapper.selectList( + new LambdaQueryWrapper() + .in(CoursePackageCourse::getPackageId, packageIds) + ); + + if (packageCourses.isEmpty()) { + log.info("租户套餐下没有关联的课程,tenantId={}", tenantId); + return List.of(); + } + + // 4. 获取课程 ID 列表 + List courseIds = packageCourses.stream() + .map(CoursePackageCourse::getCourseId) + .distinct() + .collect(Collectors.toList()); + + // 5. 查询课程详情 + List courses = courseMapper.selectBatchIds(courseIds); + + log.info("查询租户套餐下的课程成功,tenantId={}, count={}", tenantId, courses.size()); + return courses; + } + /** * 将空字符串转为 null,避免 MySQL JSON 列报错(空串不是有效 JSON) */ diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java index 158987b..1ba5a8b 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java @@ -3,11 +3,16 @@ package com.reading.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.enums.TenantPackageStatus; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.dto.request.TenantCreateRequest; import com.reading.platform.dto.request.TenantUpdateRequest; +import com.reading.platform.entity.CoursePackage; import com.reading.platform.entity.Tenant; +import com.reading.platform.entity.TenantPackage; import com.reading.platform.mapper.TenantMapper; +import com.reading.platform.mapper.TenantPackageMapper; +import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.TenantService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.time.LocalDate; import java.util.List; /** @@ -27,6 +33,8 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic implements TenantService { private final TenantMapper tenantMapper; + private final TenantPackageMapper tenantPackageMapper; + private final CoursePackageService coursePackageService; @Override @Transactional @@ -52,14 +60,7 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic tenant.setAddress(request.getAddress()); tenant.setLogoUrl(request.getLogoUrl()); - // 使用新字段 - tenant.setPackageType(request.getPackageType() != null ? request.getPackageType() : "STANDARD"); - tenant.setTeacherQuota(request.getTeacherQuota() != null ? request.getTeacherQuota() : 20); - tenant.setStudentQuota(request.getStudentQuota() != null ? request.getStudentQuota() : 200); - tenant.setStartDate(request.getStartDate()); - tenant.setExpireDate(request.getExpireDate()); - - // 兼容旧字段 + // 设置有效期相关字段(兼容旧字段) if (request.getExpireAt() != null) { tenant.setExpireAt(request.getExpireAt()); } @@ -72,10 +73,48 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic tenant.setStatus("ACTIVE"); - // 使用 MP 的 insert 方法 - tenantMapper.insert(tenant); + // 如果传入了 packageId,查询套餐信息并填充相关字段 + if (request.getPackageId() != null) { + CoursePackage coursePackage = coursePackageService.getById(request.getPackageId()); + if (coursePackage == null) { + log.warn("课程套餐不存在,packageId: {}", request.getPackageId()); + throw new BusinessException(ErrorCode.PACKAGE_NOT_FOUND, "课程套餐不存在"); + } + + // 根据套餐信息填充租户字段 + tenant.setPackageType(coursePackage.getName()); + tenant.setTeacherQuota(request.getTeacherQuota() != null ? request.getTeacherQuota() : 20); + tenant.setStudentQuota(request.getStudentQuota() != null ? request.getStudentQuota() : 200); + tenant.setStartDate(request.getStartDate()); + tenant.setExpireDate(request.getExpireDate()); + + // 使用 MP 的 insert 方法 + tenantMapper.insert(tenant); + + // 创建租户套餐关联记录 + TenantPackage tenantPackage = new TenantPackage(); + tenantPackage.setTenantId(tenant.getId()); + tenantPackage.setPackageId(request.getPackageId()); + tenantPackage.setStartDate(request.getStartDate() != null ? request.getStartDate() : LocalDate.now()); + tenantPackage.setEndDate(request.getExpireDate()); + tenantPackage.setPricePaid(coursePackage.getDiscountPrice() != null ? coursePackage.getDiscountPrice() : coursePackage.getPrice()); + tenantPackage.setStatus(TenantPackageStatus.ACTIVE); + tenantPackageMapper.insert(tenantPackage); + + log.info("租户创建成功并关联套餐,ID: {}, packageId: {}", tenant.getId(), request.getPackageId()); + } else { + // 没有传入 packageId,使用原有逻辑 + tenant.setPackageType(request.getPackageType() != null ? request.getPackageType() : "STANDARD"); + tenant.setTeacherQuota(request.getTeacherQuota() != null ? request.getTeacherQuota() : 20); + tenant.setStudentQuota(request.getStudentQuota() != null ? request.getStudentQuota() : 200); + tenant.setStartDate(request.getStartDate()); + tenant.setExpireDate(request.getExpireDate()); + + // 使用 MP 的 insert 方法 + tenantMapper.insert(tenant); + log.info("租户创建成功,ID: {}", tenant.getId()); + } - log.info("租户创建成功,ID: {}", tenant.getId()); return tenant; }