Compare commits

...

2 Commits

Author SHA1 Message Date
lesingle
01897a7ecc Merge branch 'main' of http://8.148.151.56:3000/tonytech/kindergarten_java 2026-03-03 14:50:42 +08:00
lesingle
18170609d9 fix: 修复前端API路径和后端课程管理接口
- 前端 course.ts: /courses → /admin/courses (匹配Java后端路径)
- 路由守卫: 修复token存在但role缺失时的无限循环404问题
- AdminCourseController: 新增审核相关接口 (submit/withdraw/approve/reject/unpublish/republish/direct-publish)
- AdminCourseController: 课程列表支持status过滤,显示所有状态课程
- CourseService/Impl: 新增提交审核、审批、拒绝等方法

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:49:16 +08:00
5 changed files with 165 additions and 28 deletions

View File

@ -108,7 +108,7 @@ export function getCourses(params: CourseQueryParams): Promise<{
page: number; page: number;
pageSize: number; pageSize: number;
}> { }> {
return http.get('/courses', { params }); return http.get('/admin/courses', { params });
} }
// 获取审核列表 // 获取审核列表
@ -118,82 +118,82 @@ export function getReviewList(params: CourseQueryParams): Promise<{
page: number; page: number;
pageSize: number; pageSize: number;
}> { }> {
return http.get('/courses/review', { params }); return http.get('/admin/courses/review', { params });
} }
// 获取课程包详情 // 获取课程包详情
export function getCourse(id: number): Promise<any> { export function getCourse(id: number): Promise<any> {
return http.get(`/courses/${id}`); return http.get(`/admin/courses/${id}`);
} }
// 创建课程包 // 创建课程包
export function createCourse(data: any): Promise<any> { export function createCourse(data: any): Promise<any> {
return http.post('/courses', data); return http.post('/admin/courses', data);
} }
// 更新课程包 // 更新课程包
export function updateCourse(id: number, data: any): Promise<any> { export function updateCourse(id: number, data: any): Promise<any> {
return http.put(`/courses/${id}`, data); return http.put(`/admin/courses/${id}`, data);
} }
// 删除课程包 // 删除课程包
export function deleteCourse(id: number): Promise<any> { export function deleteCourse(id: number): Promise<any> {
return http.delete(`/courses/${id}`); return http.delete(`/admin/courses/${id}`);
} }
// 验证课程完整性 // 验证课程完整性
export function validateCourse(id: number): Promise<ValidationResult> { export function validateCourse(id: number): Promise<ValidationResult> {
return http.get(`/courses/${id}/validate`); return http.get(`/admin/courses/${id}/validate`);
} }
// 提交审核 // 提交审核
export function submitCourse(id: number, copyrightConfirmed: boolean): Promise<any> { export function submitCourse(id: number, copyrightConfirmed: boolean): Promise<any> {
return http.post(`/courses/${id}/submit`, { copyrightConfirmed }); return http.post(`/admin/courses/${id}/submit`, { copyrightConfirmed });
} }
// 撤销审核 // 撤销审核
export function withdrawCourse(id: number): Promise<any> { export function withdrawCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/withdraw`); return http.post(`/admin/courses/${id}/withdraw`);
} }
// 审核通过 // 审核通过
export function approveCourse(id: number, data: { checklist?: any; comment?: string }): Promise<any> { export function approveCourse(id: number, data: { checklist?: any; comment?: string }): Promise<any> {
return http.post(`/courses/${id}/approve`, data); return http.post(`/admin/courses/${id}/approve`, data);
} }
// 审核驳回 // 审核驳回
export function rejectCourse(id: number, data: { checklist?: any; comment: string }): Promise<any> { export function rejectCourse(id: number, data: { checklist?: any; comment: string }): Promise<any> {
return http.post(`/courses/${id}/reject`, data); return http.post(`/admin/courses/${id}/reject`, data);
} }
// 直接发布(超级管理员) // 直接发布(超级管理员)
export function directPublishCourse(id: number, skipValidation?: boolean): Promise<any> { export function directPublishCourse(id: number, skipValidation?: boolean): Promise<any> {
return http.post(`/courses/${id}/direct-publish`, { skipValidation }); return http.post(`/admin/courses/${id}/direct-publish`, { skipValidation });
} }
// 发布课程包兼容旧API // 发布课程包兼容旧API
export function publishCourse(id: number): Promise<any> { export function publishCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/publish`); return http.post(`/admin/courses/${id}/publish`);
} }
// 下架课程包 // 下架课程包
export function unpublishCourse(id: number): Promise<any> { export function unpublishCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/unpublish`); return http.post(`/admin/courses/${id}/unpublish`);
} }
// 重新发布 // 重新发布
export function republishCourse(id: number): Promise<any> { export function republishCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/republish`); return http.post(`/admin/courses/${id}/republish`);
} }
// 获取课程包统计数据 // 获取课程包统计数据
export function getCourseStats(id: number): Promise<any> { export function getCourseStats(id: number): Promise<any> {
return http.get(`/courses/${id}/stats`); return http.get(`/admin/courses/${id}/stats`);
} }
// 获取版本历史 // 获取版本历史
export function getCourseVersions(id: number): Promise<any[]> { export function getCourseVersions(id: number): Promise<any[]> {
return http.get(`/courses/${id}/versions`); return http.get(`/admin/courses/${id}/versions`);
} }
// 课程状态映射 // 课程状态映射

View File

@ -447,21 +447,31 @@ router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const userRole = localStorage.getItem('role'); const userRole = localStorage.getItem('role');
// 检测无效 token 或角色缺失,清除旧数据强制重新登录
const validRoles = ['admin', 'school', 'teacher', 'parent'];
const isValidToken = token && token.split('.').length === 3;
const isValidRole = userRole && validRoles.includes(userRole);
if ((token && !isValidToken) || (userRole && !isValidRole) || (isValidToken && !userRole)) {
localStorage.clear();
next('/login');
return;
}
// 设置页面标题 // 设置页面标题
if (to.meta.title) { if (to.meta.title) {
document.title = `${to.meta.title} - 幼儿阅读教学服务平台`; document.title = `${to.meta.title} - 幼儿阅读教学服务平台`;
} }
// 需要认证但未登录 // 需要认证但未登录
if (to.meta.requiresAuth && !token) { if (to.meta.requiresAuth && !isValidToken) {
message.warning('请先登录'); message.warning('请先登录');
next('/login'); next('/login');
return; return;
} }
// 已登录用户访问登录页,跳转到对应首页 // 已登录用户访问登录页,跳转到对应首页
if (to.path === '/login' && token) { if (to.path === '/login' && isValidToken) {
const defaultRoute = userRole ? `/${userRole}/dashboard` : '/admin/dashboard'; const defaultRoute = isValidRole ? `/${userRole}/dashboard` : '/admin/dashboard';
next(defaultRoute); next(defaultRoute);
return; return;
} }
@ -469,7 +479,7 @@ router.beforeEach((to, from, next) => {
// 角色权限检查 // 角色权限检查
if (to.meta.role && userRole !== to.meta.role) { if (to.meta.role && userRole !== to.meta.role) {
message.error('没有权限访问该页面'); message.error('没有权限访问该页面');
next(`/${userRole}/dashboard`); next(isValidRole ? `/${userRole}/dashboard` : '/login');
return; return;
} }

View File

@ -45,14 +45,24 @@ public class AdminCourseController {
return Result.success(courseService.getCourseById(id)); return Result.success(courseService.getCourseById(id));
} }
@Operation(summary = "Get system course page") @Operation(summary = "Get system course page (all statuses)")
@GetMapping @GetMapping
public Result<PageResult<Course>> getCoursePage( public Result<PageResult<Course>> getCoursePage(
@RequestParam(value = "page", required = false) Integer pageNum, @RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize, @RequestParam(required = false) Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String category) { @RequestParam(required = false) String category,
Page<Course> page = courseService.getSystemCoursePage(pageNum, pageSize, keyword, category); @RequestParam(required = false) String status) {
Page<Course> page = courseService.getSystemCoursePage(pageNum, pageSize, keyword, category, status);
return Result.success(PageResult.of(page));
}
@Operation(summary = "Get courses pending review")
@GetMapping("/review")
public Result<PageResult<Course>> getReviewCoursePage(
@RequestParam(value = "page", required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize) {
Page<Course> page = courseService.getReviewCoursePage(pageNum, pageSize);
return Result.success(PageResult.of(page)); return Result.success(PageResult.of(page));
} }
@ -63,6 +73,38 @@ public class AdminCourseController {
return Result.success(); return Result.success();
} }
@Operation(summary = "Submit course for review")
@PostMapping("/{id}/submit")
public Result<Void> submitCourse(@PathVariable Long id) {
courseService.submitCourse(id);
return Result.success();
}
@Operation(summary = "Withdraw course from review")
@PostMapping("/{id}/withdraw")
public Result<Void> withdrawCourse(@PathVariable Long id) {
courseService.withdrawCourse(id);
return Result.success();
}
@Operation(summary = "Approve course")
@PostMapping("/{id}/approve")
public Result<Void> approveCourse(
@PathVariable Long id,
@RequestParam(required = false) String comment) {
courseService.approveCourse(id, comment);
return Result.success();
}
@Operation(summary = "Reject course")
@PostMapping("/{id}/reject")
public Result<Void> rejectCourse(
@PathVariable Long id,
@RequestParam(required = false) String comment) {
courseService.rejectCourse(id, comment);
return Result.success();
}
@Operation(summary = "Publish course") @Operation(summary = "Publish course")
@PostMapping("/{id}/publish") @PostMapping("/{id}/publish")
public Result<Void> publishCourse(@PathVariable Long id) { public Result<Void> publishCourse(@PathVariable Long id) {
@ -70,6 +112,27 @@ public class AdminCourseController {
return Result.success(); return Result.success();
} }
@Operation(summary = "Direct publish course (skip review)")
@PostMapping("/{id}/direct-publish")
public Result<Void> directPublishCourse(@PathVariable Long id) {
courseService.publishCourse(id);
return Result.success();
}
@Operation(summary = "Unpublish (archive) course")
@PostMapping("/{id}/unpublish")
public Result<Void> unpublishCourse(@PathVariable Long id) {
courseService.archiveCourse(id);
return Result.success();
}
@Operation(summary = "Republish course")
@PostMapping("/{id}/republish")
public Result<Void> republishCourse(@PathVariable Long id) {
courseService.publishCourse(id);
return Result.success();
}
@Operation(summary = "Archive course") @Operation(summary = "Archive course")
@PostMapping("/{id}/archive") @PostMapping("/{id}/archive")
public Result<Void> archiveCourse(@PathVariable Long id) { public Result<Void> archiveCourse(@PathVariable Long id) {

View File

@ -20,7 +20,7 @@ public interface CourseService {
Page<Course> getCoursePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String category, String status); Page<Course> getCoursePage(Long tenantId, Integer pageNum, Integer pageSize, String keyword, String category, String status);
Page<Course> getSystemCoursePage(Integer pageNum, Integer pageSize, String keyword, String category); Page<Course> getSystemCoursePage(Integer pageNum, Integer pageSize, String keyword, String category, String status);
void deleteCourse(Long id); void deleteCourse(Long id);
@ -30,4 +30,14 @@ public interface CourseService {
List<Course> getCoursesByTenantId(Long tenantId); List<Course> getCoursesByTenantId(Long tenantId);
Page<Course> getReviewCoursePage(Integer pageNum, Integer pageSize);
void submitCourse(Long id);
void withdrawCourse(Long id);
void approveCourse(Long id, String comment);
void rejectCourse(Long id, String comment);
} }

View File

@ -250,13 +250,15 @@ public class CourseServiceImpl implements CourseService {
} }
@Override @Override
public Page<Course> getSystemCoursePage(Integer pageNum, Integer pageSize, String keyword, String category) { public Page<Course> getSystemCoursePage(Integer pageNum, Integer pageSize, String keyword, String category, String status) {
Page<Course> page = PageUtils.of(pageNum, pageSize); Page<Course> page = PageUtils.of(pageNum, pageSize);
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Course::getIsSystem, 1) wrapper.eq(Course::getIsSystem, 1);
.eq(Course::getStatus, "published");
if (StringUtils.hasText(status)) {
wrapper.eq(Course::getStatus, status.toLowerCase());
}
if (StringUtils.hasText(keyword)) { if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w wrapper.and(w -> w
.like(Course::getName, keyword) .like(Course::getName, keyword)
@ -309,4 +311,56 @@ public class CourseServiceImpl implements CourseService {
); );
} }
@Override
public Page<Course> getReviewCoursePage(Integer pageNum, Integer pageSize) {
Page<Course> page = PageUtils.of(pageNum, pageSize);
LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Course::getIsSystem, 1)
.eq(Course::getStatus, "pending")
.orderByDesc(Course::getSubmittedAt);
return courseMapper.selectPage(page, wrapper);
}
@Override
@Transactional
public void submitCourse(Long id) {
Course course = getCourseById(id);
course.setStatus("pending");
course.setSubmittedAt(LocalDateTime.now());
courseMapper.updateById(course);
log.info("Course submitted for review: id={}", id);
}
@Override
@Transactional
public void withdrawCourse(Long id) {
Course course = getCourseById(id);
course.setStatus("draft");
courseMapper.updateById(course);
log.info("Course withdrawn from review: id={}", id);
}
@Override
@Transactional
public void approveCourse(Long id, String comment) {
Course course = getCourseById(id);
course.setStatus("published");
course.setReviewComment(comment);
course.setReviewedAt(LocalDateTime.now());
course.setPublishedAt(LocalDateTime.now());
courseMapper.updateById(course);
log.info("Course approved: id={}", id);
}
@Override
@Transactional
public void rejectCourse(Long id, String comment) {
Course course = getCourseById(id);
course.setStatus("rejected");
course.setReviewComment(comment);
course.setReviewedAt(LocalDateTime.now());
courseMapper.updateById(course);
log.info("Course rejected: id={}", id);
}
} }