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