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") createdBy Int @map("created_by")
changesSummary String? @map("changes_summary") // 修改说明 changesSummary String? @map("changes_summary") // 修改说明
usageCount Int @default(0) @map("usage_count") 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") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_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 POST /api/v1/admin/packages/{id}/submit
``` ```
✅ 提交成功,状态变为 `PENDING_REVIEW` ✅ 提交成功,状态变为 `PENDING`
#### 4. 审核通过 #### 4. 审核通过

View File

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

View File

@ -34,7 +34,7 @@ export interface CoursePackage {
gradeLevels?: string; gradeLevels?: string;
/** 课程数量 */ /** 课程数量 */
courseCount?: number; courseCount?: number;
/** 状态DRAFT、PENDING_REVIEW、APPROVED、REJECTED、PUBLISHED、OFFLINE */ /** 状态DRAFT、PENDING、APPROVED、REJECTED、PUBLISHED、OFFLINE */
status?: string; status?: string;
/** 提交时间 */ /** 提交时间 */
submittedAt?: 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); 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'] AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge'] ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col'] ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
@ -25,8 +23,6 @@ declare module 'vue' {
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] 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'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
@ -52,14 +48,11 @@ declare module 'vue' {
ARate: typeof import('ant-design-vue/es')['Rate'] ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space'] ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic'] 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'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch'] ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<template #extra> <template #extra>
<a-space> <a-space>
<a-select v-model:value="filters.status" placeholder="全部状态" style="width: 120px" @change="fetchPackages"> <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-option value="REJECTED">已驳回</a-select-option>
</a-select> </a-select>
<a-button @click="fetchPackages"> <a-button @click="fetchPackages">
@ -39,8 +39,8 @@
</a-tag> </a-tag>
</template> </template>
<template v-else-if="column.key === 'status'"> <template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'PENDING_REVIEW' ? 'processing' : 'error'"> <a-tag :color="record.status === 'PENDING' ? 'processing' : 'error'">
{{ record.status === 'PENDING_REVIEW' ? '待审核' : '已驳回' }} {{ record.status === 'PENDING' ? '待审核' : '已驳回' }}
</a-tag> </a-tag>
</template> </template>
<template v-else-if="column.key === 'submittedAt'"> <template v-else-if="column.key === 'submittedAt'">
@ -48,7 +48,7 @@
</template> </template>
<template v-else-if="column.key === 'actions'"> <template v-else-if="column.key === 'actions'">
<a-space> <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>
<a-button v-if="record.status === 'REJECTED'" size="small" @click="viewRejectReason(record)"> <a-button v-if="record.status === 'REJECTED'" size="small" @click="viewRejectReason(record)">
@ -107,12 +107,15 @@
</a-form> </a-form>
<div class="modal-footer"> <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 @click="closeReviewModal">取消</a-button>
<a-button type="default" danger :loading="reviewing" @click="rejectPackage"> <a-button type="default" danger :loading="reviewing" @click="rejectPackage">
驳回 驳回
</a-button> </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-button>
</a-space> </a-space>
@ -144,7 +147,7 @@ const loadingDetail = ref(false);
const packages = ref<CoursePackage[]>([]); const packages = ref<CoursePackage[]>([]);
const filters = reactive<{ status?: string }>({ const filters = reactive<{ status?: string }>({
status: 'PENDING_REVIEW', status: 'PENDING',
}); });
const pagination = reactive({ const pagination = reactive({
@ -227,7 +230,8 @@ const closeReviewModal = () => {
currentPackage.value = null; currentPackage.value = null;
}; };
const approvePackage = async () => { //
const approveOnly = async () => {
if (!currentPackage.value) return; if (!currentPackage.value) return;
reviewing.value = true; reviewing.value = true;
@ -235,6 +239,28 @@ const approvePackage = async () => {
await reviewPackage(currentPackage.value.id, { await reviewPackage(currentPackage.value.id, {
approved: true, approved: true,
comment: reviewComment.value || '审核通过', 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('审核通过,套餐已发布'); message.success('审核通过,套餐已发布');
closeReviewModal(); 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 lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* 课程管理控制器超管端 * 课程管理控制器超管端
*/ */
@ -33,12 +36,12 @@ public class AdminCourseController {
@PostMapping @PostMapping
@Operation(summary = "创建课程") @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()); log.info("收到课程创建请求name={}, themeId={}, gradeTags={}", request.getName(), request.getThemeId(), request.getGradeTags());
try { try {
Course course = courseService.createSystemCourse(request); Course course = courseService.createSystemCourse(request);
log.info("课程创建成功id={}", course.getId()); log.info("课程创建成功id={}", course.getId());
return Result.success(course); return Result.success(courseService.getCourseByIdWithLessons(course.getId()));
} catch (Exception e) { } catch (Exception e) {
log.error("课程创建失败", e); log.error("课程创建失败", e);
throw e; throw e;
@ -47,8 +50,9 @@ public class AdminCourseController {
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新课程") @Operation(summary = "更新课程")
public Result<Course> updateCourse(@PathVariable Long id, @RequestBody CourseUpdateRequest request) { public Result<CourseResponse> updateCourse(@PathVariable Long id, @RequestBody CourseUpdateRequest request) {
return Result.success(courseService.updateCourse(id, request)); courseService.updateCourse(id, request);
return Result.success(courseService.getCourseByIdWithLessons(id));
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@ -59,7 +63,7 @@ public class AdminCourseController {
@GetMapping @GetMapping
@Operation(summary = "分页查询课程") @Operation(summary = "分页查询课程")
public Result<PageResult<Course>> getCoursePage(CoursePageQueryRequest request) { public Result<PageResult<CourseResponse>> getCoursePage(CoursePageQueryRequest request) {
log.info("查询课程列表pageNum={}, pageSize={}, keyword={}, category={}, status={}, reviewOnly={}", log.info("查询课程列表pageNum={}, pageSize={}, keyword={}, category={}, status={}, reviewOnly={}",
request.getPageNum(), request.getPageSize(), request.getKeyword(), request.getCategory(), request.getStatus(), request.getReviewOnly()); request.getPageNum(), request.getPageSize(), request.getKeyword(), request.getCategory(), request.getStatus(), request.getReviewOnly());
// 页码 // 页码
@ -75,7 +79,13 @@ public class AdminCourseController {
request.getCategory(), request.getCategory(),
request.getStatus(), request.getStatus(),
request.getReviewOnly()); 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()); log.info("课程列表查询结果total={}, list={}", result.getTotal(), result.getList().size());
return Result.success(result); 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.enums.UserRole;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.CourseLessonCreateRequest; 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.CourseLesson;
import com.reading.platform.entity.LessonStep; import com.reading.platform.entity.LessonStep;
import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CourseLessonService;
@ -14,6 +17,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* 课程环节控制器超管端 * 课程环节控制器超管端
@ -28,33 +32,42 @@ public class AdminCourseLessonController {
@GetMapping("/{courseId}/lessons") @GetMapping("/{courseId}/lessons")
@Operation(summary = "获取课程的所有环节") @Operation(summary = "获取课程的所有环节")
public Result<List<CourseLesson>> findAll(@PathVariable Long courseId) { public Result<List<CourseLessonResponse>> findAll(@PathVariable Long courseId) {
return Result.success(courseLessonService.findByCourseId(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}") @GetMapping("/{courseId}/lessons/{id}")
@Operation(summary = "获取课程环节详情") @Operation(summary = "获取课程环节详情")
public Result<CourseLesson> findOne( public Result<CourseLessonResponse> findOne(
@PathVariable Long courseId, @PathVariable Long courseId,
@PathVariable Long id) { @PathVariable Long id) {
return Result.success(courseLessonService.findById(id)); CourseLesson lesson = courseLessonService.findById(id);
return Result.success(toLessonResponse(lesson));
} }
@GetMapping("/{courseId}/lessons/type/{lessonType}") @GetMapping("/{courseId}/lessons/type/{lessonType}")
@Operation(summary = "按类型获取课程环节") @Operation(summary = "按类型获取课程环节")
public Result<CourseLesson> findByType( public Result<CourseLessonResponse> findByType(
@PathVariable Long courseId, @PathVariable Long courseId,
@PathVariable String lessonType) { @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") @PostMapping("/{courseId}/lessons")
@Operation(summary = "创建课程环节") @Operation(summary = "创建课程环节")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<CourseLesson> create( public Result<CourseLessonResponse> create(
@PathVariable Long courseId, @PathVariable Long courseId,
@Valid @RequestBody CourseLessonCreateRequest request) { @Valid @RequestBody CourseLessonCreateRequest request) {
return Result.success(courseLessonService.create( CourseLesson lesson = courseLessonService.create(
courseId, courseId,
request.getLessonType(), request.getLessonType(),
request.getName(), request.getName(),
@ -72,17 +85,18 @@ public class AdminCourseLessonController {
request.getReflection(), request.getReflection(),
request.getAssessmentData(), request.getAssessmentData(),
request.getUseTemplate() request.getUseTemplate()
)); );
return Result.success(toLessonResponse(lesson));
} }
@PutMapping("/{courseId}/lessons/{id}") @PutMapping("/{courseId}/lessons/{id}")
@Operation(summary = "更新课程环节") @Operation(summary = "更新课程环节")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<CourseLesson> update( public Result<CourseLessonResponse> update(
@PathVariable Long courseId, @PathVariable Long courseId,
@PathVariable Long id, @PathVariable Long id,
@RequestBody CourseLessonCreateRequest request) { @RequestBody CourseLessonCreateRequest request) {
return Result.success(courseLessonService.update( CourseLesson lesson = courseLessonService.update(
id, id,
request.getName(), request.getName(),
request.getDescription(), request.getDescription(),
@ -99,7 +113,8 @@ public class AdminCourseLessonController {
request.getReflection(), request.getReflection(),
request.getAssessmentData(), request.getAssessmentData(),
request.getUseTemplate() request.getUseTemplate()
)); );
return Result.success(toLessonResponse(lesson));
} }
@DeleteMapping("/{courseId}/lessons/{id}") @DeleteMapping("/{courseId}/lessons/{id}")
@ -126,44 +141,50 @@ public class AdminCourseLessonController {
@GetMapping("/{courseId}/lessons/{lessonId}/steps") @GetMapping("/{courseId}/lessons/{lessonId}/steps")
@Operation(summary = "获取课时的教学环节") @Operation(summary = "获取课时的教学环节")
public Result<List<LessonStep>> findSteps( public Result<List<LessonStepResponse>> findSteps(
@PathVariable Long courseId, @PathVariable Long courseId,
@PathVariable Long lessonId) { @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") @PostMapping("/{courseId}/lessons/{lessonId}/steps")
@Operation(summary = "创建教学环节") @Operation(summary = "创建教学环节")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<LessonStep> createStep( public Result<LessonStepResponse> createStep(
@PathVariable Long courseId, @PathVariable Long courseId,
@PathVariable Long lessonId, @PathVariable Long lessonId,
@RequestBody StepCreateRequest request) { @Valid @RequestBody LessonStepCreateRequest request) {
return Result.success(courseLessonService.createStep( LessonStep step = courseLessonService.createStep(
lessonId, lessonId,
request.getName(), request.getName(),
request.getContent(), request.getContent(),
request.getDuration(), request.getDuration(),
request.getObjective(), request.getObjective(),
request.getResourceIds() request.getResourceIds()
)); );
return Result.success(toStepResponse(step));
} }
@PutMapping("/{courseId}/lessons/steps/{stepId}") @PutMapping("/{courseId}/lessons/steps/{stepId}")
@Operation(summary = "更新教学环节") @Operation(summary = "更新教学环节")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<LessonStep> updateStep( public Result<LessonStepResponse> updateStep(
@PathVariable Long courseId, @PathVariable Long courseId,
@PathVariable Long stepId, @PathVariable Long stepId,
@RequestBody StepCreateRequest request) { @Valid @RequestBody LessonStepCreateRequest request) {
return Result.success(courseLessonService.updateStep( LessonStep step = courseLessonService.updateStep(
stepId, stepId,
request.getName(), request.getName(),
request.getContent(), request.getContent(),
request.getDuration(), request.getDuration(),
request.getObjective(), request.getObjective(),
request.getResourceIds() request.getResourceIds()
)); );
return Result.success(toStepResponse(step));
} }
@DeleteMapping("/{courseId}/lessons/steps/{stepId}") @DeleteMapping("/{courseId}/lessons/steps/{stepId}")
@ -188,24 +209,56 @@ public class AdminCourseLessonController {
} }
/** /**
* 教学环节创建请求 * CourseLesson 实体转换为 CourseLessonResponse
*/ */
public static class StepCreateRequest { private CourseLessonResponse toLessonResponse(CourseLesson lesson) {
private String name; // 获取教学环节
private String content; List<LessonStep> steps = courseLessonService.findSteps(lesson.getId());
private Integer duration; List<LessonStepResponse> stepResponses = steps.stream()
private String objective; .map(this::toStepResponse)
private List<Long> resourceIds; .collect(Collectors.toList());
public String getName() { return name; } return CourseLessonResponse.builder()
public void setName(String name) { this.name = name; } .id(lesson.getId())
public String getContent() { return content; } .courseId(lesson.getCourseId())
public void setContent(String content) { this.content = content; } .lessonType(lesson.getLessonType())
public Integer getDuration() { return duration; } .name(lesson.getName())
public void setDuration(Integer duration) { this.duration = duration; } .description(lesson.getDescription())
public String getObjective() { return objective; } .duration(lesson.getDuration())
public void setObjective(String objective) { this.objective = objective; } .videoPath(lesson.getVideoPath())
public List<Long> getResourceIds() { return resourceIds; } .videoName(lesson.getVideoName())
public void setResourceIds(List<Long> resourceIds) { this.resourceIds = resourceIds; } .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; 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.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole; import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.PackageCreateRequest; 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.dto.response.CoursePackageResponse;
import com.reading.platform.entity.CoursePackage; import com.reading.platform.entity.CoursePackage;
import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.CoursePackageService;
@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* 课程套餐控制器超管端 * 课程套餐控制器超管端
@ -49,24 +51,25 @@ public class AdminPackageController {
@PostMapping @PostMapping
@Operation(summary = "创建套餐") @Operation(summary = "创建套餐")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<CoursePackage> create(@Valid @RequestBody PackageCreateRequest request) { public Result<CoursePackageResponse> create(@Valid @RequestBody PackageCreateRequest request) {
return Result.success(packageService.createPackage( CoursePackage pkg = packageService.createPackage(
request.getName(), request.getName(),
request.getDescription(), request.getDescription(),
request.getPrice(), request.getPrice(),
request.getDiscountPrice(), request.getDiscountPrice(),
request.getDiscountType(), request.getDiscountType(),
request.getGradeLevels() request.getGradeLevels()
)); );
return Result.success(packageService.findOnePackage(pkg.getId()));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新套餐") @Operation(summary = "更新套餐")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<CoursePackage> update( public Result<CoursePackageResponse> update(
@PathVariable Long id, @PathVariable Long id,
@RequestBody PackageCreateRequest request) { @RequestBody PackageCreateRequest request) {
return Result.success(packageService.updatePackage( packageService.updatePackage(
id, id,
request.getName(), request.getName(),
request.getDescription(), request.getDescription(),
@ -74,7 +77,8 @@ public class AdminPackageController {
request.getDiscountPrice(), request.getDiscountPrice(),
request.getDiscountType(), request.getDiscountType(),
request.getGradeLevels() request.getGradeLevels()
)); );
return Result.success(packageService.findOnePackage(id));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@ -108,8 +112,14 @@ public class AdminPackageController {
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<Void> review( public Result<Void> review(
@PathVariable Long id, @PathVariable Long id,
@RequestBody ReviewRequest request) { @Valid @RequestBody PackageReviewRequest request) {
packageService.reviewPackage(id, SecurityUtils.getCurrentUserId(), request.getApproved(), request.getComment()); packageService.reviewPackage(
id,
SecurityUtils.getCurrentUserId(),
request.getApproved(),
request.getComment(),
request.getPublish()
);
return Result.success(); return Result.success();
} }
@ -131,8 +141,12 @@ public class AdminPackageController {
@GetMapping("/all") @GetMapping("/all")
@Operation(summary = "查询所有已发布的套餐列表") @Operation(summary = "查询所有已发布的套餐列表")
public Result<List<CoursePackage>> getPublishedPackages() { public Result<List<CoursePackageResponse>> getPublishedPackages() {
return Result.success(packageService.findPublishedPackages()); 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") @PostMapping("/{id}/grant")
@ -140,7 +154,7 @@ public class AdminPackageController {
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<Void> grantToTenant( public Result<Void> grantToTenant(
@PathVariable Long id, @PathVariable Long id,
@RequestBody GrantRequest request) { @Valid @RequestBody PackageGrantRequest request) {
LocalDate endDate = LocalDate.parse(request.getEndDate(), DateTimeFormatter.ISO_DATE); LocalDate endDate = LocalDate.parse(request.getEndDate(), DateTimeFormatter.ISO_DATE);
packageService.renewTenantPackage( packageService.renewTenantPackage(
request.getTenantId(), request.getTenantId(),
@ -150,33 +164,4 @@ public class AdminPackageController {
); );
return Result.success(); 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.enums.UserRole;
import com.reading.platform.common.response.PageResult; import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result; 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.ResourceItem;
import com.reading.platform.entity.ResourceLibrary; import com.reading.platform.entity.ResourceLibrary;
import com.reading.platform.service.ResourceLibraryService; import com.reading.platform.service.ResourceLibraryService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
/** /**
* 资源库控制器超管端 * 资源库控制器超管端
@ -31,44 +39,52 @@ public class AdminResourceController {
@GetMapping("/libraries") @GetMapping("/libraries")
@Operation(summary = "分页查询资源库") @Operation(summary = "分页查询资源库")
public Result<PageResult<ResourceLibrary>> findAllLibraries( public Result<PageResult<ResourceLibraryResponse>> findAllLibraries(
@RequestParam(required = false) String libraryType, @RequestParam(required = false) String libraryType,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) { @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
Page<ResourceLibrary> page = resourceLibraryService.findAllLibraries(libraryType, keyword, pageNum, 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}") @GetMapping("/libraries/{id}")
@Operation(summary = "查询资源库详情") @Operation(summary = "查询资源库详情")
public Result<ResourceLibrary> findLibrary(@PathVariable String id) { public Result<ResourceLibraryResponse> findLibrary(@PathVariable String id) {
return Result.success(resourceLibraryService.findLibraryById(id)); ResourceLibrary library = resourceLibraryService.findLibraryById(id);
return Result.success(toLibraryResponse(library));
} }
@PostMapping("/libraries") @PostMapping("/libraries")
@Operation(summary = "创建资源库") @Operation(summary = "创建资源库")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<ResourceLibrary> createLibrary(@RequestBody LibraryCreateRequest request) { public Result<ResourceLibraryResponse> createLibrary(@Valid @RequestBody ResourceLibraryCreateRequest request) {
return Result.success(resourceLibraryService.createLibrary( ResourceLibrary library = resourceLibraryService.createLibrary(
request.getName(), request.getName(),
request.getType(), request.getType(),
request.getDescription(), request.getDescription(),
request.getTenantId() request.getTenantId()
)); );
return Result.success(toLibraryResponse(library));
} }
@PutMapping("/libraries/{id}") @PutMapping("/libraries/{id}")
@Operation(summary = "更新资源库") @Operation(summary = "更新资源库")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<ResourceLibrary> updateLibrary( public Result<ResourceLibraryResponse> updateLibrary(
@PathVariable String id, @PathVariable String id,
@RequestBody LibraryUpdateRequest request) { @Valid @RequestBody ResourceLibraryUpdateRequest request) {
return Result.success(resourceLibraryService.updateLibrary( ResourceLibrary library = resourceLibraryService.updateLibrary(
id, id,
request.getName(), request.getName(),
request.getDescription() request.getDescription()
)); );
return Result.success(toLibraryResponse(library));
} }
@DeleteMapping("/libraries/{id}") @DeleteMapping("/libraries/{id}")
@ -83,27 +99,33 @@ public class AdminResourceController {
@GetMapping("/items") @GetMapping("/items")
@Operation(summary = "分页查询资源项目") @Operation(summary = "分页查询资源项目")
public Result<PageResult<ResourceItem>> findAllItems( public Result<PageResult<ResourceItemResponse>> findAllItems(
@RequestParam(required = false) String libraryId, @RequestParam(required = false) String libraryId,
@RequestParam(required = false) String fileType, @RequestParam(required = false) String fileType,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "20") Integer pageSize) { @RequestParam(required = false, defaultValue = "20") Integer pageSize) {
Page<ResourceItem> page = resourceLibraryService.findAllItems(libraryId, fileType, keyword, pageNum, 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}") @GetMapping("/items/{id}")
@Operation(summary = "查询资源项目详情") @Operation(summary = "查询资源项目详情")
public Result<ResourceItem> findItem(@PathVariable String id) { public Result<ResourceItemResponse> findItem(@PathVariable String id) {
return Result.success(resourceLibraryService.findItemById(id)); ResourceItem item = resourceLibraryService.findItemById(id);
return Result.success(toItemResponse(item));
} }
@PostMapping("/items") @PostMapping("/items")
@Operation(summary = "创建资源项目") @Operation(summary = "创建资源项目")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<ResourceItem> createItem(@RequestBody ItemCreateRequest request) { public Result<ResourceItemResponse> createItem(@Valid @RequestBody ResourceItemCreateRequest request) {
return Result.success(resourceLibraryService.createItem( ResourceItem item = resourceLibraryService.createItem(
request.getLibraryId(), request.getLibraryId(),
request.getTitle(), request.getTitle(),
request.getFileType(), request.getFileType(),
@ -112,21 +134,23 @@ public class AdminResourceController {
request.getDescription(), request.getDescription(),
request.getTags(), request.getTags(),
request.getTenantId() request.getTenantId()
)); );
return Result.success(toItemResponse(item));
} }
@PutMapping("/items/{id}") @PutMapping("/items/{id}")
@Operation(summary = "更新资源项目") @Operation(summary = "更新资源项目")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<ResourceItem> updateItem( public Result<ResourceItemResponse> updateItem(
@PathVariable String id, @PathVariable String id,
@RequestBody ItemUpdateRequest request) { @Valid @RequestBody ResourceItemUpdateRequest request) {
return Result.success(resourceLibraryService.updateItem( ResourceItem item = resourceLibraryService.updateItem(
id, id,
request.getTitle(), request.getTitle(),
request.getDescription(), request.getDescription(),
request.getTags() request.getTags()
)); );
return Result.success(toItemResponse(item));
} }
@DeleteMapping("/items/{id}") @DeleteMapping("/items/{id}")
@ -154,81 +178,35 @@ public class AdminResourceController {
} }
/** /**
* 资源库创建请求 * ResourceLibrary 实体转换为 ResourceLibraryResponse
*/ */
public static class LibraryCreateRequest { private ResourceLibraryResponse toLibraryResponse(ResourceLibrary library) {
private String name; return ResourceLibraryResponse.builder()
private String type; .id(library.getId())
private String description; .tenantId(library.getTenantId())
private String tenantId; .name(library.getName())
.description(library.getDescription())
public String getName() { return name; } .type(library.getLibraryType())
public void setName(String name) { this.name = name; } .createdBy(library.getCreatedBy() != null ? String.valueOf(library.getCreatedBy()) : null)
public String getType() { return type; } .createdAt(library.getCreatedAt())
public void setType(String type) { this.type = type; } .updatedAt(library.getUpdatedAt())
public String getDescription() { return description; } .build();
public void setDescription(String description) { this.description = description; }
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
} }
/** /**
* 资源库更新请求 * ResourceItem 实体转换为 ResourceItemResponse
*/ */
public static class LibraryUpdateRequest { private ResourceItemResponse toItemResponse(ResourceItem item) {
private String name; return ResourceItemResponse.builder()
private String description; .id(item.getId())
.libraryId(item.getLibraryId())
public String getName() { return name; } .tenantId(item.getTenantId())
public void setName(String name) { this.name = name; } .type(item.getFileType())
public String getDescription() { return description; } .name(item.getTitle())
public void setDescription(String description) { this.description = description; } .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.RecentActivityItemResponse;
import com.reading.platform.dto.response.StatsResponse; import com.reading.platform.dto.response.StatsResponse;
import com.reading.platform.dto.response.StatsTrendResponse; 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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -32,55 +32,36 @@ import java.util.List;
@Tag(name = "超管端 - 统计管理") @Tag(name = "超管端 - 统计管理")
public class AdminStatsController { public class AdminStatsController {
private final StatsService statsService;
@GetMapping @GetMapping
@Operation(summary = "获取统计数据") @Operation(summary = "获取统计数据")
public Result<StatsResponse> getStats() { public Result<StatsResponse> getStats() {
// TODO: 实现统计数据查询 return Result.success(statsService.getStats());
return Result.success(StatsResponse.builder()
.totalTenants(0L)
.activeTenants(0L)
.totalTeachers(0L)
.totalStudents(0L)
.totalCourses(0L)
.totalLessons(0L)
.build());
} }
@GetMapping("/trend") @GetMapping("/trend")
@Operation(summary = "获取趋势数据") @Operation(summary = "获取趋势数据")
public Result<StatsTrendResponse> getTrendData() { public Result<StatsTrendResponse> getTrendData() {
// TODO: 实现趋势数据查询 return Result.success(statsService.getTrendData());
return Result.success(StatsTrendResponse.builder()
.dates(new ArrayList<>())
.newStudents(new ArrayList<>())
.newTeachers(new ArrayList<>())
.newCourses(new ArrayList<>())
.build());
} }
@GetMapping("/tenants/active") @GetMapping("/tenants/active")
@Operation(summary = "获取活跃租户") @Operation(summary = "获取活跃租户")
public Result<List<ActiveTenantItemResponse>> getActiveTenants(@ModelAttribute ActiveTenantsQueryRequest request) { public Result<List<ActiveTenantItemResponse>> getActiveTenants(@ModelAttribute ActiveTenantsQueryRequest request) {
// 返回数量限制 return Result.success(statsService.getActiveTenants(request));
// TODO: 实现活跃租户查询
return Result.success(new ArrayList<>());
} }
@GetMapping("/courses/popular") @GetMapping("/courses/popular")
@Operation(summary = "获取热门课程") @Operation(summary = "获取热门课程")
public Result<List<PopularCourseItemResponse>> getPopularCourses(@ModelAttribute PopularCoursesQueryRequest request) { public Result<List<PopularCourseItemResponse>> getPopularCourses(@ModelAttribute PopularCoursesQueryRequest request) {
// 返回数量限制 return Result.success(statsService.getPopularCourses(request));
// TODO: 实现热门课程查询
return Result.success(new ArrayList<>());
} }
@GetMapping("/activities") @GetMapping("/activities")
@Operation(summary = "获取最近活动") @Operation(summary = "获取最近活动")
public Result<List<RecentActivityItemResponse>> getRecentActivities(@ModelAttribute RecentActivitiesQueryRequest request) { public Result<List<RecentActivityItemResponse>> getRecentActivities(@ModelAttribute RecentActivitiesQueryRequest request) {
// 返回数量限制 return Result.success(statsService.getRecentActivities(request));
// TODO: 实现最近活动查询
return Result.success(new ArrayList<>());
} }
}
}

View File

@ -1,20 +1,14 @@
package com.reading.platform.controller.admin; package com.reading.platform.controller.admin;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.annotation.RequireRole; import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole; 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.PageResult;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.TenantCreateRequest; import com.reading.platform.dto.request.TenantCreateRequest;
import com.reading.platform.dto.request.TenantUpdateRequest; import com.reading.platform.dto.request.TenantUpdateRequest;
import com.reading.platform.dto.response.TenantResponse; 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.entity.Tenant;
import com.reading.platform.mapper.StudentMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.service.TenantService; import com.reading.platform.service.TenantService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Tag(name = "超管端 - 租户管理", description = "Tenant Management APIs for Admin") @Tag(name = "超管端 - 租户管理", description = "Tenant Management APIs for Admin")
@RestController @RestController
@ -34,29 +29,26 @@ import java.util.Map;
public class AdminTenantController { public class AdminTenantController {
private final TenantService tenantService; private final TenantService tenantService;
private final TenantMapper tenantMapper;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
@Operation(summary = "Create tenant") @Operation(summary = "Create tenant")
@PostMapping @PostMapping
public Result<TenantResponse> createTenant(@Valid @RequestBody TenantCreateRequest request) { public Result<TenantResponse> createTenant(@Valid @RequestBody TenantCreateRequest request) {
Tenant tenant = tenantService.createTenant(request); Tenant tenant = tenantService.createTenant(request);
return Result.success(tenantMapper.toVO(tenant)); return Result.success(toResponse(tenant));
} }
@Operation(summary = "Update tenant") @Operation(summary = "Update tenant")
@PutMapping("/{id}") @PutMapping("/{id}")
public Result<TenantResponse> updateTenant(@PathVariable Long id, @RequestBody TenantUpdateRequest request) { public Result<TenantResponse> updateTenant(@PathVariable Long id, @RequestBody TenantUpdateRequest request) {
Tenant tenant = tenantService.updateTenant(id, request); Tenant tenant = tenantService.updateTenant(id, request);
return Result.success(tenantMapper.toVO(tenant)); return Result.success(toResponse(tenant));
} }
@Operation(summary = "Get tenant by ID") @Operation(summary = "Get tenant by ID")
@GetMapping("/{id}") @GetMapping("/{id}")
public Result<TenantResponse> getTenant(@PathVariable Long id) { public Result<TenantResponse> getTenant(@PathVariable Long id) {
Tenant tenant = tenantService.getTenantById(id); Tenant tenant = tenantService.getTenantById(id);
return Result.success(tenantMapper.toVO(tenant)); return Result.success(toResponse(tenant));
} }
@Operation(summary = "Get tenant page") @Operation(summary = "Get tenant page")
@ -66,27 +58,15 @@ public class AdminTenantController {
@RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {
Page<Tenant> pageResult = tenantService.getTenantPage(pageNum, pageSize, keyword, status); // 调用 Service 层方法获取带统计数据的租户分页
List<TenantResponse> voList = tenantMapper.toVO(pageResult.getRecords()); Page<TenantResponse> pageResult = tenantService.getTenantPageWithStats(pageNum, pageSize, keyword, status);
// 填充教师数量和学生数量 return Result.success(PageResult.of(
for (TenantResponse vo : voList) { pageResult.getRecords(),
if (vo.getId() != null) { pageResult.getTotal(),
Long teacherCount = teacherMapper.selectCount( pageResult.getCurrent(),
new LambdaQueryWrapper<Teacher>() pageResult.getSize()
.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()));
} }
@Operation(summary = "Delete tenant") @Operation(summary = "Delete tenant")
@ -100,7 +80,10 @@ public class AdminTenantController {
@GetMapping("/active") @GetMapping("/active")
public Result<List<TenantResponse>> getAllActiveTenants() { public Result<List<TenantResponse>> getAllActiveTenants() {
List<Tenant> tenants = tenantService.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 = "获取租户统计信息") @Operation(summary = "获取租户统计信息")
@ -118,7 +101,7 @@ public class AdminTenantController {
public Result<TenantResponse> updateTenantQuota(@PathVariable Long id, @RequestBody Map<String, Object> quota) { public Result<TenantResponse> updateTenantQuota(@PathVariable Long id, @RequestBody Map<String, Object> quota) {
// TODO: 实现更新租户配额逻辑 // TODO: 实现更新租户配额逻辑
Tenant tenant = tenantService.getTenantById(id); Tenant tenant = tenantService.getTenantById(id);
return Result.success(tenantMapper.toVO(tenant)); return Result.success(toResponse(tenant));
} }
@Operation(summary = "更新租户状态") @Operation(summary = "更新租户状态")
@ -126,7 +109,7 @@ public class AdminTenantController {
public Result<TenantResponse> updateTenantStatus(@PathVariable Long id, @RequestBody Map<String, Object> status) { public Result<TenantResponse> updateTenantStatus(@PathVariable Long id, @RequestBody Map<String, Object> status) {
// TODO: 实现更新租户状态逻辑 // TODO: 实现更新租户状态逻辑
Tenant tenant = tenantService.getTenantById(id); Tenant tenant = tenantService.getTenantById(id);
return Result.success(tenantMapper.toVO(tenant)); return Result.success(toResponse(tenant));
} }
@Operation(summary = "重置租户密码") @Operation(summary = "重置租户密码")
@ -136,4 +119,34 @@ public class AdminTenantController {
return Result.success(); 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.enums.UserRole;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.ThemeCreateRequest; import com.reading.platform.dto.request.ThemeCreateRequest;
import com.reading.platform.dto.response.ThemeResponse;
import com.reading.platform.entity.Theme; import com.reading.platform.entity.Theme;
import com.reading.platform.service.ThemeService; import com.reading.platform.service.ThemeService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -13,6 +14,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* 主题字典控制器超管端 * 主题字典控制器超管端
@ -27,40 +29,47 @@ public class AdminThemeController {
@GetMapping @GetMapping
@Operation(summary = "查询所有主题") @Operation(summary = "查询所有主题")
public Result<List<Theme>> findAll() { public Result<List<ThemeResponse>> findAll() {
return Result.success(themeService.findAll()); List<Theme> themes = themeService.findAll();
List<ThemeResponse> responses = themes.stream()
.map(this::toResponse)
.collect(Collectors.toList());
return Result.success(responses);
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "查询主题详情") @Operation(summary = "查询主题详情")
public Result<Theme> findOne(@PathVariable Long id) { public Result<ThemeResponse> findOne(@PathVariable Long id) {
return Result.success(themeService.findById(id)); Theme theme = themeService.findById(id);
return Result.success(toResponse(theme));
} }
@PostMapping @PostMapping
@Operation(summary = "创建主题") @Operation(summary = "创建主题")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<Theme> create(@Valid @RequestBody ThemeCreateRequest request) { public Result<ThemeResponse> create(@Valid @RequestBody ThemeCreateRequest request) {
return Result.success(themeService.create( Theme theme = themeService.create(
request.getName(), request.getName(),
request.getDescription(), request.getDescription(),
request.getSortOrder() request.getSortOrder()
)); );
return Result.success(toResponse(theme));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新主题") @Operation(summary = "更新主题")
@RequireRole(UserRole.ADMIN) @RequireRole(UserRole.ADMIN)
public Result<Theme> update( public Result<ThemeResponse> update(
@PathVariable Long id, @PathVariable Long id,
@RequestBody ThemeCreateRequest request) { @RequestBody ThemeCreateRequest request) {
return Result.success(themeService.update( Theme theme = themeService.update(
id, id,
request.getName(), request.getName(),
request.getDescription(), request.getDescription(),
request.getSortOrder(), request.getSortOrder(),
null null
)); );
return Result.success(toResponse(theme));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@ -78,4 +87,19 @@ public class AdminThemeController {
themeService.reorder(ids); themeService.reorder(ids);
return Result.success(); 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; package com.reading.platform.controller.teacher;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.ClassMapper;
import com.reading.platform.common.mapper.CourseMapper; import com.reading.platform.common.mapper.CourseMapper;
import com.reading.platform.common.mapper.StudentMapper; import com.reading.platform.common.mapper.StudentMapper;
@ -68,7 +69,7 @@ public class TeacherCourseController {
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String category) { @RequestParam(required = false) String category) {
Long tenantId = SecurityUtils.getCurrentTenantId(); 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()); List<CourseResponse> voList = courseMapper.toVO(page.getRecords());
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); 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 { public class ResourceItemResponse {
@Schema(description = "ID") @Schema(description = "ID")
private String id; private Long id;
@Schema(description = "资源库 ID") @Schema(description = "资源库 ID")
private String libraryId; private String libraryId;

View File

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

View File

@ -37,7 +37,7 @@ public class CoursePackage extends BaseEntity {
@Schema(description = "课程数量") @Schema(description = "课程数量")
private Integer courseCount; private Integer courseCount;
@Schema(description = "状态DRAFT、PENDING_REVIEW、APPROVED、REJECTED、PUBLISHED、OFFLINE") @Schema(description = "状态DRAFT、PENDING、APPROVED、REJECTED、PUBLISHED、OFFLINE")
private String status; private String status;
@Schema(description = "提交时间") @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) @Transactional(rollbackFor = Exception.class)
public void reviewPackage(Long id, Long userId, Boolean approved, String comment) { public void reviewPackage(Long id, Long userId, Boolean approved, String comment, Boolean publish) {
log.info("审核套餐id={}, userId={}, approved={}", id, userId, approved); log.info("审核套餐id={}, userId={}, approved={}, publish={}", id, userId, approved, publish);
CoursePackage pkg = packageMapper.selectById(id); CoursePackage pkg = packageMapper.selectById(id);
if (pkg == null) { if (pkg == null) {
log.warn("套餐不存在id={}", id); log.warn("套餐不存在id={}", id);
@ -315,12 +320,27 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
throw new BusinessException("只有待审核状态的套餐可以审核"); 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.setReviewedAt(LocalDateTime.now());
pkg.setReviewedBy(userId); pkg.setReviewedBy(userId);
pkg.setReviewComment(comment); pkg.setReviewComment(comment);
packageMapper.updateById(pkg); 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.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.dto.request.TenantCreateRequest; import com.reading.platform.dto.request.TenantCreateRequest;
import com.reading.platform.dto.request.TenantUpdateRequest; import com.reading.platform.dto.request.TenantUpdateRequest;
import com.reading.platform.dto.response.TenantResponse;
import com.reading.platform.entity.Tenant; import com.reading.platform.entity.Tenant;
import java.util.List; 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<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.CourseStatus;
import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.common.enums.TenantPackageStatus; 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.common.exception.BusinessException;
import com.reading.platform.dto.request.CourseCreateRequest; import com.reading.platform.dto.request.CourseCreateRequest;
import com.reading.platform.dto.request.CourseUpdateRequest; import com.reading.platform.dto.request.CourseUpdateRequest;
@ -60,7 +61,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setDurationMinutes(request.getDurationMinutes()); course.setDurationMinutes(request.getDurationMinutes());
course.setObjectives(request.getObjectives()); course.setObjectives(request.getObjectives());
course.setStatus(CourseStatus.DRAFT.getCode()); course.setStatus(CourseStatus.DRAFT.getCode());
course.setIsSystem(0); course.setIsSystem(YesNo.NO.getCode());
// Course Package Fields // Course Package Fields
course.setCoreContent(request.getCoreContent()); course.setCoreContent(request.getCoreContent());
@ -90,10 +91,10 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setAssessmentData(nullIfEmptyJson(request.getAssessmentData())); course.setAssessmentData(nullIfEmptyJson(request.getAssessmentData()));
course.setGradeTags(nullIfEmptyJson(request.getGradeTags())); course.setGradeTags(nullIfEmptyJson(request.getGradeTags()));
course.setDomainTags(nullIfEmptyJson(request.getDomainTags())); 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.setVersion("1.0");
course.setIsLatest(1); course.setIsLatest(YesNo.YES.getCode());
course.setUsageCount(0); course.setUsageCount(0);
course.setTeacherCount(0); course.setTeacherCount(0);
@ -121,7 +122,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setDurationMinutes(request.getDurationMinutes()); course.setDurationMinutes(request.getDurationMinutes());
course.setObjectives(request.getObjectives()); course.setObjectives(request.getObjectives());
course.setStatus(CourseStatus.DRAFT.getCode()); course.setStatus(CourseStatus.DRAFT.getCode());
course.setIsSystem(1); course.setIsSystem(YesNo.YES.getCode());
// Course Package Fields // Course Package Fields
course.setCoreContent(request.getCoreContent()); course.setCoreContent(request.getCoreContent());
@ -151,10 +152,10 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setAssessmentData(nullIfEmptyJson(request.getAssessmentData())); course.setAssessmentData(nullIfEmptyJson(request.getAssessmentData()));
course.setGradeTags(nullIfEmptyJson(request.getGradeTags())); course.setGradeTags(nullIfEmptyJson(request.getGradeTags()));
course.setDomainTags(nullIfEmptyJson(request.getDomainTags())); 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.setVersion("1.0");
course.setIsLatest(1); course.setIsLatest(YesNo.YES.getCode());
course.setUsageCount(0); course.setUsageCount(0);
course.setTeacherCount(0); course.setTeacherCount(0);
@ -287,7 +288,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
course.setDomainTags(nullIfEmptyJson(request.getDomainTags())); course.setDomainTags(nullIfEmptyJson(request.getDomainTags()));
} }
if (request.getHasCollectiveLesson() != null) { if (request.getHasCollectiveLesson() != null) {
course.setHasCollectiveLesson(request.getHasCollectiveLesson() ? 1 : 0); course.setHasCollectiveLesson(YesNo.fromBoolean(request.getHasCollectiveLesson()).getCode());
} }
courseMapper.updateById(course); courseMapper.updateById(course);
@ -346,7 +347,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
wrapper.and(w -> w wrapper.and(w -> w
.eq(Course::getTenantId, tenantId) .eq(Course::getTenantId, tenantId)
.or() .or()
.eq(Course::getIsSystem, 1) .eq(Course::getIsSystem, YesNo.YES.getCode())
); );
if (StringUtils.hasText(keyword)) { if (StringUtils.hasText(keyword)) {
@ -374,7 +375,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
// 只过滤系统课程 // 只过滤系统课程
wrapper.eq(Course::getIsSystem, 1); wrapper.eq(Course::getIsSystem, YesNo.YES.getCode());
// 审核管理页仅过滤待审核和已驳回排除已通过/已发布 // 审核管理页仅过滤待审核和已驳回排除已通过/已发布
if (reviewOnly) { if (reviewOnly) {
@ -453,7 +454,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
.and(w -> w .and(w -> w
.eq(Course::getTenantId, tenantId) .eq(Course::getTenantId, tenantId)
.or() .or()
.eq(Course::getIsSystem, 1) .eq(Course::getIsSystem, YesNo.YES.getCode())
) )
.eq(Course::getStatus, CourseStatus.PUBLISHED.getCode()) .eq(Course::getStatus, CourseStatus.PUBLISHED.getCode())
.orderByAsc(Course::getName) .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.common.exception.BusinessException;
import com.reading.platform.dto.request.TenantCreateRequest; import com.reading.platform.dto.request.TenantCreateRequest;
import com.reading.platform.dto.request.TenantUpdateRequest; 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.CoursePackage;
import com.reading.platform.entity.Student;
import com.reading.platform.entity.Teacher;
import com.reading.platform.entity.Tenant; import com.reading.platform.entity.Tenant;
import com.reading.platform.entity.TenantPackage; 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.TenantMapper;
import com.reading.platform.mapper.TenantPackageMapper; import com.reading.platform.mapper.TenantPackageMapper;
import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.CoursePackageService;
@ -22,6 +27,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; 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 TenantMapper tenantMapper;
private final TenantPackageMapper tenantPackageMapper; private final TenantPackageMapper tenantPackageMapper;
private final CoursePackageService coursePackageService; private final CoursePackageService coursePackageService;
private final TeacherMapper teacherMapper;
private final StudentMapper studentMapper;
@Override @Override
@Transactional @Transactional
@ -240,4 +248,67 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
return tenants; 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();
}
} }