From 97a5eff678267b3f211f839d1f5df3f7a7eeba91 Mon Sep 17 00:00:00 2001 From: liwentao Date: Sat, 18 Jul 2026 16:21:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=9A=E5=8A=A1=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=AF=B9=E8=BD=AE=E6=92=AD?= =?UTF-8?q?=E5=9B=BE=E7=9A=84=E7=AE=A1=E7=90=86=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=A1=A8=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/GroupCourseService.java | 21 +- .../gym/manage/app/config/SystemRouter.java | 10 + .../gym/manage/common/util/RedisUtil.java | 2 +- .../manage/db/converter/BannerConverter.java | 73 +++ .../novalon/gym/manage/db/dao/BannerDao.java | 22 + .../gym/manage/db/entity/BannerEntity.java | 127 +++++ .../db/repository/BannerRepository.java | 59 +++ .../V10__Create_group_course_recommend.sql | 38 ++ .../V11__Add_group_course_qr_code_path.sql | 9 + .../V12__Add_deleted_at_to_member_user.sql | 10 + ...updated_at_to_member_card_transactions.sql | 10 + .../migration/V14__Create_payment_order.sql | 63 +++ .../db/migration/V15__Add_party_order_id.sql | 9 + .../db/migration/V16__Create_stored_card.sql | 58 +++ .../V17__Make_member_card_id_nullable.sql | 8 + .../V18__Insert_group_course_data.sql | 209 ++++++++ .../db/migration/V19__Insert_system_data.sql | 315 ++++++++++++ .../db/migration/V1__Create_all_tables.sql | 39 +- .../db/migration/V20__Add_banner_menu.sql | 18 + .../db/migration/V21__Create_banner_table.sql | 20 + .../db/migration/V2__Create_indexes.sql | 141 ++++++ .../db/migration/V3__Grant_permissions.sql | 15 + .../db/migration/V4__Create_member_tables.sql | 238 +++++++++ .../V5__Create_group_course_tables.sql | 81 ++++ .../V6__Add_booking_snapshot_fields.sql | 16 + .../migration/V7__Create_sign_in_record.sql | 52 ++ .../V8__Create_group_course_type.sql | 33 ++ .../V9__Create_course_label_tables.sql | 56 +++ .../filter/JwtAuthenticationFilter.java | 1 + .../filter/RbacAuthorizationFilter.java | 3 +- .../src/main/resources/application.yml | 2 +- .../gym/manage/notify/core/domain/Banner.java | 97 ++++ .../core/repository/IBannerRepository.java | 18 + .../notify/core/service/IBannerService.java | 20 + .../core/service/impl/BannerServiceImpl.java | 80 +++ .../manage/notify/handler/BannerHandler.java | 120 +++++ .../gym/manage/sys/config/SecurityConfig.java | 3 +- gym-manage-uniapp/api/banner.js | 16 + gym-manage-uniapp/api/booking.js | 4 +- gym-manage-uniapp/pages.json | 2 + .../pages/course-detail/course-detail.vue | 3 +- gym-manage-uniapp/pages/index/index.vue | 34 +- .../pages/my-courses/my-courses.vue | 12 +- gym-manage-uniapp/pages/search/search.vue | 172 +++++-- gym-manage-uniapp/utils/request.js | 20 + gym-manage-web/src/api/banner.api.ts | 33 ++ gym-manage-web/src/router/index.ts | 6 + gym-manage-web/src/stores/permission.ts | 2 + .../src/views/banner/BannerManagement.vue | 454 ++++++++++++++++++ .../groupcourse/GroupCourseManagement.vue | 20 +- scripts/add_banner_menu.sql | 29 ++ .../V10__Create_GroupCourse_Type_table.sql | 0 .../V11__Create_Course_Label_tables.sql | 0 ...12__Create_GroupCourse_Recommend_table.sql | 0 .../V13__Consolidated_Test_Data.sql | 0 .../V19__Add_GroupCourse_QRCode_Path.sql | 0 .../V1__Create_all_tables.sql | 407 ++++++++++++++++ .../V20__Add_deleted_at_to_member_user.sql | 0 ...updated_at_to_member_card_transactions.sql | 0 .../V22__Add_Business_Permissions.sql | 0 .../V23__Add_Coach_Test_Data.sql | 0 .../V24__Add_Second_Coach_Test_Data.sql | 0 .../V25__Create_Payment_Order_table.sql | 0 ...6__Add_party_order_id_to_payment_order.sql | 0 ...reate_StoredCard_And_PayPassword_table.sql | 0 .../V28__Add_Business_Menus.sql | 0 .../V29__Add_Member_Management_Menu.sql | 0 .../V2__Insert_initial_data.sql | 0 .../V30__Add_Member_Card_Management_Menu.sql | 0 .../V31__Add_Statistics_Menu.sql | 0 .../V32__Make_member_card_id_nullable.sql | 0 .../V3__Create_indexes.sql | 0 .../V4__Grant_permissions.sql | 0 .../V5__Create_operation_log_table.sql | 0 .../V6__Create_GroupCourse_table.sql | 0 .../V7__Create_Member_And_MemberCard.sql | 0 ...dd_GroupCourse_Booking_Snapshot_Fields.sql | 0 .../V9__Create_sign_in_record_table.sql | 0 78 files changed, 3210 insertions(+), 100 deletions(-) create mode 100644 gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/BannerConverter.java create mode 100644 gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/dao/BannerDao.java create mode 100644 gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/BannerEntity.java create mode 100644 gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/BannerRepository.java create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V10__Create_group_course_recommend.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V11__Add_group_course_qr_code_path.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V12__Add_deleted_at_to_member_user.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V13__Add_updated_at_to_member_card_transactions.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V14__Create_payment_order.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V15__Add_party_order_id.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V16__Create_stored_card.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V17__Make_member_card_id_nullable.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V19__Insert_system_data.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V20__Add_banner_menu.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V21__Create_banner_table.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V2__Create_indexes.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V3__Grant_permissions.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V4__Create_member_tables.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V6__Add_booking_snapshot_fields.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V7__Create_sign_in_record.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V8__Create_group_course_type.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V9__Create_course_label_tables.sql create mode 100644 gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/domain/Banner.java create mode 100644 gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/repository/IBannerRepository.java create mode 100644 gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/IBannerService.java create mode 100644 gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImpl.java create mode 100644 gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/BannerHandler.java create mode 100644 gym-manage-uniapp/api/banner.js create mode 100644 gym-manage-web/src/api/banner.api.ts create mode 100644 gym-manage-web/src/views/banner/BannerManagement.vue create mode 100644 scripts/add_banner_menu.sql rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V10__Create_GroupCourse_Type_table.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V11__Create_Course_Label_tables.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V12__Create_GroupCourse_Recommend_table.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V13__Consolidated_Test_Data.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V19__Add_GroupCourse_QRCode_Path.sql (100%) create mode 100644 temp_migration_backup/V1__Create_all_tables.sql rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V20__Add_deleted_at_to_member_user.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V21__Add_updated_at_to_member_card_transactions.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V22__Add_Business_Permissions.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V23__Add_Coach_Test_Data.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V24__Add_Second_Coach_Test_Data.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V25__Create_Payment_Order_table.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V26__Add_party_order_id_to_payment_order.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V27__Create_StoredCard_And_PayPassword_table.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V28__Add_Business_Menus.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V29__Add_Member_Management_Menu.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V2__Insert_initial_data.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V30__Add_Member_Card_Management_Menu.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V31__Add_Statistics_Menu.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V32__Make_member_card_id_nullable.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V3__Create_indexes.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V4__Grant_permissions.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V5__Create_operation_log_table.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V6__Create_GroupCourse_table.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V7__Create_Member_And_MemberCard.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V8__Add_GroupCourse_Booking_Snapshot_Fields.sql (100%) rename {gym-manage-api/manage-db/src/main/resources/db/migration => temp_migration_backup}/V9__Create_sign_in_record_table.sql (100%) 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 a2a44ef..aab1597 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 @@ -89,13 +89,14 @@ public class GroupCourseService implements IGroupCourseService { public Mono findDetailById(Long id) { String cacheKey = CACHE_KEY_DETAIL_PREFIX + id; - return redisUtil.get(cacheKey, String.class) + Mono cachedMono = redisUtil.get(cacheKey, String.class); + return cachedMono .flatMap(cachedJson -> { - if (cachedJson != null) { + if (cachedJson != null && !cachedJson.isEmpty()) { try { GroupCourseDetail detail = objectMapper.readValue(cachedJson, GroupCourseDetail.class); logger.info("缓存命中 - findDetailById: id={}", id); - return Mono.just(detail); + return Mono.just(detail); } catch (JsonProcessingException e) { logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage()); return redisUtil.delete(cacheKey).then(Mono.empty()); @@ -176,13 +177,14 @@ public class GroupCourseService implements IGroupCourseService { public Mono findById(Long id) { String cacheKey = CACHE_KEY_ID_PREFIX + id; - return redisUtil.get(cacheKey, String.class) + Mono cachedMono = redisUtil.get(cacheKey, String.class); + return cachedMono .flatMap(cachedJson -> { - if (cachedJson != null) { + if (cachedJson != null && !cachedJson.isEmpty()) { try { GroupCourse groupCourse = objectMapper.readValue(cachedJson, GroupCourse.class); logger.info("缓存命中 - findById: id={}", id); - return Mono.just(groupCourse); + return Mono.just(groupCourse); } catch (JsonProcessingException e) { logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage()); return redisUtil.delete(cacheKey).then(Mono.empty()); @@ -232,14 +234,15 @@ public class GroupCourseService implements IGroupCourseService { String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status; - return redisUtil.get(cacheKey, String.class) + Mono cachedMono = redisUtil.get(cacheKey, String.class); + return cachedMono .flatMap(cachedJson -> { - if (cachedJson != null) { + if (cachedJson != null && !cachedJson.isEmpty()) { try { PageResponse pageResponse = objectMapper.readValue(cachedJson, objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class)); logger.info("缓存命中 - findByPage: key={}", cacheKey); - return Mono.just(pageResponse); + return Mono.>just(pageResponse); } catch (JsonProcessingException e) { logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage()); return redisUtil.delete(cacheKey).then(Mono.empty()); 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 ef16328..a61484f 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 @@ -16,6 +16,7 @@ import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler; import cn.novalon.gym.manage.member.handler.MemberHandler; import cn.novalon.gym.manage.member.handler.MemberStoredCardHandler; import cn.novalon.gym.manage.member.handler.WechatAuthHandler; +import cn.novalon.gym.manage.notify.handler.BannerHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; import cn.novalon.gym.manage.payment.handler.PaymentHandler; @@ -63,6 +64,7 @@ public class SystemRouter { SysAuthHandler authHandler, StatsHandler statsHandler, SysDictHandler dictHandler, + BannerHandler bannerHandler, SysNoticeHandler noticeHandler, SysUserMessageHandler messageHandler, SysFileHandler fileHandler, @@ -188,6 +190,14 @@ public class SystemRouter { .PUT("/api/dict/data/{id}", dictHandler::updateDictData) .DELETE("/api/dict/data/{id}", dictHandler::deleteDictData) + // ========== 轮播图路由 ========== + .GET("/api/banners", bannerHandler::getAllBanners) + .GET("/api/banners/active", bannerHandler::getActiveBanners) + .GET("/api/banners/{id}", bannerHandler::getBannerById) + .POST("/api/banners", bannerHandler::createBanner) + .PUT("/api/banners/{id}", bannerHandler::updateBanner) + .DELETE("/api/banners/{id}", bannerHandler::deleteBanner) + // ========== 公告路由 ========== .GET("/api/notices", noticeHandler::getAllNotices) .GET("/api/notices/{id}", noticeHandler::getNoticeById) diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java index 32245a5..c411b03 100644 --- a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java @@ -43,7 +43,7 @@ public class RedisUtil { public Mono get(String key, Class clazz) { return reactiveRedisTemplate.opsForValue().get(key) .filter(clazz::isInstance) - .cast(clazz) + .map(clazz::cast) .onErrorResume(e -> { // 旧缓存数据格式不兼容时静默跳过,后续逻辑会 fallback 到 DB 查询 log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage()); diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/BannerConverter.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/BannerConverter.java new file mode 100644 index 0000000..ef2c4f2 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/converter/BannerConverter.java @@ -0,0 +1,73 @@ +package cn.novalon.gym.manage.db.converter; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import cn.novalon.gym.manage.db.entity.BannerEntity; + +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 轮播图实体转换器 + * + * @author 张翔 + * @date 2026-07-18 + */ +@Component +public class BannerConverter { + + public Banner toDomain(BannerEntity entity) { + if (entity == null) { + return null; + } + Banner domain = new Banner(); + domain.setId(entity.getId()); + domain.setImageUrl(entity.getImageUrl()); + domain.setTitle(entity.getTitle()); + domain.setSubtitle(entity.getSubtitle()); + domain.setSortOrder(entity.getSortOrder()); + domain.setIsActive(entity.getIsActive()); + domain.setLinkUrl(entity.getLinkUrl()); + domain.setCreatedAt(entity.getCreatedAt()); + domain.setUpdatedAt(entity.getUpdatedAt()); + domain.setDeletedAt(entity.getDeletedAt()); + return domain; + } + + public BannerEntity toEntity(Banner domain) { + if (domain == null) { + return null; + } + BannerEntity entity = new BannerEntity(); + entity.setId(domain.getId()); + entity.setImageUrl(domain.getImageUrl()); + entity.setTitle(domain.getTitle()); + entity.setSubtitle(domain.getSubtitle()); + entity.setSortOrder(domain.getSortOrder()); + entity.setIsActive(domain.getIsActive()); + entity.setLinkUrl(domain.getLinkUrl()); + entity.setCreatedAt(domain.getCreatedAt()); + entity.setUpdatedAt(domain.getUpdatedAt()); + entity.setDeletedAt(domain.getDeletedAt()); + return entity; + } + + public List toDomainList(List entities) { + if (entities == null) { + return null; + } + return entities.stream() + .map(this::toDomain) + .collect(Collectors.toList()); + } + + public List toEntityList(List domains) { + if (domains == null) { + return null; + } + return domains.stream() + .map(this::toEntity) + .collect(Collectors.toList()); + } +} diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/dao/BannerDao.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/dao/BannerDao.java new file mode 100644 index 0000000..eff1ad6 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/dao/BannerDao.java @@ -0,0 +1,22 @@ +package cn.novalon.gym.manage.db.dao; + +import cn.novalon.gym.manage.db.entity.BannerEntity; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.repository.Query; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Repository +public interface BannerDao extends R2dbcRepository { + + Flux findByDeletedAtIsNull(); + + Flux findByDeletedAtIsNull(Sort sort); + + @Query("SELECT * FROM banner WHERE deleted_at IS NULL ORDER BY sort_order ASC") + Flux findActiveBanners(); + + Mono deleteByIdAndDeletedAtIsNull(Long id); +} diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/BannerEntity.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/BannerEntity.java new file mode 100644 index 0000000..7717ab1 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/BannerEntity.java @@ -0,0 +1,127 @@ +package cn.novalon.gym.manage.db.entity; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * 轮播图数据库实体类 + * + * @author 张翔 + * @date 2026-07-18 + */ +@Table("banner") +public class BannerEntity { + + @Id + private Long id; + + @Column("image_url") + private String imageUrl; + + @Column("title") + private String title; + + @Column("subtitle") + private String subtitle; + + @Column("sort_order") + private Integer sortOrder; + + @Column("is_active") + private String isActive; + + @Column("link_url") + private String linkUrl; + + @Column("created_at") + private LocalDateTime createdAt; + + @Column("updated_at") + private LocalDateTime updatedAt; + + @Column("deleted_at") + private LocalDateTime deletedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSubtitle() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public String getIsActive() { + return isActive; + } + + public void setIsActive(String isActive) { + this.isActive = isActive; + } + + public String getLinkUrl() { + return linkUrl; + } + + public void setLinkUrl(String linkUrl) { + this.linkUrl = linkUrl; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public LocalDateTime getDeletedAt() { + return deletedAt; + } + + public void setDeletedAt(LocalDateTime deletedAt) { + this.deletedAt = deletedAt; + } +} diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/BannerRepository.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/BannerRepository.java new file mode 100644 index 0000000..c9f458b --- /dev/null +++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/BannerRepository.java @@ -0,0 +1,59 @@ +package cn.novalon.gym.manage.db.repository; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import cn.novalon.gym.manage.notify.core.repository.IBannerRepository; +import cn.novalon.gym.manage.db.converter.BannerConverter; +import cn.novalon.gym.manage.db.dao.BannerDao; +import cn.novalon.gym.manage.db.entity.BannerEntity; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 轮播图仓储实现类 + * + * @author 张翔 + * @date 2026-07-18 + */ +@Repository +public class BannerRepository implements IBannerRepository { + + private final BannerDao bannerDao; + private final BannerConverter bannerConverter; + + public BannerRepository(BannerDao bannerDao, BannerConverter bannerConverter) { + this.bannerDao = bannerDao; + this.bannerConverter = bannerConverter; + } + + @Override + public Flux findByDeletedAtIsNull() { + return bannerDao.findByDeletedAtIsNull(Sort.by(Sort.Direction.ASC, "sortOrder")) + .map(bannerConverter::toDomain); + } + + @Override + public Flux findActiveBanners() { + return bannerDao.findActiveBanners() + .map(bannerConverter::toDomain); + } + + @Override + public Mono findById(Long id) { + return bannerDao.findById(id) + .map(bannerConverter::toDomain); + } + + @Override + public Mono save(Banner banner) { + BannerEntity entity = bannerConverter.toEntity(banner); + return bannerDao.save(entity) + .map(bannerConverter::toDomain); + } + + @Override + public Mono deleteByIdAndDeletedAtIsNull(Long id) { + return bannerDao.deleteByIdAndDeletedAtIsNull(id); + } +} diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V10__Create_group_course_recommend.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V10__Create_group_course_recommend.sql new file mode 100644 index 0000000..61405d2 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V10__Create_group_course_recommend.sql @@ -0,0 +1,38 @@ +-- ============================================ +-- 团课推荐表 +-- 版本: V10 +-- 描述: 创建团课推荐表,用于首页推荐团课展示 +-- ============================================ + +CREATE TABLE IF NOT EXISTS group_course_recommend ( + id BIGSERIAL PRIMARY KEY, + course_id BIGINT NOT NULL, + recommend_title VARCHAR(200), + recommend_content TEXT, + recommend_reason VARCHAR(500), + priority INTEGER DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_group_course_recommend_course_id ON group_course_recommend(course_id); +CREATE INDEX IF NOT EXISTS idx_group_course_recommend_priority ON group_course_recommend(priority); +CREATE INDEX IF NOT EXISTS idx_group_course_recommend_is_active ON group_course_recommend(is_active); + +COMMENT ON TABLE group_course_recommend IS '团课推荐表'; +COMMENT ON COLUMN group_course_recommend.id IS '主键ID'; +COMMENT ON COLUMN group_course_recommend.course_id IS '团课ID(关联group_course.id)'; +COMMENT ON COLUMN group_course_recommend.recommend_title IS '推荐标题'; +COMMENT ON COLUMN group_course_recommend.recommend_content IS '推荐内容'; +COMMENT ON COLUMN group_course_recommend.recommend_reason IS '推荐理由'; +COMMENT ON COLUMN group_course_recommend.priority IS '优先级(数字越大优先级越高)'; +COMMENT ON COLUMN group_course_recommend.is_active IS '是否启用'; +COMMENT ON COLUMN group_course_recommend.create_by IS '创建人'; +COMMENT ON COLUMN group_course_recommend.update_by IS '更新人'; +COMMENT ON COLUMN group_course_recommend.created_at IS '创建时间'; +COMMENT ON COLUMN group_course_recommend.updated_at IS '更新时间'; +COMMENT ON COLUMN group_course_recommend.deleted_at IS '删除时间(软删除)'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V11__Add_group_course_qr_code_path.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V11__Add_group_course_qr_code_path.sql new file mode 100644 index 0000000..98923df --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V11__Add_group_course_qr_code_path.sql @@ -0,0 +1,9 @@ +-- ============================================ +-- 为团课表添加二维码路径字段 +-- 版本: V11 +-- 描述: 添加团课二维码路径,用于扫码签到 +-- ============================================ + +ALTER TABLE group_course ADD COLUMN IF NOT EXISTS qr_code_path VARCHAR(500); + +COMMENT ON COLUMN group_course.qr_code_path IS '二维码图片路径'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V12__Add_deleted_at_to_member_user.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V12__Add_deleted_at_to_member_user.sql new file mode 100644 index 0000000..ef760b1 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V12__Add_deleted_at_to_member_user.sql @@ -0,0 +1,10 @@ +-- ============================================ +-- 为 member_user 表添加 deleted_at 字段 +-- 版本: V12 +-- 描述: 补充软删除时间字段,与BaseEntity映射对齐 +-- ============================================ + +ALTER TABLE member_user + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP; + +COMMENT ON COLUMN member_user.deleted_at IS '软删除时间'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Add_updated_at_to_member_card_transactions.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Add_updated_at_to_member_card_transactions.sql new file mode 100644 index 0000000..3f58785 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Add_updated_at_to_member_card_transactions.sql @@ -0,0 +1,10 @@ +-- ============================================ +-- 为 member_card_transactions 表添加 updated_at 字段 +-- 版本: V13 +-- 描述: 补充更新时间字段,与BaseEntity映射对齐 +-- ============================================ + +ALTER TABLE member_card_transactions + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; + +COMMENT ON COLUMN member_card_transactions.updated_at IS '更新时间'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V14__Create_payment_order.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V14__Create_payment_order.sql new file mode 100644 index 0000000..61edb3b --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V14__Create_payment_order.sql @@ -0,0 +1,63 @@ +-- ============================================ +-- 支付订单表 +-- 版本: V14 +-- 描述: 创建支付订单表,用于记录支付交易信息 +-- ============================================ + +CREATE TABLE IF NOT EXISTS payment_order ( + id BIGSERIAL PRIMARY KEY, + order_no VARCHAR(64) NOT NULL UNIQUE, + member_id BIGINT, + order_type VARCHAR(32), + goods_desc VARCHAR(256), + trans_amt DECIMAL(12, 2), + trade_type VARCHAR(32), + pay_status VARCHAR(16) DEFAULT 'PENDING', + huifu_id VARCHAR(64), + req_seq_id VARCHAR(64), + req_date VARCHAR(16), + hf_seq_id VARCHAR(64), + pay_info TEXT, + qr_code TEXT, + pay_url TEXT, + remark VARCHAR(512), + notify_url VARCHAR(512), + pay_time TIMESTAMP, + expire_time TIMESTAMP, + error_code VARCHAR(64), + error_msg VARCHAR(512), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_payment_order_order_no ON payment_order(order_no); +CREATE INDEX IF NOT EXISTS idx_payment_order_member_id ON payment_order(member_id); +CREATE INDEX IF NOT EXISTS idx_payment_order_req_seq_id ON payment_order(req_seq_id); +CREATE INDEX IF NOT EXISTS idx_payment_order_status ON payment_order(pay_status); +CREATE INDEX IF NOT EXISTS idx_payment_order_expire_time ON payment_order(expire_time); + +COMMENT ON TABLE payment_order IS '支付订单表'; +COMMENT ON COLUMN payment_order.order_no IS '订单号'; +COMMENT ON COLUMN payment_order.member_id IS '会员ID'; +COMMENT ON COLUMN payment_order.order_type IS '订单类型'; +COMMENT ON COLUMN payment_order.goods_desc IS '商品描述'; +COMMENT ON COLUMN payment_order.trans_amt IS '交易金额'; +COMMENT ON COLUMN payment_order.trade_type IS '交易类型'; +COMMENT ON COLUMN payment_order.pay_status IS '支付状态:PENDING-待支付,SUCCESS-成功,FAILED-失败'; +COMMENT ON COLUMN payment_order.huifu_id IS '汇付商户ID'; +COMMENT ON COLUMN payment_order.req_seq_id IS '请求流水号'; +COMMENT ON COLUMN payment_order.req_date IS '请求日期'; +COMMENT ON COLUMN payment_order.hf_seq_id IS '汇付流水号'; +COMMENT ON COLUMN payment_order.pay_info IS '支付信息'; +COMMENT ON COLUMN payment_order.qr_code IS '二维码'; +COMMENT ON COLUMN payment_order.pay_url IS '支付链接'; +COMMENT ON COLUMN payment_order.remark IS '备注'; +COMMENT ON COLUMN payment_order.notify_url IS '通知地址'; +COMMENT ON COLUMN payment_order.pay_time IS '支付时间'; +COMMENT ON COLUMN payment_order.expire_time IS '过期时间'; +COMMENT ON COLUMN payment_order.error_code IS '错误码'; +COMMENT ON COLUMN payment_order.error_msg IS '错误信息'; +COMMENT ON COLUMN payment_order.created_at IS '创建时间'; +COMMENT ON COLUMN payment_order.updated_at IS '更新时间'; +COMMENT ON COLUMN payment_order.deleted_at IS '删除时间'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V15__Add_party_order_id.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V15__Add_party_order_id.sql new file mode 100644 index 0000000..72cda57 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V15__Add_party_order_id.sql @@ -0,0 +1,9 @@ +-- ============================================ +-- 支付订单表添加 party_order_id 字段 +-- 版本: V15 +-- 描述: 添加汇付支付返回的商户订单号 +-- ============================================ + +ALTER TABLE payment_order ADD COLUMN IF NOT EXISTS party_order_id VARCHAR(64); + +COMMENT ON COLUMN payment_order.party_order_id IS '汇付商户订单号'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V16__Create_stored_card.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V16__Create_stored_card.sql new file mode 100644 index 0000000..8d64f83 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V16__Create_stored_card.sql @@ -0,0 +1,58 @@ +-- ============================================ +-- 储值卡相关表 +-- 版本: V16 +-- 描述: 创建储值卡表和充值记录表,每个会员只有一张储值卡 +-- ============================================ + +-- 储值卡表(包含支付密码) +CREATE TABLE IF NOT EXISTS member_stored_card ( + id BIGSERIAL PRIMARY KEY, + member_id BIGINT NOT NULL, + balance DECIMAL(10, 2) DEFAULT 0.00, + total_recharge DECIMAL(10, 2) DEFAULT 0.00, + total_consume DECIMAL(10, 2) DEFAULT 0.00, + pay_password VARCHAR(255), + is_pay_password_set BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_member_stored_card_member_id ON member_stored_card(member_id) WHERE deleted_at IS NULL; + +COMMENT ON TABLE member_stored_card IS '储值卡表,每个会员只有一张储值卡,包含支付密码'; +COMMENT ON COLUMN member_stored_card.member_id IS '会员用户ID'; +COMMENT ON COLUMN member_stored_card.balance IS '储值卡余额'; +COMMENT ON COLUMN member_stored_card.total_recharge IS '累计充值金额'; +COMMENT ON COLUMN member_stored_card.total_consume IS '累计消费金额'; +COMMENT ON COLUMN member_stored_card.pay_password IS '支付密码(BCrypt加密)'; +COMMENT ON COLUMN member_stored_card.is_pay_password_set IS '是否已设置支付密码'; + +-- 储值卡充值记录表 +CREATE TABLE IF NOT EXISTS member_stored_card_recharge ( + id BIGSERIAL PRIMARY KEY, + member_id BIGINT NOT NULL, + order_no VARCHAR(64) NOT NULL, + recharge_amount DECIMAL(10, 2) DEFAULT 0.00, + bonus_amount DECIMAL(10, 2) DEFAULT 0.00, + total_amount DECIMAL(10, 2) DEFAULT 0.00, + pay_amount DECIMAL(10, 2) DEFAULT 0.00, + status VARCHAR(32) DEFAULT 'PENDING', + pay_time TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_stored_card_recharge_member_id ON member_stored_card_recharge(member_id) WHERE deleted_at IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_stored_card_recharge_order_no ON member_stored_card_recharge(order_no) WHERE deleted_at IS NULL; + +COMMENT ON TABLE member_stored_card_recharge IS '储值卡充值记录表'; +COMMENT ON COLUMN member_stored_card_recharge.member_id IS '会员用户ID'; +COMMENT ON COLUMN member_stored_card_recharge.order_no IS '订单号'; +COMMENT ON COLUMN member_stored_card_recharge.recharge_amount IS '充值金额'; +COMMENT ON COLUMN member_stored_card_recharge.bonus_amount IS '赠送金额'; +COMMENT ON COLUMN member_stored_card_recharge.total_amount IS '到账总金额(充值+赠送)'; +COMMENT ON COLUMN member_stored_card_recharge.pay_amount IS '实际支付金额'; +COMMENT ON COLUMN member_stored_card_recharge.status IS '充值状态:PENDING-待支付,SUCCESS-成功,FAILED-失败'; +COMMENT ON COLUMN member_stored_card_recharge.pay_time IS '支付时间'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V17__Make_member_card_id_nullable.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V17__Make_member_card_id_nullable.sql new file mode 100644 index 0000000..6bf2e9e --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V17__Make_member_card_id_nullable.sql @@ -0,0 +1,8 @@ +-- ============================================ +-- 将 group_course_booking 表的 member_card_id 改为可空 +-- 版本: V17 +-- 描述: 预约团课不再需要会员卡,member_card_id 允许为空 +-- ============================================ + +ALTER TABLE group_course_booking + ALTER COLUMN member_card_id DROP NOT NULL; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql new file mode 100644 index 0000000..4fcc7e9 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql @@ -0,0 +1,209 @@ +-- ============================================ +-- 团课测试数据 +-- 版本: V18 +-- 描述: 插入团课标签、类型、关联关系及课程数据 +-- 5个团课类型,14个标签,每类型3个标签,每类型5个团课 +-- ============================================ + +-- ============================================ +-- 1. 团课标签数据(14个) +-- ============================================ + +INSERT INTO course_label (label_name, color, description) VALUES +('适合新手', '#52c41a', '适合健身初学者,动作简单易学'), +('中级过渡', '#faad14', '适合有一定基础的学员,逐步提升难度'), +('高级进阶', '#f5222d', '适合高级学员,强度和技巧要求较高'), +('减脂塑形', '#722ed1', '高效燃烧卡路里,助力减脂和体型塑造'), +('增肌强化', '#13c2c2', '注重肌肉力量和围度增长'), +('柔韧性训练', '#eb2f96', '提升身体柔韧性和关节活动度'), +('核心训练', '#1890ff', '强化核心肌群,改善身体稳定性'), +('心肺训练', '#fa8c16', '提升心肺功能和耐力水平'), +('低冲击', '#52c41a', '低冲击运动,对关节友好'), +('高强度', '#f5222d', '高强度训练,短时间内最大化训练效果'), +('团体互动', '#722ed1', '注重团队协作和互动氛围'), +('私教推荐', '#13c2c2', '私教推荐的高效训练课程'), +('热门课程', '#ff1493', '人气较高,广受学员欢迎'), +('新课上线', '#00ced1', '新上线课程,欢迎尝鲜体验') +ON CONFLICT DO NOTHING; + + +-- ============================================ +-- 2. 团课类型数据(5个) +-- ============================================ + +INSERT INTO group_course_type (type_name, base_difficulty, description, category) VALUES +('瑜伽', 3, '通过呼吸调控与体式练习,提升身体柔韧性、力量与心灵平静,适合各年龄段人群', '柔韧与平衡类'), +('动感单车', 5, '伴随动感音乐节奏进行室内骑行,高效燃烧卡路里,释放压力,体验极速快感', '基础有氧与热身'), +('HIIT训练', 8, '高强度间歇训练,通过短时爆发动作与间歇休息交替,短时间内达到最大燃脂效果', '高强度与爆发力'), +('普拉提', 4, '注重核心肌群控制、脊柱对齐和呼吸配合,有效改善体态,预防运动损伤', '柔韧与平衡类'), +('搏击操', 6, '融合拳击、踢腿、格斗动作的有氧运动,宣泄压力同时高效塑形,增强协调性', '高强度与爆发力') +ON CONFLICT DO NOTHING; + + +-- ============================================ +-- 3. 类型-标签关联数据(每类型3个标签,共15条) +-- ============================================ + +INSERT INTO course_type_label (type_id, label_id) +SELECT gt.id, cl.id FROM group_course_type gt, course_label cl +WHERE gt.type_name = '瑜伽' AND cl.label_name = '柔韧性训练' + OR gt.type_name = '瑜伽' AND cl.label_name = '低冲击' + OR gt.type_name = '瑜伽' AND cl.label_name = '适合新手' + OR gt.type_name = '动感单车' AND cl.label_name = '心肺训练' + OR gt.type_name = '动感单车' AND cl.label_name = '热门课程' + OR gt.type_name = '动感单车' AND cl.label_name = '中级过渡' + OR gt.type_name = 'HIIT训练' AND cl.label_name = '高强度' + OR gt.type_name = 'HIIT训练' AND cl.label_name = '减脂塑形' + OR gt.type_name = 'HIIT训练' AND cl.label_name = '高级进阶' + OR gt.type_name = '普拉提' AND cl.label_name = '核心训练' + OR gt.type_name = '普拉提' AND cl.label_name = '柔韧性训练' + OR gt.type_name = '普拉提' AND cl.label_name = '适合新手' + OR gt.type_name = '搏击操' AND cl.label_name = '高强度' + OR gt.type_name = '搏击操' AND cl.label_name = '减脂塑形' + OR gt.type_name = '搏击操' AND cl.label_name = '心肺训练' +ON CONFLICT DO NOTHING; + + +-- ============================================ +-- 4. 团课课程数据(5个类型 × 5个课程 = 25个) +-- start_time / end_time 分布在 2026-07-20 至 2026-08-02 +-- ============================================ + +-- ---------- 瑜伽课程(5个)---------- + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '哈他瑜伽入门', gt.id, '2026-07-20 08:00:00'::TIMESTAMP, '2026-07-20 09:00:00'::TIMESTAMP, 25, 12, '0', '瑜伽室A', '/static/course/yoga_hatha.jpg', '经典哈他瑜伽,从基础体式入手,配合呼吸引导,帮助初学者建立正确的瑜伽练习基础,感受身心的和谐统一。', 88.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '瑜伽'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '流瑜伽', gt.id, '2026-07-22 10:00:00'::TIMESTAMP, '2026-07-22 11:00:00'::TIMESTAMP, 20, 18, '0', '瑜伽室A', '/static/course/yoga_flow.jpg', '体式之间流畅串联,如行云流水般一气呵成。以拜日式为根基,结合站姿、平衡和扭转,提升力量与柔韧的整合能力。', 98.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '瑜伽'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '阴瑜伽', gt.id, '2026-07-24 18:00:00'::TIMESTAMP, '2026-07-24 19:15:00'::TIMESTAMP, 20, 8, '0', '瑜伽室B', '/static/course/yoga_yin.jpg', '阴瑜伽以长时间保持体式为特点,深度伸展筋膜和结缔组织,帮助你释放身体深层紧张,缓解一周的工作疲劳。适合所有级别。', 88.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '瑜伽'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '阿斯汤加瑜伽', gt.id, '2026-07-21 07:00:00'::TIMESTAMP, '2026-07-21 08:30:00'::TIMESTAMP, 15, 10, '0', '瑜伽室A', '/static/course/yoga_ashtanga.jpg', '阿斯汤加瑜伽以固定的体式序列和独特的呼吸法为核心,节奏紧凑,强度较高,能有效提升力量、柔韧和专注力。建议有一定瑜伽基础者参加。', 128.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '瑜伽'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '空中瑜伽', gt.id, '2026-07-23 19:00:00'::TIMESTAMP, '2026-07-23 20:00:00'::TIMESTAMP, 12, 12, '0', '瑜伽室B', '/static/course/yoga_aerial.jpg', '利用悬挂的瑜伽吊床完成各种体式,借助重力让脊柱自然延展,体验"飞翔"般的自由感。深受女性学员喜爱,名额有限请提前预约。', 128.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '瑜伽'; + + +-- ---------- 动感单车课程(5个)---------- + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '初级燃脂骑行', gt.id, '2026-07-20 19:00:00'::TIMESTAMP, '2026-07-20 19:45:00'::TIMESTAMP, 30, 22, '0', '单车房', '/static/course/spin_beginner.jpg', '适合新手的入门级骑行课程,教练将带领你掌握正确的骑行姿势和阻力调节技巧。在动感音乐中轻松燃烧300-400卡路里。', 58.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '动感单车'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '节奏爬坡', gt.id, '2026-07-22 18:30:00'::TIMESTAMP, '2026-07-22 19:15:00'::TIMESTAMP, 30, 15, '0', '单车房', '/static/course/spin_climb.jpg', '模拟山路爬坡骑行,通过逐渐增加阻力来挑战你的耐力和意志力。每一段爬坡后都有短暂的平路冲刺,节奏感十足,大汗淋漓的畅快体验。', 68.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '动感单车'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '极速冲刺', gt.id, '2026-07-24 20:00:00'::TIMESTAMP, '2026-07-24 20:45:00'::TIMESTAMP, 25, 25, '0', '单车房', '/static/course/spin_sprint.jpg', '高强度冲刺为主题的骑行课,多组30秒极限冲刺搭配短暂恢复,彻底引爆你的心肺极限。周五夜间的爆款课程,约满速度极快!', 68.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '动感单车'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '耐力骑行', gt.id, '2026-07-25 10:00:00'::TIMESTAMP, '2026-07-25 11:00:00'::TIMESTAMP, 30, 18, '0', '单车房', '/static/course/spin_endurance.jpg', '60分钟中低强度耐力骑行,适合周末早晨唤醒身体。在稳定的节奏中持续燃脂,配以舒缓的收尾拉伸,开启元气满满的周末。', 58.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '动感单车'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '音乐主题骑行', gt.id, '2026-07-21 19:30:00'::TIMESTAMP, '2026-07-21 20:15:00'::TIMESTAMP, 30, 20, '0', '单车房', '/static/course/spin_music.jpg', '以热门流行音乐为主题的骑行派对!每首歌曲对应一套骑行组合,跟着节拍加速、爬坡、冲刺,在音乐中忘记疲惫。', 68.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '动感单车'; + + +-- ---------- HIIT训练课程(5个)---------- + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT 'Tabata燃脂', gt.id, '2026-07-20 07:00:00'::TIMESTAMP, '2026-07-20 07:30:00'::TIMESTAMP, 15, 8, '0', '多功能训练区', '/static/course/hiit_tabata.jpg', '经典的Tabata模式:20秒全力运动 + 10秒休息,循环8轮共4分钟,搭配热身和拉伸,30分钟全身燃爆。早晨空腹训练燃脂效果加倍!', 98.00, 'system' +FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '全身循环训练', gt.id, '2026-07-22 12:00:00'::TIMESTAMP, '2026-07-22 12:45:00'::TIMESTAMP, 15, 12, '0', '多功能训练区', '/static/course/hiit_circuit.jpg', '午间燃脂利器!8个动作站点轮流进行,涵盖深蹲跳、波比跳、壶铃摇摆、登山跑等,每个动作45秒,休息15秒,3轮循环让你全身湿透。', 98.00, 'system' +FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '核心爆发力', gt.id, '2026-07-24 17:30:00'::TIMESTAMP, '2026-07-24 18:15:00'::TIMESTAMP, 12, 5, '0', '多功能训练区', '/static/course/hiit_core.jpg', '专注于核心肌群的HIIT训练,结合药球砸地、悬垂举腿、俄罗斯转体和平板支撑变式,打造钢铁般的核心力量,提升所有运动表现。', 108.00, 'system' +FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '战绳挑战', gt.id, '2026-07-25 08:00:00'::TIMESTAMP, '2026-07-25 08:45:00'::TIMESTAMP, 10, 10, '0', '户外训练区', '/static/course/hiit_battle.jpg', '风靡全球的战绳训练!通过波浪、猛击、旋转等动作模式激活全身肌群,30秒一组高强度间歇,燃脂同时雕刻上肢线条。名额有限,已约满!', 128.00, 'system' +FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '药球HIIT', gt.id, '2026-07-26 10:00:00'::TIMESTAMP, '2026-07-26 10:45:00'::TIMESTAMP, 12, 6, '0', '多功能训练区', '/static/course/hiit_medball.jpg', '以药球为主要工具的HIIT训练。药球砸地、旋转投掷、过头抛接等动作兼具力量和爆发力训练,动作多样不枯燥,周日上午的爆汗之选。', 108.00, 'system' +FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; + + +-- ---------- 普拉提课程(5个)---------- + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '垫上普拉提', gt.id, '2026-07-21 10:00:00'::TIMESTAMP, '2026-07-21 11:00:00'::TIMESTAMP, 20, 14, '0', '普拉提室', '/static/course/pilates_mat.jpg', '经典垫上普拉提,通过精准的动作控制激活深层核心肌群。从呼吸到骨盆稳定,从脊柱逐节卷动到四肢协调,一节课改善你的身体感知。', 108.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '普拉提'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '器械普拉提', gt.id, '2026-07-23 09:00:00'::TIMESTAMP, '2026-07-23 10:00:00'::TIMESTAMP, 10, 8, '0', '普拉提室', '/static/course/pilates_reformer.jpg', '使用专业普拉提核心床(Reformer)进行训练,弹簧阻力提供精准的负荷控制,适合需要针对性改善体态、康复训练或追求高效塑形的学员。', 158.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '普拉提'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '核心塑形', gt.id, '2026-07-25 14:00:00'::TIMESTAMP, '2026-07-25 15:00:00'::TIMESTAMP, 15, 9, '0', '普拉提室', '/static/course/pilates_sculpt.jpg', '专注于腹部、腰部和臀部的普拉提塑形课程。百次拍击、剪刀腿、肩桥等经典动作持续刺激目标肌群,打造紧致核心线条。', 128.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '普拉提'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '脊柱健康', gt.id, '2026-07-20 16:00:00'::TIMESTAMP, '2026-07-20 17:00:00'::TIMESTAMP, 15, 6, '0', '普拉提室', '/static/course/pilates_spine.jpg', '专为久坐办公人群设计,通过普拉提脊柱逐节运动改善驼背、圆肩等不良体态。融合猫牛式、脊柱旋转和游泳式,给你的脊柱一次深度保养。', 108.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '普拉提'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '产后恢复普拉提', gt.id, '2026-07-22 11:00:00'::TIMESTAMP, '2026-07-22 12:00:00'::TIMESTAMP, 10, 4, '0', '普拉提室', '/static/course/pilates_postnatal.jpg', '专为产后妈妈设计的温和修复课程,重点激活盆底肌和腹横肌,循序渐进恢复核心力量。小班教学,教练一对一关注每位学员的姿势质量。', 158.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '普拉提'; + + +-- ---------- 搏击操课程(5个)---------- + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '有氧搏击基础', gt.id, '2026-07-21 18:00:00'::TIMESTAMP, '2026-07-21 19:00:00'::TIMESTAMP, 25, 20, '0', '操厅A', '/static/course/boxing_basic.jpg', '零基础友好的搏击操入门课。从直拳、摆拳、勾拳到前踢、侧踹,教练分步讲解每个动作的要领,配合动感音乐串联成完整的搏击组合。', 68.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '搏击操'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT 'Kickboxing进阶', gt.id, '2026-07-23 19:00:00'::TIMESTAMP, '2026-07-23 20:00:00'::TIMESTAMP, 20, 15, '0', '操厅A', '/static/course/boxing_kickboxing.jpg', '融合踢拳技术的进阶搏击课程,增加连击组合、闪躲步法和膝肘技术。强度较基础课明显提升,汗如雨下的同时释放工作压力,宣泄效果一流。', 88.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '搏击操'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '综合格斗体能', gt.id, '2026-07-25 16:00:00'::TIMESTAMP, '2026-07-25 17:00:00'::TIMESTAMP, 20, 12, '0', '操厅A', '/static/course/boxing_mma.jpg', '融合拳击、泰拳、摔跤元素的综合体能训练。沙袋击打、地面对抗和爆发力训练交替进行,全面提升力量、速度和耐力。周六下午的硬核之选!', 98.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '搏击操'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '燃脂拳击', gt.id, '2026-07-20 12:00:00'::TIMESTAMP, '2026-07-20 13:00:00'::TIMESTAMP, 25, 16, '0', '操厅A', '/static/course/boxing_fatburn.jpg', '午间燃脂拳击课。空击组合与沙袋击打间歇进行,心率维持在高燃脂区间。一节课可消耗500-700卡路里,中午来打拳比喝咖啡更提神!', 68.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '搏击操'; + +INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) +SELECT '泰拳基础', gt.id, '2026-07-26 15:00:00'::TIMESTAMP, '2026-07-26 16:00:00'::TIMESTAMP, 15, 7, '0', '操厅A', '/static/course/boxing_muaythai.jpg', '学习泰拳的八肢艺术(双拳、双腿、双膝、双肘),从基本站架到肘膝组合,感受泰拳的刚猛魅力。小班教学保证动作纠正质量。', 98.00, 'system' +FROM group_course_type gt WHERE gt.type_name = '搏击操'; + + +-- ============================================ +-- 5. 团课推荐数据(每类型推荐1个热门课程,共5条) +-- ============================================ + +INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active) +SELECT gc.id, '清晨之选 - 哈他瑜伽入门', '周一早晨8点,用温和的瑜伽唤醒身体,开启高效一周', '最受欢迎的入门瑜伽课,教练耐心细致', 5, TRUE +FROM group_course gc WHERE gc.course_name = '哈他瑜伽入门'; + +INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active) +SELECT gc.id, '周五狂欢 - 极速冲刺', '周五晚8点,用极速骑行释放一周的疲惫与压力', '单车房王牌课程,周周爆满,燃烧500+卡路里', 4, TRUE +FROM group_course gc WHERE gc.course_name = '极速冲刺'; + +INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active) +SELECT gc.id, '午间爆汗 - 全身循环训练', '午休45分钟,8个站点循环,高效利用碎片时间燃脂', '午间最受欢迎的HIIT课,适合忙碌的上班族', 3, TRUE +FROM group_course gc WHERE gc.course_name = '全身循环训练'; + +INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active) +SELECT gc.id, '久坐救星 - 脊柱健康普拉提', '专为久坐族设计的脊柱保养课,改善驼背圆肩', '全网好评,学员反馈"一节课腰就不疼了"', 2, TRUE +FROM group_course gc WHERE gc.course_name = '脊柱健康'; + +INSERT INTO group_course_recommend (course_id, recommend_title, recommend_content, recommend_reason, priority, is_active) +SELECT gc.id, '释压首选 - 有氧搏击基础', '零基础也能打!在拳拳到肉的快感中忘记烦恼', '新手友好,发泄压力效果拔群,超过200名学员推荐', 1, TRUE +FROM group_course gc WHERE gc.course_name = '有氧搏击基础'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V19__Insert_system_data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V19__Insert_system_data.sql new file mode 100644 index 0000000..9f6d5da --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V19__Insert_system_data.sql @@ -0,0 +1,315 @@ +-- ============================================ +-- V19: 系统初始数据(角色/用户/权限/菜单/字典/配置) +-- 来源:合并原 V2 + V22 + V28 + V29 + V30 + V31 +-- ============================================ + +-- ============================================ +-- 1. 角色数据 +-- ============================================ +INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by, created_at, updated_at) +VALUES +(1, '超级管理员', 'admin', 1, 1, 'system', 'system', NOW(), NOW()), +(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system', NOW(), NOW()), +(3, '普通用户', 'normal_user', 3, 1, 'system', 'system', NOW(), NOW()), +(4, '访客', 'guest', 4, 1, 'system', 'system', NOW(), NOW()); + +SELECT setval('sys_role_id_seq', 4); + +-- ============================================ +-- 2. 用户数据(密码均为 Test@123) +-- ============================================ +INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by, created_at, updated_at) +VALUES +(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system', NOW(), NOW()), +(2, 'user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'user@novalon.com', '13800138001', '普通用户', 1, 'system', 'system', NOW(), NOW()), +(10,'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system', NOW(), NOW()) +ON CONFLICT (username) DO UPDATE SET + password = EXCLUDED.password, + status = EXCLUDED.status; + +SELECT setval('sys_user_id_seq', 10); + +-- ============================================ +-- 3. 用户角色关联 +-- ============================================ +INSERT INTO user_role (user_id, role_id, created_by, created_at) +VALUES +(1, 1, 'system', NOW()), +(2, 3, 'system', NOW()), +(10, 1, 'system', NOW()) +ON CONFLICT (user_id, role_id) DO NOTHING; + +-- ============================================ +-- 4. 权限数据 +-- ============================================ + +-- 4.1 系统管理权限 +INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES +('用户查看', 'system:user:view', '/api/users', 'GET', '查看用户列表', 1, 'system', 'system', NOW(), NOW()), +('用户创建', 'system:user:create', '/api/users', 'POST', '创建用户', 1, 'system', 'system', NOW(), NOW()), +('用户编辑', 'system:user:edit', '/api/users', 'PUT', '编辑用户', 1, 'system', 'system', NOW(), NOW()), +('用户删除', 'system:user:delete', '/api/users', 'DELETE', '删除用户', 1, 'system', 'system', NOW(), NOW()), +('角色查看', 'system:role:view', '/api/roles', 'GET', '查看角色列表', 1, 'system', 'system', NOW(), NOW()), +('角色创建', 'system:role:create', '/api/roles', 'POST', '创建角色', 1, 'system', 'system', NOW(), NOW()), +('角色编辑', 'system:role:edit', '/api/roles', 'PUT', '编辑角色', 1, 'system', 'system', NOW(), NOW()), +('角色删除', 'system:role:delete', '/api/roles', 'DELETE', '删除角色', 1, 'system', 'system', NOW(), NOW()), +('角色分配权限', 'system:role:assign', '/api/roles/*/permissions', 'POST', '为角色分配权限', 1, 'system', 'system', NOW(), NOW()), +('权限查看', 'system:permission:view', '/api/permissions', 'GET', '查看权限列表', 1, 'system', 'system', NOW(), NOW()), +('权限创建', 'system:permission:create', '/api/permissions', 'POST', '创建权限', 1, 'system', 'system', NOW(), NOW()), +('权限编辑', 'system:permission:edit', '/api/permissions', 'PUT', '编辑权限', 1, 'system', 'system', NOW(), NOW()), +('权限删除', 'system:permission:delete', '/api/permissions', 'DELETE', '删除权限', 1, 'system', 'system', NOW(), NOW()), +('菜单查看', 'system:menu:view', '/api/menus', 'GET', '查看菜单列表', 1, 'system', 'system', NOW(), NOW()), +('菜单创建', 'system:menu:create', '/api/menus', 'POST', '创建菜单', 1, 'system', 'system', NOW(), NOW()), +('菜单编辑', 'system:menu:edit', '/api/menus', 'PUT', '编辑菜单', 1, 'system', 'system', NOW(), NOW()), +('菜单删除', 'system:menu:delete', '/api/menus', 'DELETE', '删除菜单', 1, 'system', 'system', NOW(), NOW()), +('字典查看', 'system:dict:view', '/api/dict', 'GET', '查看字典列表', 1, 'system', 'system', NOW(), NOW()), +('字典创建', 'system:dict:create', '/api/dict', 'POST', '创建字典', 1, 'system', 'system', NOW(), NOW()), +('字典编辑', 'system:dict:edit', '/api/dict', 'PUT', '编辑字典', 1, 'system', 'system', NOW(), NOW()), +('字典删除', 'system:dict:delete', '/api/dict', 'DELETE', '删除字典', 1, 'system', 'system', NOW(), NOW()), +('配置查看', 'system:config:view', '/api/config', 'GET', '查看系统配置', 1, 'system', 'system', NOW(), NOW()), +('配置创建', 'system:config:create', '/api/config', 'POST', '创建系统配置', 1, 'system', 'system', NOW(), NOW()), +('配置编辑', 'system:config:edit', '/api/config', 'PUT', '编辑系统配置', 1, 'system', 'system', NOW(), NOW()), +('配置删除', 'system:config:delete', '/api/config', 'DELETE', '删除系统配置', 1, 'system', 'system', NOW(), NOW()), +('日志查看', 'system:log:view', '/api/logs', 'GET', '查看日志', 1, 'system', 'system', NOW(), NOW()), +('文件上传', 'system:file:upload', '/api/files/upload', 'POST', '上传文件', 1, 'system', 'system', NOW(), NOW()), +('文件下载', 'system:file:download', '/api/files/download', 'GET', '下载文件', 1, 'system', 'system', NOW(), NOW()), +('文件删除', 'system:file:delete', '/api/files', 'DELETE', '删除文件', 1, 'system', 'system', NOW(), NOW()), +('公告查看', 'system:notice:view', '/api/notices', 'GET', '查看公告', 1, 'system', 'system', NOW(), NOW()), +('公告创建', 'system:notice:create', '/api/notices', 'POST', '创建公告', 1, 'system', 'system', NOW(), NOW()), +('公告编辑', 'system:notice:edit', '/api/notices', 'PUT', '编辑公告', 1, 'system', 'system', NOW(), NOW()), +('公告删除', 'system:notice:delete', '/api/notices', 'DELETE', '删除公告', 1, 'system', 'system', NOW(), NOW()); + +-- 4.2 业务模块权限 +INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES +-- 会员管理 +('会员查看', 'business:member:view', '/api/admin/members', 'GET', '查看会员列表及详情', 1, 'system', 'system', NOW(), NOW()), +('会员编辑', 'business:member:edit', '/api/admin/member', 'PUT', '编辑会员信息', 1, 'system', 'system', NOW(), NOW()), +('会员手机修改', 'business:member:phone', '/api/admin/member/phone', 'POST', '管理员修改会员手机号', 1, 'system', 'system', NOW(), NOW()), +-- 会员卡管理 +('会员卡查看', 'business:memberCard:view', '/api/member-cards', 'GET', '查看会员卡列表及详情', 1, 'system', 'system', NOW(), NOW()), +('会员卡创建', 'business:memberCard:create', '/api/member-cards', 'POST', '创建会员卡类型', 1, 'system', 'system', NOW(), NOW()), +('会员卡编辑', 'business:memberCard:edit', '/api/member-cards', 'PUT', '编辑会员卡类型', 1, 'system', 'system', NOW(), NOW()), +('会员卡删除', 'business:memberCard:delete', '/api/member-cards', 'DELETE', '删除会员卡类型', 1, 'system', 'system', NOW(), NOW()), +('会员卡购买', 'business:memberCard:purchase', '/api/member-card-records/purchase', 'POST', '为会员购买/绑定会员卡', 1, 'system', 'system', NOW(), NOW()), +('会员卡退卡', 'business:memberCard:refund', '/api/member-card-records/refund', 'POST', '会员卡退卡/解绑', 1, 'system', 'system', NOW(), NOW()), +-- 团课管理 +('团课查看', 'business:groupCourse:view', '/api/groupCourse', 'GET', '查看团课列表及详情', 1, 'system', 'system', NOW(), NOW()), +('团课创建', 'business:groupCourse:create', '/api/groupCourse', 'POST', '创建团课', 1, 'system', 'system', NOW(), NOW()), +('团课编辑', 'business:groupCourse:edit', '/api/groupCourse', 'PUT', '编辑团课信息', 1, 'system', 'system', NOW(), NOW()), +('团课删除', 'business:groupCourse:delete', '/api/groupCourse', 'DELETE', '删除团课', 1, 'system', 'system', NOW(), NOW()), +('团课取消', 'business:groupCourse:cancel', '/api/groupCourse/cancel', 'POST', '取消团课', 1, 'system', 'system', NOW(), NOW()), +-- 团课类型管理 +('团课类型查看', 'business:groupCourseType:view', '/api/groupCourse/types', 'GET', '查看团课类型列表', 1, 'system', 'system', NOW(), NOW()), +('团课类型创建', 'business:groupCourseType:create', '/api/groupCourse/types', 'POST', '创建团课类型', 1, 'system', 'system', NOW(), NOW()), +('团课类型编辑', 'business:groupCourseType:edit', '/api/groupCourse/types', 'PUT', '编辑团课类型', 1, 'system', 'system', NOW(), NOW()), +('团课类型删除', 'business:groupCourseType:delete', '/api/groupCourse/types', 'DELETE', '删除团课类型', 1, 'system', 'system', NOW(), NOW()), +-- 团课推荐管理 +('团课推荐查看', 'business:groupCourseRecommend:view', '/api/groupCourse/recommend', 'GET', '查看推荐团课列表', 1, 'system', 'system', NOW(), NOW()), +('团课推荐创建', 'business:groupCourseRecommend:create','/api/groupCourse/recommend', 'POST', '创建推荐团课', 1, 'system', 'system', NOW(), NOW()), +('团课推荐编辑', 'business:groupCourseRecommend:edit', '/api/groupCourse/recommend', 'PUT', '编辑推荐团课', 1, 'system', 'system', NOW(), NOW()), +('团课推荐删除', 'business:groupCourseRecommend:delete','/api/groupCourse/recommend', 'DELETE', '删除推荐团课', 1, 'system', 'system', NOW(), NOW()), +-- 团课预约管理 +('团课预约查看', 'business:groupCourseBooking:view', '/api/groupCourse/bookings', 'GET', '查看团课预约记录', 1, 'system', 'system', NOW(), NOW()), +('团课预约创建', 'business:groupCourseBooking:create', '/api/groupCourse/book', 'POST', '预约团课', 1, 'system', 'system', NOW(), NOW()), +('团课预约取消', 'business:groupCourseBooking:cancel', '/api/groupCourse/booking/cancel', 'POST', '取消团课预约', 1, 'system', 'system', NOW(), NOW()), +-- 签到管理 +('签到查看', 'business:checkIn:view', '/api/checkIn/records', 'GET', '查看签到记录', 1, 'system', 'system', NOW(), NOW()), +('签到操作', 'business:checkIn:create', '/api/checkIn', 'POST', '执行签到操作', 1, 'system', 'system', NOW(), NOW()), +('签到导出', 'business:checkIn:export', '/api/checkIn/records/export', 'GET', '导出签到记录', 1, 'system', 'system', NOW(), NOW()), +-- 数据统计 +('数据统计查看', 'business:dataCount:view', '/api/datacount', 'GET', '查看数据统计报表', 1, 'system', 'system', NOW(), NOW()), +('数据统计导出', 'business:dataCount:export', '/api/datacount/export', 'GET', '导出统计报表', 1, 'system', 'system', NOW(), NOW()), +-- 员工管理 +('员工查看', 'business:employee:view', '/api/employees/page', 'GET', '查看员工列表', 1, 'system', 'system', NOW(), NOW()), +('员工创建', 'business:employee:create', '/api/users', 'POST', '创建员工账号', 1, 'system', 'system', NOW(), NOW()), +('员工编辑', 'business:employee:edit', '/api/users', 'PUT', '编辑员工信息', 1, 'system', 'system', NOW(), NOW()), +('员工删除', 'business:employee:delete', '/api/users', 'DELETE', '删除/禁用员工账号', 1, 'system', 'system', NOW(), NOW()), +('员工分配角色', 'business:employee:assignRole', '/api/users/roles', 'POST', '为员工分配角色', 1, 'system', 'system', NOW(), NOW()); + +-- ============================================ +-- 5. 为超级管理员角色分配所有权限 +-- ============================================ +INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by, created_at, updated_at) +SELECT 1, id, 'system', 'system', NOW(), NOW() FROM sys_permission WHERE status = 1 + AND id NOT IN (SELECT permission_id FROM sys_role_permission WHERE role_id = 1); + +-- ============================================ +-- 6. 菜单数据 +-- ============================================ + +-- 6.1 一级菜单 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(1, '系统管理', 0, 1, 'M', NULL, NULL, 1, NOW(), NOW()), +(2, '审计日志', 0, 2, 'M', NULL, NULL, 1, NOW(), NOW()), +(3, '系统监控', 0, 3, 'M', NULL, NULL, 1, NOW(), NOW()), +(400, '团课管理', 0, 4, 'M', NULL, NULL, 1, NOW(), NOW()), +(500, '会员管理', 0, 5, 'M', NULL, NULL, 1, NOW(), NOW()), +(502, '会员卡管理', 0, 6, 'M', NULL, NULL, 1, NOW(), NOW()), +(504, '数据统计', 0, 7, 'M', NULL, NULL, 1, NOW(), NOW()); + +-- 6.2 系统管理 - 子菜单 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(11, '用户管理', 1, 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()), +(12, '角色管理', 1, 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()), +(13, '菜单管理', 1, 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()), +(14, '部门管理', 1, 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()), +(15, '字典管理', 1, 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()), +(16, '参数管理', 1, 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()), +(17, '通知公告', 1, 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()), +(18, '文件管理', 1, 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW()), +(19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/index', 1, NOW(), NOW()); + +-- 6.3 系统管理 - 按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +-- 用户管理按钮 +(111, '用户查询', 11, 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()), +(112, '用户新增', 11, 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()), +(113, '用户修改', 11, 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()), +(114, '用户删除', 11, 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()), +(115, '用户导出', 11, 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()), +(116, '用户导入', 11, 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()), +(117, '重置密码', 11, 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW()), +-- 角色管理按钮 +(121, '角色查询', 12, 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()), +(122, '角色新增', 12, 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()), +(123, '角色修改', 12, 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()), +(124, '角色删除', 12, 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()), +(125, '角色导出', 12, 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW()), +-- 菜单管理按钮 +(131, '菜单查询', 13, 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()), +(132, '菜单新增', 13, 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()), +(133, '菜单修改', 13, 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()), +(134, '菜单删除', 13, 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW()); + +-- 6.4 审计日志 - 子菜单 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(21, '操作日志', 2, 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()), +(22, '登录日志', 2, 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()), +(23, '异常日志', 2, 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW()); + +-- 6.5 审计日志 - 按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(211, '操作查询', 21, 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()), +(212, '操作删除', 21, 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()), +(213, '操作导出', 21, 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW()), +(221, '登录查询', 22, 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()), +(222, '登录删除', 22, 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()), +(223, '登录导出', 22, 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW()), +(231, '异常查询', 23, 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()), +(232, '异常删除', 23, 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()), +(233, '异常导出', 23, 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW()); + +-- 6.6 系统监控 - 子菜单 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(31, '在线用户', 3, 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()), +(32, '定时任务', 3, 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()), +(33, '数据监控', 3, 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()), +(34, '服务监控', 3, 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()), +(35, '缓存监控', 3, 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW()); + +-- 6.7 系统监控 - 按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(311, '在线查询', 31, 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()), +(312, '在线强退', 31, 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW()), +(321, '任务查询', 32, 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()), +(322, '任务新增', 32, 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()), +(323, '任务修改', 32, 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()), +(324, '任务删除', 32, 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()), +(325, '任务执行', 32, 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW()); + +-- 6.8 团课管理 - 子菜单 +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()); + +-- 6.9 团课管理 - 按钮权限 +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()), +(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()), +(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()), +(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()); + +-- 6.10 会员管理 - 子菜单 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(501, '会员管理', 500, 1, 'C', 'business:member:list', 'member/member/index', 1, NOW(), NOW()), +(503, '会员卡管理', 502, 1, 'C', 'business:memberCard:list', 'member/card/index', 1, NOW(), NOW()), +(505, '数据统计看板', 504, 1, 'C', 'business:statistics:view', 'statistics/dashboard/index', 1, NOW(), NOW()); + +-- 6.11 会员管理 - 按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(5011, '会员查询', 501, 1, 'F', 'business:member:query', NULL, 1, NOW(), NOW()), +(5012, '会员新增', 501, 2, 'F', 'business:member:add', NULL, 1, NOW(), NOW()), +(5013, '会员修改', 501, 3, 'F', 'business:member:edit', NULL, 1, NOW(), NOW()), +(5014, '会员删除', 501, 4, 'F', 'business:member:remove', NULL, 1, NOW(), NOW()), +(5015, '会员详情', 501, 5, 'F', 'business:member:detail', NULL, 1, NOW(), NOW()), +(5031, '会员卡查询', 503, 1, 'F', 'business:memberCard:query', NULL, 1, NOW(), NOW()), +(5032, '会员卡新增', 503, 2, 'F', 'business:memberCard:add', NULL, 1, NOW(), NOW()), +(5033, '会员卡修改', 503, 3, 'F', 'business:memberCard:edit', NULL, 1, NOW(), NOW()), +(5034, '会员卡删除', 503, 4, 'F', 'business:memberCard:remove', NULL, 1, NOW(), NOW()), +(5051, '数据统计导出', 505, 1, 'F', 'business:statistics:export', NULL, 1, NOW(), NOW()); + +-- ============================================ +-- 7. 字典数据 +-- ============================================ + +-- 7.1 字典类型 +INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by, created_at, updated_at) +VALUES +('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system', NOW(), NOW()), +('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system', NOW(), NOW()), +('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system', NOW(), NOW()), +('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system', NOW(), NOW()) +ON CONFLICT (dict_type) DO NOTHING; + +-- 7.2 字典数据 +INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by, created_at, updated_at) +VALUES +(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), +(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()), +(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), +(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()), +(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), +(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()), +(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), +(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()); + +-- ============================================ +-- 8. 系统配置 +-- ============================================ +INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by, created_at, updated_at) +VALUES +('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system', NOW(), NOW()), +('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue','Y', 'system', 'system', NOW(), NOW()), +('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()), +('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system', NOW(), NOW()), +('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled','true', 'Y', 'system', 'system', NOW(), NOW()), +('团课-取消预约免手续费次数', 'group_course.cancel_free_limit','3', 'Y', 'system', 'system', NOW(), NOW()) +ON CONFLICT (config_key) DO NOTHING; + +-- ============================================ +-- 9. 重置所有序列值 +-- ============================================ +SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user)); +SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role)); +SELECT setval('user_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM user_role)); +SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission)); +SELECT setval('sys_role_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role_permission)); +SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu)); +SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type)); +SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data)); +SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config)); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql index ab538c2..9c1b50d 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V1__Create_all_tables.sql @@ -1,6 +1,8 @@ +-- ============================================ -- Novalon管理系统数据库初始化脚本 -- 版本: V1 --- 描述: 创建所有核心表结构(合并版) +-- 描述: 创建所有核心系统表结构 +-- ============================================ -- ============================================ -- 用户与角色相关表 @@ -140,7 +142,7 @@ CREATE TABLE IF NOT EXISTS sys_dict_data ( deleted_at TIMESTAMP ); --- 字典表(通用字典) +-- 通用字典表 CREATE TABLE IF NOT EXISTS sys_dictionary ( id BIGSERIAL PRIMARY KEY, type VARCHAR(100) NOT NULL, @@ -165,7 +167,7 @@ CREATE TABLE IF NOT EXISTS sys_config ( config_name VARCHAR(100) NOT NULL, config_key VARCHAR(100) NOT NULL UNIQUE, config_value VARCHAR(500) NOT NULL, - config_type VARCHAR(1) DEFAULT 'N', + config_type VARCHAR(1) DEFAULT 'Y', create_by VARCHAR(50), update_by VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -355,15 +357,6 @@ COMMENT ON TABLE sys_dict_data IS '字典数据表'; COMMENT ON TABLE sys_dictionary IS '通用字典表'; COMMENT ON TABLE sys_config IS '系统配置表'; COMMENT ON TABLE sys_login_log IS '登录日志表'; -COMMENT ON TABLE sys_exception_log IS '异常日志表'; -COMMENT ON TABLE operation_log IS '操作日志表'; -COMMENT ON TABLE audit_log IS '审计日志表'; -COMMENT ON TABLE audit_log_archive IS '审计日志归档表'; -COMMENT ON TABLE sys_notice IS '系统公告表'; -COMMENT ON TABLE sys_user_message IS '用户消息表'; -COMMENT ON TABLE sys_file IS '文件管理表'; -COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表'; - COMMENT ON TABLE sys_exception_log IS '异常日志表'; COMMENT ON COLUMN sys_exception_log.id IS '主键ID'; COMMENT ON COLUMN sys_exception_log.username IS '操作用户'; @@ -375,7 +368,22 @@ COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息'; COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈'; COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址'; COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间'; - +COMMENT ON TABLE operation_log IS '操作日志表'; +COMMENT ON COLUMN operation_log.id IS '主键ID'; +COMMENT ON COLUMN operation_log.username IS '操作用户'; +COMMENT ON COLUMN operation_log.operation IS '操作描述'; +COMMENT ON COLUMN operation_log.method IS '请求方法'; +COMMENT ON COLUMN operation_log.params IS '请求参数'; +COMMENT ON COLUMN operation_log.result IS '操作结果'; +COMMENT ON COLUMN operation_log.ip IS 'IP地址'; +COMMENT ON COLUMN operation_log.duration IS '执行时长(毫秒)'; +COMMENT ON COLUMN operation_log.status IS '操作状态(0成功 1失败)'; +COMMENT ON COLUMN operation_log.error_msg IS '错误消息'; +COMMENT ON COLUMN operation_log.create_by IS '创建人'; +COMMENT ON COLUMN operation_log.update_by IS '更新人'; +COMMENT ON COLUMN operation_log.created_at IS '创建时间'; +COMMENT ON COLUMN operation_log.updated_at IS '更新时间'; +COMMENT ON COLUMN operation_log.deleted_at IS '删除时间'; COMMENT ON TABLE audit_log IS '审计日志表'; COMMENT ON COLUMN audit_log.id IS '主键ID'; COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)'; @@ -389,7 +397,6 @@ COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表'; COMMENT ON COLUMN audit_log.ip_address IS 'IP地址'; COMMENT ON COLUMN audit_log.description IS '操作描述'; COMMENT ON COLUMN audit_log.created_at IS '记录创建时间'; - COMMENT ON TABLE audit_log_archive IS '审计日志归档表'; COMMENT ON COLUMN audit_log_archive.id IS '主键ID'; COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)'; @@ -405,3 +412,7 @@ COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理'; COMMENT ON COLUMN audit_log_archive.description IS '操作描述'; COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间'; COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间'; +COMMENT ON TABLE sys_notice IS '系统公告表'; +COMMENT ON TABLE sys_user_message IS '用户消息表'; +COMMENT ON TABLE sys_file IS '文件管理表'; +COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Add_banner_menu.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Add_banner_menu.sql new file mode 100644 index 0000000..10465f5 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Add_banner_menu.sql @@ -0,0 +1,18 @@ +-- ============================================ +-- V20: 新增轮播图管理菜单 +-- 描述: 为首页轮播图管理功能创建菜单项 +-- ============================================ + +-- 轮播图管理子菜单(页面),归属系统管理目录 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/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 +(191, '轮播图查询', 19, 1, 'F', 'system:banner:query', NULL, 1, NOW(), NOW()), +(192, '轮播图新增', 19, 2, 'F', 'system:banner:add', NULL, 1, NOW(), NOW()), +(193, '轮播图修改', 19, 3, 'F', 'system:banner:edit', NULL, 1, NOW(), NOW()), +(194, '轮播图删除', 19, 4, 'F', 'system:banner:remove', NULL, 1, NOW(), NOW()); + +-- 重置菜单序列 +SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu)); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V21__Create_banner_table.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V21__Create_banner_table.sql new file mode 100644 index 0000000..75a040c --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V21__Create_banner_table.sql @@ -0,0 +1,20 @@ +-- ============================================ +-- V21: 创建轮播图表并更新菜单为顶级菜单 +-- ============================================ + +-- 创建 banner 表 +CREATE TABLE IF NOT EXISTS banner ( + id BIGSERIAL PRIMARY KEY, + image_url VARCHAR(512) NOT NULL, + title VARCHAR(100) NOT NULL, + subtitle TEXT, + sort_order INT DEFAULT 0, + is_active VARCHAR(1) DEFAULT '1', + link_url VARCHAR(512), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 将轮播图管理菜单从系统管理子菜单调整为顶级菜单(parent_id=0, sort=8) +UPDATE sys_menu SET parent_id = 0, order_num = 8 WHERE id = 19; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Create_indexes.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Create_indexes.sql new file mode 100644 index 0000000..bc9b5b7 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Create_indexes.sql @@ -0,0 +1,141 @@ +-- ============================================ +-- Novalon管理系统索引优化脚本 +-- 版本: V2 +-- 描述: 为核心系统表创建必要的索引以提升查询性能 +-- ============================================ + +-- ============================================ +-- 用户与角色表索引 +-- ============================================ + +-- 用户表索引 +CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username); +CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email); +CREATE INDEX IF NOT EXISTS idx_users_status ON sys_user(status); +CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON sys_user(deleted_at); + +-- 角色表索引 +CREATE INDEX IF NOT EXISTS idx_roles_role_key ON sys_role(role_key); +CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status); +CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON sys_role(deleted_at); + +-- 用户角色关联表索引 +CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id); +CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id); + +-- ============================================ +-- 权限表索引 +-- ============================================ + +-- 权限表索引 +CREATE INDEX IF NOT EXISTS idx_permission_code ON sys_permission(permission_code); +CREATE INDEX IF NOT EXISTS idx_permission_resource ON sys_permission(resource); +CREATE INDEX IF NOT EXISTS idx_permission_status ON sys_permission(status); + +-- 角色权限关联表索引 +CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON sys_role_permission(role_id); +CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON sys_role_permission(permission_id); + +-- ============================================ +-- 菜单表索引 +-- ============================================ + +CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id); +CREATE INDEX IF NOT EXISTS idx_sys_menu_status ON sys_menu(status); +CREATE INDEX IF NOT EXISTS idx_sys_menu_deleted_at ON sys_menu(deleted_at); + +-- ============================================ +-- 字典表索引 +-- ============================================ + +-- 字典类型表索引 +CREATE INDEX IF NOT EXISTS idx_sys_dict_type_dict_type ON sys_dict_type(dict_type); +CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status); +CREATE INDEX IF NOT EXISTS idx_sys_dict_type_deleted_at ON sys_dict_type(deleted_at); + +-- 字典数据表索引 +CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_type ON sys_dict_data(dict_type); +CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_value ON sys_dict_data(dict_value); +CREATE INDEX IF NOT EXISTS idx_sys_dict_data_status ON sys_dict_data(status); +CREATE INDEX IF NOT EXISTS idx_sys_dict_data_deleted_at ON sys_dict_data(deleted_at); + +-- 通用字典表索引 +CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type ON sys_dictionary(type); +CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type_code ON sys_dictionary(type, code); +CREATE INDEX IF NOT EXISTS idx_sys_dictionary_deleted_at ON sys_dictionary(deleted_at); + +-- ============================================ +-- 系统配置表索引 +-- ============================================ + +CREATE INDEX IF NOT EXISTS idx_sys_config_config_key ON sys_config(config_key); +CREATE INDEX IF NOT EXISTS idx_sys_config_config_type ON sys_config(config_type); +CREATE INDEX IF NOT EXISTS idx_sys_config_deleted_at ON sys_config(deleted_at); + +-- ============================================ +-- 日志表索引 +-- ============================================ + +-- 登录日志表索引 +CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username); +CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip); +CREATE INDEX IF NOT EXISTS idx_sys_login_log_status ON sys_login_log(status); +CREATE INDEX IF NOT EXISTS idx_sys_login_log_login_time ON sys_login_log(login_time); + +-- 异常日志表索引 +CREATE INDEX IF NOT EXISTS idx_sys_exception_log_username ON sys_exception_log(username); +CREATE INDEX IF NOT EXISTS idx_sys_exception_log_exception_name ON sys_exception_log(exception_name); +CREATE INDEX IF NOT EXISTS idx_sys_exception_log_create_time ON sys_exception_log(create_time); + +-- 操作日志表索引 +CREATE INDEX IF NOT EXISTS idx_operation_log_username ON operation_log(username); +CREATE INDEX IF NOT EXISTS idx_operation_log_operation ON operation_log(operation); +CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON operation_log(created_at); +CREATE INDEX IF NOT EXISTS idx_operation_log_status ON operation_log(status); +CREATE INDEX IF NOT EXISTS idx_operation_log_deleted_at ON operation_log(deleted_at); + +-- 审计日志表索引 +CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type); +CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type); +CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator); +CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time); +CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id); + +-- 审计日志归档表索引 +CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_type ON audit_log_archive(entity_type); +CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_id ON audit_log_archive(entity_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_type ON audit_log_archive(operation_type); +CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operator ON audit_log_archive(operator); +CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_time ON audit_log_archive(operation_time); +CREATE INDEX IF NOT EXISTS idx_audit_log_archive_archived_at ON audit_log_archive(archived_at); + +-- ============================================ +-- 通知与消息表索引 +-- ============================================ + +-- 系统公告表索引 +CREATE INDEX IF NOT EXISTS idx_sys_notice_notice_type ON sys_notice(notice_type); +CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status); +CREATE INDEX IF NOT EXISTS idx_sys_notice_deleted_at ON sys_notice(deleted_at); + +-- 用户消息表索引 +CREATE INDEX IF NOT EXISTS idx_sys_user_message_user_id ON sys_user_message(user_id); +CREATE INDEX IF NOT EXISTS idx_sys_user_message_notice_id ON sys_user_message(notice_id); +CREATE INDEX IF NOT EXISTS idx_sys_user_message_is_read ON sys_user_message(is_read); +CREATE INDEX IF NOT EXISTS idx_sys_user_message_deleted_at ON sys_user_message(deleted_at); + +-- ============================================ +-- 文件管理表索引 +-- ============================================ + +CREATE INDEX IF NOT EXISTS idx_sys_file_file_type ON sys_file(file_type); +CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at); + +-- ============================================ +-- OAuth2客户端表索引 +-- ============================================ + +CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id); +CREATE INDEX IF NOT EXISTS idx_oauth2_client_enabled ON oauth2_client(enabled); +CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V3__Grant_permissions.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V3__Grant_permissions.sql new file mode 100644 index 0000000..e8348d5 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V3__Grant_permissions.sql @@ -0,0 +1,15 @@ +-- ============================================ +-- Novalon管理系统权限授予脚本 +-- 版本: V3 +-- 描述: 为novalon用户授予所有表的访问权限 +-- ============================================ + +-- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限 +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO novalon; + +-- 授予所有序列的使用权限 +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO novalon; + +-- 设置默认权限,使未来创建的表自动授予novalon用户权限 +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO novalon; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO novalon; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V4__Create_member_tables.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V4__Create_member_tables.sql new file mode 100644 index 0000000..3b9a91a --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V4__Create_member_tables.sql @@ -0,0 +1,238 @@ +-- ============================================ +-- 会员相关表 +-- 版本: V4 +-- 描述: 创建会员、微信用户、会员卡类型、会员卡记录、交易流水、退款申请等表 +-- ============================================ + +-- ============================================ +-- 1. member_user 表(会员表) +-- ============================================ + +CREATE TABLE IF NOT EXISTS member_user ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + member_no VARCHAR(50) NOT NULL UNIQUE, + nickname VARCHAR(100), + phone VARCHAR(255), + gender INTEGER DEFAULT 0, + birthday DATE, + address VARCHAR(500), + avatar VARCHAR(500), + subscribed BOOLEAN DEFAULT FALSE, + last_login_at TIMESTAMP, + + union_id VARCHAR(100), + miniapp_open_id VARCHAR(100), + official_open_id VARCHAR(100), + + is_deleted BOOLEAN DEFAULT FALSE +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no); +CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id); +CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_official_openid ON member_user(official_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_phone ON member_user(phone); +CREATE INDEX IF NOT EXISTS idx_member_user_is_deleted ON member_user(is_deleted); + +COMMENT ON TABLE member_user IS '会员表'; +COMMENT ON COLUMN member_user.id IS '主键ID'; +COMMENT ON COLUMN member_user.created_at IS '创建时间'; +COMMENT ON COLUMN member_user.updated_at IS '更新时间'; +COMMENT ON COLUMN member_user.member_no IS '会员编号(唯一,格式:MEM + 8位随机字符)'; +COMMENT ON COLUMN member_user.nickname IS '昵称'; +COMMENT ON COLUMN member_user.phone IS '手机号(AES-128-CBC加密存储)'; +COMMENT ON COLUMN member_user.gender IS '性别:0-未知,1-男,2-女'; +COMMENT ON COLUMN member_user.birthday IS '生日'; +COMMENT ON COLUMN member_user.address IS '地址'; +COMMENT ON COLUMN member_user.avatar IS '头像URL'; +COMMENT ON COLUMN member_user.subscribed IS '是否关注服务号:true-已关注,false-未关注'; +COMMENT ON COLUMN member_user.last_login_at IS '最后登录时间'; +COMMENT ON COLUMN member_user.union_id IS '微信UnionID(用户在开放平台的唯一标识,跨应用相同)'; +COMMENT ON COLUMN member_user.miniapp_open_id IS '小程序OpenID(用户在当前小程序的唯一标识)'; +COMMENT ON COLUMN member_user.official_open_id IS '服务号OpenID(用户在当前服务号的唯一标识)'; +COMMENT ON COLUMN member_user.is_deleted IS '是否删除(软删除标记):false-正常,true-已删除'; + +-- ============================================ +-- 2. wechat_user 表(微信用户表) +-- ============================================ + +CREATE TABLE IF NOT EXISTS wechat_user ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + member_id BIGINT NOT NULL, + + union_id VARCHAR(100), + miniapp_openid VARCHAR(100), + mp_openid VARCHAR(100), + + is_subscribed BOOLEAN DEFAULT FALSE, + subscribe_time TIMESTAMP, + unsubscribe_time TIMESTAMP +); + +ALTER TABLE wechat_user + ADD CONSTRAINT fk_wechat_user_member + FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE CASCADE; + +CREATE INDEX IF NOT EXISTS idx_wechat_user_union_id ON wechat_user(union_id); +CREATE INDEX IF NOT EXISTS idx_wechat_user_miniapp_openid ON wechat_user(miniapp_openid); +CREATE INDEX IF NOT EXISTS idx_wechat_user_mp_openid ON wechat_user(mp_openid); +CREATE INDEX IF NOT EXISTS idx_wechat_user_member_id ON wechat_user(member_id); + +COMMENT ON TABLE wechat_user IS '微信用户表'; +COMMENT ON COLUMN wechat_user.id IS '主键ID'; +COMMENT ON COLUMN wechat_user.created_at IS '创建时间'; +COMMENT ON COLUMN wechat_user.updated_at IS '更新时间'; +COMMENT ON COLUMN wechat_user.member_id IS '会员ID(关联 member_user 表的 id 字段)'; +COMMENT ON COLUMN wechat_user.union_id IS '微信UnionID(用户在开放平台的唯一标识)'; +COMMENT ON COLUMN wechat_user.miniapp_openid IS '小程序OpenID(用户在当前小程序的唯一标识)'; +COMMENT ON COLUMN wechat_user.mp_openid IS '服务号OpenID(用户在当前服务号的唯一标识)'; +COMMENT ON COLUMN wechat_user.is_subscribed IS '是否关注服务号:true-已关注,false-未关注'; +COMMENT ON COLUMN wechat_user.subscribe_time IS '首次关注时间'; +COMMENT ON COLUMN wechat_user.unsubscribe_time IS '最后一次取消关注时间'; + +-- ============================================ +-- 3. member_card 表(会员卡类型表) +-- ============================================ + +CREATE TABLE IF NOT EXISTS member_card ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + + member_card_id BIGSERIAL, + member_card_name VARCHAR(100) NOT NULL, + member_card_type VARCHAR(20) NOT NULL, + member_card_price DECIMAL(10, 2) NOT NULL, + member_card_validity_days INTEGER, + member_card_total_times INTEGER, + member_card_amount DECIMAL(10, 2), + member_card_status INTEGER DEFAULT 1 NOT NULL, + extra_config TEXT DEFAULT '{}' +); + +COMMENT ON TABLE member_card IS '会员卡类型表'; +COMMENT ON COLUMN member_card.member_card_id IS '会员卡ID'; +COMMENT ON COLUMN member_card.member_card_name IS '会员卡名称'; +COMMENT ON COLUMN member_card.member_card_type IS '会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡'; +COMMENT ON COLUMN member_card.member_card_price IS '会员卡价格'; +COMMENT ON COLUMN member_card.member_card_validity_days IS '有效天数(时长卡用)'; +COMMENT ON COLUMN member_card.member_card_total_times IS '总次数(次卡用)'; +COMMENT ON COLUMN member_card.member_card_amount IS '面额(储值卡用)'; +COMMENT ON COLUMN member_card.member_card_status IS '状态:0-下架, 1-上架'; +COMMENT ON COLUMN member_card.extra_config IS '扩展配置(JSON格式)'; + +-- ============================================ +-- 4. member_card_record 表(会员卡记录表) +-- ============================================ + +CREATE TABLE IF NOT EXISTS member_card_record ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + + member_card_record_id BIGSERIAL, + member_id BIGINT NOT NULL, + member_card_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + remaining_times INTEGER DEFAULT 0, + remaining_amount DECIMAL(10, 2) DEFAULT 0.00, + expire_time TIMESTAMP, + source_order_id BIGINT, + purchase_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + version INTEGER DEFAULT 0 NOT NULL, + card_composition TEXT +); + +CREATE INDEX IF NOT EXISTS idx_member_card_record_member_id ON member_card_record(member_id); +CREATE INDEX IF NOT EXISTS idx_member_card_record_status ON member_card_record(status); +CREATE INDEX IF NOT EXISTS idx_member_card_record_expire_time ON member_card_record(expire_time); +CREATE INDEX IF NOT EXISTS idx_member_card_record_member_status ON member_card_record(member_id, status); +CREATE INDEX IF NOT EXISTS idx_member_card_record_status_expire ON member_card_record(status, expire_time) + WHERE status = 'ACTIVE'; + +COMMENT ON TABLE member_card_record IS '会员卡记录表(会员持有的卡)'; +COMMENT ON COLUMN member_card_record.member_card_record_id IS '会员卡记录ID'; +COMMENT ON COLUMN member_card_record.member_id IS '会员ID'; +COMMENT ON COLUMN member_card_record.member_card_id IS '会员卡类型ID'; +COMMENT ON COLUMN member_card_record.status IS '状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款'; +COMMENT ON COLUMN member_card_record.remaining_times IS '剩余次数'; +COMMENT ON COLUMN member_card_record.remaining_amount IS '剩余金额'; +COMMENT ON COLUMN member_card_record.expire_time IS '到期时间'; +COMMENT ON COLUMN member_card_record.source_order_id IS '来源订单ID'; +COMMENT ON COLUMN member_card_record.purchase_time IS '购买时间'; +COMMENT ON COLUMN member_card_record.version IS '乐观锁版本号'; +COMMENT ON COLUMN member_card_record.card_composition IS '卡片组成(JSON格式,用于组合卡)'; + +-- ============================================ +-- 5. member_card_transactions 表(会员卡交易流水表) +-- ============================================ + +CREATE TABLE IF NOT EXISTS member_card_transactions ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + member_card_record_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + member_card_id BIGINT NOT NULL, + operation_type VARCHAR(20) NOT NULL, + change_amount INTEGER DEFAULT 0, + change_balance DECIMAL(10, 2) DEFAULT 0.00, + after_remaining_count INTEGER DEFAULT 0, + after_remaining_balance DECIMAL(10, 2) DEFAULT 0.00, + related_biz_type VARCHAR(20), + source_order_id BIGINT, + remark VARCHAR(500), + is_archived BOOLEAN DEFAULT FALSE, + archived_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_id ON member_card_transactions(member_id); +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_record_id ON member_card_transactions(member_card_record_id); +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_created_at ON member_card_transactions(created_at); +CREATE INDEX IF NOT EXISTS idx_member_card_transactions_member_type_time + ON member_card_transactions(member_id, operation_type, created_at); + +COMMENT ON TABLE member_card_transactions IS '会员卡交易流水表'; +COMMENT ON COLUMN member_card_transactions.operation_type IS '操作类型:PURCHASE-购买, DEDUCT-扣次/扣费, RENEW-续费, REFUND-退款, EXPIRE-过期'; +COMMENT ON COLUMN member_card_transactions.change_amount IS '变动次数'; +COMMENT ON COLUMN member_card_transactions.change_balance IS '变动金额'; +COMMENT ON COLUMN member_card_transactions.after_remaining_count IS '变动后剩余次数'; +COMMENT ON COLUMN member_card_transactions.after_remaining_balance IS '变动后剩余金额'; +COMMENT ON COLUMN member_card_transactions.related_biz_type IS '关联业务类型:GROUP_CLASS-团课, PT_CLASS-私教, CHECK_IN-签到'; +COMMENT ON COLUMN member_card_transactions.is_archived IS '是否已归档'; +COMMENT ON COLUMN member_card_transactions.archived_at IS '归档时间'; + +-- ============================================ +-- 6. refund_application 表(退款申请表) +-- ============================================ + +CREATE TABLE IF NOT EXISTS refund_application ( + id BIGSERIAL PRIMARY KEY, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + + record_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + reason VARCHAR(500), + apply_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + audit_time TIMESTAMP, + auditor_id BIGINT, + audit_remark VARCHAR(500), + refund_amount DECIMAL(10, 2) +); + +CREATE INDEX IF NOT EXISTS idx_refund_application_record_id ON refund_application(record_id); +CREATE INDEX IF NOT EXISTS idx_refund_application_status ON refund_application(status); + +COMMENT ON TABLE refund_application IS '退款申请表'; +COMMENT ON COLUMN refund_application.status IS '状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql new file mode 100644 index 0000000..a456b63 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql @@ -0,0 +1,81 @@ +-- ============================================ +-- 团课相关表 +-- 版本: V5 +-- 描述: 创建团课课程表和团课预约记录表 +-- ============================================ + +-- 团课课程表 +CREATE TABLE IF NOT EXISTS group_course ( + id BIGSERIAL PRIMARY KEY, + course_name VARCHAR(100) NOT NULL, + coach_id BIGINT, + course_type BIGINT, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL, + max_members INTEGER DEFAULT 20, + current_members INTEGER DEFAULT 0, + status VARCHAR(1) DEFAULT '0', + actual_start_time TIMESTAMP, + actual_end_time TIMESTAMP, + location VARCHAR(255), + cover_image VARCHAR(500), + description TEXT, + stored_value_amount DECIMAL(10,2) DEFAULT 0, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 团课预约记录表 +CREATE TABLE IF NOT EXISTS group_course_booking ( + id BIGSERIAL PRIMARY KEY, + course_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + member_card_id BIGINT NOT NULL, + booking_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(1) DEFAULT '0', + cancel_time TIMESTAMP, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +COMMENT ON TABLE group_course IS '团课课程表'; +COMMENT ON COLUMN group_course.id IS '主键ID'; +COMMENT ON COLUMN group_course.course_name IS '课程名称'; +COMMENT ON COLUMN group_course.coach_id IS '教练ID(关联sys_user)'; +COMMENT ON COLUMN group_course.course_type IS '课程类型(如瑜伽/普拉提/动感单车)'; +COMMENT ON COLUMN group_course.start_time IS '开始时间'; +COMMENT ON COLUMN group_course.end_time IS '结束时间'; +COMMENT ON COLUMN group_course.max_members IS '最大参与人数'; +COMMENT ON COLUMN group_course.current_members IS '当前参与人数'; +COMMENT ON COLUMN group_course.status IS '状态(0正常 1已取消 2已结束 3进行中 4超时)'; +COMMENT ON COLUMN group_course.actual_start_time IS '实际开课时间(教练点击开课时记录)'; +COMMENT ON COLUMN group_course.actual_end_time IS '实际结课时间(教练点击结课时记录)'; +COMMENT ON COLUMN group_course.location IS '上课地点'; +COMMENT ON COLUMN group_course.cover_image IS '封面图URL'; +COMMENT ON COLUMN group_course.description IS '课程描述'; +COMMENT ON COLUMN group_course.stored_value_amount IS '储值卡额度(消耗金额)'; +COMMENT ON COLUMN group_course.create_by IS '创建人'; +COMMENT ON COLUMN group_course.update_by IS '更新人'; +COMMENT ON COLUMN group_course.created_at IS '创建时间'; +COMMENT ON COLUMN group_course.updated_at IS '更新时间'; +COMMENT ON COLUMN group_course.deleted_at IS '删除时间(软删除)'; + +COMMENT ON TABLE group_course_booking IS '团课预约记录表'; +COMMENT ON COLUMN group_course_booking.id IS '主键ID'; +COMMENT ON COLUMN group_course_booking.course_id IS '团课ID'; +COMMENT ON COLUMN group_course_booking.member_id IS '用户ID'; +COMMENT ON COLUMN group_course_booking.member_card_id IS '会员卡ID'; +COMMENT ON COLUMN group_course_booking.booking_time IS '预约时间'; +COMMENT ON COLUMN group_course_booking.status IS '状态(0已预约 1已取消 2已出席 3缺席)'; +COMMENT ON COLUMN group_course_booking.cancel_time IS '取消时间'; +COMMENT ON COLUMN group_course_booking.create_by IS '创建人'; +COMMENT ON COLUMN group_course_booking.update_by IS '更新人'; +COMMENT ON COLUMN group_course_booking.created_at IS '创建时间'; +COMMENT ON COLUMN group_course_booking.updated_at IS '更新时间'; +COMMENT ON COLUMN group_course_booking.deleted_at IS '删除时间(软删除)'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V6__Add_booking_snapshot_fields.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V6__Add_booking_snapshot_fields.sql new file mode 100644 index 0000000..9778517 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V6__Add_booking_snapshot_fields.sql @@ -0,0 +1,16 @@ +-- ============================================ +-- 为团课预约记录表添加课程冗余字段 +-- 版本: V6 +-- 描述: 保存预约时的课程快照信息 +-- ============================================ + +ALTER TABLE group_course_booking + ADD COLUMN IF NOT EXISTS course_name VARCHAR(100), + ADD COLUMN IF NOT EXISTS course_start_time TIMESTAMP, + ADD COLUMN IF NOT EXISTS course_end_time TIMESTAMP, + ADD COLUMN IF NOT EXISTS location VARCHAR(255); + +COMMENT ON COLUMN group_course_booking.course_name IS '课程名称(冗余字段,保存预约时的课程快照)'; +COMMENT ON COLUMN group_course_booking.course_start_time IS '课程开始时间(冗余字段,保存预约时的课程快照)'; +COMMENT ON COLUMN group_course_booking.course_end_time IS '课程结束时间(冗余字段,保存预约时的课程快照)'; +COMMENT ON COLUMN group_course_booking.location IS '上课地点(冗余字段,保存预约时的课程快照)'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V7__Create_sign_in_record.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V7__Create_sign_in_record.sql new file mode 100644 index 0000000..d68c53a --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V7__Create_sign_in_record.sql @@ -0,0 +1,52 @@ +-- ============================================ +-- 会员到店签到记录表 +-- 版本: V7 +-- 描述: 创建sign_in_record表,用于记录会员签到信息 +-- ============================================ + +CREATE TABLE IF NOT EXISTS sign_in_record ( + id BIGSERIAL PRIMARY KEY, + member_id BIGINT NOT NULL, + member_card_id BIGINT, + sign_in_time TIMESTAMP NOT NULL, + sign_in_type VARCHAR(20) NOT NULL, + sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', + verification_details TEXT, + fail_reason VARCHAR(500), + operator_id BIGINT, + operator_name VARCHAR(100), + device_info VARCHAR(200), + ip_address VARCHAR(50), + source VARCHAR(20) NOT NULL, + is_delete BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time); +CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time); + +COMMENT ON TABLE sign_in_record IS '会员到店签到记录表'; +COMMENT ON COLUMN sign_in_record.id IS '自增主键'; +COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member表'; +COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID'; +COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间'; +COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别'; +COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败'; +COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据'; +COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案'; +COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL'; +COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余'; +COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号'; +COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址'; +COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端'; +COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除'; +COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间'; +COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V8__Create_group_course_type.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V8__Create_group_course_type.sql new file mode 100644 index 0000000..942af5a --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V8__Create_group_course_type.sql @@ -0,0 +1,33 @@ +-- ============================================ +-- 团课类型表 +-- 版本: V8 +-- 描述: 创建团课类型表,用于分类管理团课 +-- ============================================ + +CREATE TABLE IF NOT EXISTS group_course_type ( + id BIGSERIAL PRIMARY KEY, + type_name VARCHAR(100) NOT NULL, + base_difficulty INTEGER DEFAULT 1, + description TEXT, + category VARCHAR(50), + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_group_course_type_type_name ON group_course_type(type_name); +CREATE INDEX IF NOT EXISTS idx_group_course_type_category ON group_course_type(category); + +COMMENT ON TABLE group_course_type IS '团课类型表'; +COMMENT ON COLUMN group_course_type.id IS '主键ID'; +COMMENT ON COLUMN group_course_type.type_name IS '类型名称'; +COMMENT ON COLUMN group_course_type.base_difficulty IS '基础难度(1-10)'; +COMMENT ON COLUMN group_course_type.description IS '类型描述'; +COMMENT ON COLUMN group_course_type.category IS '分类(如:有氧、力量、柔韧等)'; +COMMENT ON COLUMN group_course_type.create_by IS '创建人'; +COMMENT ON COLUMN group_course_type.update_by IS '更新人'; +COMMENT ON COLUMN group_course_type.created_at IS '创建时间'; +COMMENT ON COLUMN group_course_type.updated_at IS '更新时间'; +COMMENT ON COLUMN group_course_type.deleted_at IS '删除时间(软删除)'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V9__Create_course_label_tables.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V9__Create_course_label_tables.sql new file mode 100644 index 0000000..0c193b8 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V9__Create_course_label_tables.sql @@ -0,0 +1,56 @@ +-- ============================================ +-- 团课标签相关表 +-- 版本: V9 +-- 描述: 创建团课标签表和类型-标签关联表 +-- ============================================ + +-- 团课标签表 +CREATE TABLE IF NOT EXISTS course_label ( + id BIGSERIAL PRIMARY KEY, + label_name VARCHAR(50) NOT NULL, + color VARCHAR(20) DEFAULT '#1890ff', + description VARCHAR(200), + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 团课类型-标签关联表 +CREATE TABLE IF NOT EXISTS course_type_label ( + id BIGSERIAL PRIMARY KEY, + type_id BIGINT NOT NULL, + label_id BIGINT NOT NULL, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP, + UNIQUE (type_id, label_id) +); + +CREATE INDEX IF NOT EXISTS idx_course_label_label_name ON course_label(label_name); +CREATE INDEX IF NOT EXISTS idx_course_type_label_type_id ON course_type_label(type_id); +CREATE INDEX IF NOT EXISTS idx_course_type_label_label_id ON course_type_label(label_id); + +COMMENT ON TABLE course_label IS '团课标签表'; +COMMENT ON COLUMN course_label.id IS '主键ID'; +COMMENT ON COLUMN course_label.label_name IS '标签名称'; +COMMENT ON COLUMN course_label.color IS '标签颜色(十六进制)'; +COMMENT ON COLUMN course_label.description IS '标签描述'; +COMMENT ON COLUMN course_label.create_by IS '创建人'; +COMMENT ON COLUMN course_label.update_by IS '更新人'; +COMMENT ON COLUMN course_label.created_at IS '创建时间'; +COMMENT ON COLUMN course_label.updated_at IS '更新时间'; +COMMENT ON COLUMN course_label.deleted_at IS '删除时间(软删除)'; + +COMMENT ON TABLE course_type_label IS '团课类型-标签关联表'; +COMMENT ON COLUMN course_type_label.id IS '主键ID'; +COMMENT ON COLUMN course_type_label.type_id IS '团课类型ID'; +COMMENT ON COLUMN course_type_label.label_id IS '标签ID'; +COMMENT ON COLUMN course_type_label.create_by IS '创建人'; +COMMENT ON COLUMN course_type_label.update_by IS '更新人'; +COMMENT ON COLUMN course_type_label.created_at IS '创建时间'; +COMMENT ON COLUMN course_type_label.updated_at IS '更新时间'; +COMMENT ON COLUMN course_type_label.deleted_at IS '删除时间(软删除)'; diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java index b743860..501026b 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java @@ -64,6 +64,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory findByDeletedAtIsNull(); + + Flux findActiveBanners(); + + Mono findById(Long id); + + Mono save(Banner banner); + + Mono deleteByIdAndDeletedAtIsNull(Long id); +} diff --git a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/IBannerService.java b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/IBannerService.java new file mode 100644 index 0000000..238d881 --- /dev/null +++ b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/IBannerService.java @@ -0,0 +1,20 @@ +package cn.novalon.gym.manage.notify.core.service; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface IBannerService { + + Flux getAllBanners(); + + Flux getActiveBanners(); + + Mono getBannerById(Long id); + + Mono createBanner(Banner banner); + + Mono updateBanner(Long id, Banner banner); + + Mono deleteBanner(Long id); +} diff --git a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImpl.java b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImpl.java new file mode 100644 index 0000000..227678b --- /dev/null +++ b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImpl.java @@ -0,0 +1,80 @@ +package cn.novalon.gym.manage.notify.core.service.impl; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import cn.novalon.gym.manage.notify.core.repository.IBannerRepository; +import cn.novalon.gym.manage.notify.core.service.IBannerService; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Service +public class BannerServiceImpl implements IBannerService { + + private final IBannerRepository bannerRepository; + + public BannerServiceImpl(IBannerRepository bannerRepository) { + this.bannerRepository = bannerRepository; + } + + @Override + public Flux getAllBanners() { + return bannerRepository.findByDeletedAtIsNull(); + } + + @Override + public Flux getActiveBanners() { + return bannerRepository.findActiveBanners(); + } + + @Override + public Mono getBannerById(Long id) { + return bannerRepository.findById(id) + .filter(banner -> banner.getDeletedAt() == null); + } + + @Override + public Mono createBanner(Banner banner) { + banner.setCreatedAt(LocalDateTime.now()); + return bannerRepository.save(banner); + } + + @Override + public Mono updateBanner(Long id, Banner banner) { + return bannerRepository.findById(id) + .flatMap(existing -> { + if (banner.getImageUrl() != null) { + existing.setImageUrl(banner.getImageUrl()); + } + if (banner.getTitle() != null) { + existing.setTitle(banner.getTitle()); + } + if (banner.getSubtitle() != null) { + existing.setSubtitle(banner.getSubtitle()); + } + if (banner.getSortOrder() != null) { + existing.setSortOrder(banner.getSortOrder()); + } + if (banner.getIsActive() != null) { + existing.setIsActive(banner.getIsActive()); + } + if (banner.getLinkUrl() != null) { + existing.setLinkUrl(banner.getLinkUrl()); + } + existing.setUpdatedAt(LocalDateTime.now()); + return bannerRepository.save(existing); + }); + } + + @Override + public Mono deleteBanner(Long id) { + return bannerRepository.findById(id) + .filter(banner -> banner.getDeletedAt() == null) + .flatMap(banner -> { + banner.setDeletedAt(LocalDateTime.now()); + return bannerRepository.save(banner); + }) + .then(); + } +} diff --git a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/BannerHandler.java b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/BannerHandler.java new file mode 100644 index 0000000..81e621a --- /dev/null +++ b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/BannerHandler.java @@ -0,0 +1,120 @@ +package cn.novalon.gym.manage.notify.handler; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import cn.novalon.gym.manage.notify.core.service.IBannerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.Map; + +@Component +@Tag(name = "轮播图管理", description = "首页轮播图相关操作") +public class BannerHandler { + + private final IBannerService bannerService; + + public BannerHandler(IBannerService bannerService) { + this.bannerService = bannerService; + } + + @Operation(summary = "获取所有轮播图", description = "获取系统中所有轮播图列表(按排序升序)") + public Mono getAllBanners(ServerRequest request) { + Flux banners = bannerService.getAllBanners(); + return ServerResponse.ok().body(banners, Banner.class); + } + + @Operation(summary = "获取启用的轮播图", description = "获取所有启用状态的轮播图(按排序升序),供外部页面调用") + public Mono getActiveBanners(ServerRequest request) { + Flux banners = bannerService.getActiveBanners(); + return ServerResponse.ok().body(banners, Banner.class); + } + + @Operation(summary = "根据ID获取轮播图", description = "根据轮播图ID获取详细信息") + public Mono getBannerById(ServerRequest request) { + Long id = Long.parseLong(request.pathVariable("id")); + return bannerService.getBannerById(id) + .flatMap(banner -> ServerResponse.ok().bodyValue(banner)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + @Operation(summary = "创建轮播图", description = "创建新轮播图") + public Mono createBanner(ServerRequest request) { + return request.bodyToMono(Map.class) + .map(this::mapToBanner) + .filter(banner -> banner.getImageUrl() != null && !banner.getImageUrl().trim().isEmpty()) + .switchIfEmpty(Mono.error(new IllegalArgumentException("轮播图图片地址不能为空"))) + .filter(banner -> banner.getTitle() != null && !banner.getTitle().trim().isEmpty()) + .switchIfEmpty(Mono.error(new IllegalArgumentException("轮播图标题不能为空"))) + .flatMap(banner -> { + if (banner.getSortOrder() == null) { + banner.setSortOrder(0); + } + if (banner.getIsActive() == null) { + banner.setIsActive("1"); + } + return bannerService.createBanner(banner); + }) + .flatMap(banner -> ServerResponse.ok().bodyValue(banner)) + .onErrorResume(IllegalArgumentException.class, ex -> + ServerResponse.badRequest().bodyValue(Map.of( + "code", 400, + "message", ex.getMessage(), + "timestamp", LocalDateTime.now() + )) + ); + } + + @Operation(summary = "更新轮播图", description = "更新轮播图信息") + public Mono updateBanner(ServerRequest request) { + Long id = Long.parseLong(request.pathVariable("id")); + return request.bodyToMono(Map.class) + .map(this::mapToBanner) + .flatMap(banner -> bannerService.updateBanner(id, banner)) + .flatMap(banner -> ServerResponse.ok().bodyValue(banner)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + @Operation(summary = "删除轮播图", description = "删除指定轮播图(软删除)") + public Mono deleteBanner(ServerRequest request) { + Long id = Long.parseLong(request.pathVariable("id")); + return bannerService.getBannerById(id) + .filter(banner -> banner.getDeletedAt() == null) + .flatMap(banner -> bannerService.deleteBanner(id) + .then(ServerResponse.noContent().build())) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + /** + * 将前端传来的 Map 转换为 Banner 领域对象 + * 兼容 isActive 以 boolean 或 string 形式传入 + */ + private Banner mapToBanner(Map body) { + Banner banner = new Banner(); + banner.setImageUrl((String) body.get("imageUrl")); + banner.setTitle((String) body.get("title")); + banner.setSubtitle((String) body.get("subtitle")); + banner.setLinkUrl((String) body.get("linkUrl")); + + // sortOrder 处理 + Object sortOrderObj = body.get("sortOrder"); + if (sortOrderObj instanceof Number) { + banner.setSortOrder(((Number) sortOrderObj).intValue()); + } + + // isActive 兼容 boolean 和 string + Object isActiveObj = body.get("isActive"); + if (isActiveObj instanceof Boolean) { + banner.setIsActive((Boolean) isActiveObj ? "1" : "0"); + } else if (isActiveObj instanceof String) { + banner.setIsActive((String) isActiveObj); + } + + return banner; + } +} diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java index 754e243..930814e 100644 --- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java @@ -65,7 +65,8 @@ public class SecurityConfig { .pathMatchers("/api/payment/create").permitAll() .pathMatchers("/api/payment/query").permitAll() .pathMatchers("/api/payment/notify").permitAll() - .pathMatchers("/api/payment/refund").permitAll(); + .pathMatchers("/api/payment/refund").permitAll() + .pathMatchers("/api/files/**").permitAll(); if (isDevOrTest) { spec.pathMatchers("/swagger-ui.html").permitAll() diff --git a/gym-manage-uniapp/api/banner.js b/gym-manage-uniapp/api/banner.js new file mode 100644 index 0000000..cdaefbe --- /dev/null +++ b/gym-manage-uniapp/api/banner.js @@ -0,0 +1,16 @@ +const http = require('../utils/request') + +/** 获取启用的轮播图列表(按 sortOrder 升序) */ +function getActiveBanners() { + return http.get('/banners/active').then(res => { + console.log('[Banner API] 响应数据:', JSON.stringify(res)) + return res + }).catch(err => { + console.error('[Banner API] 请求失败:', err) + throw err + }) +} + +module.exports = { + getActiveBanners +} diff --git a/gym-manage-uniapp/api/booking.js b/gym-manage-uniapp/api/booking.js index 380ce89..d8be151 100644 --- a/gym-manage-uniapp/api/booking.js +++ b/gym-manage-uniapp/api/booking.js @@ -6,8 +6,8 @@ function bookCourse(params) { } /** 取消预约 */ -function cancelBooking(bookingId) { - return http.post('/groupCourse/booking/' + bookingId + '/cancel') +function cancelBooking(bookingId, memberId) { + return http.post('/groupCourse/booking/' + bookingId + '/cancel', { memberId }) } /** 获取会员的预约列表 */ diff --git a/gym-manage-uniapp/pages.json b/gym-manage-uniapp/pages.json index 64b5e85..a4046b7 100644 --- a/gym-manage-uniapp/pages.json +++ b/gym-manage-uniapp/pages.json @@ -58,6 +58,8 @@ "selectedColor": "#00E676", "backgroundColor": "#1A1A1A", "borderStyle": "black", + "iconWidth": "24px", + "iconHeight": "24px", "list": [ { "pagePath": "pages/index/index", diff --git a/gym-manage-uniapp/pages/course-detail/course-detail.vue b/gym-manage-uniapp/pages/course-detail/course-detail.vue index 2dbe5a5..82654db 100644 --- a/gym-manage-uniapp/pages/course-detail/course-detail.vue +++ b/gym-manage-uniapp/pages/course-detail/course-detail.vue @@ -128,6 +128,7 @@ const courseApi = require('../../api/course') const bookingApi = require('../../api/booking') const store = require('../../store/index') + const { resolveCoverUrl } = require('../../utils/request') export default { data() { @@ -279,7 +280,7 @@ storedValueAmount: detail.storedValueAmount != null ? detail.storedValueAmount : 0, statusText: statusText, statusClass: statusClass, - coverImage: detail.coverImage || '', + coverImage: resolveCoverUrl(detail.coverImage), coverError: false } } diff --git a/gym-manage-uniapp/pages/index/index.vue b/gym-manage-uniapp/pages/index/index.vue index da8868f..2283632 100644 --- a/gym-manage-uniapp/pages/index/index.vue +++ b/gym-manage-uniapp/pages/index/index.vue @@ -74,6 +74,8 @@ const store = require('../../store/index') const memberApi = require('../../api/member') const courseApi = require('../../api/course') + const bannerApi = require('../../api/banner') + const { resolveCoverUrl } = require('../../utils/request') export default { data() { @@ -88,11 +90,7 @@ totalSignDays: 0, pendingBookings: 0 }, - banners: [ - { imageUrl: 'https://img.zcool.cn/community/01f5c65e5d9c6ca801216518e1d2e0.jpg@1280w_1l_2o_100sh.jpg', title: '夏季燃脂计划', subtitle: 'HIIT + 动感单车,高效燃脂 7 天挑战' }, - { imageUrl: 'https://img.zcool.cn/community/01a5fe5e5d9c4ba80121985ac87c5f.jpg@1280w_1l_2o_100sh.jpg', title: '瑜伽月卡特惠', subtitle: '新人专享 5 折,流瑜伽 / 普拉提任选' }, - { imageUrl: 'https://img.zcool.cn/community/0177c85e5d9c84a8012165182ef63f.jpg@1280w_1l_2o_100sh.jpg', title: '私教一对一体验', subtitle: '免费体测 + 定制训练计划,限 30 名' } - ], + banners: [], recommendCourses: [] } }, @@ -128,10 +126,11 @@ async loadData() { this.loading = true try { - // 并行获取会员信息和推荐课程 - const [memberResult, recommendResult] = await Promise.allSettled([ + // 并行获取会员信息、推荐课程和轮播图 + const [memberResult, recommendResult, bannerResult] = await Promise.allSettled([ memberApi.getMemberInfo(), - courseApi.getActiveRecommendations() + courseApi.getActiveRecommendations(), + bannerApi.getActiveBanners() ]) // 处理会员信息 @@ -153,13 +152,30 @@ return { id: gc.id || r.courseId, courseName: gc.courseName || '推荐课程', - imageUrl: gc.coverImage || '', + imageUrl: resolveCoverUrl(gc.coverImage), recommendReason: r.recommendReason || '', priceLabel: (gc.storedValueAmount > 0 ? '¥' + gc.storedValueAmount : '免费'), bookedCount: gc.currentMembers || 0 } }) } + // 处理轮播图 + if (bannerResult.status === 'fulfilled') { + const rawBanners = bannerResult.value || [] + console.log('[Index] 获取到轮播图原始数据:', rawBanners.length, '条') + this.banners = rawBanners.map(b => { + const resolvedUrl = resolveCoverUrl(b.imageUrl) + console.log('[Index] Banner id=' + b.id + ' imageUrl原始=' + b.imageUrl + ' → 解析后=' + resolvedUrl) + return { + imageUrl: resolvedUrl, + title: b.title || '', + subtitle: b.subtitle || '' + } + }) + console.log('[Index] 最终 banners:', JSON.stringify(this.banners)) + } else { + console.error('[Index] 轮播图请求失败:', bannerResult.reason) + } } catch (e) { console.error('首页数据加载失败:', e) } finally { diff --git a/gym-manage-uniapp/pages/my-courses/my-courses.vue b/gym-manage-uniapp/pages/my-courses/my-courses.vue index 927847e..d32901d 100644 --- a/gym-manage-uniapp/pages/my-courses/my-courses.vue +++ b/gym-manage-uniapp/pages/my-courses/my-courses.vue @@ -179,9 +179,15 @@ if (!res.confirm) return this.cancelling = item.id try { - await bookingApi.cancelBooking(item.id) + const memberInfo = store.getMemberInfo() + const memberId = memberInfo ? (memberInfo.memberId || memberInfo.id) : null + if (!memberId) { + uni.showToast({ title: '请先登录', icon: 'none' }) + return + } + await bookingApi.cancelBooking(item.id, memberId) + await this.loadBookings() uni.showToast({ title: '已取消', icon: 'success' }) - this.loadBookings() } catch (e) { console.error('取消预约失败:', e) const msg = (e.data && e.data.message) || '取消失败,请重试' @@ -245,8 +251,8 @@ return } await bookingApi.signIn(Number(scannedCourseId), memberId) + await this.loadBookings() uni.showToast({ title: '签到成功', icon: 'success' }) - this.loadBookings() } catch (e) { console.error('签到失败:', e) const msg = (e.data && e.data.message) || '签到失败,请重试' diff --git a/gym-manage-uniapp/pages/search/search.vue b/gym-manage-uniapp/pages/search/search.vue index ef1302a..6876b29 100644 --- a/gym-manage-uniapp/pages/search/search.vue +++ b/gym-manage-uniapp/pages/search/search.vue @@ -74,40 +74,46 @@ - {{ course.typeName.slice(0, 2) }} + + + {{ course.typeName }} + + + + + {{ course.difficultyLabel }} + + {{ course.courseName }} - - {{ course.category }} - {{ course.difficultyLabel }} - + + {{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }} + + + {{ label.labelName }} + + + 🕓 {{ course.shortDate }} {{ course.shortTime }} - {{ course.endShortTime }} {{ course.duration }} + 📍 {{ course.location }} - + 已预约 {{ course.currentMembers }}/{{ course.maxMembers }}人 + - - {{ label.labelName }} - - - {{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }} - - - - @@ -127,6 +133,7 @@ const courseApi = require('../../api/course') const bookingApi = require('../../api/booking') const store = require('../../store/index') + const { resolveCoverUrl } = require('../../utils/request') export default { data() { @@ -153,7 +160,8 @@ { label: '难度', value: 'difficulty' }, { label: '热度', value: 'popular' } ], - courses: [] + courses: [], + activeBookedCourseIds: new Set() // 用户当前已预约(status=0)的课程ID集合 } }, onLoad() { @@ -172,8 +180,11 @@ this.loadCourses() this.loadTypes() }, + onShow() { + this.loadActiveBookings() + }, onPullDownRefresh() { - Promise.all([this.loadCourses(), this.loadTypes()]) + Promise.all([this.loadCourses(), this.loadTypes(), this.loadActiveBookings()]) .finally(() => { uni.stopPullDownRefresh() }) }, computed: { @@ -191,7 +202,7 @@ }, filteredCourses() { let list = this.courses.map(c => { - const bd = c.baseDifficulty || c.calculatedDifficulty || 5 + const bd = this.getTypeDifficulty(c.courseType) const st = c.startTime || '' const et = c.endTime || '' // 兼容 "2026-07-15T16:45:00" 和 "2026-07-15 16:45:00" 两种格式 @@ -226,14 +237,22 @@ else if (statusStr !== 0) statusLabel = '已关闭' if (!canBook && statusStr === 0 && isFull) statusLabel = '已约满' + const diffLevel = bd <= 3 ? 'low' : bd <= 6 ? 'mid' : 'high' + const diffLabel = bd <= 3 ? '入门' : bd <= 6 ? '中等' : '困难' + const diffColor = diffLevel === 'low' ? '#00E676' : diffLevel === 'mid' ? '#FFA502' : '#FF5252' + const diffStars = Math.min(Math.max(Math.ceil(bd / 2), 1), 5) + return { ...c, currentMembers: curMembers, maxMembers: maxMems, isFull: isFull, canBook: canBook, - difficultyLevel: bd <= 3 ? 'low' : bd <= 6 ? 'mid' : 'high', - difficultyLabel: bd <= 3 ? '入门' : bd <= 6 ? '中等' : '困难', + difficultyLevel: diffLevel, + difficultyLabel: diffLabel, + difficultyColor: diffColor, + difficultyStars: diffStars, + difficultyPercent: Math.round(bd * 10), baseDifficulty: bd, shortDate: md + '-' + dd, shortTime: hh + ':' + mm, @@ -247,8 +266,8 @@ status: statusStr, statusLabel: statusLabel, storedValueAmount: c.storedValueAmount != null ? c.storedValueAmount : 0, - labels: c.labels || [], - coverImage: c.coverImage || '' + labels: this.getTypeLabels(c.courseType), + coverImage: resolveCoverUrl(c.coverImage), } }) if (this.searchKeyword) { @@ -280,12 +299,26 @@ return ta < tb ? -1 : ta > tb ? 1 : 0 }) } - // 只显示可预约的团课 - list = list.filter(c => c.canBook) + // 只显示可预约的团课,且排除用户已主动预约的课程 + list = list.filter(c => c.canBook && !this.activeBookedCourseIds.has(c.id)) return list } }, methods: { + // 从已加载的课程类型中匹配标签 + getTypeLabels(courseType) { + const type = this.getTypeInfo(courseType) + return type && type.labels ? type.labels : [] + }, + // 从已加载的课程类型中匹配难度 + getTypeDifficulty(courseType) { + const type = this.getTypeInfo(courseType) + return type && type.baseDifficulty != null ? type.baseDifficulty : 5 + }, + // 查找课程类型信息 + getTypeInfo(courseType) { + return this.courseTypes.find(t => String(t.id) === String(courseType)) + }, // 判断某行是否可见:展开时全部可见,折叠时只有 visibleRowStart 行可见 isRowVisible(rowIndex) { if (this.typesExpanded) return true @@ -313,6 +346,24 @@ console.error('加载课程类型失败:', e) } }, + async loadActiveBookings() { + try { + const memberInfo = store.getMemberInfo() + const memberId = memberInfo ? (memberInfo.memberId || memberInfo.id) : null + if (!memberId) { + this.activeBookedCourseIds = new Set() + return + } + const list = await bookingApi.getMemberBookings(memberId) + // 只记录状态为"0"(已预约)的课程ID,status=1(已取消)、2(已出席)、3(缺席)不挡 + const bookings = Array.isArray(list) ? list : (list.content || []) + this.activeBookedCourseIds = new Set( + bookings.filter(b => String(b.status) === '0').map(b => b.courseId) + ) + } catch (e) { + console.error('加载预约记录失败:', e) + } + }, onSearchInput() {}, clearSearch() { this.searchKeyword = '' }, onCoverLoad(course) { course.coverLoaded = true }, @@ -408,35 +459,56 @@ .sort-item.active { color: #00C853; font-weight: 600; } .course-list-inner { padding: 12px 16px 0; } - .course-item { margin-bottom: 12px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; } - .course-card { background: #FFFFFF; border-radius: 20px; padding: 14px; display: flex; } - .course-cover { width: 72px; height: 72px; border-radius: 12px; background: rgba(0,230,118,0.12); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 14px; overflow: hidden; position: relative; } - .cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F0F0F0 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; } + .course-item { margin-bottom: 14px; border-radius: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.07); overflow: hidden; } + .course-card { background: #FFFFFF; border-radius: 16px; display: flex; flex-direction: column; } + + /* ---- 封面区域 ---- */ + .course-cover { width: 100%; height: 170px; background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; } + .cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F5F5F5 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .cover-img { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s; z-index: 0; } .cover-img.loaded { opacity: 1; z-index: 2; } - .cover-text { font-size: 22px; font-weight: 700; color: #00C853; } - .course-body { flex: 1; display: flex; flex-direction: column; min-width: 0; } - .course-top { display: flex; justify-content: space-between; align-items: flex-start; } - .course-name { font-size: 16px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .course-tags { display: flex; flex-shrink: 0; margin-left: 6px; } - .course-category { font-size: 11px; color: #00C853; background: rgba(0,230,118,0.12); padding: 1px 8px; border-radius: 20px; font-weight: 500; margin-right: 4px; } - .course-difficulty { padding: 1px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; } - .diff-low { background: rgba(0,230,118,0.15); color: #00C853; } - .diff-mid { background: rgba(255,165,2,0.15); color: #FFA502; } - .diff-high { background: rgba(255,82,82,0.15); color: #FF5252; } - .course-meta { display: flex; align-items: center; font-size: 12px; color: #7A7E84; margin-top: 4px; } + + /* 无封面时的占位 */ + .cover-placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 0; } + .cover-icon { font-size: 36px; margin-bottom: 4px; } + .cover-type-name { font-size: 16px; font-weight: 700; color: #00C853; } + + /* 封面覆盖层 - 难度角标 */ + .cover-overlay { position: absolute; top: 10px; right: 10px; z-index: 5; } + .diff-badge { display: flex; align-items: center; padding: 4px 10px; border-radius: 20px; background: rgba(0,0,0,0.55); backdrop-filter: blur(4px); } + .diff-badge-star { font-size: 11px; margin-right: 1px; } + .diff-badge-label { font-size: 11px; font-weight: 600; margin-left: 4px; } + .diff-badge.diff-low .diff-badge-star { color: #00E676; } + .diff-badge.diff-low .diff-badge-label { color: #69F0AE; } + .diff-badge.diff-mid .diff-badge-star { color: #FFA502; } + .diff-badge.diff-mid .diff-badge-label { color: #FFBE4D; } + .diff-badge.diff-high .diff-badge-star { color: #FF5252; } + .diff-badge.diff-high .diff-badge-label { color: #FF8A80; } + + /* ---- 卡片内容 ---- */ + .course-body { padding: 14px 16px 16px; display: flex; flex-direction: column; } + .course-top { display: flex; justify-content: space-between; align-items: center; } + .course-name { font-size: 17px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .course-price { font-size: 20px; font-weight: 700; color: #00C853; flex-shrink: 0; margin-left: 8px; } + .course-price.free { color: #00C853; } + + /* 标签 */ + .course-labels { display: flex; flex-wrap: wrap; margin-top: 8px; } + .course-label { font-size: 11px; padding: 2px 8px; border-radius: 10px; margin-right: 6px; margin-bottom: 4px; } + + /* 元数据行 */ + .course-meta { display: flex; align-items: center; font-size: 13px; color: #7A7E84; margin-top: 6px; } + .meta-icon { font-size: 12px; margin-right: 4px; flex-shrink: 0; } .course-date { flex-shrink: 0; } .course-time { margin-left: 8px; } .course-duration { margin-left: 8px; color: #00C853; font-weight: 500; } .course-location { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .course-capacity { color: #7A7E84; } - .course-labels { display: flex; flex-wrap: wrap; margin-top: 6px; } - .course-label { font-size: 10px; padding: 1px 6px; border-radius: 10px; margin-right: 4px; margin-bottom: 2px; } - .course-bottom { text-align: right; margin-top: 6px; } - .course-price { font-size: 18px; font-weight: 700; color: #00C853; display: block; margin-top: 6px; } - .course-price.free { color: #00C853; } - .btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 6px 16px; border-radius: 40px; font-weight: 700; font-size: 13px; line-height: 1.4; box-shadow: 0 2px 8px rgba(0,230,118,0.3); } + + /* 底部预约行 */ + .course-bottom { display: flex; align-items: center; margin-top: 12px; padding-top: 10px; border-top: 1px solid #F0F0F0; } + .course-capacity { font-size: 13px; color: #7A7E84; flex: 1; } + .btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 10px 28px; border-radius: 40px; font-weight: 700; font-size: 15px; line-height: 1.2; flex-shrink: 0; margin-left: auto; text-align: center; box-shadow: 0 2px 8px rgba(0,230,118,0.3); } .btn-book::after { border: none; } .btn-book.disabled { background: #E0E0E0; color: #BDBDBD; box-shadow: none; } diff --git a/gym-manage-uniapp/utils/request.js b/gym-manage-uniapp/utils/request.js index e0f84f2..d5e2fd2 100644 --- a/gym-manage-uniapp/utils/request.js +++ b/gym-manage-uniapp/utils/request.js @@ -60,4 +60,24 @@ http.interceptors.response.use( } ) +/** + * 将 coverImage 字段值解析为完整图片 URL + * - /api/files/{id}/preview → 拼接 baseURL 后返回 + * - 纯数字 → 构造 /files/{id}/preview 后拼接 baseURL + * - 其他 → 原样返回 + */ +function resolveCoverUrl(coverImage) { + if (!coverImage) return '' + // 新格式:/api/files/{id}/preview + if (/^\/api\/files\/\d+\/preview$/.test(coverImage)) { + return BASE_URL + coverImage.substring(4) // 去掉 /api + } + // 旧格式:纯数字 ID + if (/^\d+$/.test(coverImage)) { + return BASE_URL + '/files/' + coverImage + '/preview' + } + return coverImage +} + module.exports = http +module.exports.resolveCoverUrl = resolveCoverUrl diff --git a/gym-manage-web/src/api/banner.api.ts b/gym-manage-web/src/api/banner.api.ts new file mode 100644 index 0000000..6ff8835 --- /dev/null +++ b/gym-manage-web/src/api/banner.api.ts @@ -0,0 +1,33 @@ +import request from '@/utils/request' + +export interface Banner { + id?: number + imageUrl: string + title: string + subtitle?: string + sortOrder?: number + isActive?: boolean + linkUrl?: string + createdAt?: string + updatedAt?: string +} + +export const bannerApi = { + getAll: () => + request.get('/banners'), + + getActive: () => + request.get('/banners/active'), + + getById: (id: number) => + request.get(`/banners/${id}`), + + create: (data: Banner) => + request.post('/banners', data), + + update: (id: number, data: Banner) => + request.put(`/banners/${id}`, data), + + delete: (id: number) => + request.delete(`/banners/${id}`), +} diff --git a/gym-manage-web/src/router/index.ts b/gym-manage-web/src/router/index.ts index 303e2ea..901a1d9 100644 --- a/gym-manage-web/src/router/index.ts +++ b/gym-manage-web/src/router/index.ts @@ -76,6 +76,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/notify/NoticeManagement.vue'), meta: { title: '通知公告' } }, + { + path: 'banners', + name: 'BannerManagement', + component: () => import('@/views/banner/BannerManagement.vue'), + meta: { title: '轮播图管理' } + }, { path: 'loginlog', name: 'LoginLog', diff --git a/gym-manage-web/src/stores/permission.ts b/gym-manage-web/src/stores/permission.ts index 513427e..c68b36e 100644 --- a/gym-manage-web/src/stores/permission.ts +++ b/gym-manage-web/src/stores/permission.ts @@ -45,6 +45,7 @@ function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] { 'member/member/index': '/members', 'member/card/index': '/member-cards', 'statistics/dashboard/index': '/statistics', + 'banner/index': '/banners', } const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F') @@ -107,6 +108,7 @@ function getMenuIcon(menuName: string): string { '会员管理': 'Avatar', '会员卡管理': 'CreditCard', '数据统计': 'DataAnalysis', + '轮播图管理': 'Picture', } return iconMap[menuName] || 'Document' } diff --git a/gym-manage-web/src/views/banner/BannerManagement.vue b/gym-manage-web/src/views/banner/BannerManagement.vue new file mode 100644 index 0000000..3d6e56a --- /dev/null +++ b/gym-manage-web/src/views/banner/BannerManagement.vue @@ -0,0 +1,454 @@ + + + + + diff --git a/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue b/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue index 570695d..4043007 100644 --- a/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue +++ b/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue @@ -312,10 +312,12 @@ const loadRowCover = async (coverImage: string | undefined) => { if (!coverImage) return // already loaded if (coverCache[coverImage]) return - // if it's a numeric file ID, fetch via preview API - if (/^\d+$/.test(coverImage)) { + // Extract file ID from either /api/files/{id}/preview format or pure numeric string + const match = coverImage.match(/\/api\/files\/(\d+)\/preview$/) + const fileId = match ? match[1] : (/^\d+$/.test(coverImage) ? coverImage : null) + if (fileId) { try { - const blob: any = await request.get(`/files/${coverImage}/preview`, { responseType: 'blob' }) + const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' }) coverCache[coverImage] = URL.createObjectURL(blob) } catch { // leave empty, template will show "No data" @@ -413,9 +415,9 @@ const uploadCover = async (options: any) => { }) // res is the SysFile object (axios interceptor unwrapped response.data) if (res && res.id) { - formState.coverImage = String(res.id) + formState.coverImage = `/api/files/${res.id}/preview` // immediately fetch preview for the newly uploaded file - loadCoverPreview(String(res.id)) + loadCoverPreview(formState.coverImage) } ElMessage.success('封面上传成功') } catch { @@ -499,10 +501,12 @@ const loadCoverPreview = async (val: string) => { coverPreviewSrc.value = '' return } - // If it looks like a numeric file ID, fetch via preview API - if (/^\d+$/.test(val)) { + // Extract file ID from either /api/files/{id}/preview format or pure numeric string + const match = val.match(/\/api\/files\/(\d+)\/preview$/) + const fileId = match ? match[1] : (/^\d+$/.test(val) ? val : null) + if (fileId) { try { - const blob: any = await request.get(`/files/${val}/preview`, { responseType: 'blob' }) + const blob: any = await request.get(`/files/${fileId}/preview`, { responseType: 'blob' }) const oldUrl = coverPreviewSrc.value if (oldUrl) URL.revokeObjectURL(oldUrl) coverPreviewSrc.value = URL.createObjectURL(blob) diff --git a/scripts/add_banner_menu.sql b/scripts/add_banner_menu.sql new file mode 100644 index 0000000..19f46b2 --- /dev/null +++ b/scripts/add_banner_menu.sql @@ -0,0 +1,29 @@ +-- ============================================ +-- 轮播图管理 - 菜单数据插入(可独立执行) +-- 使用方法: 直接在数据库中运行此 SQL +-- ============================================ + +-- 轮播图管理菜单项(顶级菜单 parent_id=0) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) +SELECT 19, '轮播图管理', 0, 8, 'C', 'system:banner:list', 'banner/index', 1, NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 19); + +-- 按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) +SELECT 191, '轮播图查询', 19, 1, 'F', 'system:banner:query', NULL, 1, NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 191); + +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) +SELECT 192, '轮播图新增', 19, 2, 'F', 'system:banner:add', NULL, 1, NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 192); + +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) +SELECT 193, '轮播图修改', 19, 3, 'F', 'system:banner:edit', NULL, 1, NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 193); + +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) +SELECT 194, '轮播图删除', 19, 4, 'F', 'system:banner:remove', NULL, 1, NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 194); + +-- 重置序列 +SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu)); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V10__Create_GroupCourse_Type_table.sql b/temp_migration_backup/V10__Create_GroupCourse_Type_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V10__Create_GroupCourse_Type_table.sql rename to temp_migration_backup/V10__Create_GroupCourse_Type_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V11__Create_Course_Label_tables.sql b/temp_migration_backup/V11__Create_Course_Label_tables.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V11__Create_Course_Label_tables.sql rename to temp_migration_backup/V11__Create_Course_Label_tables.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V12__Create_GroupCourse_Recommend_table.sql b/temp_migration_backup/V12__Create_GroupCourse_Recommend_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V12__Create_GroupCourse_Recommend_table.sql rename to temp_migration_backup/V12__Create_GroupCourse_Recommend_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Consolidated_Test_Data.sql b/temp_migration_backup/V13__Consolidated_Test_Data.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V13__Consolidated_Test_Data.sql rename to temp_migration_backup/V13__Consolidated_Test_Data.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V19__Add_GroupCourse_QRCode_Path.sql b/temp_migration_backup/V19__Add_GroupCourse_QRCode_Path.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V19__Add_GroupCourse_QRCode_Path.sql rename to temp_migration_backup/V19__Add_GroupCourse_QRCode_Path.sql diff --git a/temp_migration_backup/V1__Create_all_tables.sql b/temp_migration_backup/V1__Create_all_tables.sql new file mode 100644 index 0000000..ab538c2 --- /dev/null +++ b/temp_migration_backup/V1__Create_all_tables.sql @@ -0,0 +1,407 @@ +-- Novalon管理系统数据库初始化脚本 +-- 版本: V1 +-- 描述: 创建所有核心表结构(合并版) + +-- ============================================ +-- 用户与角色相关表 +-- ============================================ + +-- 用户表 +CREATE TABLE IF NOT EXISTS sys_user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + email VARCHAR(100), + phone VARCHAR(20), + nickname VARCHAR(100), + status INTEGER DEFAULT 1, + role_id BIGINT, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 角色表 +CREATE TABLE IF NOT EXISTS sys_role ( + id BIGSERIAL PRIMARY KEY, + role_name VARCHAR(100) NOT NULL, + role_key VARCHAR(100) NOT NULL UNIQUE, + role_sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 用户角色关联表(支持多对多关系) +CREATE TABLE IF NOT EXISTS user_role ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50), + CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE, + CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE, + CONSTRAINT uk_user_role UNIQUE (user_id, role_id) +); + +-- ============================================ +-- 权限相关表 +-- ============================================ + +-- 权限表 +CREATE TABLE IF NOT EXISTS sys_permission ( + id BIGSERIAL PRIMARY KEY, + permission_name VARCHAR(100) NOT NULL, + permission_code VARCHAR(100) NOT NULL UNIQUE, + resource VARCHAR(200) NOT NULL, + action VARCHAR(50) NOT NULL, + description VARCHAR(500), + status INTEGER DEFAULT 1, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 角色权限关联表 +CREATE TABLE IF NOT EXISTS sys_role_permission ( + id BIGSERIAL PRIMARY KEY, + role_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE, + UNIQUE (role_id, permission_id) +); + +-- ============================================ +-- 菜单相关表 +-- ============================================ + +-- 菜单表 +CREATE TABLE IF NOT EXISTS sys_menu ( + id BIGSERIAL PRIMARY KEY, + menu_name VARCHAR(50) NOT NULL, + parent_id BIGINT DEFAULT 0, + order_num INTEGER DEFAULT 0, + menu_type VARCHAR(1) DEFAULT 'C', + perms VARCHAR(100), + component VARCHAR(200), + status INTEGER DEFAULT 1, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- ============================================ +-- 字典相关表 +-- ============================================ + +-- 字典类型表 +CREATE TABLE IF NOT EXISTS sys_dict_type ( + id BIGSERIAL PRIMARY KEY, + dict_name VARCHAR(100) NOT NULL, + dict_type VARCHAR(100) NOT NULL UNIQUE, + status VARCHAR(1) DEFAULT '0', + remark VARCHAR(500), + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 字典数据表 +CREATE TABLE IF NOT EXISTS sys_dict_data ( + id BIGSERIAL PRIMARY KEY, + dict_sort INTEGER DEFAULT 0, + dict_label VARCHAR(100) NOT NULL, + dict_value VARCHAR(100) NOT NULL, + dict_type VARCHAR(100) NOT NULL, + css_class VARCHAR(100), + list_class VARCHAR(100), + is_default VARCHAR(1) DEFAULT 'N', + status VARCHAR(1) DEFAULT '0', + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 字典表(通用字典) +CREATE TABLE IF NOT EXISTS sys_dictionary ( + id BIGSERIAL PRIMARY KEY, + type VARCHAR(100) NOT NULL, + code VARCHAR(100) NOT NULL, + name VARCHAR(100) NOT NULL, + value VARCHAR(500), + remark VARCHAR(500), + sort INTEGER DEFAULT 0, + create_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- ============================================ +-- 系统配置表 +-- ============================================ + +-- 系统配置表 +CREATE TABLE IF NOT EXISTS sys_config ( + id BIGSERIAL PRIMARY KEY, + config_name VARCHAR(100) NOT NULL, + config_key VARCHAR(100) NOT NULL UNIQUE, + config_value VARCHAR(500) NOT NULL, + config_type VARCHAR(1) DEFAULT 'N', + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- ============================================ +-- 日志相关表 +-- ============================================ + +-- 登录日志表 +CREATE TABLE IF NOT EXISTS sys_login_log ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50), + ip VARCHAR(50), + location VARCHAR(255), + browser VARCHAR(50), + os VARCHAR(50), + status VARCHAR(1), + message VARCHAR(255), + login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 异常日志表 +CREATE TABLE IF NOT EXISTS sys_exception_log ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50), + title VARCHAR(100), + exception_name VARCHAR(100), + method_name VARCHAR(255), + method_params TEXT, + exception_msg TEXT, + exception_stack TEXT, + ip VARCHAR(50), + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 操作日志表 +CREATE TABLE IF NOT EXISTS operation_log ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(50), + operation VARCHAR(100), + method VARCHAR(200), + params TEXT, + result TEXT, + ip VARCHAR(50), + duration BIGINT, + status VARCHAR(1) DEFAULT '0', + error_msg TEXT, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 审计日志表 +CREATE TABLE IF NOT EXISTS audit_log ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(100) NOT NULL, + entity_id BIGINT, + operation_type VARCHAR(20) NOT NULL, + operator VARCHAR(100), + operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + before_data JSONB, + after_data JSONB, + changed_fields TEXT[], + ip_address VARCHAR(50), + user_agent TEXT, + description TEXT, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 审计日志归档表 +CREATE TABLE IF NOT EXISTS audit_log_archive ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(100) NOT NULL, + entity_id BIGINT, + operation_type VARCHAR(20) NOT NULL, + operator VARCHAR(100), + operation_time TIMESTAMP, + before_data JSONB, + after_data JSONB, + changed_fields TEXT[], + ip_address VARCHAR(50), + user_agent TEXT, + description TEXT, + created_at TIMESTAMP, + archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================ +-- 通知与消息表 +-- ============================================ + +-- 系统公告表 +CREATE TABLE IF NOT EXISTS sys_notice ( + id BIGSERIAL PRIMARY KEY, + notice_title VARCHAR(50) NOT NULL, + notice_type VARCHAR(1) NOT NULL, + notice_content TEXT, + status VARCHAR(1) DEFAULT '0', + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- 用户消息表 +CREATE TABLE IF NOT EXISTS sys_user_message ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + notice_id BIGINT, + message_title VARCHAR(255), + message_content TEXT, + is_read VARCHAR(1) DEFAULT '0', + read_time TIMESTAMP, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- ============================================ +-- 文件管理表 +-- ============================================ + +-- 文件管理表 +CREATE TABLE IF NOT EXISTS sys_file ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size BIGINT, + file_type VARCHAR(100), + file_extension VARCHAR(10), + storage_type VARCHAR(50), + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- ============================================ +-- OAuth2相关表 +-- ============================================ + +-- OAuth2客户端表 +CREATE TABLE IF NOT EXISTS oauth2_client ( + id BIGSERIAL PRIMARY KEY, + client_id VARCHAR(100) NOT NULL UNIQUE, + client_secret VARCHAR(255) NOT NULL, + client_name VARCHAR(100), + web_server_redirect_uri VARCHAR(500), + scope VARCHAR(500), + authorized_grant_types VARCHAR(500), + access_token_validity_seconds INTEGER, + refresh_token_validity_seconds INTEGER, + auto_approve VARCHAR(1) DEFAULT 'false', + enabled VARCHAR(1) DEFAULT 'true', + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- ============================================ +-- 表注释 +-- ============================================ + +COMMENT ON TABLE sys_user IS '系统用户表'; +COMMENT ON TABLE sys_role IS '系统角色表'; +COMMENT ON TABLE user_role IS '用户角色关联表'; +COMMENT ON TABLE sys_permission IS '系统权限表'; +COMMENT ON TABLE sys_role_permission IS '角色权限关联表'; +COMMENT ON TABLE sys_menu IS '系统菜单表'; +COMMENT ON TABLE sys_dict_type IS '字典类型表'; +COMMENT ON TABLE sys_dict_data IS '字典数据表'; +COMMENT ON TABLE sys_dictionary IS '通用字典表'; +COMMENT ON TABLE sys_config IS '系统配置表'; +COMMENT ON TABLE sys_login_log IS '登录日志表'; +COMMENT ON TABLE sys_exception_log IS '异常日志表'; +COMMENT ON TABLE operation_log IS '操作日志表'; +COMMENT ON TABLE audit_log IS '审计日志表'; +COMMENT ON TABLE audit_log_archive IS '审计日志归档表'; +COMMENT ON TABLE sys_notice IS '系统公告表'; +COMMENT ON TABLE sys_user_message IS '用户消息表'; +COMMENT ON TABLE sys_file IS '文件管理表'; +COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表'; + +COMMENT ON TABLE sys_exception_log IS '异常日志表'; +COMMENT ON COLUMN sys_exception_log.id IS '主键ID'; +COMMENT ON COLUMN sys_exception_log.username IS '操作用户'; +COMMENT ON COLUMN sys_exception_log.title IS '异常标题'; +COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称'; +COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称'; +COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数'; +COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息'; +COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈'; +COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址'; +COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间'; + +COMMENT ON TABLE audit_log IS '审计日志表'; +COMMENT ON COLUMN audit_log.id IS '主键ID'; +COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)'; +COMMENT ON COLUMN audit_log.entity_id IS '实体ID'; +COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE)'; +COMMENT ON COLUMN audit_log.operator IS '操作人'; +COMMENT ON COLUMN audit_log.operation_time IS '操作时间'; +COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)'; +COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)'; +COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表'; +COMMENT ON COLUMN audit_log.ip_address IS 'IP地址'; +COMMENT ON COLUMN audit_log.description IS '操作描述'; +COMMENT ON COLUMN audit_log.created_at IS '记录创建时间'; + +COMMENT ON TABLE audit_log_archive IS '审计日志归档表'; +COMMENT ON COLUMN audit_log_archive.id IS '主键ID'; +COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)'; +COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID'; +COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE)'; +COMMENT ON COLUMN audit_log_archive.operator IS '操作人'; +COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间'; +COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)'; +COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)'; +COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表'; +COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址'; +COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理'; +COMMENT ON COLUMN audit_log_archive.description IS '操作描述'; +COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间'; +COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Add_deleted_at_to_member_user.sql b/temp_migration_backup/V20__Add_deleted_at_to_member_user.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V20__Add_deleted_at_to_member_user.sql rename to temp_migration_backup/V20__Add_deleted_at_to_member_user.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V21__Add_updated_at_to_member_card_transactions.sql b/temp_migration_backup/V21__Add_updated_at_to_member_card_transactions.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V21__Add_updated_at_to_member_card_transactions.sql rename to temp_migration_backup/V21__Add_updated_at_to_member_card_transactions.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V22__Add_Business_Permissions.sql b/temp_migration_backup/V22__Add_Business_Permissions.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V22__Add_Business_Permissions.sql rename to temp_migration_backup/V22__Add_Business_Permissions.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Add_Coach_Test_Data.sql b/temp_migration_backup/V23__Add_Coach_Test_Data.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V23__Add_Coach_Test_Data.sql rename to temp_migration_backup/V23__Add_Coach_Test_Data.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Add_Second_Coach_Test_Data.sql b/temp_migration_backup/V24__Add_Second_Coach_Test_Data.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V24__Add_Second_Coach_Test_Data.sql rename to temp_migration_backup/V24__Add_Second_Coach_Test_Data.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V25__Create_Payment_Order_table.sql b/temp_migration_backup/V25__Create_Payment_Order_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V25__Create_Payment_Order_table.sql rename to temp_migration_backup/V25__Create_Payment_Order_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V26__Add_party_order_id_to_payment_order.sql b/temp_migration_backup/V26__Add_party_order_id_to_payment_order.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V26__Add_party_order_id_to_payment_order.sql rename to temp_migration_backup/V26__Add_party_order_id_to_payment_order.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V27__Create_StoredCard_And_PayPassword_table.sql b/temp_migration_backup/V27__Create_StoredCard_And_PayPassword_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V27__Create_StoredCard_And_PayPassword_table.sql rename to temp_migration_backup/V27__Create_StoredCard_And_PayPassword_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V28__Add_Business_Menus.sql b/temp_migration_backup/V28__Add_Business_Menus.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V28__Add_Business_Menus.sql rename to temp_migration_backup/V28__Add_Business_Menus.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V29__Add_Member_Management_Menu.sql b/temp_migration_backup/V29__Add_Member_Management_Menu.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V29__Add_Member_Management_Menu.sql rename to temp_migration_backup/V29__Add_Member_Management_Menu.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V2__Insert_initial_data.sql b/temp_migration_backup/V2__Insert_initial_data.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V2__Insert_initial_data.sql rename to temp_migration_backup/V2__Insert_initial_data.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V30__Add_Member_Card_Management_Menu.sql b/temp_migration_backup/V30__Add_Member_Card_Management_Menu.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V30__Add_Member_Card_Management_Menu.sql rename to temp_migration_backup/V30__Add_Member_Card_Management_Menu.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V31__Add_Statistics_Menu.sql b/temp_migration_backup/V31__Add_Statistics_Menu.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V31__Add_Statistics_Menu.sql rename to temp_migration_backup/V31__Add_Statistics_Menu.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Make_member_card_id_nullable.sql b/temp_migration_backup/V32__Make_member_card_id_nullable.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V32__Make_member_card_id_nullable.sql rename to temp_migration_backup/V32__Make_member_card_id_nullable.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V3__Create_indexes.sql b/temp_migration_backup/V3__Create_indexes.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V3__Create_indexes.sql rename to temp_migration_backup/V3__Create_indexes.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V4__Grant_permissions.sql b/temp_migration_backup/V4__Grant_permissions.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V4__Grant_permissions.sql rename to temp_migration_backup/V4__Grant_permissions.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_operation_log_table.sql b/temp_migration_backup/V5__Create_operation_log_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_operation_log_table.sql rename to temp_migration_backup/V5__Create_operation_log_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V6__Create_GroupCourse_table.sql b/temp_migration_backup/V6__Create_GroupCourse_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V6__Create_GroupCourse_table.sql rename to temp_migration_backup/V6__Create_GroupCourse_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V7__Create_Member_And_MemberCard.sql b/temp_migration_backup/V7__Create_Member_And_MemberCard.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V7__Create_Member_And_MemberCard.sql rename to temp_migration_backup/V7__Create_Member_And_MemberCard.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V8__Add_GroupCourse_Booking_Snapshot_Fields.sql b/temp_migration_backup/V8__Add_GroupCourse_Booking_Snapshot_Fields.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V8__Add_GroupCourse_Booking_Snapshot_Fields.sql rename to temp_migration_backup/V8__Add_GroupCourse_Booking_Snapshot_Fields.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V9__Create_sign_in_record_table.sql b/temp_migration_backup/V9__Create_sign_in_record_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V9__Create_sign_in_record_table.sql rename to temp_migration_backup/V9__Create_sign_in_record_table.sql