diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java index ce05953..07f2e24 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java @@ -63,13 +63,24 @@ public class MemberCardHandler { @Operation(summary = "更新会员卡类型", description = "更新会员卡类型信息") public Mono updateMemberCard(ServerRequest request) { Long id = Long.valueOf(request.pathVariable("id")); - return request.bodyToMono(MemberCard.class) - .flatMap(card -> { - card.setMemberCardId(id); - return memberCardService.save(card); + return memberCardService.findByMemberCardIdAndDeletedAtIsNull(id) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡不存在: " + id))) + .zipWith(request.bodyToMono(MemberCard.class)) + .flatMap(tuple -> { + MemberCard existing = tuple.getT1(); + MemberCard body = tuple.getT2(); + // 仅更新非 null 字段,避免 null 覆盖已有值 + if (body.getMemberCardName() != null) existing.setMemberCardName(body.getMemberCardName()); + if (body.getMemberCardType() != null) existing.setMemberCardType(body.getMemberCardType()); + if (body.getMemberCardPrice() != null) existing.setMemberCardPrice(body.getMemberCardPrice()); + if (body.getMemberCardValidityDays() != null) existing.setMemberCardValidityDays(body.getMemberCardValidityDays()); + if (body.getMemberCardTotalTimes() != null) existing.setMemberCardTotalTimes(body.getMemberCardTotalTimes()); + if (body.getMemberCardAmount() != null) existing.setMemberCardAmount(body.getMemberCardAmount()); + if (body.getMemberCardStatus() != null) existing.setMemberCardStatus(body.getMemberCardStatus()); + if (body.getExtraConfig() != null) existing.setExtraConfig(body.getExtraConfig()); + return memberCardService.save(existing); }) - .flatMap(updated -> ServerResponse.ok().bodyValue(updated)) - .switchIfEmpty(ServerResponse.notFound().build()); + .flatMap(updated -> ServerResponse.ok().bodyValue(updated)); } @Operation(summary = "删除会员卡类型", description = "逻辑删除会员卡类型") diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java index ea6882c..98fdb7c 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java @@ -46,7 +46,7 @@ public interface MemberCardRepository extends R2dbcRepository "AND (:type IS NULL OR member_card_type = :type) " + "AND (:minPrice IS NULL OR member_card_price >= :minPrice) " + "AND (:maxPrice IS NULL OR member_card_price <= :maxPrice) " + - "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + "ORDER BY id ASC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") Flux findWithConditions(Integer status, String name, String type, Double minPrice, Double maxPrice, Pageable pageable); 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 10b1b1e..ef16328 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 @@ -257,8 +257,11 @@ public class SystemRouter { // ===== 会员卡类型管理 ===== .GET("/api/member-cards/active", memberCardHandler::getActiveCards) + .GET("/api/member-cards", memberCardHandler::listMemberCards) .GET("/api/member-cards/{memberCardId}", memberCardHandler::getMemberCardById) .POST("/api/member-cards", memberCardHandler::createMemberCard) + .PUT("/api/member-cards/{id}", memberCardHandler::updateMemberCard) + .DELETE("/api/member-cards/{id}", memberCardHandler::deleteMemberCard) // ===== 会员卡记录管理(核心业务)===== .POST("/api/member-card-records/purchase", memberCardRecordHandler::purchaseCard) diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V30__Add_Member_Card_Management_Menu.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V30__Add_Member_Card_Management_Menu.sql new file mode 100644 index 0000000..420f7bf --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V30__Add_Member_Card_Management_Menu.sql @@ -0,0 +1,22 @@ +-- ============================================ +-- V30: 新增会员卡管理菜单 +-- 描述: 为会员卡管理功能创建菜单项 +-- ============================================ + +-- 会员卡管理一级菜单(目录) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(502, '会员卡管理', 0, 6, 'M', NULL, NULL, 1, NOW(), NOW()); + +-- 会员卡管理子菜单(页面) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(503, '会员卡管理', 502, 1, 'C', 'business:memberCard:list', 'member/card/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 +(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()); + +-- 重置菜单序列 +SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu)); diff --git a/gym-manage-web/src/api/member.api.ts b/gym-manage-web/src/api/member.api.ts new file mode 100644 index 0000000..136900d --- /dev/null +++ b/gym-manage-web/src/api/member.api.ts @@ -0,0 +1,107 @@ +import request from '@/utils/request' + +// ==================== 会员基础信息 ==================== + +export interface Member { + id?: number | string + memberNo?: string + nickname?: string + phone?: string + gender?: number + genderDesc?: string + avatar?: string + createdAt?: string + updatedAt?: string +} + +export interface MemberDetail { + id?: number + memberNo?: string + nickname?: string + phone?: string + gender?: number + genderDesc?: string + birthday?: string + address?: string + avatar?: string + subscribed?: boolean + lastLoginAt?: string + createdAt?: string + memberCards?: MemberCardInfo[] + activeCardCount?: number + inactiveCardCount?: number +} + +export interface MemberCardInfo { + id?: number + memberCardId?: number + memberCardName?: string + memberCardType?: string + memberCardTypeDesc?: string + memberCardPrice?: number + memberCardValidityDays?: number + memberCardTotalTimes?: number + memberCardAmount?: number + memberCardStatus?: string + memberCardStatusDesc?: string + extraConfig?: string + createdAt?: string + updatedAt?: string +} + +export interface MemberCardRecord { + id?: number + memberCardRecordId?: number + memberId?: number + memberCardId?: number + memberCardName?: string + memberCardType?: string + memberCardPrice?: number + memberCardValidityDays?: number + memberCardTotalTimes?: number + status?: string + remainingTimes?: number + remainingAmount?: number + expireTime?: string + purchaseTime?: string +} + +export interface UpdateMemberDto { + nickname?: string + gender?: string + birthday?: string + avatar?: string + address?: string +} + +export interface UpdatePhoneDto { + phone: string +} + +export const memberApi = { + /** 搜索会员 */ + search: (params: { searchValue?: string; pageNum?: number; pageSize?: number }) => + request.get('/admin/members', { params }), + + /** 获取所有会员 */ + getAll: (params: { pageNum?: number; pageSize?: number; sortField?: string; sortOrder?: string }) => + request.get('/admin/members/all', { params }), + + /** 获取会员详情 */ + getDetail: (id: number) => + request.get(`/admin/member/${id}`), + + /** 更新会员信息 */ + update: (id: number, data: UpdateMemberDto) => + request.put(`/admin/member/${id}`, data), + + /** 管理员更新会员手机号 */ + updatePhone: (id: number, data: UpdatePhoneDto) => + request.post(`/admin/member/${id}/phone`, data), + + /** 获取会员绑定的会员卡记录 */ + getMemberCardRecords: (memberId: number) => + request.get(`/member-card-records/my-cards-with-status/${memberId}`, { + params: { status: 'all' }, + }), +} diff --git a/gym-manage-web/src/router/index.ts b/gym-manage-web/src/router/index.ts index 8840e5d..b638465 100644 --- a/gym-manage-web/src/router/index.ts +++ b/gym-manage-web/src/router/index.ts @@ -117,6 +117,18 @@ const routes: RouteRecordRaw[] = [ name: 'CourseRecommendManagement', component: () => import('@/views/groupcourse/CourseRecommendManagement.vue'), meta: { title: '团课推荐' } + }, + { + path: 'members', + name: 'MemberManagement', + component: () => import('@/views/member/MemberManagement.vue'), + meta: { title: '会员管理' } + }, + { + path: 'member-cards', + name: 'MemberCardManagement', + component: () => import('@/views/member/MemberCardManagement.vue'), + meta: { title: '会员卡管理' } } ] } diff --git a/gym-manage-web/src/stores/permission.ts b/gym-manage-web/src/stores/permission.ts index ad9aa98..9fd43d0 100644 --- a/gym-manage-web/src/stores/permission.ts +++ b/gym-manage-web/src/stores/permission.ts @@ -42,6 +42,8 @@ function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] { 'gymCourse/type/index': '/courses/types', 'gymCourse/label/index': '/courses/labels', 'gymCourse/recommend/index': '/courses/recommend', + 'member/member/index': '/members', + 'member/card/index': '/member-cards', } const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F') @@ -100,7 +102,9 @@ function getMenuIcon(menuName: string): string { '团课管理': 'TrophyBase', '团课类型': 'Grid', '团课标签': 'Discount', - '团课推荐': 'Star' + '团课推荐': 'Star', + '会员管理': 'Avatar', + '会员卡管理': 'CreditCard', } return iconMap[menuName] || 'Document' } diff --git a/gym-manage-web/src/views/member/MemberManagement.vue b/gym-manage-web/src/views/member/MemberManagement.vue new file mode 100644 index 0000000..1e68cca --- /dev/null +++ b/gym-manage-web/src/views/member/MemberManagement.vue @@ -0,0 +1,642 @@ + + + + +