Compare commits

..

2 Commits

Author SHA1 Message Date
En
673214481d feat: 课程包功能完善与代码优化
后端:
- 新增 YesNo 枚举类
- 新增 LessonStepCreateRequest、PackageGrantRequest 等 DTO
- 新增 ResourceItemCreateRequest、ResourceLibraryCreateRequest
- 新增 StatsService 统计服务实现
- 优化 AdminCourseController、AdminResourceController 等控制器
- 完善 TenantService 套餐授权功能

前端:
- 优化套餐详情页和列表页展示
- 更新自动生成的 API 类型定义

文档:
- 更新设计文档和开发日志

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:03:02 +08:00
En
d57affd2ee feat: 套餐审核支持通过时同时发布
- 后端 PackageReviewRequest 新增 publish 字段
- 后端 CoursePackageService.reviewPackage 支持审核通过后直接发布
- 前端审核弹窗拆分为"通过"和"通过并发布"两个按钮

状态流转:
- 驳回: status → REJECTED
- 仅通过: status → APPROVED
- 通过并发布: status → PUBLISHED

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:00:49 +08:00
34 changed files with 959 additions and 302 deletions

View File

@ -47,7 +47,7 @@ model SchoolCourse {
createdBy Int @map("created_by")
changesSummary String? @map("changes_summary") // 修改说明
usageCount Int @default(0) @map("usage_count")
status String @default("ACTIVE") // ACTIVE, PENDING_REVIEW, REJECTED
status String @default("ACTIVE") // ACTIVE, PENDING, REJECTED
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

View File

@ -82,7 +82,7 @@ PUT /api/v1/admin/packages/{id}/courses
POST /api/v1/admin/packages/{id}/submit
```
✅ 提交成功,状态变为 `PENDING_REVIEW`
✅ 提交成功,状态变为 `PENDING`
#### 4. 审核通过

View File

@ -241,7 +241,7 @@ export class CoursePackageService {
return this.prisma.coursePackage.update({
where: { id },
data: {
status: 'PENDING_REVIEW',
status: 'PENDING',
submittedAt: new Date(),
submittedBy: userId,
},
@ -262,7 +262,7 @@ export class CoursePackageService {
throw new Error('套餐不存在');
}
if (pkg.status !== 'PENDING_REVIEW') {
if (pkg.status !== 'PENDING') {
throw new Error('只有待审核状态的套餐可以审核');
}

View File

@ -34,7 +34,7 @@ export interface CoursePackage {
gradeLevels?: string;
/** 课程数量 */
courseCount?: number;
/** 状态DRAFT、PENDING_REVIEW、APPROVED、REJECTED、PUBLISHED、OFFLINE */
/** 状态DRAFT、PENDING、APPROVED、REJECTED、PUBLISHED、OFFLINE */
status?: string;
/** 提交时间 */
submittedAt?: string;

View File

@ -97,7 +97,7 @@ export function submitPackage(id: number | string) {
}
// 审核套餐
export function reviewPackage(id: number | string, data: { approved: boolean; comment?: string }) {
export function reviewPackage(id: number | string, data: { approved: boolean; comment?: string; publish?: boolean }) {
return http.post(`/v1/admin/packages/${id}/review`, data);
}

View File

@ -11,10 +11,8 @@ declare module 'vue' {
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
@ -25,8 +23,6 @@ declare module 'vue' {
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
@ -52,14 +48,11 @@ 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']
ASpin: typeof import('ant-design-vue/es')['Spin']
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']

View File

@ -84,7 +84,7 @@ const courseColumns = [
const statusColors: Record<string, string> = {
DRAFT: 'default',
PENDING_REVIEW: 'processing',
PENDING: 'processing',
APPROVED: 'success',
REJECTED: 'error',
PUBLISHED: 'blue',
@ -93,7 +93,7 @@ const statusColors: Record<string, string> = {
const statusTexts: Record<string, string> = {
DRAFT: '草稿',
PENDING_REVIEW: '待审核',
PENDING: '待审核',
APPROVED: '已通过',
REJECTED: '已拒绝',
PUBLISHED: '已发布',

View File

@ -27,7 +27,7 @@
@change="fetchData"
>
<a-select-option value="DRAFT">草稿</a-select-option>
<a-select-option value="PENDING_REVIEW">待审核</a-select-option>
<a-select-option value="PENDING">待审核</a-select-option>
<a-select-option value="APPROVED">已通过</a-select-option>
<a-select-option value="REJECTED">已拒绝</a-select-option>
<a-select-option value="PUBLISHED">已发布</a-select-option>
@ -81,7 +81,7 @@
<a-button
type="link"
size="small"
v-if="record.status === 'PENDING_REVIEW'"
v-if="record.status === 'PENDING'"
@click="handleReview(record)"
>
审核
@ -143,7 +143,7 @@ const columns = [
const statusColors: Record<string, string> = {
DRAFT: 'default',
PENDING_REVIEW: 'processing',
PENDING: 'processing',
APPROVED: 'success',
REJECTED: 'error',
PUBLISHED: 'blue',
@ -152,7 +152,7 @@ const statusColors: Record<string, string> = {
const statusTexts: Record<string, string> = {
DRAFT: '草稿',
PENDING_REVIEW: '待审核',
PENDING: '待审核',
APPROVED: '已通过',
REJECTED: '已拒绝',
PUBLISHED: '已发布',
@ -183,7 +183,7 @@ const fetchData = async () => {
pagination.total = res.total || 0;
//
try {
const pendingRes = await getPackageList({ status: 'PENDING_REVIEW', pageNum: 1, pageSize: 1 }) as any;
const pendingRes = await getPackageList({ status: 'PENDING', pageNum: 1, pageSize: 1 }) as any;
pendingCount.value = pendingRes.total || 0;
} catch {
pendingCount.value = 0;

View File

@ -5,7 +5,7 @@
<template #extra>
<a-space>
<a-select v-model:value="filters.status" placeholder="全部状态" style="width: 120px" @change="fetchPackages">
<a-select-option value="PENDING_REVIEW">待审核</a-select-option>
<a-select-option value="PENDING">待审核</a-select-option>
<a-select-option value="REJECTED">已驳回</a-select-option>
</a-select>
<a-button @click="fetchPackages">
@ -39,8 +39,8 @@
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'PENDING_REVIEW' ? 'processing' : 'error'">
{{ record.status === 'PENDING_REVIEW' ? '待审核' : '已驳回' }}
<a-tag :color="record.status === 'PENDING' ? 'processing' : 'error'">
{{ record.status === 'PENDING' ? '待审核' : '已驳回' }}
</a-tag>
</template>
<template v-else-if="column.key === 'submittedAt'">
@ -48,7 +48,7 @@
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button v-if="record.status === 'PENDING_REVIEW'" type="primary" size="small" @click="showReviewModal(record)">
<a-button v-if="record.status === 'PENDING'" type="primary" size="small" @click="showReviewModal(record)">
审核
</a-button>
<a-button v-if="record.status === 'REJECTED'" size="small" @click="viewRejectReason(record)">
@ -107,12 +107,15 @@
</a-form>
<div class="modal-footer">
<a-space v-if="currentPackage.status === 'PENDING_REVIEW'">
<a-space v-if="currentPackage.status === 'PENDING'">
<a-button @click="closeReviewModal">取消</a-button>
<a-button type="default" danger :loading="reviewing" @click="rejectPackage">
驳回
</a-button>
<a-button type="primary" :loading="reviewing" @click="approvePackage">
<a-button :loading="reviewing" @click="approveOnly">
通过
</a-button>
<a-button type="primary" :loading="reviewing" @click="approveAndPublish">
通过并发布
</a-button>
</a-space>
@ -144,7 +147,7 @@ const loadingDetail = ref(false);
const packages = ref<CoursePackage[]>([]);
const filters = reactive<{ status?: string }>({
status: 'PENDING_REVIEW',
status: 'PENDING',
});
const pagination = reactive({
@ -227,7 +230,8 @@ const closeReviewModal = () => {
currentPackage.value = null;
};
const approvePackage = async () => {
//
const approveOnly = async () => {
if (!currentPackage.value) return;
reviewing.value = true;
@ -235,6 +239,28 @@ const approvePackage = async () => {
await reviewPackage(currentPackage.value.id, {
approved: true,
comment: reviewComment.value || '审核通过',
publish: false,
});
message.success('审核通过');
closeReviewModal();
fetchPackages();
} catch (error: any) {
message.error(error.response?.data?.message || '审核失败');
} finally {
reviewing.value = false;
}
};
//
const approveAndPublish = async () => {
if (!currentPackage.value) return;
reviewing.value = true;
try {
await reviewPackage(currentPackage.value.id, {
approved: true,
comment: reviewComment.value || '审核通过',
publish: true,
});
message.success('审核通过,套餐已发布');
closeReviewModal();

View File

@ -0,0 +1,58 @@
package com.reading.platform.common.enums;
import lombok.Getter;
/**
* /否枚举
* 用于替代数据库中的 0/1 魔法值
*/
@Getter
public enum YesNo {
NO(0, ""),
YES(1, "");
private final Integer code;
private final String description;
YesNo(Integer code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据 code 获取枚举
*/
public static YesNo fromCode(Integer code) {
if (code == null) {
return NO;
}
for (YesNo value : values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return NO;
}
/**
* 根据 Boolean 获取枚举
*/
public static YesNo fromBoolean(Boolean bool) {
return Boolean.TRUE.equals(bool) ? YES : NO;
}
/**
* 判断是否为""
*/
public static boolean isYes(Integer code) {
return YES.getCode().equals(code);
}
/**
* 判断是否为""
*/
public static boolean isNo(Integer code) {
return NO.getCode().equals(code);
}
}

View File

@ -18,6 +18,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* 课程管理控制器超管端
*/
@ -33,12 +36,12 @@ public class AdminCourseController {
@PostMapping
@Operation(summary = "创建课程")
public Result<Course> createCourse(@Valid @RequestBody CourseCreateRequest request) {
public Result<CourseResponse> createCourse(@Valid @RequestBody CourseCreateRequest request) {
log.info("收到课程创建请求name={}, themeId={}, gradeTags={}", request.getName(), request.getThemeId(), request.getGradeTags());
try {
Course course = courseService.createSystemCourse(request);
log.info("课程创建成功id={}", course.getId());
return Result.success(course);
return Result.success(courseService.getCourseByIdWithLessons(course.getId()));
} catch (Exception e) {
log.error("课程创建失败", e);
throw e;
@ -47,8 +50,9 @@ public class AdminCourseController {
@PutMapping("/{id}")
@Operation(summary = "更新课程")
public Result<Course> updateCourse(@PathVariable Long id, @RequestBody CourseUpdateRequest request) {
return Result.success(courseService.updateCourse(id, request));
public Result<CourseResponse> updateCourse(@PathVariable Long id, @RequestBody CourseUpdateRequest request) {
courseService.updateCourse(id, request);
return Result.success(courseService.getCourseByIdWithLessons(id));
}
@GetMapping("/{id}")
@ -59,7 +63,7 @@ public class AdminCourseController {
@GetMapping
@Operation(summary = "分页查询课程")
public Result<PageResult<Course>> getCoursePage(CoursePageQueryRequest request) {
public Result<PageResult<CourseResponse>> getCoursePage(CoursePageQueryRequest request) {
log.info("查询课程列表pageNum={}, pageSize={}, keyword={}, category={}, status={}, reviewOnly={}",
request.getPageNum(), request.getPageSize(), request.getKeyword(), request.getCategory(), request.getStatus(), request.getReviewOnly());
// 页码
@ -75,7 +79,13 @@ public class AdminCourseController {
request.getCategory(),
request.getStatus(),
request.getReviewOnly());
PageResult<Course> result = PageResult.of(page);
// 转换为 CourseResponse
List<CourseResponse> responseList = page.getRecords().stream()
.map(course -> courseService.getCourseByIdWithLessons(course.getId()))
.collect(Collectors.toList());
PageResult<CourseResponse> result = PageResult.of(responseList, page.getTotal(), page.getCurrent(), page.getSize());
log.info("课程列表查询结果total={}, list={}", result.getTotal(), result.getList().size());
return Result.success(result);
}

View File

@ -4,6 +4,9 @@ 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.CourseLessonCreateRequest;
import com.reading.platform.dto.request.LessonStepCreateRequest;
import com.reading.platform.dto.response.CourseLessonResponse;
import com.reading.platform.dto.response.LessonStepResponse;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.entity.LessonStep;
import com.reading.platform.service.CourseLessonService;
@ -14,6 +17,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* 课程环节控制器超管端
@ -28,33 +32,42 @@ public class AdminCourseLessonController {
@GetMapping("/{courseId}/lessons")
@Operation(summary = "获取课程的所有环节")
public Result<List<CourseLesson>> findAll(@PathVariable Long courseId) {
return Result.success(courseLessonService.findByCourseId(courseId));
public Result<List<CourseLessonResponse>> findAll(@PathVariable Long courseId) {
List<CourseLesson> lessons = courseLessonService.findByCourseId(courseId);
List<CourseLessonResponse> responses = lessons.stream()
.map(this::toLessonResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
@GetMapping("/{courseId}/lessons/{id}")
@Operation(summary = "获取课程环节详情")
public Result<CourseLesson> findOne(
public Result<CourseLessonResponse> findOne(
@PathVariable Long courseId,
@PathVariable Long id) {
return Result.success(courseLessonService.findById(id));
CourseLesson lesson = courseLessonService.findById(id);
return Result.success(toLessonResponse(lesson));
}
@GetMapping("/{courseId}/lessons/type/{lessonType}")
@Operation(summary = "按类型获取课程环节")
public Result<CourseLesson> findByType(
public Result<CourseLessonResponse> findByType(
@PathVariable Long courseId,
@PathVariable String lessonType) {
return Result.success(courseLessonService.findByType(courseId, lessonType));
CourseLesson lesson = courseLessonService.findByType(courseId, lessonType);
if (lesson == null) {
return Result.success(null);
}
return Result.success(toLessonResponse(lesson));
}
@PostMapping("/{courseId}/lessons")
@Operation(summary = "创建课程环节")
@RequireRole(UserRole.ADMIN)
public Result<CourseLesson> create(
public Result<CourseLessonResponse> create(
@PathVariable Long courseId,
@Valid @RequestBody CourseLessonCreateRequest request) {
return Result.success(courseLessonService.create(
CourseLesson lesson = courseLessonService.create(
courseId,
request.getLessonType(),
request.getName(),
@ -72,17 +85,18 @@ public class AdminCourseLessonController {
request.getReflection(),
request.getAssessmentData(),
request.getUseTemplate()
));
);
return Result.success(toLessonResponse(lesson));
}
@PutMapping("/{courseId}/lessons/{id}")
@Operation(summary = "更新课程环节")
@RequireRole(UserRole.ADMIN)
public Result<CourseLesson> update(
public Result<CourseLessonResponse> update(
@PathVariable Long courseId,
@PathVariable Long id,
@RequestBody CourseLessonCreateRequest request) {
return Result.success(courseLessonService.update(
CourseLesson lesson = courseLessonService.update(
id,
request.getName(),
request.getDescription(),
@ -99,7 +113,8 @@ public class AdminCourseLessonController {
request.getReflection(),
request.getAssessmentData(),
request.getUseTemplate()
));
);
return Result.success(toLessonResponse(lesson));
}
@DeleteMapping("/{courseId}/lessons/{id}")
@ -126,44 +141,50 @@ public class AdminCourseLessonController {
@GetMapping("/{courseId}/lessons/{lessonId}/steps")
@Operation(summary = "获取课时的教学环节")
public Result<List<LessonStep>> findSteps(
public Result<List<LessonStepResponse>> findSteps(
@PathVariable Long courseId,
@PathVariable Long lessonId) {
return Result.success(courseLessonService.findSteps(lessonId));
List<LessonStep> steps = courseLessonService.findSteps(lessonId);
List<LessonStepResponse> responses = steps.stream()
.map(this::toStepResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
@PostMapping("/{courseId}/lessons/{lessonId}/steps")
@Operation(summary = "创建教学环节")
@RequireRole(UserRole.ADMIN)
public Result<LessonStep> createStep(
public Result<LessonStepResponse> createStep(
@PathVariable Long courseId,
@PathVariable Long lessonId,
@RequestBody StepCreateRequest request) {
return Result.success(courseLessonService.createStep(
@Valid @RequestBody LessonStepCreateRequest request) {
LessonStep step = courseLessonService.createStep(
lessonId,
request.getName(),
request.getContent(),
request.getDuration(),
request.getObjective(),
request.getResourceIds()
));
);
return Result.success(toStepResponse(step));
}
@PutMapping("/{courseId}/lessons/steps/{stepId}")
@Operation(summary = "更新教学环节")
@RequireRole(UserRole.ADMIN)
public Result<LessonStep> updateStep(
public Result<LessonStepResponse> updateStep(
@PathVariable Long courseId,
@PathVariable Long stepId,
@RequestBody StepCreateRequest request) {
return Result.success(courseLessonService.updateStep(
@Valid @RequestBody LessonStepCreateRequest request) {
LessonStep step = courseLessonService.updateStep(
stepId,
request.getName(),
request.getContent(),
request.getDuration(),
request.getObjective(),
request.getResourceIds()
));
);
return Result.success(toStepResponse(step));
}
@DeleteMapping("/{courseId}/lessons/steps/{stepId}")
@ -188,24 +209,56 @@ public class AdminCourseLessonController {
}
/**
* 教学环节创建请求
* CourseLesson 实体转换为 CourseLessonResponse
*/
public static class StepCreateRequest {
private String name;
private String content;
private Integer duration;
private String objective;
private List<Long> resourceIds;
private CourseLessonResponse toLessonResponse(CourseLesson lesson) {
// 获取教学环节
List<LessonStep> steps = courseLessonService.findSteps(lesson.getId());
List<LessonStepResponse> stepResponses = steps.stream()
.map(this::toStepResponse)
.collect(Collectors.toList());
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Integer getDuration() { return duration; }
public void setDuration(Integer duration) { this.duration = duration; }
public String getObjective() { return objective; }
public void setObjective(String objective) { this.objective = objective; }
public List<Long> getResourceIds() { return resourceIds; }
public void setResourceIds(List<Long> resourceIds) { this.resourceIds = resourceIds; }
return CourseLessonResponse.builder()
.id(lesson.getId())
.courseId(lesson.getCourseId())
.lessonType(lesson.getLessonType())
.name(lesson.getName())
.description(lesson.getDescription())
.duration(lesson.getDuration())
.videoPath(lesson.getVideoPath())
.videoName(lesson.getVideoName())
.pptPath(lesson.getPptPath())
.pptName(lesson.getPptName())
.pdfPath(lesson.getPdfPath())
.pdfName(lesson.getPdfName())
.objectives(lesson.getObjectives())
.preparation(lesson.getPreparation())
.extension(lesson.getExtension())
.reflection(lesson.getReflection())
.assessmentData(lesson.getAssessmentData())
.useTemplate(lesson.getUseTemplate())
.sortOrder(lesson.getSortOrder())
.steps(stepResponses)
.createdAt(lesson.getCreatedAt())
.updatedAt(lesson.getUpdatedAt())
.build();
}
}
/**
* LessonStep 实体转换为 LessonStepResponse
*/
private LessonStepResponse toStepResponse(LessonStep step) {
return LessonStepResponse.builder()
.id(step.getId())
.lessonId(step.getLessonId())
.name(step.getName())
.content(step.getContent())
.duration(step.getDuration())
.objective(step.getObjective())
.resourceIds(step.getResourceIds())
.sortOrder(step.getSortOrder())
.createdAt(step.getCreatedAt())
.updatedAt(step.getUpdatedAt())
.build();
}
}

View File

@ -1,12 +1,13 @@
package com.reading.platform.controller.admin;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.PackageCreateRequest;
import com.reading.platform.dto.request.PackageGrantRequest;
import com.reading.platform.dto.request.PackageReviewRequest;
import com.reading.platform.dto.response.CoursePackageResponse;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.service.CoursePackageService;
@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 课程套餐控制器超管端
@ -49,24 +51,25 @@ public class AdminPackageController {
@PostMapping
@Operation(summary = "创建套餐")
@RequireRole(UserRole.ADMIN)
public Result<CoursePackage> create(@Valid @RequestBody PackageCreateRequest request) {
return Result.success(packageService.createPackage(
public Result<CoursePackageResponse> create(@Valid @RequestBody PackageCreateRequest request) {
CoursePackage pkg = packageService.createPackage(
request.getName(),
request.getDescription(),
request.getPrice(),
request.getDiscountPrice(),
request.getDiscountType(),
request.getGradeLevels()
));
);
return Result.success(packageService.findOnePackage(pkg.getId()));
}
@PutMapping("/{id}")
@Operation(summary = "更新套餐")
@RequireRole(UserRole.ADMIN)
public Result<CoursePackage> update(
public Result<CoursePackageResponse> update(
@PathVariable Long id,
@RequestBody PackageCreateRequest request) {
return Result.success(packageService.updatePackage(
packageService.updatePackage(
id,
request.getName(),
request.getDescription(),
@ -74,7 +77,8 @@ public class AdminPackageController {
request.getDiscountPrice(),
request.getDiscountType(),
request.getGradeLevels()
));
);
return Result.success(packageService.findOnePackage(id));
}
@DeleteMapping("/{id}")
@ -108,8 +112,14 @@ public class AdminPackageController {
@RequireRole(UserRole.ADMIN)
public Result<Void> review(
@PathVariable Long id,
@RequestBody ReviewRequest request) {
packageService.reviewPackage(id, SecurityUtils.getCurrentUserId(), request.getApproved(), request.getComment());
@Valid @RequestBody PackageReviewRequest request) {
packageService.reviewPackage(
id,
SecurityUtils.getCurrentUserId(),
request.getApproved(),
request.getComment(),
request.getPublish()
);
return Result.success();
}
@ -131,8 +141,12 @@ public class AdminPackageController {
@GetMapping("/all")
@Operation(summary = "查询所有已发布的套餐列表")
public Result<List<CoursePackage>> getPublishedPackages() {
return Result.success(packageService.findPublishedPackages());
public Result<List<CoursePackageResponse>> getPublishedPackages() {
List<CoursePackage> packages = packageService.findPublishedPackages();
List<CoursePackageResponse> responses = packages.stream()
.map(pkg -> packageService.findOnePackage(pkg.getId()))
.collect(Collectors.toList());
return Result.success(responses);
}
@PostMapping("/{id}/grant")
@ -140,7 +154,7 @@ public class AdminPackageController {
@RequireRole(UserRole.ADMIN)
public Result<Void> grantToTenant(
@PathVariable Long id,
@RequestBody GrantRequest request) {
@Valid @RequestBody PackageGrantRequest request) {
LocalDate endDate = LocalDate.parse(request.getEndDate(), DateTimeFormatter.ISO_DATE);
packageService.renewTenantPackage(
request.getTenantId(),
@ -150,33 +164,4 @@ public class AdminPackageController {
);
return Result.success();
}
/**
* 审核请求
*/
public static class ReviewRequest {
private Boolean approved;
private String comment;
public Boolean getApproved() { return approved; }
public void setApproved(Boolean approved) { this.approved = approved; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
}
/**
* 授权请求
*/
public static class GrantRequest {
private Long tenantId;
private String endDate;
private Long pricePaid;
public Long getTenantId() { return tenantId; }
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public String getEndDate() { return endDate; }
public void setEndDate(String endDate) { this.endDate = endDate; }
public Long getPricePaid() { return pricePaid; }
public void setPricePaid(Long pricePaid) { this.pricePaid = pricePaid; }
}
}

View File

@ -5,16 +5,24 @@ import com.reading.platform.common.annotation.RequireRole;
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.ResourceLibraryCreateRequest;
import com.reading.platform.dto.request.ResourceLibraryUpdateRequest;
import com.reading.platform.dto.request.ResourceItemCreateRequest;
import com.reading.platform.dto.request.ResourceItemUpdateRequest;
import com.reading.platform.dto.response.ResourceLibraryResponse;
import com.reading.platform.dto.response.ResourceItemResponse;
import com.reading.platform.entity.ResourceItem;
import com.reading.platform.entity.ResourceLibrary;
import com.reading.platform.service.ResourceLibraryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 资源库控制器超管端
@ -31,44 +39,52 @@ public class AdminResourceController {
@GetMapping("/libraries")
@Operation(summary = "分页查询资源库")
public Result<PageResult<ResourceLibrary>> findAllLibraries(
public Result<PageResult<ResourceLibraryResponse>> findAllLibraries(
@RequestParam(required = false) String libraryType,
@RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) {
Page<ResourceLibrary> page = resourceLibraryService.findAllLibraries(libraryType, keyword, pageNum, pageSize);
return Result.success(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
List<ResourceLibraryResponse> responses = page.getRecords().stream()
.map(this::toLibraryResponse)
.collect(Collectors.toList());
return Result.success(PageResult.of(responses, page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/libraries/{id}")
@Operation(summary = "查询资源库详情")
public Result<ResourceLibrary> findLibrary(@PathVariable String id) {
return Result.success(resourceLibraryService.findLibraryById(id));
public Result<ResourceLibraryResponse> findLibrary(@PathVariable String id) {
ResourceLibrary library = resourceLibraryService.findLibraryById(id);
return Result.success(toLibraryResponse(library));
}
@PostMapping("/libraries")
@Operation(summary = "创建资源库")
@RequireRole(UserRole.ADMIN)
public Result<ResourceLibrary> createLibrary(@RequestBody LibraryCreateRequest request) {
return Result.success(resourceLibraryService.createLibrary(
public Result<ResourceLibraryResponse> createLibrary(@Valid @RequestBody ResourceLibraryCreateRequest request) {
ResourceLibrary library = resourceLibraryService.createLibrary(
request.getName(),
request.getType(),
request.getDescription(),
request.getTenantId()
));
);
return Result.success(toLibraryResponse(library));
}
@PutMapping("/libraries/{id}")
@Operation(summary = "更新资源库")
@RequireRole(UserRole.ADMIN)
public Result<ResourceLibrary> updateLibrary(
public Result<ResourceLibraryResponse> updateLibrary(
@PathVariable String id,
@RequestBody LibraryUpdateRequest request) {
return Result.success(resourceLibraryService.updateLibrary(
@Valid @RequestBody ResourceLibraryUpdateRequest request) {
ResourceLibrary library = resourceLibraryService.updateLibrary(
id,
request.getName(),
request.getDescription()
));
);
return Result.success(toLibraryResponse(library));
}
@DeleteMapping("/libraries/{id}")
@ -83,27 +99,33 @@ public class AdminResourceController {
@GetMapping("/items")
@Operation(summary = "分页查询资源项目")
public Result<PageResult<ResourceItem>> findAllItems(
public Result<PageResult<ResourceItemResponse>> findAllItems(
@RequestParam(required = false) String libraryId,
@RequestParam(required = false) String fileType,
@RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "20") Integer pageSize) {
Page<ResourceItem> page = resourceLibraryService.findAllItems(libraryId, fileType, keyword, pageNum, pageSize);
return Result.success(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
List<ResourceItemResponse> responses = page.getRecords().stream()
.map(this::toItemResponse)
.collect(Collectors.toList());
return Result.success(PageResult.of(responses, page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/items/{id}")
@Operation(summary = "查询资源项目详情")
public Result<ResourceItem> findItem(@PathVariable String id) {
return Result.success(resourceLibraryService.findItemById(id));
public Result<ResourceItemResponse> findItem(@PathVariable String id) {
ResourceItem item = resourceLibraryService.findItemById(id);
return Result.success(toItemResponse(item));
}
@PostMapping("/items")
@Operation(summary = "创建资源项目")
@RequireRole(UserRole.ADMIN)
public Result<ResourceItem> createItem(@RequestBody ItemCreateRequest request) {
return Result.success(resourceLibraryService.createItem(
public Result<ResourceItemResponse> createItem(@Valid @RequestBody ResourceItemCreateRequest request) {
ResourceItem item = resourceLibraryService.createItem(
request.getLibraryId(),
request.getTitle(),
request.getFileType(),
@ -112,21 +134,23 @@ public class AdminResourceController {
request.getDescription(),
request.getTags(),
request.getTenantId()
));
);
return Result.success(toItemResponse(item));
}
@PutMapping("/items/{id}")
@Operation(summary = "更新资源项目")
@RequireRole(UserRole.ADMIN)
public Result<ResourceItem> updateItem(
public Result<ResourceItemResponse> updateItem(
@PathVariable String id,
@RequestBody ItemUpdateRequest request) {
return Result.success(resourceLibraryService.updateItem(
@Valid @RequestBody ResourceItemUpdateRequest request) {
ResourceItem item = resourceLibraryService.updateItem(
id,
request.getTitle(),
request.getDescription(),
request.getTags()
));
);
return Result.success(toItemResponse(item));
}
@DeleteMapping("/items/{id}")
@ -154,81 +178,35 @@ public class AdminResourceController {
}
/**
* 资源库创建请求
* ResourceLibrary 实体转换为 ResourceLibraryResponse
*/
public static class LibraryCreateRequest {
private String name;
private String type;
private String description;
private String tenantId;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
private ResourceLibraryResponse toLibraryResponse(ResourceLibrary library) {
return ResourceLibraryResponse.builder()
.id(library.getId())
.tenantId(library.getTenantId())
.name(library.getName())
.description(library.getDescription())
.type(library.getLibraryType())
.createdBy(library.getCreatedBy() != null ? String.valueOf(library.getCreatedBy()) : null)
.createdAt(library.getCreatedAt())
.updatedAt(library.getUpdatedAt())
.build();
}
/**
* 资源库更新请求
* ResourceItem 实体转换为 ResourceItemResponse
*/
public static class LibraryUpdateRequest {
private String name;
private String description;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
private ResourceItemResponse toItemResponse(ResourceItem item) {
return ResourceItemResponse.builder()
.id(item.getId())
.libraryId(item.getLibraryId())
.tenantId(item.getTenantId())
.type(item.getFileType())
.name(item.getTitle())
.description(item.getDescription())
.status(item.getStatus())
.createdAt(item.getCreatedAt())
.updatedAt(item.getUpdatedAt())
.build();
}
/**
* 资源项目创建请求数字资源
*/
public static class ItemCreateRequest {
private String libraryId;
private String title;
private String fileType;
private String filePath;
private Long fileSize;
private String description;
private String tags;
private String tenantId;
public String getLibraryId() { return libraryId; }
public void setLibraryId(String libraryId) { this.libraryId = libraryId; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
}
/**
* 资源项目更新请求数字资源
*/
public static class ItemUpdateRequest {
private String title;
private String description;
private String tags;
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
}
}
}

View File

@ -11,6 +11,7 @@ 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 com.reading.platform.service.StatsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -19,7 +20,6 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
@ -32,55 +32,36 @@ import java.util.List;
@Tag(name = "超管端 - 统计管理")
public class AdminStatsController {
private final StatsService statsService;
@GetMapping
@Operation(summary = "获取统计数据")
public Result<StatsResponse> getStats() {
// TODO: 实现统计数据查询
return Result.success(StatsResponse.builder()
.totalTenants(0L)
.activeTenants(0L)
.totalTeachers(0L)
.totalStudents(0L)
.totalCourses(0L)
.totalLessons(0L)
.build());
return Result.success(statsService.getStats());
}
@GetMapping("/trend")
@Operation(summary = "获取趋势数据")
public Result<StatsTrendResponse> getTrendData() {
// TODO: 实现趋势数据查询
return Result.success(StatsTrendResponse.builder()
.dates(new ArrayList<>())
.newStudents(new ArrayList<>())
.newTeachers(new ArrayList<>())
.newCourses(new ArrayList<>())
.build());
return Result.success(statsService.getTrendData());
}
@GetMapping("/tenants/active")
@Operation(summary = "获取活跃租户")
public Result<List<ActiveTenantItemResponse>> getActiveTenants(@ModelAttribute ActiveTenantsQueryRequest request) {
// 返回数量限制
// TODO: 实现活跃租户查询
return Result.success(new ArrayList<>());
return Result.success(statsService.getActiveTenants(request));
}
@GetMapping("/courses/popular")
@Operation(summary = "获取热门课程")
public Result<List<PopularCourseItemResponse>> getPopularCourses(@ModelAttribute PopularCoursesQueryRequest request) {
// 返回数量限制
// TODO: 实现热门课程查询
return Result.success(new ArrayList<>());
return Result.success(statsService.getPopularCourses(request));
}
@GetMapping("/activities")
@Operation(summary = "获取最近活动")
public Result<List<RecentActivityItemResponse>> getRecentActivities(@ModelAttribute RecentActivitiesQueryRequest request) {
// 返回数量限制
// TODO: 实现最近活动查询
return Result.success(new ArrayList<>());
return Result.success(statsService.getRecentActivities(request));
}
}
}

View File

@ -1,20 +1,14 @@
package com.reading.platform.controller.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.mapper.TenantMapper;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.TenantCreateRequest;
import com.reading.platform.dto.request.TenantUpdateRequest;
import com.reading.platform.dto.response.TenantResponse;
import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.Tenant;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.service.TenantService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -25,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Tag(name = "超管端 - 租户管理", description = "Tenant Management APIs for Admin")
@RestController
@ -34,29 +29,26 @@ import java.util.Map;
public class AdminTenantController {
private final TenantService tenantService;
private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
@Operation(summary = "Create tenant")
@PostMapping
public Result<TenantResponse> createTenant(@Valid @RequestBody TenantCreateRequest request) {
Tenant tenant = tenantService.createTenant(request);
return Result.success(tenantMapper.toVO(tenant));
return Result.success(toResponse(tenant));
}
@Operation(summary = "Update tenant")
@PutMapping("/{id}")
public Result<TenantResponse> updateTenant(@PathVariable Long id, @RequestBody TenantUpdateRequest request) {
Tenant tenant = tenantService.updateTenant(id, request);
return Result.success(tenantMapper.toVO(tenant));
return Result.success(toResponse(tenant));
}
@Operation(summary = "Get tenant by ID")
@GetMapping("/{id}")
public Result<TenantResponse> getTenant(@PathVariable Long id) {
Tenant tenant = tenantService.getTenantById(id);
return Result.success(tenantMapper.toVO(tenant));
return Result.success(toResponse(tenant));
}
@Operation(summary = "Get tenant page")
@ -66,27 +58,15 @@ public class AdminTenantController {
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) {
Page<Tenant> pageResult = tenantService.getTenantPage(pageNum, pageSize, keyword, status);
List<TenantResponse> voList = tenantMapper.toVO(pageResult.getRecords());
// 调用 Service 层方法获取带统计数据的租户分页
Page<TenantResponse> pageResult = tenantService.getTenantPageWithStats(pageNum, pageSize, keyword, status);
// 填充教师数量和学生数量
for (TenantResponse vo : voList) {
if (vo.getId() != null) {
Long teacherCount = teacherMapper.selectCount(
new LambdaQueryWrapper<Teacher>()
.eq(Teacher::getTenantId, vo.getId())
);
vo.setTeacherCount(teacherCount != null ? teacherCount.intValue() : 0);
Long studentCount = studentMapper.selectCount(
new LambdaQueryWrapper<Student>()
.eq(Student::getTenantId, vo.getId())
);
vo.setStudentCount(studentCount != null ? studentCount.intValue() : 0);
}
}
return Result.success(PageResult.of(voList, pageResult.getTotal(), pageResult.getCurrent(), pageResult.getSize()));
return Result.success(PageResult.of(
pageResult.getRecords(),
pageResult.getTotal(),
pageResult.getCurrent(),
pageResult.getSize()
));
}
@Operation(summary = "Delete tenant")
@ -100,7 +80,10 @@ public class AdminTenantController {
@GetMapping("/active")
public Result<List<TenantResponse>> getAllActiveTenants() {
List<Tenant> tenants = tenantService.getAllActiveTenants();
return Result.success(tenantMapper.toVO(tenants));
List<TenantResponse> responses = tenants.stream()
.map(this::toResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
@Operation(summary = "获取租户统计信息")
@ -118,7 +101,7 @@ public class AdminTenantController {
public Result<TenantResponse> updateTenantQuota(@PathVariable Long id, @RequestBody Map<String, Object> quota) {
// TODO: 实现更新租户配额逻辑
Tenant tenant = tenantService.getTenantById(id);
return Result.success(tenantMapper.toVO(tenant));
return Result.success(toResponse(tenant));
}
@Operation(summary = "更新租户状态")
@ -126,7 +109,7 @@ public class AdminTenantController {
public Result<TenantResponse> updateTenantStatus(@PathVariable Long id, @RequestBody Map<String, Object> status) {
// TODO: 实现更新租户状态逻辑
Tenant tenant = tenantService.getTenantById(id);
return Result.success(tenantMapper.toVO(tenant));
return Result.success(toResponse(tenant));
}
@Operation(summary = "重置租户密码")
@ -136,4 +119,34 @@ public class AdminTenantController {
return Result.success();
}
}
/**
* Tenant 实体转换为 TenantResponse
*/
private TenantResponse toResponse(Tenant tenant) {
return TenantResponse.builder()
.id(tenant.getId())
.name(tenant.getName())
.code(tenant.getCode())
.username(tenant.getUsername())
.contactName(tenant.getContactName())
.contactPhone(tenant.getContactPhone())
.contactEmail(tenant.getContactEmail())
.address(tenant.getAddress())
.logoUrl(tenant.getLogoUrl())
.status(tenant.getStatus())
.expireAt(tenant.getExpireAt())
.maxStudents(tenant.getMaxStudents())
.maxTeachers(tenant.getMaxTeachers())
.packageType(tenant.getPackageType())
.teacherQuota(tenant.getTeacherQuota())
.studentQuota(tenant.getStudentQuota())
.storageQuota(tenant.getStorageQuota())
.storageUsed(tenant.getStorageUsed())
.startDate(tenant.getStartDate())
.expireDate(tenant.getExpireDate())
.createdAt(tenant.getCreatedAt())
.updatedAt(tenant.getUpdatedAt())
.build();
}
}

View File

@ -4,6 +4,7 @@ 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.ThemeCreateRequest;
import com.reading.platform.dto.response.ThemeResponse;
import com.reading.platform.entity.Theme;
import com.reading.platform.service.ThemeService;
import io.swagger.v3.oas.annotations.Operation;
@ -13,6 +14,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* 主题字典控制器超管端
@ -27,40 +29,47 @@ public class AdminThemeController {
@GetMapping
@Operation(summary = "查询所有主题")
public Result<List<Theme>> findAll() {
return Result.success(themeService.findAll());
public Result<List<ThemeResponse>> findAll() {
List<Theme> themes = themeService.findAll();
List<ThemeResponse> responses = themes.stream()
.map(this::toResponse)
.collect(Collectors.toList());
return Result.success(responses);
}
@GetMapping("/{id}")
@Operation(summary = "查询主题详情")
public Result<Theme> findOne(@PathVariable Long id) {
return Result.success(themeService.findById(id));
public Result<ThemeResponse> findOne(@PathVariable Long id) {
Theme theme = themeService.findById(id);
return Result.success(toResponse(theme));
}
@PostMapping
@Operation(summary = "创建主题")
@RequireRole(UserRole.ADMIN)
public Result<Theme> create(@Valid @RequestBody ThemeCreateRequest request) {
return Result.success(themeService.create(
public Result<ThemeResponse> create(@Valid @RequestBody ThemeCreateRequest request) {
Theme theme = themeService.create(
request.getName(),
request.getDescription(),
request.getSortOrder()
));
);
return Result.success(toResponse(theme));
}
@PutMapping("/{id}")
@Operation(summary = "更新主题")
@RequireRole(UserRole.ADMIN)
public Result<Theme> update(
public Result<ThemeResponse> update(
@PathVariable Long id,
@RequestBody ThemeCreateRequest request) {
return Result.success(themeService.update(
Theme theme = themeService.update(
id,
request.getName(),
request.getDescription(),
request.getSortOrder(),
null
));
);
return Result.success(toResponse(theme));
}
@DeleteMapping("/{id}")
@ -78,4 +87,19 @@ public class AdminThemeController {
themeService.reorder(ids);
return Result.success();
}
}
/**
* Theme 实体转换为 ThemeResponse
*/
private ThemeResponse toResponse(Theme theme) {
return ThemeResponse.builder()
.id(theme.getId())
.name(theme.getName())
.description(theme.getDescription())
.sortOrder(theme.getSortOrder())
.status(theme.getStatus())
.createdAt(theme.getCreatedAt())
.updatedAt(theme.getUpdatedAt())
.build();
}
}

View File

@ -1,6 +1,7 @@
package com.reading.platform.controller.teacher;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.enums.CourseStatus;
import com.reading.platform.common.mapper.ClassMapper;
import com.reading.platform.common.mapper.CourseMapper;
import com.reading.platform.common.mapper.StudentMapper;
@ -68,7 +69,7 @@ public class TeacherCourseController {
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Page<Course> page = courseService.getCoursePage(tenantId, pageNum, pageSize, keyword, category, "published");
Page<Course> page = courseService.getCoursePage(tenantId, pageNum, pageSize, keyword, category, CourseStatus.PUBLISHED.getCode());
List<CourseResponse> voList = courseMapper.toVO(page.getRecords());
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
}

View File

@ -0,0 +1,29 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 教学环节创建请求
*/
@Data
@Schema(description = "教学环节创建请求")
public class LessonStepCreateRequest {
@Schema(description = "环节名称")
private String name;
@Schema(description = "环节内容")
private String content;
@Schema(description = "时长(分钟)")
private Integer duration;
@Schema(description = "教学目标")
private String objective;
@Schema(description = "关联资源 ID 列表")
private List<Long> resourceIds;
}

View File

@ -0,0 +1,21 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 套餐授权请求
*/
@Data
@Schema(description = "套餐授权请求")
public class PackageGrantRequest {
@Schema(description = "租户 ID")
private Long tenantId;
@Schema(description = "结束日期")
private String endDate;
@Schema(description = "支付金额(分)")
private Long pricePaid;
}

View File

@ -0,0 +1,21 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 套餐审核请求
*/
@Data
@Schema(description = "套餐审核请求")
public class PackageReviewRequest {
@Schema(description = "是否通过")
private Boolean approved;
@Schema(description = "审核意见")
private String comment;
@Schema(description = "是否同时发布(仅当 approved=true 时有效)")
private Boolean publish;
}

View File

@ -0,0 +1,36 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 资源项目创建请求
*/
@Data
@Schema(description = "资源项目创建请求")
public class ResourceItemCreateRequest {
@Schema(description = "资源库 ID")
private String libraryId;
@Schema(description = "资源标题")
private String title;
@Schema(description = "文件类型")
private String fileType;
@Schema(description = "文件路径")
private String filePath;
@Schema(description = "文件大小(字节)")
private Long fileSize;
@Schema(description = "资源描述")
private String description;
@Schema(description = "标签")
private String tags;
@Schema(description = "租户 ID")
private String tenantId;
}

View File

@ -0,0 +1,21 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 资源项目更新请求
*/
@Data
@Schema(description = "资源项目更新请求")
public class ResourceItemUpdateRequest {
@Schema(description = "资源标题")
private String title;
@Schema(description = "资源描述")
private String description;
@Schema(description = "标签")
private String tags;
}

View File

@ -0,0 +1,24 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 资源库创建请求
*/
@Data
@Schema(description = "资源库创建请求")
public class ResourceLibraryCreateRequest {
@Schema(description = "资源库名称")
private String name;
@Schema(description = "资源库类型")
private String type;
@Schema(description = "资源库描述")
private String description;
@Schema(description = "租户 ID")
private String tenantId;
}

View File

@ -0,0 +1,18 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 资源库更新请求
*/
@Data
@Schema(description = "资源库更新请求")
public class ResourceLibraryUpdateRequest {
@Schema(description = "资源库名称")
private String name;
@Schema(description = "资源库描述")
private String description;
}

View File

@ -16,7 +16,7 @@ import java.time.LocalDateTime;
public class ResourceItemResponse {
@Schema(description = "ID")
private String id;
private Long id;
@Schema(description = "资源库 ID")
private String libraryId;

View File

@ -16,7 +16,7 @@ import java.time.LocalDateTime;
public class ResourceLibraryResponse {
@Schema(description = "ID")
private String id;
private Long id;
@Schema(description = "租户 ID")
private String tenantId;

View File

@ -37,7 +37,7 @@ public class CoursePackage extends BaseEntity {
@Schema(description = "课程数量")
private Integer courseCount;
@Schema(description = "状态DRAFT、PENDING_REVIEW、APPROVED、REJECTED、PUBLISHED、OFFLINE")
@Schema(description = "状态DRAFT、PENDING、APPROVED、REJECTED、PUBLISHED、OFFLINE")
private String status;
@Schema(description = "提交时间")

View File

@ -300,10 +300,15 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
/**
* 审核套餐
* @param id 套餐ID
* @param userId 审核人ID
* @param approved 是否通过
* @param comment 审核意见
* @param publish 是否同时发布仅当 approved=true 时有效
*/
@Transactional(rollbackFor = Exception.class)
public void reviewPackage(Long id, Long userId, Boolean approved, String comment) {
log.info("审核套餐id={}, userId={}, approved={}", id, userId, approved);
public void reviewPackage(Long id, Long userId, Boolean approved, String comment, Boolean publish) {
log.info("审核套餐id={}, userId={}, approved={}, publish={}", id, userId, approved, publish);
CoursePackage pkg = packageMapper.selectById(id);
if (pkg == null) {
log.warn("套餐不存在id={}", id);
@ -315,12 +320,27 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
throw new BusinessException("只有待审核状态的套餐可以审核");
}
pkg.setStatus(approved ? CourseStatus.APPROVED.getCode() : CourseStatus.REJECTED.getCode());
// 如果驳回
if (!Boolean.TRUE.equals(approved)) {
pkg.setStatus(CourseStatus.REJECTED.getCode());
}
// 如果通过且同时发布
else if (Boolean.TRUE.equals(publish)) {
pkg.setStatus(CourseStatus.PUBLISHED.getCode());
pkg.setPublishedAt(LocalDateTime.now());
}
// 仅通过
else {
pkg.setStatus(CourseStatus.APPROVED.getCode());
}
pkg.setReviewedAt(LocalDateTime.now());
pkg.setReviewedBy(userId);
pkg.setReviewComment(comment);
packageMapper.updateById(pkg);
log.info("套餐审核成功id={}, result={}", id, approved ? "approved" : "rejected");
log.info("套餐审核成功id={}, result={}", id,
!Boolean.TRUE.equals(approved) ? "rejected" :
(Boolean.TRUE.equals(publish) ? "approved_and_published" : "approved"));
}
/**

View File

@ -0,0 +1,43 @@
package com.reading.platform.service;
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 java.util.List;
/**
* 统计服务接口
*/
public interface StatsService {
/**
* 获取统计数据
*/
StatsResponse getStats();
/**
* 获取趋势数据
*/
StatsTrendResponse getTrendData();
/**
* 获取活跃租户
*/
List<ActiveTenantItemResponse> getActiveTenants(ActiveTenantsQueryRequest request);
/**
* 获取热门课程
*/
List<PopularCourseItemResponse> getPopularCourses(PopularCoursesQueryRequest request);
/**
* 获取最近活动
*/
List<RecentActivityItemResponse> getRecentActivities(RecentActivitiesQueryRequest request);
}

View File

@ -3,6 +3,7 @@ package com.reading.platform.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.dto.request.TenantCreateRequest;
import com.reading.platform.dto.request.TenantUpdateRequest;
import com.reading.platform.dto.response.TenantResponse;
import com.reading.platform.entity.Tenant;
import java.util.List;
@ -32,6 +33,11 @@ public interface TenantService extends com.baomidou.mybatisplus.extension.servic
*/
Page<Tenant> getTenantPage(Integer pageNum, Integer pageSize, String keyword, String status);
/**
* 分页查询租户带教师和学生统计
*/
Page<TenantResponse> getTenantPageWithStats(Integer pageNum, Integer pageSize, String keyword, String status);
/**
* 删除租户
*/

View File

@ -6,6 +6,7 @@ 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.enums.YesNo;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.dto.request.CourseCreateRequest;
import com.reading.platform.dto.request.CourseUpdateRequest;
@ -60,7 +61,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setDurationMinutes(request.getDurationMinutes());
course.setObjectives(request.getObjectives());
course.setStatus(CourseStatus.DRAFT.getCode());
course.setIsSystem(0);
course.setIsSystem(YesNo.NO.getCode());
// Course Package Fields
course.setCoreContent(request.getCoreContent());
@ -90,10 +91,10 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setAssessmentData(nullIfEmptyJson(request.getAssessmentData()));
course.setGradeTags(nullIfEmptyJson(request.getGradeTags()));
course.setDomainTags(nullIfEmptyJson(request.getDomainTags()));
course.setHasCollectiveLesson(request.getHasCollectiveLesson() != null && request.getHasCollectiveLesson() ? 1 : 0);
course.setHasCollectiveLesson(YesNo.fromBoolean(request.getHasCollectiveLesson()).getCode());
course.setVersion("1.0");
course.setIsLatest(1);
course.setIsLatest(YesNo.YES.getCode());
course.setUsageCount(0);
course.setTeacherCount(0);
@ -121,7 +122,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setDurationMinutes(request.getDurationMinutes());
course.setObjectives(request.getObjectives());
course.setStatus(CourseStatus.DRAFT.getCode());
course.setIsSystem(1);
course.setIsSystem(YesNo.YES.getCode());
// Course Package Fields
course.setCoreContent(request.getCoreContent());
@ -151,10 +152,10 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setAssessmentData(nullIfEmptyJson(request.getAssessmentData()));
course.setGradeTags(nullIfEmptyJson(request.getGradeTags()));
course.setDomainTags(nullIfEmptyJson(request.getDomainTags()));
course.setHasCollectiveLesson(request.getHasCollectiveLesson() != null && request.getHasCollectiveLesson() ? 1 : 0);
course.setHasCollectiveLesson(YesNo.fromBoolean(request.getHasCollectiveLesson()).getCode());
course.setVersion("1.0");
course.setIsLatest(1);
course.setIsLatest(YesNo.YES.getCode());
course.setUsageCount(0);
course.setTeacherCount(0);
@ -287,7 +288,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setDomainTags(nullIfEmptyJson(request.getDomainTags()));
}
if (request.getHasCollectiveLesson() != null) {
course.setHasCollectiveLesson(request.getHasCollectiveLesson() ? 1 : 0);
course.setHasCollectiveLesson(YesNo.fromBoolean(request.getHasCollectiveLesson()).getCode());
}
courseMapper.updateById(course);
@ -346,7 +347,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
wrapper.and(w -> w
.eq(Course::getTenantId, tenantId)
.or()
.eq(Course::getIsSystem, 1)
.eq(Course::getIsSystem, YesNo.YES.getCode())
);
if (StringUtils.hasText(keyword)) {
@ -374,7 +375,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
// 只过滤系统课程
wrapper.eq(Course::getIsSystem, 1);
wrapper.eq(Course::getIsSystem, YesNo.YES.getCode());
// 审核管理页仅过滤待审核和已驳回排除已通过/已发布
if (reviewOnly) {
@ -453,7 +454,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
.and(w -> w
.eq(Course::getTenantId, tenantId)
.or()
.eq(Course::getIsSystem, 1)
.eq(Course::getIsSystem, YesNo.YES.getCode())
)
.eq(Course::getStatus, CourseStatus.PUBLISHED.getCode())
.orderByAsc(Course::getName)

View File

@ -0,0 +1,224 @@
package com.reading.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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 com.reading.platform.entity.Course;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.Tenant;
import com.reading.platform.mapper.CourseLessonMapper;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.mapper.TenantMapper;
import com.reading.platform.service.StatsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 统计服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StatsServiceImpl implements StatsService {
private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
private final CourseMapper courseMapper;
private final CourseLessonMapper courseLessonMapper;
@Override
public StatsResponse getStats() {
log.info("获取统计数据");
// 租户总数
Long totalTenants = tenantMapper.selectCount(null);
// 活跃租户数
Long activeTenants = tenantMapper.selectCount(
new LambdaQueryWrapper<Tenant>().eq(Tenant::getStatus, "ACTIVE")
);
// 教师总数
Long totalTeachers = teacherMapper.selectCount(null);
// 学生总数
Long totalStudents = studentMapper.selectCount(null);
// 课程总数
Long totalCourses = courseMapper.selectCount(null);
// 课时总数
Long totalLessons = courseLessonMapper.selectCount(null);
return StatsResponse.builder()
.totalTenants(totalTenants)
.activeTenants(activeTenants)
.totalTeachers(totalTeachers)
.totalStudents(totalStudents)
.totalCourses(totalCourses)
.totalLessons(totalLessons)
.build();
}
@Override
public StatsTrendResponse getTrendData() {
log.info("获取趋势数据");
LocalDateTime now = LocalDateTime.now();
List<String> dates = new ArrayList<>();
List<Integer> newStudents = new ArrayList<>();
List<Integer> newTeachers = new ArrayList<>();
List<Integer> newCourses = new ArrayList<>();
// 获取最近 7 天的趋势数据
for (int i = 6; i >= 0; i--) {
LocalDate date = now.minusDays(i).toLocalDate();
dates.add(date.format(DateTimeFormatter.ofPattern("MM-dd")));
// 当天新增学生数
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long students = studentMapper.selectCount(
new LambdaQueryWrapper<Student>()
.ge(Student::getCreatedAt, dayStart)
.lt(Student::getCreatedAt, dayEnd)
);
newStudents.add(students != null ? students.intValue() : 0);
// 当天新增教师数
Long teachers = teacherMapper.selectCount(
new LambdaQueryWrapper<Teacher>()
.ge(Teacher::getCreatedAt, dayStart)
.lt(Teacher::getCreatedAt, dayEnd)
);
newTeachers.add(teachers != null ? teachers.intValue() : 0);
// 当天新增课程数
Long courses = courseMapper.selectCount(
new LambdaQueryWrapper<Course>()
.ge(Course::getCreatedAt, dayStart)
.lt(Course::getCreatedAt, dayEnd)
);
newCourses.add(courses != null ? courses.intValue() : 0);
}
return StatsTrendResponse.builder()
.dates(dates)
.newStudents(newStudents)
.newTeachers(newTeachers)
.newCourses(newCourses)
.build();
}
@Override
public List<ActiveTenantItemResponse> getActiveTenants(ActiveTenantsQueryRequest request) {
log.info("获取活跃租户limit={}", request != null ? request.getLimit() : 10);
int limit = request != null && request.getLimit() != null ? request.getLimit() : 10;
// 查询所有活跃租户
List<Tenant> tenants = tenantMapper.selectList(
new LambdaQueryWrapper<Tenant>()
.eq(Tenant::getStatus, "ACTIVE")
.orderByDesc(Tenant::getUpdatedAt)
.last("LIMIT " + limit)
);
return tenants.stream().map(tenant -> {
// 查询该租户的活跃用户数教师+学生
Long teacherCount = teacherMapper.selectCount(
new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenant.getId())
);
Long studentCount = studentMapper.selectCount(
new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenant.getId())
);
// 查询该租户使用的课程数通过租户套餐
// 简化处理返回 0
int courseCount = 0;
return ActiveTenantItemResponse.builder()
.tenantId(tenant.getId())
.tenantName(tenant.getName())
.activeUsers((teacherCount != null ? teacherCount.intValue() : 0) +
(studentCount != null ? studentCount.intValue() : 0))
.courseCount(courseCount)
.build();
}).collect(Collectors.toList());
}
@Override
public List<PopularCourseItemResponse> getPopularCourses(PopularCoursesQueryRequest request) {
log.info("获取热门课程limit={}", request != null ? request.getLimit() : 10);
int limit = request != null && request.getLimit() != null ? request.getLimit() : 10;
// 查询使用次数最多的课程
List<Course> courses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.eq(Course::getIsSystem, 1)
.orderByDesc(Course::getUsageCount)
.last("LIMIT " + limit)
);
return courses.stream().map(course -> PopularCourseItemResponse.builder()
.courseId(course.getId())
.courseName(course.getName())
.usageCount(course.getUsageCount() != null ? course.getUsageCount() : 0)
.teacherCount(course.getTeacherCount() != null ? course.getTeacherCount() : 0)
.build()
).collect(Collectors.toList());
}
@Override
public List<RecentActivityItemResponse> getRecentActivities(RecentActivitiesQueryRequest request) {
log.info("获取最近活动limit={}", request != null ? request.getLimit() : 10);
int limit = request != null && request.getLimit() != null ? request.getLimit() : 10;
// 由于没有专门的活动记录表这里返回空列表
// 实际项目中应该从操作日志表获取
List<RecentActivityItemResponse> activities = new ArrayList<>();
// 可以从最近的课程更新中生成一些活动记录
List<Course> recentCourses = courseMapper.selectList(
new LambdaQueryWrapper<Course>()
.orderByDesc(Course::getUpdatedAt)
.last("LIMIT " + limit)
);
for (Course course : recentCourses) {
activities.add(RecentActivityItemResponse.builder()
.activityId(course.getId())
.activityType("COURSE_UPDATE")
.description("课程「" + course.getName() + "」已更新")
.operatorId(course.getSubmittedBy())
.operatorName("系统")
.operationTime(course.getUpdatedAt())
.build());
}
return activities;
}
}

View File

@ -7,9 +7,14 @@ 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.dto.response.TenantResponse;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.Tenant;
import com.reading.platform.entity.TenantPackage;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.mapper.TenantMapper;
import com.reading.platform.mapper.TenantPackageMapper;
import com.reading.platform.service.CoursePackageService;
@ -22,6 +27,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
/**
* 租户服务实现类
@ -35,6 +41,8 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
private final TenantMapper tenantMapper;
private final TenantPackageMapper tenantPackageMapper;
private final CoursePackageService coursePackageService;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
@Override
@Transactional
@ -240,4 +248,67 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
return tenants;
}
@Override
public Page<TenantResponse> getTenantPageWithStats(Integer pageNum, Integer pageSize, String keyword, String status) {
log.debug("分页查询租户(带统计),页码:{},每页数量:{}", pageNum, pageSize);
// 先查询租户分页数据
Page<Tenant> tenantPage = getTenantPage(pageNum, pageSize, keyword, status);
// 转换为 TenantResponse 并填充统计信息
List<TenantResponse> responses = tenantPage.getRecords().stream()
.map(this::toResponseWithStats)
.collect(Collectors.toList());
// 构建返回的分页对象
Page<TenantResponse> resultPage = new Page<>(tenantPage.getCurrent(), tenantPage.getSize(), tenantPage.getTotal());
resultPage.setRecords(responses);
return resultPage;
}
/**
* Tenant 实体转换为 TenantResponse带教师和学生统计
*/
private TenantResponse toResponseWithStats(Tenant tenant) {
// 查询教师数量
Long teacherCount = teacherMapper.selectCount(
new LambdaQueryWrapper<Teacher>()
.eq(Teacher::getTenantId, tenant.getId())
);
// 查询学生数量
Long studentCount = studentMapper.selectCount(
new LambdaQueryWrapper<Student>()
.eq(Student::getTenantId, tenant.getId())
);
return TenantResponse.builder()
.id(tenant.getId())
.name(tenant.getName())
.code(tenant.getCode())
.username(tenant.getUsername())
.contactName(tenant.getContactName())
.contactPhone(tenant.getContactPhone())
.contactEmail(tenant.getContactEmail())
.address(tenant.getAddress())
.logoUrl(tenant.getLogoUrl())
.status(tenant.getStatus())
.expireAt(tenant.getExpireAt())
.maxStudents(tenant.getMaxStudents())
.maxTeachers(tenant.getMaxTeachers())
.packageType(tenant.getPackageType())
.teacherQuota(tenant.getTeacherQuota())
.studentQuota(tenant.getStudentQuota())
.storageQuota(tenant.getStorageQuota())
.storageUsed(tenant.getStorageUsed())
.teacherCount(teacherCount != null ? teacherCount.intValue() : 0)
.studentCount(studentCount != null ? studentCount.intValue() : 0)
.startDate(tenant.getStartDate())
.expireDate(tenant.getExpireDate())
.createdAt(tenant.getCreatedAt())
.updatedAt(tenant.getUpdatedAt())
.build();
}
}