diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java index c2473b3..911f869 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.groupcourse.dao; +import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; import org.springframework.data.domain.Sort; @@ -226,4 +227,98 @@ public interface GroupCourseDao extends R2dbcRepository return spec.map((row, meta) -> row.get(0, Long.class)).one(); } + + /** + * 分页查询团课(支持 keyword 和 status 过滤) + */ + default Flux findByPageFiltered(DatabaseClient databaseClient, PageRequest pageRequest) { + StringBuilder sql = new StringBuilder("SELECT * FROM group_course WHERE deleted_at IS NULL"); + List conditions = new ArrayList<>(); + + if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) { + conditions.add("course_name ILIKE :keyword"); + } + if (pageRequest.getStatus() != null && !pageRequest.getStatus().isEmpty()) { + conditions.add("status = :status"); + } + + if (!conditions.isEmpty()) { + sql.append(" AND ").append(String.join(" AND ", conditions)); + } + + String sort = pageRequest.getSort() != null ? pageRequest.getSort() : "id"; + String order = "desc".equalsIgnoreCase(pageRequest.getOrder()) ? "DESC" : "ASC"; + sql.append(" ORDER BY ").append(sort).append(" ").append(order); + + int size = pageRequest.getSize(); + if (size < 1) size = 10; + if (size > 100) size = 100; + int offset = pageRequest.getPage() * size; + sql.append(" LIMIT :limit OFFSET :offset"); + + DatabaseClient.GenericExecuteSpec spec = databaseClient.sql(sql.toString()); + + if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) { + spec = spec.bind("keyword", "%" + pageRequest.getKeyword() + "%"); + } + if (pageRequest.getStatus() != null && !pageRequest.getStatus().isEmpty()) { + spec = spec.bind("status", pageRequest.getStatus()); + } + spec = spec.bind("limit", size); + spec = spec.bind("offset", offset); + + return spec.map((row, meta) -> { + GroupCourseEntity entity = new GroupCourseEntity(); + entity.setId(row.get("id", Long.class)); + entity.setCourseName(row.get("course_name", String.class)); + entity.setCoachId(row.get("coach_id", Long.class)); + entity.setCourseType(row.get("course_type", Long.class)); + entity.setStartTime(row.get("start_time", LocalDateTime.class)); + entity.setEndTime(row.get("end_time", LocalDateTime.class)); + entity.setMaxMembers(row.get("max_members", Integer.class)); + entity.setCurrentMembers(row.get("current_members", Integer.class)); + String statusStr = row.get("status", String.class); + entity.setStatus(statusStr != null ? Long.parseLong(statusStr) : null); + entity.setLocation(row.get("location", String.class)); + entity.setCoverImage(row.get("cover_image", String.class)); + entity.setDescription(row.get("description", String.class)); + entity.setStoredValueAmount(row.get("stored_value_amount", java.math.BigDecimal.class)); + entity.setCreateBy(row.get("create_by", String.class)); + entity.setUpdateBy(row.get("update_by", String.class)); + entity.setCreatedAt(row.get("created_at", LocalDateTime.class)); + entity.setUpdatedAt(row.get("updated_at", LocalDateTime.class)); + entity.setDeletedAt(row.get("deleted_at", LocalDateTime.class)); + return entity; + }).all(); + } + + /** + * 分页查询团课总数(支持 keyword 和 status 过滤) + */ + default Mono countByPageFiltered(DatabaseClient databaseClient, PageRequest pageRequest) { + StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL"); + List conditions = new ArrayList<>(); + + if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) { + conditions.add("course_name ILIKE :keyword"); + } + if (pageRequest.getStatus() != null && !pageRequest.getStatus().isEmpty()) { + conditions.add("status = :status"); + } + + if (!conditions.isEmpty()) { + sql.append(" AND ").append(String.join(" AND ", conditions)); + } + + DatabaseClient.GenericExecuteSpec spec = databaseClient.sql(sql.toString()); + + if (pageRequest.getKeyword() != null && !pageRequest.getKeyword().isEmpty()) { + spec = spec.bind("keyword", "%" + pageRequest.getKeyword() + "%"); + } + if (pageRequest.getStatus() != null && !pageRequest.getStatus().isEmpty()) { + spec = spec.bind("status", pageRequest.getStatus()); + } + + return spec.map((row, meta) -> row.get(0, Long.class)).one(); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/CourseLabelHandler.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/CourseLabelHandler.java index ea3a6b3..4ca9bee 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/CourseLabelHandler.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/CourseLabelHandler.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.groupcourse.handler; +import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.groupcourse.domain.CourseLabel; import cn.novalon.gym.manage.groupcourse.service.ICourseLabelService; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +30,13 @@ public class CourseLabelHandler { .body(courseLabelService.findAll(), CourseLabel.class); } + @Operation(summary = "分页查询标签", description = "支持按关键词筛选并分页查询标签") + public Mono getLabelsByPage(ServerRequest request) { + return request.bodyToMono(PageRequest.class) + .flatMap(pageRequest -> courseLabelService.findByPage(pageRequest) + .flatMap(response -> ServerResponse.ok().bodyValue(response))); + } + @Operation(summary = "根据ID获取标签", description = "根据ID获取标签详情") public Mono getLabelById(ServerRequest request) { Long id = Long.valueOf(request.pathVariable("id")); @@ -147,17 +155,17 @@ public class CourseLabelHandler { return request.bodyToMono(Map.class) .flatMap(body -> { @SuppressWarnings("unchecked") - List labelIdsInt = (List) body.get("labelIds"); - - if (labelIdsInt == null || labelIdsInt.isEmpty()) { + List rawIds = (List) body.get("labelIds"); + + if (rawIds == null || rawIds.isEmpty()) { Map error = new HashMap<>(); error.put("success", false); error.put("message", "labelIds不能为空"); return ServerResponse.badRequest().bodyValue(error); } - - List labelIds = labelIdsInt.stream() - .map(Integer::longValue) + + List labelIds = rawIds.stream() + .map(id -> Long.valueOf(String.valueOf(id))) .collect(java.util.stream.Collectors.toList()); return courseLabelService.addLabelsToType(typeId, labelIds) diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseTypeHandler.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseTypeHandler.java index a0b2a57..b405a11 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseTypeHandler.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseTypeHandler.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.groupcourse.handler; +import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService; import io.swagger.v3.oas.annotations.Operation; @@ -134,4 +135,12 @@ public class GroupCourseTypeHandler { return ServerResponse.badRequest().bodyValue(response); }); } + + @Operation(summary = "分页查询团课类型", description = "支持按关键词、分类筛选并分页查询团课类型") + public Mono getGroupCourseTypesByPage(ServerRequest request) { + return request.bodyToMono(PageRequest.class) + .flatMap(pageRequest -> groupCourseTypeService.findByPage(pageRequest) + .flatMap(response -> ServerResponse.ok().bodyValue(response)) + ); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/ICourseLabelRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/ICourseLabelRepository.java index e922354..b036fd7 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/ICourseLabelRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/ICourseLabelRepository.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.repository; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.domain.CourseLabel; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,4 +31,6 @@ public interface ICourseLabelRepository { Mono removeLabelFromType(Long typeId, Long labelId); Mono clearLabelsFromType(Long typeId); + + Mono> findByPage(PageRequest pageRequest); } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseTypeRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseTypeRepository.java index 3a30617..d0762f5 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseTypeRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/IGroupCourseTypeRepository.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.repository; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,4 +27,6 @@ public interface IGroupCourseTypeRepository { Mono update(GroupCourseType groupCourseType); Mono deleteById(Long id); + + Mono> findByPage(PageRequest pageRequest); } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/CourseLabelRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/CourseLabelRepository.java index 27de680..103287f 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/CourseLabelRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/CourseLabelRepository.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.repository.impl; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter; import cn.novalon.gym.manage.groupcourse.dao.CourseLabelDao; import cn.novalon.gym.manage.groupcourse.dao.CourseTypeLabelDao; @@ -9,12 +11,17 @@ import cn.novalon.gym.manage.groupcourse.entity.CourseTypeLabelEntity; import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Repository @@ -26,12 +33,14 @@ public class CourseLabelRepository implements ICourseLabelRepository { private final CourseLabelDao courseLabelDao; private final CourseTypeLabelDao courseTypeLabelDao; private final GroupCourseConverter converter; + private final R2dbcEntityTemplate r2dbcEntityTemplate; public CourseLabelRepository(CourseLabelDao courseLabelDao, CourseTypeLabelDao courseTypeLabelDao, - GroupCourseConverter converter) { + GroupCourseConverter converter, R2dbcEntityTemplate r2dbcEntityTemplate) { this.courseLabelDao = courseLabelDao; this.courseTypeLabelDao = courseTypeLabelDao; this.converter = converter; + this.r2dbcEntityTemplate = r2dbcEntityTemplate; } @Override @@ -158,4 +167,41 @@ public class CourseLabelRepository implements ICourseLabelRepository { } return entity; } + + @Override + public Mono> findByPage(PageRequest pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + String sort = pageRequest.getSort(); + String order = pageRequest.getOrder(); + String keyword = pageRequest.getKeyword(); + + Sort sortObj = Sort.unsorted(); + if (sort != null && !sort.isEmpty()) { + sortObj = Sort.by(Sort.Direction.fromString(order), sort); + } + + List criteriaList = new ArrayList<>(); + criteriaList.add(Criteria.where("deleted_at").isNull()); + if (keyword != null && !keyword.isEmpty()) { + criteriaList.add(Criteria.where("label_name").like("%" + keyword + "%").ignoreCase(true)); + } + + Criteria criteria = criteriaList.isEmpty() ? Criteria.empty() : Criteria.from(criteriaList); + Query query = Query.query(criteria).with(org.springframework.data.domain.PageRequest.of(page, size, sortObj)); + + return r2dbcEntityTemplate.select(CourseLabelEntity.class) + .matching(query) + .all() + .collectList() + .zipWith(r2dbcEntityTemplate.count(Query.query(criteria), CourseLabelEntity.class)) + .map(tuple -> { + long total = tuple.getT2(); + int totalPages = (int) Math.ceil((double) total / size); + List list = tuple.getT1().stream() + .map(this::toCourseLabel) + .toList(); + return new PageResponse<>(list, totalPages, total, page, size); + }); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseRepository.java index 2519b5b..67729aa 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseRepository.java @@ -97,37 +97,19 @@ public class GroupCourseRepository implements IGroupCourseRepository { public Mono> findByPageAndNotDeleted(PageRequest pageRequest) { int page = pageRequest.getPage(); int size = pageRequest.getSize(); - String sort = pageRequest.getSort(); - String order = pageRequest.getOrder(); - Sort sortObj = Sort.unsorted(); - if (sort != null && !sort.isEmpty()) { - sortObj = Sort.by(Sort.Direction.fromString(order), sort); - } - - org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj); - - return groupCourseDao.findAllByDeletedAtIsNull(sortObj) - .collectList() - .zipWith(groupCourseDao.findAllByDeletedAtIsNull().count()) - .map(tuple -> { - List allEntities = tuple.getT1(); - long total = tuple.getT2(); - - int fromIndex = page * size; - int toIndex = Math.min(fromIndex + size, allEntities.size()); - - List courseList; - if (fromIndex < allEntities.size()) { - courseList = allEntities.subList(fromIndex, toIndex).stream() - .map(groupCourseConverter::toDomain) - .toList(); - } else { - courseList = List.of(); + return groupCourseDao.countByPageFiltered(r2dbcEntityTemplate.getDatabaseClient(), pageRequest) + .flatMap(total -> { + if (total == 0) { + return Mono.just(new PageResponse<>(List.of(), 0, 0L, page, size)); } - - int totalPages = (int) Math.ceil((double) total / size); - return new PageResponse<>(courseList, totalPages, total, page, size); + return groupCourseDao.findByPageFiltered(r2dbcEntityTemplate.getDatabaseClient(), pageRequest) + .map(groupCourseConverter::toDomain) + .collectList() + .map(courseList -> { + int totalPages = (int) Math.ceil((double) total / size); + return new PageResponse<>(courseList, totalPages, total, page, size); + }); }); } diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseTypeRepository.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseTypeRepository.java index 4d42a1d..1aa9f4e 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseTypeRepository.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/repository/impl/GroupCourseTypeRepository.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.repository.impl; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseTypeDao; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType; @@ -7,12 +9,18 @@ import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Repository @Transactional @@ -22,10 +30,13 @@ public class GroupCourseTypeRepository implements IGroupCourseTypeRepository { private final GroupCourseTypeDao groupCourseTypeDao; private final GroupCourseConverter converter; + private final R2dbcEntityTemplate r2dbcEntityTemplate; - public GroupCourseTypeRepository(GroupCourseTypeDao groupCourseTypeDao, GroupCourseConverter converter) { + public GroupCourseTypeRepository(GroupCourseTypeDao groupCourseTypeDao, GroupCourseConverter converter, + R2dbcEntityTemplate r2dbcEntityTemplate) { this.groupCourseTypeDao = groupCourseTypeDao; this.converter = converter; + this.r2dbcEntityTemplate = r2dbcEntityTemplate; } @Override @@ -129,4 +140,46 @@ public class GroupCourseTypeRepository implements IGroupCourseTypeRepository { return groupCourseTypeDao.softDelete(id, LocalDateTime.now()) .then(); } + + @Override + public Mono> findByPage(PageRequest pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + String order = pageRequest.getOrder(); + String sort = pageRequest.getSort(); + String keyword = pageRequest.getKeyword(); + String category = pageRequest.getCategory(); + + Sort sortObj = Sort.unsorted(); + if (sort != null && !sort.isEmpty()) { + sortObj = Sort.by(Sort.Direction.fromString(order), sort); + } + + // Build dynamic criteria + List criteriaList = new ArrayList<>(); + criteriaList.add(Criteria.where("deleted_at").isNull()); + if (keyword != null && !keyword.isEmpty()) { + criteriaList.add(Criteria.where("type_name").like("%" + keyword + "%").ignoreCase(true)); + } + if (category != null && !category.isEmpty()) { + criteriaList.add(Criteria.where("category").is(category)); + } + + Criteria criteria = criteriaList.isEmpty() ? Criteria.empty() : Criteria.from(criteriaList); + Query query = Query.query(criteria).with(org.springframework.data.domain.PageRequest.of(page, size, sortObj)); + + return r2dbcEntityTemplate.select(GroupCourseTypeEntity.class) + .matching(query) + .all() + .collectList() + .zipWith(r2dbcEntityTemplate.count(Query.query(criteria), GroupCourseTypeEntity.class)) + .map(tuple -> { + long total = tuple.getT2(); + int totalPages = (int) Math.ceil((double) total / size); + List list = tuple.getT1().stream() + .map(converter::toGroupCourseType) + .toList(); + return new PageResponse<>(list, totalPages, total, page, size); + }); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/ICourseLabelService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/ICourseLabelService.java index 038dfc9..95098d3 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/ICourseLabelService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/ICourseLabelService.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.service; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.domain.CourseLabel; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,4 +29,6 @@ public interface ICourseLabelService { Mono removeLabelFromType(Long typeId, Long labelId); Mono clearLabelsFromType(Long typeId); + + Mono> findByPage(PageRequest pageRequest); } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseTypeService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseTypeService.java index 3e8a921..574682e 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseTypeService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/IGroupCourseTypeService.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.service; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,4 +31,6 @@ public interface IGroupCourseTypeService { * @return 分类名称列表 */ Flux findCategories(); + + Mono> findByPage(PageRequest pageRequest); } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/CourseLabelService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/CourseLabelService.java index cb43eeb..0155470 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/CourseLabelService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/CourseLabelService.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.groupcourse.service.impl; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.groupcourse.domain.CourseLabel; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; @@ -109,4 +111,12 @@ public class CourseLabelService implements ICourseLabelService { .doOnSuccess(v -> logger.info("清空类型标签成功 - typeId={}", typeId)) .doOnError(error -> logger.error("清空类型标签失败 - typeId={}, error: {}", typeId, error.getMessage())); } + + @Override + public Mono> findByPage(PageRequest pageRequest) { + return courseLabelRepository.findByPage(pageRequest) + .doOnSuccess(result -> logger.info("分页查询标签成功 - page={}, size={}, total={}", + pageRequest.getPage(), pageRequest.getSize(), result.getTotalElements())) + .doOnError(error -> logger.error("分页查询标签失败 - error: {}", error.getMessage())); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java index bf1b7e0..eb14b37 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java @@ -224,8 +224,9 @@ public class GroupCourseService implements IGroupCourseService { String sort = pageRequest.getSort(); String order = pageRequest.getOrder(); String keyword = pageRequest.getKeyword() != null ? pageRequest.getKeyword() : ""; + String status = pageRequest.getStatus() != null ? pageRequest.getStatus() : ""; - String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword; + String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status; return redisUtil.get(cacheKey, String.class) .flatMap(cachedJson -> { @@ -536,14 +537,15 @@ public class GroupCourseService implements IGroupCourseService { @Override public Mono delete(Long id) { - // 先查询课程状态,只有已取消的课程才能删除 + // 只有已取消或已结束的课程才能删除 return groupCourseRepository.findByIdAndDeletedAtIsNull(id) .switchIfEmpty(Mono.error(new RuntimeException("团课不存在"))) .flatMap(course -> { - // 检查课程状态是否为已取消(状态码1) - if (course.getStatus() == null || !course.getStatus().equals(CourseStatus.CANCELLED.getValue())) { - return Mono.error(new RuntimeException("只有已取消的课程才能删除,当前状态: " + - (course.getStatus() != null ? course.getStatus() : "未知"))); + // 检查课程状态是否为已取消(状态码1)或已结束(状态码2) + Long status = course.getStatus(); + if (status == null || (!status.equals(CourseStatus.CANCELLED.getValue()) && !status.equals(CourseStatus.ENDED.getValue()))) { + return Mono.error(new RuntimeException("只有已取消或已结束的课程才能删除,当前状态: " + + (status != null ? status : "未知"))); } // 删除课程 diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseTypeService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseTypeService.java index 67ec080..a18347e 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseTypeService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseTypeService.java @@ -1,6 +1,9 @@ package cn.novalon.gym.manage.groupcourse.service.impl; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType; +import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService; import org.slf4j.Logger; @@ -9,18 +12,18 @@ import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.HashSet; -import java.util.Set; - @Service public class GroupCourseTypeService implements IGroupCourseTypeService { private static final Logger logger = LoggerFactory.getLogger(GroupCourseTypeService.class); private final IGroupCourseTypeRepository groupCourseTypeRepository; + private final IGroupCourseRepository groupCourseRepository; - public GroupCourseTypeService(IGroupCourseTypeRepository groupCourseTypeRepository) { + public GroupCourseTypeService(IGroupCourseTypeRepository groupCourseTypeRepository, + IGroupCourseRepository groupCourseRepository) { this.groupCourseTypeRepository = groupCourseTypeRepository; + this.groupCourseRepository = groupCourseRepository; } @Override @@ -71,9 +74,17 @@ public class GroupCourseTypeService implements IGroupCourseTypeService { @Override public Mono delete(Long id) { - return groupCourseTypeRepository.deleteById(id) - .doOnSuccess(v -> logger.info("团课类型删除成功 - id={}", id)) - .doOnError(error -> logger.error("团课类型删除失败 - id={}, error: {}", id, error.getMessage())); + // 检查是否有团课依赖该类型 + return groupCourseRepository.findByCourseType(id) + .hasElements() + .flatMap(hasDependents -> { + if (hasDependents) { + return Mono.error(new RuntimeException("该类型下存在团课,无法删除")); + } + return groupCourseTypeRepository.deleteById(id) + .doOnSuccess(v -> logger.info("团课类型删除成功 - id={}", id)) + .doOnError(error -> logger.error("团课类型删除失败 - id={}, error: {}", id, error.getMessage())); + }); } @Override @@ -83,4 +94,12 @@ public class GroupCourseTypeService implements IGroupCourseTypeService { .filter(category -> category != null && !category.isEmpty()) .distinct(); } + + @Override + public Mono> findByPage(PageRequest pageRequest) { + return groupCourseTypeRepository.findByPage(pageRequest) + .doOnSuccess(result -> logger.info("分页查询团课类型成功 - page={}, size={}, total={}", + pageRequest.getPage(), pageRequest.getSize(), result.getTotalElements())) + .doOnError(error -> logger.error("分页查询团课类型失败 - error: {}", error.getMessage())); + } } \ No newline at end of file diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java index dfaef6c..10b1b1e 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java @@ -305,6 +305,7 @@ public class SystemRouter { .GET("/api/groupCourse/types/category/{category}", groupCourseTypeHandler::getGroupCourseTypesByCategory) .GET("/api/groupCourse/types/{id}", groupCourseTypeHandler::getGroupCourseTypeById) .POST("/api/groupCourse/types", groupCourseTypeHandler::createGroupCourseType) + .POST("/api/groupCourse/types/page", groupCourseTypeHandler::getGroupCourseTypesByPage) .PUT("/api/groupCourse/types/{id}", groupCourseTypeHandler::updateGroupCourseType) .DELETE("/api/groupCourse/types/{id}", groupCourseTypeHandler::deleteGroupCourseType) @@ -313,6 +314,7 @@ public class SystemRouter { .GET("/api/groupCourse/labels/search", courseLabelHandler::searchLabels) .GET("/api/groupCourse/labels/{id}", courseLabelHandler::getLabelById) .GET("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::getLabelsByTypeId) + .POST("/api/groupCourse/labels/page", courseLabelHandler::getLabelsByPage) .POST("/api/groupCourse/labels", courseLabelHandler::createLabel) .PUT("/api/groupCourse/labels/{id}", courseLabelHandler::updateLabel) .DELETE("/api/groupCourse/labels/{id}", courseLabelHandler::deleteLabel) diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/dto/PageRequest.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/dto/PageRequest.java index 9054f19..6215032 100644 --- a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/dto/PageRequest.java +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/dto/PageRequest.java @@ -12,6 +12,8 @@ public class PageRequest { private String sort = "id"; private String order = "asc"; private String keyword; + private String status; + private String category; public int getPage() { return page; @@ -52,4 +54,20 @@ public class PageRequest { public void setKeyword(String keyword) { this.keyword = keyword; } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } } diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V28__Add_Business_Menus.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V28__Add_Business_Menus.sql new file mode 100644 index 0000000..25f722c --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V28__Add_Business_Menus.sql @@ -0,0 +1,47 @@ +-- ============================================ +-- V28: 新增业务模块菜单 +-- 描述: 为团课管理、团课类型、团课标签、团课推荐创建菜单项 +-- ============================================ + +-- 团课管理一级菜单(目录) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(400, '团课管理', 0, 4, 'M', NULL, NULL, 1, NOW(), NOW()); + +-- 团课管理子菜单(页面) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(401, '团课管理', 400, 1, 'C', 'business:groupCourse:list', 'gymCourse/course/index', 1, NOW(), NOW()), +(402, '团课类型', 400, 2, 'C', 'business:groupCourseType:list', 'gymCourse/type/index', 1, NOW(), NOW()), +(403, '类型标签', 400, 3, 'C', 'business:groupCourseLabel:list', 'gymCourse/label/index', 1, NOW(), NOW()), +(404, '团课推荐', 400, 4, 'C', 'business:groupCourseRecommend:list', 'gymCourse/recommend/index', 1, NOW(), NOW()); + +-- 团课管理按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(4011, '团课查询', 401, 1, 'F', 'business:groupCourse:query', NULL, 1, NOW(), NOW()), +(4012, '团课新增', 401, 2, 'F', 'business:groupCourse:add', NULL, 1, NOW(), NOW()), +(4013, '团课修改', 401, 3, 'F', 'business:groupCourse:edit', NULL, 1, NOW(), NOW()), +(4014, '团课删除', 401, 4, 'F', 'business:groupCourse:remove', NULL, 1, NOW(), NOW()), +(4015, '团课取消', 401, 5, 'F', 'business:groupCourse:cancel', NULL, 1, NOW(), NOW()); + +-- 团课类型按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(4021, '类型查询', 402, 1, 'F', 'business:groupCourseType:query', NULL, 1, NOW(), NOW()), +(4022, '类型新增', 402, 2, 'F', 'business:groupCourseType:add', NULL, 1, NOW(), NOW()), +(4023, '类型修改', 402, 3, 'F', 'business:groupCourseType:edit', NULL, 1, NOW(), NOW()), +(4024, '类型删除', 402, 4, 'F', 'business:groupCourseType:remove', NULL, 1, NOW(), NOW()); + +-- 团课标签按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(4031, '标签查询', 403, 1, 'F', 'business:groupCourseLabel:query', NULL, 1, NOW(), NOW()), +(4032, '标签新增', 403, 2, 'F', 'business:groupCourseLabel:add', NULL, 1, NOW(), NOW()), +(4033, '标签修改', 403, 3, 'F', 'business:groupCourseLabel:edit', NULL, 1, NOW(), NOW()), +(4034, '标签删除', 403, 4, 'F', 'business:groupCourseLabel:remove', NULL, 1, NOW(), NOW()); + +-- 团课推荐按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(4041, '推荐查询', 404, 1, 'F', 'business:groupCourseRecommend:query', NULL, 1, NOW(), NOW()), +(4042, '推荐新增', 404, 2, 'F', 'business:groupCourseRecommend:add', NULL, 1, NOW(), NOW()), +(4043, '推荐修改', 404, 3, 'F', 'business:groupCourseRecommend:edit', NULL, 1, NOW(), NOW()), +(4044, '推荐删除', 404, 4, 'F', 'business:groupCourseRecommend:remove', NULL, 1, NOW(), NOW()); + +-- 重置菜单序列 +SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu)); diff --git a/gym-manage-web/src/api/groupCourse.api.ts b/gym-manage-web/src/api/groupCourse.api.ts index 379d8d9..f1a061d 100644 --- a/gym-manage-web/src/api/groupCourse.api.ts +++ b/gym-manage-web/src/api/groupCourse.api.ts @@ -91,6 +91,23 @@ export interface GroupCourseType { updatedAt?: string } +export interface GroupCourseTypePageRequest { + page: number + size: number + keyword?: string + category?: string + sort?: string + order?: 'asc' | 'desc' +} + +export interface CourseLabelPageRequest { + page: number + size: number + keyword?: string + sort?: string + order?: 'asc' | 'desc' +} + export const groupCourseTypeApi = { getAll: () => request.get('/groupCourse/types'), @@ -102,11 +119,14 @@ export const groupCourseTypeApi = { request.get('/groupCourse/types/categories'), getByCategory: (category: string) => - request.get(`/groupCourse/types/category/${category}`), + request.get(`/groupCourse/types/category/${encodeURIComponent(category)}`), getById: (id: number) => request.get(`/groupCourse/types/${id}`), + getPage: (params: GroupCourseTypePageRequest) => + request.post>('/groupCourse/types/page', params), + create: (data: GroupCourseType) => request.post('/groupCourse/types', data), @@ -143,6 +163,9 @@ export const courseLabelApi = { getByTypeId: (typeId: number) => request.get(`/groupCourse/types/${typeId}/labels`), + getPage: (params: CourseLabelPageRequest) => + request.post>('/groupCourse/labels/page', params), + create: (data: CourseLabel) => request.post('/groupCourse/labels', data), @@ -153,7 +176,7 @@ export const courseLabelApi = { request.delete(`/groupCourse/labels/${id}`), addLabelsToType: (typeId: number, labelIds: number[]) => - request.post(`/groupCourse/types/${typeId}/labels`, labelIds), + request.post(`/groupCourse/types/${typeId}/labels`, { labelIds }), removeLabelFromType: (typeId: number, labelId: number) => request.delete(`/groupCourse/types/${typeId}/labels/${labelId}`), diff --git a/gym-manage-web/src/layouts/DefaultLayout.vue b/gym-manage-web/src/layouts/DefaultLayout.vue index d346ccb..69468ec 100644 --- a/gym-manage-web/src/layouts/DefaultLayout.vue +++ b/gym-manage-web/src/layouts/DefaultLayout.vue @@ -97,15 +97,15 @@ onMounted(async () => { if (!token) { router.push('/login') - } else if (!permissionStore.loaded) { - permissionStore.initFromStorage() - - if (!permissionStore.loaded || permissionStore.menus.length === 0) { - try { - await permissionStore.fetchUserMenus() - } catch (error) { - console.error('获取用户菜单失败:', error) - } + } else { + // 先尝试从 localStorage 快速恢复,再异步获取最新菜单 + if (!permissionStore.loaded) { + permissionStore.initFromStorage() + } + try { + await permissionStore.fetchUserMenus() + } catch (error) { + console.error('获取用户菜单失败:', error) } } }) diff --git a/gym-manage-web/src/stores/permission.ts b/gym-manage-web/src/stores/permission.ts index 119fff9..ad9aa98 100644 --- a/gym-manage-web/src/stores/permission.ts +++ b/gym-manage-web/src/stores/permission.ts @@ -38,6 +38,10 @@ function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] { 'audit/operation/index': '/oplog', 'audit/login/index': '/loginlog', 'audit/exception/index': '/exceptionlog', + 'gymCourse/course/index': '/courses', + 'gymCourse/type/index': '/courses/types', + 'gymCourse/label/index': '/courses/labels', + 'gymCourse/recommend/index': '/courses/recommend', } const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F') @@ -92,7 +96,11 @@ function getMenuIcon(menuName: string): string { '文件管理': 'Folder', '操作日志': 'Document', '登录日志': 'Document', - '异常日志': 'Warning' + '异常日志': 'Warning', + '团课管理': 'TrophyBase', + '团课类型': 'Grid', + '团课标签': 'Discount', + '团课推荐': 'Star' } return iconMap[menuName] || 'Document' } diff --git a/gym-manage-web/src/views/groupcourse/CourseLabelManagement.vue b/gym-manage-web/src/views/groupcourse/CourseLabelManagement.vue index 1ecc710..36e57b3 100644 --- a/gym-manage-web/src/views/groupcourse/CourseLabelManagement.vue +++ b/gym-manage-web/src/views/groupcourse/CourseLabelManagement.vue @@ -71,6 +71,17 @@ + ([]) const searchKeyword = ref('') +const currentPage = ref(1) +const pageSize = ref(10) +const total = ref(0) + const modalVisible = ref(false) const modalTitle = ref('') const formRef = ref() @@ -138,12 +153,17 @@ const formRules = { const fetchData = async () => { loading.value = true try { - dataSource.value = await courseLabelApi.getAll() - if (searchKeyword.value) { - dataSource.value = dataSource.value.filter((item) => - item.labelName.toLowerCase().includes(searchKeyword.value.toLowerCase()) - ) + const params: CourseLabelPageRequest = { + page: currentPage.value - 1, + size: pageSize.value, + sort: 'id', + order: 'asc', } + if (searchKeyword.value) params.keyword = searchKeyword.value + + const res = await courseLabelApi.getPage(params) + dataSource.value = res.content + total.value = Number(res.totalElements) } catch { ElMessage.error('加载数据失败') } finally { @@ -151,7 +171,16 @@ const fetchData = async () => { } } -const handleSearch = () => fetchData() +const onPageChange = () => fetchData() +const onSizeChange = () => { + currentPage.value = 1 + fetchData() +} + +const handleSearch = () => { + currentPage.value = 1 + fetchData() +} const handleAdd = () => { modalTitle.value = '新增标签' @@ -191,10 +220,15 @@ const handleModalOk = async () => { if (!formRef.value) return try { await formRef.value.validate() + // Convert RGBA to hex before submitting + const data = { ...formState } + if (data.color?.startsWith('rgba')) { + data.color = rgbaToHex(data.color) + } if (formState.id) { - await courseLabelApi.update(formState.id, formState) + await courseLabelApi.update(formState.id, data) } else { - await courseLabelApi.create(formState) + await courseLabelApi.create(data) } ElMessage.success('操作成功') modalVisible.value = false @@ -206,6 +240,19 @@ const handleModalOk = async () => { } } +/** Convert rgba(r, g, b, a) to #rrggbb or #rrggbbaa hex */ +const rgbaToHex = (rgba: string): string => { + const match = rgba.match(/[\d.]+/g) + if (!match || match.length < 3) return rgba + const r = parseInt(match[0]) + const g = parseInt(match[1]) + const b = parseInt(match[2]) + const a = match.length >= 4 ? Math.round(parseFloat(match[3]) * 255) : 255 + const toHex = (n: number) => n.toString(16).padStart(2, '0') + const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}` + return a < 255 ? `${hex}${toHex(a)}` : hex +} + onMounted(() => fetchData()) diff --git a/gym-manage-web/src/views/groupcourse/CourseRecommendManagement.vue b/gym-manage-web/src/views/groupcourse/CourseRecommendManagement.vue index 3cff64d..b5e0933 100644 --- a/gym-manage-web/src/views/groupcourse/CourseRecommendManagement.vue +++ b/gym-manage-web/src/views/groupcourse/CourseRecommendManagement.vue @@ -4,20 +4,28 @@ + - - + + + + - +