From 6ea23b5a015fdca075ae40a0bf84ebef07e7784e Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Sun, 10 May 2026 17:10:28 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-member/.gitignore | 47 +++ .../gym-member/MEMBER_USER_TABLE_SIMPLE.sql | 58 +++ gym-manage-api/gym-member/merge_comments.py | 102 +++++ gym-manage-api/gym-member/pom.xml | 246 ++++++++++++ .../gym-member/spotbugs-exclude.xml | 18 + .../member/config/HttpClientConfig.java | 22 ++ .../member/config/WechatProperties.java | 66 ++++ .../member/dto/AdminUpdatePhoneDto.java | 17 + .../gym/manage/member/dto/WechatLoginDto.java | 29 ++ .../member/dto/WechatOfficialEventDto.java | 35 ++ .../gym/manage/member/entity/BaseEntity.java | 41 ++ .../gym/manage/member/entity/Member.java | 81 ++++ .../gym/manage/member/entity/WechatUser.java | 55 +++ .../manage/member/handler/MemberHandler.java | 140 +++++++ .../member/handler/WechatAuthHandler.java | 68 ++++ .../handler/WechatOfficialEventHandler.java | 218 +++++++++++ .../member/repository/MemberRepository.java | 28 ++ .../repository/WechatUserRepository.java | 29 ++ .../manage/member/service/MemberService.java | 30 ++ .../member/service/WechatApiService.java | 48 +++ .../member/service/WechatAuthService.java | 31 ++ .../member/service/WechatOfficialService.java | 54 +++ .../service/impl/MemberServiceImpl.java | 117 ++++++ .../service/impl/WechatApiServiceImpl.java | 260 ++++++++++++ .../service/impl/WechatAuthServiceImpl.java | 369 ++++++++++++++++++ .../impl/WechatOfficialServiceImpl.java | 313 +++++++++++++++ .../gym/manage/member/util/AesUtil.java | 77 ++++ .../manage/member/util/MemberNoGenerator.java | 53 +++ .../manage/member/util/WechatPhoneUtil.java | 65 +++ .../gym/manage/member/vo/MemberInfoVO.java | 46 +++ .../gym/manage/member/vo/WechatLoginVO.java | 38 ++ .../manage/member/vo/WechatUserInfoVO.java | 72 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/application.yml | 18 + .../src/main/resources/db/schema.sql | 138 +++++++ .../gym/manage/member/MemberModuleTest.java | 16 + .../gym-member/test-member-apis.bat | 56 +++ .../gym-member/test-member-apis.ps1 | 140 +++++++ 38 files changed, 3242 insertions(+) create mode 100644 gym-manage-api/gym-member/.gitignore create mode 100644 gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql create mode 100644 gym-manage-api/gym-member/merge_comments.py create mode 100644 gym-manage-api/gym-member/pom.xml create mode 100644 gym-manage-api/gym-member/spotbugs-exclude.xml create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java create mode 100644 gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 gym-manage-api/gym-member/src/main/resources/application.yml create mode 100644 gym-manage-api/gym-member/src/main/resources/db/schema.sql create mode 100644 gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java create mode 100644 gym-manage-api/gym-member/test-member-apis.bat create mode 100644 gym-manage-api/gym-member/test-member-apis.ps1 diff --git a/gym-manage-api/gym-member/.gitignore b/gym-manage-api/gym-member/.gitignore new file mode 100644 index 0000000..9d5d968 --- /dev/null +++ b/gym-manage-api/gym-member/.gitignore @@ -0,0 +1,47 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Maven ### +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +### System Files ### +.DS_Store +Thumbs.db diff --git a/gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql b/gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql new file mode 100644 index 0000000..aef3540 --- /dev/null +++ b/gym-manage-api/gym-member/MEMBER_USER_TABLE_SIMPLE.sql @@ -0,0 +1,58 @@ +-- ============================================ +-- 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 TIMESTAMP, + 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 '会员编号(唯一)'; +COMMENT ON COLUMN member_user.nickname IS '昵称'; +COMMENT ON COLUMN member_user.phone IS '手机号(AES加密存储)'; +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 '是否关注服务号'; +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 '是否删除(软删除标记)'; diff --git a/gym-manage-api/gym-member/merge_comments.py b/gym-manage-api/gym-member/merge_comments.py new file mode 100644 index 0000000..3c1000c --- /dev/null +++ b/gym-manage-api/gym-member/merge_comments.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +import os +import re + +base_path = r"c:\Users\13603\Desktop\健身房项目\gym-manage\gym-manage-api\gym-member\src\main\java\cn\novalon\gym\manage\member" + +count = 0 +for root, dirs, files in os.walk(base_path): + for file in files: + if file.endswith('.java'): + filepath = os.path.join(root, file) + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # 匹配模式:两个连续的注释块(类文档 + 作者注释) + pattern = r'(/\*\*[\s\S]*?\*/)\s*\n(/\*\*\s*\n\s*\*\s*@author\s+.+\s*\n\s*\*\s*@date\s+\d{4}-\d{2}-\d{2}\s*\n\s*\*/)' + + match = re.search(pattern, content) + if match: + class_doc = match.group(1) + author_doc = match.group(2) + + # 提取作者信息 + author_match = re.search(r'@author\s+(.+)', author_doc) + date_match = re.search(r'@date\s+(\d{4}-\d{2}-\d{2})', author_doc) + + if author_match and date_match: + author = author_match.group(1).strip() + date = date_match.group(1).strip() + + # 提取类文档的内容(去掉 /** 和 */) + class_doc_content = re.sub(r'/\*\*\s*\n|\s*\*/', '', class_doc).strip() + # 清理每行开头的多余星号和空格 + class_doc_lines = class_doc_content.split('\n') + cleaned_lines = [] + for line in class_doc_lines: + cleaned_line = re.sub(r'^\s*\*\s?', '', line).strip() + if cleaned_line: + cleaned_lines.append(cleaned_line) + class_doc_content = '\n * '.join(cleaned_lines) + + # 构建合并后的注释 + merged_doc = f"""/** + * {class_doc_content} + * + * @author {author} + * @date {date} + */""" + + # 替换原文 + new_content = content[:match.start()] + merged_doc + content[match.end():] + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + + count += 1 + print(f"已合并: {file}") + else: + # 检查是否有单独的作者注释在类文档之前 + pattern2 = r'/\*\*\s*\n\s*\*\s*@author\s+.+\s*\n\s*\*\s*@date\s+\d{4}-\d{2}-\d{2}\s*\n\s*\*/\s*\n(/\*\*[\s\S]*?\*/)' + match2 = re.search(pattern2, content) + if match2: + author_doc = match2.group(0).split('\n/**')[0] + '\n/**' + class_doc = '/**' + match2.group(1) + + # 提取作者信息 + author_match = re.search(r'@author\s+(.+)', author_doc) + date_match = re.search(r'@date\s+(\d{4}-\d{2}-\d{2})', author_doc) + + if author_match and date_match: + author = author_match.group(1).strip() + date = date_match.group(1).strip() + + # 提取类文档的内容 + class_doc_content = re.sub(r'/\*\*\s*\n|\s*\*/', '', class_doc).strip() + # 清理每行开头的多余星号和空格 + class_doc_lines = class_doc_content.split('\n') + cleaned_lines = [] + for line in class_doc_lines: + cleaned_line = re.sub(r'^\s*\*\s?', '', line).strip() + if cleaned_line: + cleaned_lines.append(cleaned_line) + class_doc_content = '\n * '.join(cleaned_lines) + + # 构建合并后的注释 + merged_doc = f"""/** + * {class_doc_content} + * + * @author {author} + * @date {date} + */""" + + # 替换原文 + new_content = content[:match2.start()] + merged_doc + content[match2.end():] + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + + count += 1 + print(f"已合并: {file}") + +print(f"\n完成!共合并 {count} 个文件") diff --git a/gym-manage-api/gym-member/pom.xml b/gym-manage-api/gym-member/pom.xml new file mode 100644 index 0000000..00d2673 --- /dev/null +++ b/gym-manage-api/gym-member/pom.xml @@ -0,0 +1,246 @@ + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-member + jar + + Gym Member + Member Management Module - Frontend User Services + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + cn.novalon.gym.manage + manage-db + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-aop + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.data + spring-data-commons + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + io.github.resilience4j + resilience4j-spring-boot3 + + + io.github.resilience4j + resilience4j-reactor + + + org.testcontainers + testcontainers + 1.21.4 + test + + + org.testcontainers + postgresql + 1.21.4 + test + + + org.testcontainers + junit-jupiter + 1.21.4 + test + + + com.h2database + h2 + test + + + io.r2dbc + r2dbc-h2 + test + + + org.postgresql + r2dbc-postgresql + test + + + + com.github.binarywang + weixin-java-miniapp + 4.6.0 + + + com.github.binarywang + weixin-java-mp + 4.6.0 + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + package + + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.60 + + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.0 + + + com.github.spotbugs + spotbugs + 4.8.6 + + + + + spotbugs-check + verify + + check + + + + + Max + High + true + spotbugs-exclude.xml + + + + + diff --git a/gym-manage-api/gym-member/spotbugs-exclude.xml b/gym-manage-api/gym-member/spotbugs-exclude.xml new file mode 100644 index 0000000..fd9cd91 --- /dev/null +++ b/gym-manage-api/gym-member/spotbugs-exclude.xml @@ -0,0 +1,18 @@ + diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java new file mode 100644 index 0000000..a3be578 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/HttpClientConfig.java @@ -0,0 +1,22 @@ +package cn.novalon.gym.manage.member.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * HTTP 客户端配置 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Configuration +public class HttpClientConfig { + + // WebClient Bean,用于调用微信 API + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java new file mode 100644 index 0000000..9527e6b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java @@ -0,0 +1,66 @@ +package cn.novalon.gym.manage.member.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 微信配置 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Component +@ConfigurationProperties(prefix = "wechat") +// 指定属性文件位置(当前在gym-member模块的application.yml) +@PropertySource(value = "classpath:application.yml", ignoreResourceNotFound = true) +public class WechatProperties { + + // 小程序配置 + private MiniApp miniapp = new MiniApp(); + + // 服务号配置 + private Mp mp = new Mp(); + + // 手机号加密配置 + private PhoneEncryption phoneEncryption = new PhoneEncryption(); + + @Data + public static class MiniApp { + // 小程序 AppID + private String appId; + + // 小程序 AppSecret + private String appSecret; + } + + @Data + public static class Mp { + // 服务号 AppID + private String appId; + + // 服务号 AppSecret + private String appSecret; + + // Token 验证信息 + private String token; + + // EncodingAESKey + private String aesKey; + + // 回调地址(微信服务号事件推送 URL) + private String callbackUrl; + } + + @Data + public static class PhoneEncryption { + // 手机号加密密钥 + private String secretKey; + + // 初始化向量 IV + private String iv; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java new file mode 100644 index 0000000..0854b49 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/AdminUpdatePhoneDto.java @@ -0,0 +1,17 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +/** + * 更新手机号Dto + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +public class AdminUpdatePhoneDto { + + // 手机号 + private String phone; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java new file mode 100644 index 0000000..b8f706d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatLoginDto.java @@ -0,0 +1,29 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; + +/** + * 微信小程序登录 DTO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatLoginDto { + + // 微信小程序登录 code + @NotBlank(message = "登录code不能为空") + private String code; + + // 手机号code + private String phoneCode; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java new file mode 100644 index 0000000..602f3d3 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/WechatOfficialEventDto.java @@ -0,0 +1,35 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +/** + * 微信服务号事件 DTO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +public class WechatOfficialEventDto { + + // 微信号 + private String toUserName; + + // 发送方帐号(一个 OpenID) + private String fromUserName; + + // 消息创建时间(整型) + private Long createTime; + + // 消息类型,event + private String msgType; + + // 事件类型:subscribe(关注)/ unsubscribe(取消关注) + private String event; + + // 事件 KEY + private String eventKey; + + // 二维码 ticket(获取二维码图片) + private String ticket; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java new file mode 100644 index 0000000..1ad7597 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java @@ -0,0 +1,41 @@ +package cn.novalon.gym.manage.member.entity; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.Persistable; +import org.springframework.data.relational.core.mapping.Column; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 会员模块实体基类 + * + * @author 付嘉 + * @date 2026-05-08 + */ +@Data +public abstract class BaseEntity implements Persistable { + + // ID + @Id + private Long id; + + // 创建时间 + @CreatedDate + @Column("created_at") + private LocalDateTime createdAt; + + // 更新时间 + @LastModifiedDate + @Column("updated_at") + private LocalDateTime updatedAt; + + // 判断当前实体是否是新建的 + @Override + public boolean isNew() { + return createdAt == null; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java new file mode 100644 index 0000000..36c7ba4 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -0,0 +1,81 @@ +package cn.novalon.gym.manage.member.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 会员实体类 - 对应 member_user 表 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("member_user") +public class Member extends BaseEntity { + + //会员号 + @Column("member_no") + private String memberNo; + + //昵称 + @Column("nickname") + private String nickname; + + //手机号(AES 加密存储 + @Column("phone") + private String phone; + + //性别 + @Column("gender") + private Integer gender; + + //生日 + @Column("birthday") + private Date birthday; + + //地址 + @Column("address") + private String address; + + //是否关注服务号 + @Column("subscribed") + private Boolean subscribed; + + // 最后登录时间 + @Column("last_login_at") + private LocalDateTime lastLoginAt; + + // 头像 + @Column("avatar") + private String avatar; + + // 微信UnionID + @Column("union_id") + private String unionId; + + // 微信OpenID小程序 + @Column("miniapp_open_id") + private String miniappOpenId; + + // 服务号openid + @Column("official_open_id") + private String officialOpenId; + + // 软删除 + @Column("is_deleted") + private Boolean isDeleted; + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java new file mode 100644 index 0000000..2b7a8e0 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/WechatUser.java @@ -0,0 +1,55 @@ +package cn.novalon.gym.manage.member.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * 微信用户信息 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Table("wechat_user") +public class WechatUser extends BaseEntity { + + // 会员 ID + @Column("member_id") + private Long memberId; + + // 微信 UnionID + @Column("union_id") + private String unionId; + + // 小程序 OpenID + @Column("miniapp_openid") + private String miniappOpenid; + + // 服务号 OpenID + @Column("mp_openid") + private String mpOpenid; + + // 是否关注服务号 + @Column("is_subscribed") + private Boolean isSubscribed; + + // 首次关注时间公众号的时间 + @Column("subscribe_time") + private LocalDateTime subscribeTime; + + // 最后一次取消关注的时间 + @Column("unsubscribe_time") + private LocalDateTime unsubscribeTime; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java new file mode 100644 index 0000000..271d499 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -0,0 +1,140 @@ +package cn.novalon.gym.manage.member.handler; + +import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto; +import cn.novalon.gym.manage.member.service.MemberService; +import cn.novalon.gym.manage.member.service.WechatAuthService; +import cn.novalon.gym.manage.member.service.WechatOfficialService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.http.MediaType; +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.Mono; + +/** + * 会员信息处理器 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class MemberHandler { + + private final MemberService memberService; + private final WechatAuthService wechatAuthService; + private final WechatOfficialService wechatOfficialService; + + /** + * 获取会员信息 + * + * GET /api/member/info + * Header: X-Member-Id: 123 + */ + public Mono getMemberInfo(ServerRequest request) { + + String memberIdStr = request.headers().firstHeader("X-Member-Id"); + long memberId = NumberUtils.toLong(memberIdStr,0L); + + if (memberId <= 0) throw new IllegalArgumentException("获取会员信息失败: memberId 无效"); + + log.info("获取会员信息, memberId: {}", memberId); + + return memberService.getMemberInfo(memberId) + .flatMap(info -> { + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(info); + }); + } + + /** + * 绑定手机号(微信小程序) + * + * POST /api/member/phone/bind?code=PHONE_CODE + * Header: X-Member-Id: 123 + */ + public Mono bindPhone(ServerRequest request) { + + String memberIdStr = request.headers().firstHeader("X-Member-Id"); + Long memberId = NumberUtils.toLong(memberIdStr, 0L); + + if (memberId <= 0) throw new IllegalArgumentException("绑定手机号失败: memberId 无效"); + + String phoneCode = request.queryParam("phoneCode").orElse(""); + + if (phoneCode == null || phoneCode.trim().isEmpty()) throw new IllegalArgumentException("手机号code不能为空"); + + log.info("收到绑定手机号请求, memberId: {}, phoneCode: {}", memberId, phoneCode); + + return wechatAuthService.bindPhone(memberId, phoneCode) + .flatMap(success -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(success)); + } + + /** + * 查询服务号关注状态 + * + * GET /api/member/subscribe/status + * Header: X-Member-Id: 123 + * + */ + public Mono checkSubscribeStatus(ServerRequest request) { + + String memberIdStr = request.headers().firstHeader("X-Member-Id"); + long memberId = NumberUtils.toLong(memberIdStr,0L); + + if (memberId <= 0) throw new IllegalArgumentException("查询服务号关注状态失败: memberId 无效"); + + log.info("查询服务号关注状态, memberId: {}", memberId); + + return wechatOfficialService.checkSubscribeStatus(memberId) + .flatMap(subscribed -> { + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(subscribed); + }); + } + + /** + * 管理员更新手机号 + * + * POST /api/admin/members/123/phone + * Body: { "phone": "13800138000" } + * + */ + public Mono adminUpdatePhone(ServerRequest request) { + + String memberIdStr = request.pathVariable("id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + + if (memberId <= 0) throw new IllegalArgumentException("更新手机号失败: memberId 无效"); + + log.info("收到更新手机号请求, memberId: {}", memberId); + + return request.bodyToMono(AdminUpdatePhoneDto.class) + .flatMap(body -> { + String phone = body.getPhone(); + + if (phone == null || phone.isEmpty()) return Mono.error(new IllegalArgumentException("手机号不能为空")); + + if (!phone.matches("^1[3-9]\\d{9}$")) return Mono.error(new IllegalArgumentException("手机号格式不正确")); + + log.info("开始更新手机号, memberId: {}, phone: {}", memberId, phone); + + return memberService.adminUpdatePhone(memberId, phone); + }) + .flatMap(success -> { + log.info("手机号更新成功, memberId: {}", memberId); + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(success); + }); + } + +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java new file mode 100644 index 0000000..7ba7fa8 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java @@ -0,0 +1,68 @@ +package cn.novalon.gym.manage.member.handler; + +import cn.novalon.gym.manage.member.dto.WechatLoginDto; +import cn.novalon.gym.manage.member.service.WechatAuthService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +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.Mono; + +/** + * 微信֤ + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class WechatAuthHandler { + + private final WechatAuthService wechatAuthService; + private final WechatOfficialEventHandler wechatOfficialEventHandler; + + /** + * 小程序更新 + * + * POST /api/member/auth/miniapp/login + * Body: {"code": "wx_login_code"} + * + * @param request ServerRequest + * @return Mono 登录响应 + */ + public Mono miniappLogin(ServerRequest request) { + log.info("收到小程序登录请求"); + + return request.bodyToMono(WechatLoginDto.class) + .flatMap(loginRequest -> { + log.info("开始微信AuthService, code: {}", loginRequest.getCode()); + return wechatAuthService.miniappLogin(loginRequest); + }) + .flatMap(response -> { + log.info("更新成功, memberId: {}", response.getMemberId()); + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response); + }); + } + + /** + * 更新ص + * + * POST /api/member/auth/mp/callback + * Body: subscribeopenid + * + */ + public Mono mpCallback(ServerRequest request) { + return wechatOfficialEventHandler.handleEvent(request); + } + + // ֤微信ŷǩ + public Mono verifyMpSignature(ServerRequest request) { + return wechatOfficialEventHandler.verifySignature(request); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java new file mode 100644 index 0000000..d35ef87 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java @@ -0,0 +1,218 @@ +package cn.novalon.gym.manage.member.handler; + +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.service.WechatOfficialService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +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.Mono; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; + +/** + * 微信ŷ更新 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class WechatOfficialEventHandler { + + private final WechatOfficialService wechatOfficialService; + private final WechatProperties wechatProperties; + + /** + * 微信ŷ͵更新 + * + * ʽXML + * Ӧʽsuccess 头像地址 + */ + public Mono handleEvent(ServerRequest request) { + return request.bodyToMono(String.class) + .flatMap(xmlBody -> { + log.info("收到微信ŷ {}", xmlBody); + + // TODO: XML为WechatOfficialEventDto + // Ŀǰ򻯴ֱ获取openidevent + + String openId = extractOpenId(xmlBody); + String event = extractEvent(xmlBody); + + if (openId == null || event == null) { + log.error("޷微信更新"); + return ServerResponse.badRequest().bodyValue("error"); + } + + log.info(" openId={}, event={}", openId, event); + + // 更新ʹ + if ("subscribe".equals(event)) { + return wechatOfficialService.handleSubscribeEvent(openId) + .then(ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success")); + } else if ("unsubscribe".equals(event)) { + return wechatOfficialService.handleUnsubscribeEvent(openId) + .then(ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success")); + } else { + log.warn("δ֪更新: {}", event); + return ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success"); + } + }) + .onErrorResume(e -> { + log.error("微信更新失败", e); + return ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("success"); // ʹ失败Ҳsuccess微信 + }); + } + + /** + * ֤微信ŷǩ + * + * GET֤头像地址 + */ + public Mono verifySignature(ServerRequest request) { + String signature = request.queryParam("signature").orElse(""); + String timestamp = request.queryParam("timestamp").orElse(""); + String nonce = request.queryParam("nonce").orElse(""); + String echostr = request.queryParam("echostr").orElse(""); + + log.info("========== 微信ǩ֤=========="); + log.info("收到IJ:"); + log.info(" signature: {}", signature); + log.info(" timestamp: {}", timestamp); + log.info(" nonce: {}", nonce); + log.info(" echostr: {}", echostr); + + // 获取õToken + String token = wechatProperties.getMp().getToken(); + log.info("õToken: {}", token); + + // ֤ǩ + if (checkSignature(signature, timestamp, nonce, token)) { + log.info("ǩ֤成功echostr: {}", echostr); + log.info("========== 微信ǩ֤ =========="); + return ServerResponse.ok() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue(echostr); + } else { + log.warn("ǩ֤失败"); + log.info("========== 微信ǩ֤ =========="); + return ServerResponse.badRequest() + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("error"); + } + } + + /** + * ֤ǩ + * + * @param signature 微信żǩ + * @param timestamp 创建时间 + * @param nonce + * @param token Token + * @return Ƿ֤通过 + */ + private boolean checkSignature(String signature, String timestamp, String nonce, String token) { + // 1. tokentimestampnonceֵ + String[] arr = new String[]{token, timestamp, nonce}; + Arrays.sort(arr); + + // 2. 头像地址ƴӳһ头像地址 + StringBuilder sb = new StringBuilder(); + for (String str : arr) { + sb.append(str); + } + + // 3. ƴӺ头像地址sha1 + String encrypted = sha1(sb.toString()); + log.debug("õǩ {}", encrypted); + + // 4. ܺ头像地址signature会员 + return encrypted != null && encrypted.equalsIgnoreCase(signature); + } + + /** + * SHA1 + * + * @param str 头像地址 + * @return ܺ头像地址 + */ + private String sha1(String str) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : digest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (Exception e) { + log.error("SHA1失败", e); + return null; + } + } + + /** + * XML OpenID + */ + private String extractOpenId(String xml) { + int start = xml.indexOf(""); + int end = xml.indexOf(""); + if (start != -1 && end != -1) { + String value = xml.substring(start + 14, end); + // ȥ CDATA + return cleanCdata(value); + } + return null; + } + + /** + * XML 获取更新 + */ + private String extractEvent(String xml) { + int start = xml.indexOf(""); + int end = xml.indexOf(""); + if (start != -1 && end != -1) { + String value = xml.substring(start + 7, end); + // ȥ CDATA + return cleanCdata(value); + } + return null; + } + + /** + * CDATA + * : -> subscribe + */ + private String cleanCdata(String value) { + if (value == null) { + return null; + } + // ȥǰհ + value = value.trim(); + // CDATA获取м + // ʽ: + if (value.startsWith("")) { + return value.substring(9, value.length() - 3); + } + return value; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java new file mode 100644 index 0000000..1e248cf --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java @@ -0,0 +1,28 @@ +package cn.novalon.gym.manage.member.repository; + +import cn.novalon.gym.manage.member.entity.Member; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +/** + * 会员Repository + * @author 付嘉 + * @date 2026-05-01 + */ + +@Repository +public interface MemberRepository extends R2dbcRepository { + + // UnionID查询会员 + Mono findByUnionId(String unionId); + + // 小程序OpenID查询会员 + Mono findByMiniappOpenId(String miniappOpenId); + + // 服务号OpenID查询会员 + Mono findByOfficialOpenId(String officialOpenId); + + // 手机号查询 + Mono findByPhone(String phone); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java new file mode 100644 index 0000000..a0afe0b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/WechatUserRepository.java @@ -0,0 +1,29 @@ +package cn.novalon.gym.manage.member.repository; + +import cn.novalon.gym.manage.member.entity.WechatUser; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +/** + * 微信用户Repository + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Repository +public interface WechatUserRepository extends R2dbcRepository { + + // 通过UnionID查询微信用户 + Mono findByUnionId(String unionId); + + // 通过小程序OpenID查询微信用户 + Mono findByMiniappOpenid(String miniappOpenid); + + // 通过服务号OpenID查询微信用户 + Mono findByMpOpenid(String mpOpenid); + + // 通过会员ID查询微信用户 + Mono findByMemberId(Long memberId); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java new file mode 100644 index 0000000..23a2ea8 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java @@ -0,0 +1,30 @@ +package cn.novalon.gym.manage.member.service; + +import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import reactor.core.publisher.Mono; + +/** + * 会员服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface MemberService { + + /** + * 获取会员信息 + * + * @param memberId 会员ID + * @return 会员信息 + */ + Mono getMemberInfo(Long memberId); + + /** + * 管理端更新会员手机号 + * + * @param memberId 会员ID + * @param phone 明文手机号 + * @return 是否成功 + */ + Mono adminUpdatePhone(Long memberId, String phone); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java new file mode 100644 index 0000000..3258cb9 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatApiService.java @@ -0,0 +1,48 @@ +package cn.novalon.gym.manage.member.service; + +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * 微信API服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface WechatApiService { + + /** + * 小程序- 通过code获取session_key/openid + * + * @param code 小程序登录的code + * @return Mono> session_keyopenidunionid + */ + Mono> jsCode2Session(String code); + + /** + * 获取手机号- 通过code获取手机号 + * + * @param code 小程序登录的code + * @return Mono ܺ手机号 + */ + Mono getPhoneNumber(String code); + + /** + * 获取Access Token + * + * @param appType 应用类型miniapp-小程序mp + * @return Mono access_token + */ + Mono getAccessToken(String appType); + + /** + * 验证签名 + * + * @param signature 微信签名 + * @param timestamp 创建时间 + * @param nonce 随机字符串 + * @return boolean 签名是否有效 + */ + boolean checkSignature(String signature, String timestamp, String nonce); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java new file mode 100644 index 0000000..fc52de9 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatAuthService.java @@ -0,0 +1,31 @@ +package cn.novalon.gym.manage.member.service; + +import cn.novalon.gym.manage.member.dto.WechatLoginDto; +import cn.novalon.gym.manage.member.vo.WechatLoginVO; +import reactor.core.publisher.Mono; + +/** + * 微信授权服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface WechatAuthService { + + /** + * 小程序 + * + * @param request 小程序登录请求 + * @return 小程序登录响应 + */ + Mono miniappLogin(WechatLoginDto request); + + /** + * 手机号 + * + * @param memberId 会员ID + * @param code 微信手机号code + * @return 是否绑定成功 + */ + Mono bindPhone(Long memberId, String code); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java new file mode 100644 index 0000000..0a78d6f --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/WechatOfficialService.java @@ -0,0 +1,54 @@ +package cn.novalon.gym.manage.member.service; + +import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; +import reactor.core.publisher.Mono; + +/** + * 微信公众号服务接口 + * + * @author 付嘉 + * @date 2026-05-01 + */ +public interface WechatOfficialService { + + /** + * 处理订阅事件 + * + * @param openId OpenID + * @return Mono + */ + Mono handleSubscribeEvent(String openId); + + /** + * 处理取消订阅事件 + * + * @param openId OpenID + * @return Mono + */ + Mono handleUnsubscribeEvent(String openId); + + /** + * 获取微信用户信息 + * + * @param openId OpenID + * @return Mono 用户信息 + */ + Mono getUserInfo(String openId); + + /** + * 通过 UnionID 关联小程序用户 + * + * @param unionId 微信 UnionID + * @param officialOpenId OpenID + * @return Mono 是否关联成功 + */ + Mono linkByUnionId(String unionId, String officialOpenId); + + /** + * 查询会员订阅状态 + * + * @param memberId 会员ID + * @return Mono true=已订阅false=未订阅 + */ + Mono checkSubscribeStatus(Long memberId); +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java new file mode 100644 index 0000000..6509616 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -0,0 +1,117 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.repository.MemberRepository; +import cn.novalon.gym.manage.member.service.MemberService; +import cn.novalon.gym.manage.member.util.AesUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * 会员服务实现 + * + * @author 付嘉 + * @date 2026-05-01 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MemberServiceImpl implements MemberService { + + private final MemberRepository memberRepository; + + @Value("${wechat.aes.secret-key:}") + private String aesSecretKey; + + @Value("${wechat.aes.iv:}") + private String aesIv; + + @Override + public Mono getMemberInfo(Long memberId) { + return memberRepository.findById(memberId) + .map(this::buildMemberInfoResponse); + } + + // 会员信息响应 + private MemberInfoVO buildMemberInfoResponse(Member member) { + String phone = member.getPhone(); + String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null; + + return MemberInfoVO.builder() + .id(member.getId()) + .nickname(member.getNickname()) + .phone(maskedPhone) + .gender(member.getGender()) + .birthday(member.getBirthday()) + .avatar(member.getAvatar()) + .hasPhone(phone != null) + .isSubscribed(member.getSubscribed() != null && member.getSubscribed()) + .build(); + } + + @Override + public Mono adminUpdatePhone(Long memberId, String phone) { + log.info("管理端录入手机号, memberId: {}, phone: {}", memberId, phone); + + String encryptedPhone; + try { + encryptedPhone = AesUtil.encrypt(phone, aesSecretKey, aesIv); + log.info("手机号加密成功"); + } catch (Exception e) { + log.error("手机号加密失败", e); + return Mono.error(new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "手机号加密失败: " + e.getMessage() + )); + } + + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + if (existingMember.getId().equals(memberId)) { + log.warn("手机号已是当前用户的: memberId={}", memberId); + return Mono.error(new ResponseStatusException( + HttpStatus.CONFLICT, + "重复绑定" + )); + } else { + log.warn("手机号已被其他用户绑定: memberId={}, existingMemberId={}", + memberId, existingMember.getId()); + return Mono.error(new ResponseStatusException( + HttpStatus.CONFLICT, + "该手机号已被其他会员绑定" + )); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("手机号未被占用,可以绑定"); + return updateMemberPhone(memberId, encryptedPhone); + })); + } + + // 更新会员手机号 + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { + return memberRepository.findById(memberId) + .flatMap(member -> { + member.setPhone(encryptedPhone); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .map(savedMember -> { + log.info("手机号录入成功, memberId: {}", savedMember.getId()); + return true; + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.error("会员不存在: memberId={}", memberId); + return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "会员不存在")); + })); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java new file mode 100644 index 0000000..8b818f1 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -0,0 +1,260 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.service.WechatApiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信API服务实现 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatApiServiceImpl implements WechatApiService { + + private final WechatProperties wechatProperties; + + /** + * WebClient实例 - 用于发送HTTP请求 + * 最大内存大小为10MB + */ + private final WebClient webClient = WebClient.builder() + .baseUrl("https://api.weixin.qq.com") + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) + .build(); + + /** + * 小程序 + * + * @param code 小程序登录的code + * @return Mono> session_keyopenidunionid响应格式 + */ + @Override + public Mono> jsCode2Session(String code) { + log.info("微信jsCode2Session API"); + log.info("信息 - AppID: {}, AppSecret {}", + wechatProperties.getMiniapp().getAppId(), + wechatProperties.getMiniapp().getAppSecret() != null ? + wechatProperties.getMiniapp().getAppSecret().substring(0, Math.min(4, wechatProperties.getMiniapp().getAppSecret().length())) + "***" : "null"); + log.info(" - code: {}", code); + + return webClient.get() + // 构建URI + .uri(uriBuilder -> uriBuilder + .path("/sns/jscode2session") + .queryParam("appid", wechatProperties.getMiniapp().getAppId()) + .queryParam("secret", wechatProperties.getMiniapp().getAppSecret()) + .queryParam("js_code", code) + .queryParam("grant_type", "authorization_code") + .build()) + // 获取响应 + .retrieve() + .bodyToMono(String.class) + // 处理响应 + .map(responseBody -> { + log.info("微信API响应: {}", responseBody); + + // 解析JSON响应 + Map response; + try { + // 使用Jackson解析JSON响应 + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + response = mapper.readValue(responseBody, Map.class); + } catch (Exception e) { + log.error("微信API响应失败", e); + throw new RuntimeException("微信API响应失败: " + e.getMessage()); + } + + Map result = new HashMap<>(); + + // 检查错误码 + if (response.containsKey("errcode")) { + Integer errcode = (Integer) response.get("errcode"); + String errmsg = (String) response.get("errmsg"); + log.error("微信API失败, errcode: {}, errmsg: {}", errcode, errmsg); + throw new RuntimeException("微信API失败 [" + errcode + "]: " + errmsg); + } + + // 获取session_keyopenidunionid + result.put("session_key", (String) response.get("session_key")); + result.put("openid", (String) response.get("openid")); + result.put("unionid", (String) response.get("unionid")); + + log.info("微信API响应成功, openid: {}, unionid: {}", + result.get("openid"), result.get("unionid")); + return result; + }) + // 异常处理 + .onErrorResume(e -> { + log.error("微信API响应异常 - URL: https://api.weixin.qq.com/sns/jscode2session"); + log.error("异常: {}", e.getClass().getName()); + log.error("异常信息: {}", e.getMessage()); + if (e.getCause() != null) { + log.error("异常原因: {}", e.getCause().getMessage()); + } + return Mono.error(new RuntimeException("微信API响应异常 " + e.getMessage())); + }); + } + + /** + * 获取手机号 + * + * @param code 手机号或获取手机号的code + * @return Mono 手机号 + */ + @Override + public Mono getPhoneNumber(String code) { + log.debug("微信getPhoneNumber API, code: {}", code); + + return getAccessToken("miniapp") + .flatMap(accessToken -> { + + Map requestBody = new HashMap<>(); + requestBody.put("code", code); + + return webClient.post() + .uri(uriBuilder -> uriBuilder + .path("/wxa/business/getuserphonenumber") + .queryParam("access_token", accessToken) + .build()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + if (response.containsKey("errcode") && + (Integer) response.get("errcode") == 0) { + + Map phoneInfo = + (Map) response.get("phone_info"); + String phoneNumber = (String) phoneInfo.get("purePhoneNumber"); + + log.info("获取手机号成功{}", phoneNumber); + return phoneNumber; + } else { + String errmsg = (String) response.get("errmsg"); + log.error("获取手机号失败 {}", errmsg); + throw new RuntimeException("获取手机号失败 " + errmsg); + } + }); + }) + // + .onErrorResume(e -> { + log.error("获取手机号失败", e); + return Mono.error(new RuntimeException("获取手机号失败 " + e.getMessage())); + }); + } + + /** + * 获取Access Token + * + * @param appType 应用类型 + * @return Mono access_token + */ + @Override + public Mono getAccessToken(String appType) { + log.debug("获取access_token, appType: {}", appType); + + // TODO: 实现缓存逻辑 + // 使用 Caffeine 或 Redis 缓存 + // 目前直接调用微信API + + String appId, appSecret; + if ("miniapp".equals(appType)) { + appId = wechatProperties.getMiniapp().getAppId(); + appSecret = wechatProperties.getMiniapp().getAppSecret(); + } else { + appId = wechatProperties.getMp().getAppId(); + appSecret = wechatProperties.getMp().getAppSecret(); + } + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/cgi-bin/token") + .queryParam("grant_type", "client_credential") + .queryParam("appid", appId) + .queryParam("secret", appSecret) + .build()) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + if (response.containsKey("access_token")) { + String accessToken = (String) response.get("access_token"); + Integer expiresIn = (Integer) response.get("expires_in"); + log.info("获取access_token成功, expires_in: {}s", expiresIn); + + // TODO: 加入缓存 + // cache.put("wechat:token:" + appType, accessToken, expiresIn - 200, TimeUnit.SECONDS); + + return accessToken; + } else { + String errmsg = (String) response.get("errmsg"); + log.error("获取access_token失败: {}", errmsg); + throw new RuntimeException("获取access_token失败: " + errmsg); + } + }); + } + + /** + * 验证微信消息签名 + * + * @param signature 微信消息签名 + * @param timestamp 创建时间戳 + * @param nonce 随机字符串 + * @return boolean true-签名有效false-签名无效 + */ + @Override + public boolean checkSignature(String signature, String timestamp, String nonce) { + log.debug("验证微信消息签名, signature: {}, timestamp: {}, nonce: {}", + signature, timestamp, nonce); + + try { + String token = wechatProperties.getMp().getToken(); + + String[] arr = new String[]{token, timestamp, nonce}; + Arrays.sort(arr); + + StringBuilder content = new StringBuilder(); + for (String s : arr) { + content.append(s); + } + + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(content.toString().getBytes(StandardCharsets.UTF_8)); + + StringBuilder hexString = new StringBuilder(); + for (byte b : digest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + String calculatedSignature = hexString.toString(); + + boolean isValid = calculatedSignature.equals(signature); + log.debug("验证微信消息签名结果: {}", isValid ? "通过" : "失败"); + + return isValid; + } catch (Exception e) { + log.error("验证微信消息签名异常", e); + return false; + } + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java new file mode 100644 index 0000000..3d5fe99 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -0,0 +1,369 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.common.config.JwtProperties; +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.dto.WechatLoginDto; +import cn.novalon.gym.manage.member.vo.WechatLoginVO; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.repository.MemberRepository; +import cn.novalon.gym.manage.member.service.WechatApiService; +import cn.novalon.gym.manage.member.service.WechatAuthService; +import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.MemberNoGenerator; +import cn.novalon.gym.manage.member.util.WechatPhoneUtil; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信认证服务实现 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatAuthServiceImpl implements WechatAuthService { + + private final WechatApiService wechatApiService; + private final MemberRepository memberRepository; + private final JwtProperties jwtProperties; + private final WechatProperties wechatProperties; + private final WechatPhoneUtil wechatPhoneUtil; + + /** + * 小程序登录 - 通过微信 code 完成登录 + * + * @param request 微信登录请求 + * @return Mono 微信登录响应 + */ + @Override + public Mono miniappLogin(WechatLoginDto request) { + log.info("开始小程序登录"); + + return wechatApiService.jsCode2Session(request.getCode()) + .flatMap(sessionData -> { + String openid = sessionData.get("openid"); + String unionId = sessionData.get("unionid"); + String sessionKey = sessionData.get("session_key"); + + log.info("微信 API 返回: openid={}, unionid={}", openid, unionId); + + if (unionId != null && !unionId.isEmpty()) { + return memberRepository.findByUnionId(unionId) + .flatMap(member -> { + log.info("找到会员, memberId: {}", member.getId()); + + if (member.getMiniappOpenId() == null || member.getMiniappOpenId().isEmpty()) { + log.info("用户已有 UnionID,补充小程序 OpenID, memberId: {}", member.getId()); + member.setMiniappOpenId(openid); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + } else { + log.info("老用户登录,更新最后登录时间, memberId: {}", member.getId()); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("UnionID 未找到,尝试通过小程序 OpenID 查询, openid: {}", openid); + return memberRepository.findByMiniappOpenId(openid) + .flatMap(member -> { + log.info("找到会员, memberId: {}", member.getId()); + member.setUnionId(unionId); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID 也未找到,创建新会员(无 UnionID), openid: {}", openid); + return createNewMember(unionId, openid, sessionKey, request.getPhoneCode()); + })); + })); + } else { + log.warn("微信 API 未返回 UnionID,尝试通过小程序 OpenID 查询, openid: {}", openid); + return memberRepository.findByMiniappOpenId(openid) + .flatMap(member -> { + log.info("找到会员, memberId: {}", member.getId()); + member.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(member) + .flatMap(savedMember -> { + WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); + return Mono.just(response); + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID也未找到,创建新会员(无UnionID标识信息)"); + return createNewMember(unionId, openid, sessionKey, request.getPhoneCode()); + })); + } + }) + .onErrorResume(e -> { + log.error("小程序登录失败", e); + return Mono.error(new RuntimeException("登录失败: " + e.getMessage())); + }); + } + + /** + * 绑定手机号 - 通过微信 code 获取并绑定手机号 + * + * @param memberId 会员 ID + * @param code 微信手机号 code + * @return Mono + */ + @Override + public Mono bindPhone(Long memberId, String code) { + log.info("开始绑定手机号, memberId: {}", memberId); + + return wechatApiService.getPhoneNumber(code) + .flatMap(phoneNumber -> { + log.info("获取手机号: {}", phoneNumber); + + String encryptedPhone = encryptPhone(phoneNumber); + + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + if (!existingMember.getId().equals(memberId)) { + log.warn("手机号已被其他会员绑定, currentMemberId={}, existingMemberId={}", memberId, existingMember.getId()); + return Mono.error(new RuntimeException("手机号已被其他会员绑定")); + } else { + log.info("更新会员手机号, memberId: {}", memberId); + return updateMemberPhone(memberId, encryptedPhone); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("该手机号未被使用,直接绑定到当前会员, memberId: {}", memberId); + return updateMemberPhone(memberId, encryptedPhone); + })); + }) + .onErrorResume(e -> { + log.error("绑定手机号失败", e); + return Mono.error(new RuntimeException("绑定失败: " + e.getMessage())); + }); + } + + /** + * 更新会员手机号 + * + * @param memberId 会员 ID + * @param encryptedPhone 加密后的手机号(Base64 编码) + * @return Mono 是否更新成功 + */ + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { + return memberRepository.findById(memberId) + .flatMap(member -> { + member.setPhone(encryptedPhone); + member.setLastLoginAt(LocalDateTime.now()); + return memberRepository.save(member) + .map(savedMember -> { + log.info("更新会员手机号成功, memberId: {}", savedMember.getId()); + return true; + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.error("会员不存在, memberId={}", memberId); + return Mono.error(new RuntimeException("会员不存在")); + })); + } + + /** + * 手机号加密 + * + * @param phoneNumber 明文手机号 + * @return 加密后的手机号(Base64 编码) + */ + private String encryptPhone(String phoneNumber) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + + String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv); + + log.debug("手机号加密成功"); + return encryptedPhone; + } catch (Exception e) { + log.error("手机号加密失败", e); + throw new RuntimeException("手机号加密失败 " + e.getMessage()); + } + } + + /** + * AES 解密手机号 + * + * @param encryptedPhone 加密后的手机号(Base64 编码) + * @return 明文手机号 + */ + public String decryptPhone(String encryptedPhone) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + + String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv); + + log.debug("手机号解密成功"); + return phoneNumber; + } catch (Exception e) { + log.error("手机号解密失败", e); + throw new RuntimeException("手机号解密失败 " + e.getMessage()); + } + } + + /** + * 创建新会员(首次登录) + * + * @param unionId 微信 UnionID + * @param openid 小程序 OpenID + * @param sessionKey 会话密钥 + * @param phoneCode 手机号 code(可选,前端调用 wx.getPhoneNumber()) + * @return Mono 登录响应 + */ + private Mono createNewMember(String unionId, String openid, String sessionKey, String phoneCode) { + log.info("开始创建新会员, unionId: {}, openid: {}", unionId, openid); + + // Step 1: 生成会员号 + String memberNo = MemberNoGenerator.generate(); + log.info("生成会员号: {}", memberNo); + + // Step 2: 构建 Member 实体(仅保存标识信息) + Member member = Member.builder() + .memberNo(memberNo) // 小程序注册时自动生成会员号 + .unionId(unionId) // 存储 UnionID 用于统一身份 + .miniappOpenId(openid) // 存储小程序 OpenID + .lastLoginAt(LocalDateTime.now()) // 记录首次登录时间 + .build(); + + log.info("用户未注册,创建新会员(仅保存标识信息)"); + + // Step 3: 保存 Member + return memberRepository.save(member) + .flatMap(savedMember -> { + log.info("保存 Member 成功, id: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo()); + + // Step 4: 如果有 phoneCode,尝试获取手机号 + if (phoneCode != null && !phoneCode.isEmpty()) { + log.info("检测到 phoneCode,尝试获取手机号"); + return wechatPhoneUtil.getPhoneNumber(phoneCode) + .flatMap(phoneNumber -> { + if (phoneNumber != null && !phoneNumber.isEmpty()) { + log.info("获取到手机号: {}", phoneNumber); + // 加密手机号 + String encryptedPhone = encryptPhone(phoneNumber); + // 为新会员绑定手机号 + savedMember.setPhone(encryptedPhone); + return memberRepository.save(savedMember) + .doOnSuccess(m -> log.info("新用户手机号绑定成功")) + .thenReturn(buildLoginResponse(savedMember, true, sessionKey)); + } else { + log.warn("未获取到手机号"); + return Mono.just(buildLoginResponse(savedMember, true, sessionKey)); + } + }); + } else { + // 没有 phoneCode,直接返回 + return Mono.just(buildLoginResponse(savedMember, true, sessionKey)); + } + }); + } + + /** + * 构建登录响应 - 封装返回数据(前端调用) + * + * @param member 会员实体 + * @param isNewUser 是否为新用户 + * @param sessionKey 会话密钥(后续可用于解密) + * @return WechatLoginVO 登录响应 + */ + private WechatLoginVO buildLoginResponse(Member member, boolean isNewUser, String sessionKey) { + log.debug("构建登录响应, memberId: {}, isNewUser: {}", member.getId(), isNewUser); + + boolean needCompleteInfo = member.getNickname() == null || member.getPhone() == null; + if (needCompleteInfo) { + log.info("用户需要补全信息: nickname={}, phone={}", + member.getNickname() != null ? "已有" : "未设置", + member.getPhone() != null ? "已绑定" : "未绑定"); + } + + // 生成 Access Token(使用 Gateway 相同的方式) + String accessToken = generateJwtToken(member.getId(), "access"); + // 生成 Refresh Token(有效期更长) + String refreshToken = generateJwtToken(member.getId(), "refresh", 7L * 24 * 60 * 60 * 1000); + + log.info("JWT Token 生成成功, memberId: {}", member.getId()); + + // 计算过期时间(秒) + long expiresIn = jwtProperties.getExpiration() / 1000; + + return WechatLoginVO.builder() + .memberId(member.getId()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn((int) expiresIn) + .isNewUser(isNewUser) + .needCompleteInfo(needCompleteInfo) + .build(); + } + + /** + * 生成 JWT Token(与 Gateway 一致) + * + * @param memberId 会员 ID + * @param tokenType Token 类型(access/refresh) + * @return JWT Token 字符串 + */ + private String generateJwtToken(Long memberId, String tokenType) { + return generateJwtToken(memberId, tokenType, jwtProperties.getExpiration()); + } + + /** + * 生成 JWT Token(自定义过期时间) + * + * @param memberId 会员 ID + * @param tokenType Token 类型(access/refresh) + * @param expirationMs 过期时间(毫秒) + * @return JWT Token 字符串 + */ + private String generateJwtToken(Long memberId, String tokenType, long expirationMs) { + SecretKey key = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expirationMs); + + Map claims = new HashMap<>(); + claims.put("memberId", memberId); + claims.put("tokenType", tokenType); + + return Jwts.builder() + .setClaims(claims) + .setSubject(String.valueOf(memberId)) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(key) + .compact(); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java new file mode 100644 index 0000000..4e3a3d0 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -0,0 +1,313 @@ +package cn.novalon.gym.manage.member.service.impl; + +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.repository.MemberRepository; +import cn.novalon.gym.manage.member.service.WechatOfficialService; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信服务号服务实现类 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class WechatOfficialServiceImpl implements WechatOfficialService { + + private final MemberRepository memberRepository; + private final WechatProperties wechatProperties; + private final WebClient webClient; + private final ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + /** + * 处理关注事件 + */ + @Override + public Mono handleSubscribeEvent(String openId) { + log.info("处理关注事件, openId: {}", openId); + + return getUserInfo(openId) + .flatMap(userInfo -> { + String unionId = userInfo.getUnionid(); + log.info("获取到用户信息 unionId: {}, nickname: {}", unionId, userInfo.getNickname()); + + if (unionId != null && !unionId.isEmpty()) { + return memberRepository.findByUnionId(unionId) + .flatMap(existingMember -> { + log.info("通过UnionID找到已有会员, memberId: {}", existingMember.getId()); + + if (existingMember.getOfficialOpenId() == null || existingMember.getOfficialOpenId().isEmpty()) { + log.info("用户先使用小程序,更新服务号OpenID: {}", openId); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + existingMember.setOfficialOpenId(openId); + + if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) { + existingMember.setNickname(userInfo.getNickname()); + } + + if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) { + existingMember.setAvatar(userInfo.getHeadimgurl()); + } + + return memberRepository.save(existingMember) + .then(sendWelcomeMessage(openId)); + } else { + log.info("老用户关注服务号: memberId={}", existingMember.getId()); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(existingMember) + .then(sendWelcomeMessage(openId)); + } + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("UnionID未找到,降级到服务号OpenID查询: {}", openId); + return memberRepository.findByOfficialOpenId(openId) + .flatMap(existingMember -> { + log.info("通过服务号OpenID找到已有会员,更新UnionID, memberId: {}", existingMember.getId()); + existingMember.setUnionId(unionId); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(existingMember) + .then(sendWelcomeMessage(openId)); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID也未找到,创建新用户"); + return createNewMemberFromOfficial(unionId, openId) + .then(sendWelcomeMessage(openId)); + })); + })); + } else { + log.warn("用户没有UnionID,尝试通过服务号OpenID查询"); + return memberRepository.findByOfficialOpenId(openId) + .flatMap(existingMember -> { + log.info("通过服务号OpenID找到已有会员, memberId: {}", existingMember.getId()); + existingMember.setSubscribed(true); + existingMember.setLastLoginAt(LocalDateTime.now()); + + return memberRepository.save(existingMember) + .then(sendWelcomeMessage(openId)); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("OpenID也未找到,创建新会员(无UnionID)"); + return createNewMemberFromOfficial(unionId, openId) + .then(sendWelcomeMessage(openId)); + })); + } + }) + .then(); + } + + /** + * 处理取消关注事件 + */ + @Override + public Mono handleUnsubscribeEvent(String openId) { + log.info("处理取消关注事件, openId: {}", openId); + + return memberRepository.findByOfficialOpenId(openId) + .flatMap(member -> { + log.info("找到会员,更新为未关注状态, memberId: {}", member.getId()); + member.setSubscribed(false); + member.setLastLoginAt(LocalDateTime.now()); + return memberRepository.save(member); + }) + .then() + .switchIfEmpty(Mono.defer(() -> { + log.warn("未找到对应的会员记录, officialOpenId: {}", openId); + return Mono.empty(); + })) + .onErrorResume(e -> { + log.error("处理取消关注事件失败", e); + return Mono.empty(); + }); + } + + /** + * 获取微信用户信息 + */ + @Override + public Mono getUserInfo(String openId) { + log.debug("获取微信用户信息, openId: {}", openId); + + // 获取AccessToken + return getAccessToken() + .flatMap(accessToken -> { + String url = "https://api.weixin.qq.com/cgi-bin/user/info" + + "?access_token=" + accessToken + + "&openid=" + openId + + "&lang=zh_CN"; + + return webClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(String.class) + .map(responseJson -> { + log.info("微信API原始响应: {}", responseJson); + try { + WechatUserInfoVO userInfo = objectMapper.readValue(responseJson, WechatUserInfoVO.class); + log.info("解析后的用户信息 - unionId: {}, nickname: {}, openid: {}", + userInfo.getUnionid(), userInfo.getNickname(), userInfo.getOpenid()); + return userInfo; + } catch (Exception e) { + log.error("解析微信用户信息失败", e); + throw new RuntimeException("解析微信用户信息失败", e); + } + }); + }); + } + + /** + * 通过 UnionID 关联小程序用户和服务号用户 + */ + @Override + public Mono linkByUnionId(String unionId, String officialOpenId) { + log.debug("关联小程序用户和服务号用户 unionId: {}, officialOpenId: {}", unionId, officialOpenId); + + return memberRepository.findByUnionId(unionId) + .flatMap(member -> { + member.setSubscribed(true); + member.setLastLoginAt(LocalDateTime.now()); + + if (officialOpenId != null && !officialOpenId.isEmpty()) { + member.setOfficialOpenId(officialOpenId); + } + + return memberRepository.save(member) + .map(savedMember -> { + log.info("关联成功, memberId: {}", savedMember.getId()); + return true; + }); + }) + .switchIfEmpty(Mono.defer(() -> { + log.warn("未找到对应的会员记录, unionId: {}", unionId); + return Mono.just(false); + })) + .onErrorResume(e -> { + log.error("关联失败", e); + return Mono.just(false); + }); + } + + /** + * 查询用户关注状态 + */ + @Override + public Mono checkSubscribeStatus(Long memberId) { + return memberRepository.findById(memberId) + .map(member -> { + Boolean subscribed = member.getSubscribed(); + return subscribed != null && subscribed; + }) + .defaultIfEmpty(false); + } + + /** + * 从服务号用户信息创建新会员 + */ + private Mono createNewMemberFromOfficial(String unionId, String openId) { + Member member = Member.builder() + .unionId(unionId) + .officialOpenId(openId) + .subscribed(true) + .lastLoginAt(LocalDateTime.now()) + .build(); + + log.info("新用户关注服务号,仅保存标识信息(UnionID和OpenID)"); + + return memberRepository.save(member) + .doOnSuccess(savedMember -> + log.info("从服务号创建新会员成功, memberId: {}", savedMember.getId())) + .then(); + } + + /** + * 获取微信AccessToken + * + * TODO: 应该使用缓存,避免频繁请求 + */ + private Mono getAccessToken() { + String appId = wechatProperties.getMp().getAppId(); + String appSecret = wechatProperties.getMp().getAppSecret(); + + String url = "https://api.weixin.qq.com/cgi-bin/token" + + "?grant_type=client_credential" + + "&appid=" + appId + + "&secret=" + appSecret; + + return webClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(Map.class) + .map(response -> { + if (response.containsKey("errcode")) { + throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg")); + } + return (String) response.get("access_token"); + }); + } + + /** + * 发送欢迎消息给用户 + */ + private Mono sendWelcomeMessage(String openId) { + log.info("发送欢迎消息给 openId: {}", openId); + + // 真正调用微信 API 发送消息(测试模式和生产模式都执行) + return getAccessToken() + .flatMap(accessToken -> { + String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send" + + "?access_token=" + accessToken; + + // 构建客服消息�? + Map messageBody = new HashMap<>(); + messageBody.put("touser", openId); + messageBody.put("msgtype", "text"); + + Map text = new HashMap<>(); + text.put("content", "欢迎使用"); + messageBody.put("text", text); + + return webClient.post() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(messageBody) + .retrieve() + .bodyToMono(Map.class) + .doOnSuccess(response -> { + if (response.containsKey("errcode") && !"0".equals(String.valueOf(response.get("errcode")))) { + log.error("发送欢迎消息失败: {}", response.get("errmsg")); + } else { + log.info("欢迎消息发送成功, openId: {}", openId); + } + }) + .doOnError(error -> log.error("发送欢迎消息异常", error)) + .then(); + }) + .onErrorResume(e -> { + log.error("发送欢迎消息失败, openId: {}", openId, e); + return Mono.empty(); // 即使发送失败也不影响主流程 + }); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java new file mode 100644 index 0000000..f54a003 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java @@ -0,0 +1,77 @@ +package cn.novalon.gym.manage.member.util; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * AES加密工具类 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +public class AesUtil { + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; + + /** + * AES 解密 + * + * @param encryptedData 加密数据,Base64编码 + * @param key AES密钥,Base64编码(32字节) + * @param iv 初始化向量IV,Base64编码(16字节) + * @return 解密后的字符串 + */ + public static String decrypt(String encryptedData, String key, String iv) { + try { + byte[] dataByte = Base64.getDecoder().decode(encryptedData); + byte[] keyByte = Base64.getDecoder().decode(key); + byte[] ivByte = Base64.getDecoder().decode(iv); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); + IvParameterSpec ivParameterSpec = new IvParameterSpec(ivByte); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + + byte[] resultByte = cipher.doFinal(dataByte); + return new String(resultByte, StandardCharsets.UTF_8); + } catch (Exception e) { + log.error("AES解密失败", e); + throw new RuntimeException("解密失败", e); + } + } + + /** + * AES 加密 + * + * @param data 原始数据 + * @param key AES密钥,Base64编码(32字节) + * @param iv 初始化向量IV,Base64编码(16字节) + * @return Base64编码的加密数据 + */ + public static String encrypt(String data, String key, String iv) { + try { + byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); + byte[] keyByte = Base64.getDecoder().decode(key); + byte[] ivByte = Base64.getDecoder().decode(iv); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); + IvParameterSpec ivParameterSpec = new IvParameterSpec(ivByte); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + + byte[] resultByte = cipher.doFinal(dataByte); + return Base64.getEncoder().encodeToString(resultByte); + } catch (Exception e) { + log.error("AES加密失败", e); + throw new RuntimeException("加密失败", e); + } + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java new file mode 100644 index 0000000..2163923 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/MemberNoGenerator.java @@ -0,0 +1,53 @@ +package cn.novalon.gym.manage.member.util; + +import lombok.extern.slf4j.Slf4j; + +import java.security.SecureRandom; + +/** + * 会员号生成器 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +public class MemberNoGenerator { + + // 会员号前缀 + private static final String PREFIX = "GYM"; + + // 随机数长度 + private static final int RANDOM_LENGTH = 8; + + // 字符集(排除易混淆字符 0/O, 1/I/l + private static final String CHARACTERS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + + // SecureRandom 实例(线程安全) + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + // 生成会员号 + public static String generate() { + StringBuilder sb = new StringBuilder(PREFIX); + + for (int i = 0; i < RANDOM_LENGTH; i++) { + int index = SECURE_RANDOM.nextInt(CHARACTERS.length()); + sb.append(CHARACTERS.charAt(index)); + } + + String memberNo = sb.toString(); + log.debug("生成会员号: {}", memberNo); + + return memberNo; + } + + // 批量生成会员号 + // count 生成数量 + public static String[] generateBatch(int count) { + String[] memberNos = new String[count]; + for (int i = 0; i < count; i++) { + memberNos[i] = generate(); + } + return memberNos; + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java new file mode 100644 index 0000000..e4f3c4e --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java @@ -0,0 +1,65 @@ +package cn.novalon.gym.manage.member.util; + +import cn.novalon.gym.manage.member.service.WechatApiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 微信手机号获取工具类 + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Slf4j +@Component +@RequiredArgsConstructor +public class WechatPhoneUtil { + + private final WechatApiService wechatApiService; + + /** + * 通过phoneCode获取手机号 + * + * @param phoneCode 手机号 code + * @return Mono 手机号,获取失败则返回 null + */ + public Mono getPhoneNumber(String phoneCode) { + if (phoneCode == null || phoneCode.isEmpty()) { + log.debug("未提供phoneCode获取手机号"); + return Mono.empty(); + } + + log.info("开始获取手机号, phoneCode: {}", phoneCode); + + return wechatApiService.getPhoneNumber(phoneCode) + .doOnSuccess(phoneNumber -> { + if (phoneNumber != null && !phoneNumber.isEmpty()) { + log.info("获取手机号成功: {}", maskPhone(phoneNumber)); + } else { + log.warn("获取手机号失败"); + } + }) + .doOnError(e -> { + log.warn("获取手机号失败 {}", e.getMessage()); + }) + .onErrorResume(e -> { + return Mono.empty(); + }); + } + + /** + * 手机号脱敏处理 + * + * @param phone 手机号 + * @return 脱敏后的手机号,如:138****8000 + */ + private String maskPhone(String phone) { + if (phone == null || phone.length() < 7) { + return "***"; + } + return phone.substring(0, 3) + "****" + phone.substring(7); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java new file mode 100644 index 0000000..d8b60e4 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java @@ -0,0 +1,46 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 会员信息 VO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberInfoVO { + + // 会员 ID + private Long id; + + // 昵称 + private String nickname; + + // 手机号(脱敏) + private String phone; + + // 性别 + private Integer gender; + + // 生日 + private Date birthday; + + // 头像 + private String avatar; + + // 是否已绑定手机号 + private Boolean hasPhone; + + // 是否已关注公众号 + private Boolean isSubscribed; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java new file mode 100644 index 0000000..b47370b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatLoginVO.java @@ -0,0 +1,38 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 微信登录VO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatLoginVO { + + // 会员 ID + private Long memberId; + + // Access Token(访问令牌) + private String accessToken; + + // Refresh Token(刷新令牌) + private String refreshToken; + + // Token 过期时间(秒) + private Integer expiresIn; + + // 是否为新用户 + private Boolean isNewUser; + + // 是否需要补全信息(昵称、手机号等) + private Boolean needCompleteInfo; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java new file mode 100644 index 0000000..8fc4735 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/WechatUserInfoVO.java @@ -0,0 +1,72 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +/** + * 微信用户信息 VO + * + * @author 付嘉 + * @date 2026-05-01 + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WechatUserInfoVO { + + // 用户是否关注该公众号(true-已关注,false-未关注) + private Boolean subscribe; + + // 服务号 OpenID + private String openid; + + // 用户昵称 + private String nickname; + + // 性别(1-男,2-女,0-未知) + private Integer sex; + + // 国家 + private String country; + + // 省份 + private String province; + + // 城市 + private String city; + + // 语言 + private String language; + + // 头像 URL + private String headimgurl; + + // 关注时间 + private Long subscribeTime; + + // UnionID + private String unionid; + + // 公众号运营者对粉丝的备注 + private String remark; + + // 用户所在的分组 ID + private Integer groupid; + + // 用户被打上的标签 ID 列表 + private List tagidList; + + // 关注来源 + private Integer subscribeScene; + + // 二维码场景值 + private String qrScene; + + // 二维码场景值字符串 + private String qrSceneStr; +} diff --git a/gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..18cc11f --- /dev/null +++ b/gym-manage-api/gym-member/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.novalon.gym.manage.member.config.WechatProperties diff --git a/gym-manage-api/gym-member/src/main/resources/application.yml b/gym-manage-api/gym-member/src/main/resources/application.yml new file mode 100644 index 0000000..789379f --- /dev/null +++ b/gym-manage-api/gym-member/src/main/resources/application.yml @@ -0,0 +1,18 @@ +# 微信配置(测试环境使用模拟数据) +wechat: + # Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境) + mock-enabled: false + miniapp: + app-id: wx4d480112b426100b + app-secret: 78548f0c0ff66c73d3e8b071897eb1e5 + mp: + app-id: wx6f138c9aacc8a0e8 + app-secret: 5df2e315e9268e96a43bb2cce1d2270b + token: test_token + aes-key: ${WECHAT_MP_AESKEY:test_aes_key} + # 服务器回调地址(微信服务器推送事件的URL) + callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback + # 手机号加密配置 + phone-encryption: + secret-key: dGVzdF9zZWNyZXRfa2V5X2Zvcl9waG9uZV9lbmNyeXB0aW9uMTI= + iv: dGVzdF9pdl9mb3JfcGhvbmU= diff --git a/gym-manage-api/gym-member/src/main/resources/db/schema.sql b/gym-manage-api/gym-member/src/main/resources/db/schema.sql new file mode 100644 index 0000000..ea35cdd --- /dev/null +++ b/gym-manage-api/gym-member/src/main/resources/db/schema.sql @@ -0,0 +1,138 @@ +-- ============================================ +-- 1. member_user 表(会员表) +-- ============================================ + +-- Step 1: 删除已存在的表(如果需要重建) +-- DROP TABLE IF EXISTS member_user CASCADE; + +-- Step 2: 创建 member_user 表 +CREATE TABLE IF NOT EXISTS member_user ( + -- ========== 主键和基础字段(来自BaseEntity)========== + id BIGSERIAL PRIMARY KEY, -- 主键ID,自增 + 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), -- 手机号(AES加密存储) + gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女 + birthday TIMESTAMP, -- 生日 + address VARCHAR(500), -- 地址 + avatar VARCHAR(500), -- 头像URL + subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号 + last_login_at TIMESTAMP, -- 最后登录时间 + + -- ========== 微信相关字段 ========== + union_id VARCHAR(100), -- 微信UnionID(跨应用唯一标识) + miniapp_open_id VARCHAR(100), -- 小程序OpenID + official_open_id VARCHAR(100), -- 服务号OpenID + + -- ========== 软删除字段 ========== + is_deleted BOOLEAN DEFAULT FALSE -- 是否删除(软删除标记) +); + +-- Step 3: 创建索引 +-- 会员编号索引(唯一索引,加速查询) +CREATE UNIQUE INDEX IF NOT EXISTS idx_member_user_member_no ON member_user(member_no); + +-- UnionID索引(加速跨平台用户查找) +CREATE INDEX IF NOT EXISTS idx_member_user_union_id ON member_user(union_id); + +-- 小程序OpenID索引(加速小程序登录查询) +CREATE INDEX IF NOT EXISTS idx_member_user_miniapp_openid ON member_user(miniapp_open_id); + +-- 服务号OpenID索引(加速服务号事件处理) +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); + +-- Step 4: 添加注释 +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 表(微信用户表) +-- ============================================ + +-- Step 1: 删除已存在的表(如果需要重建) +-- DROP TABLE IF EXISTS wechat_user CASCADE; + +-- Step 2: 创建 wechat_user 表 +CREATE TABLE IF NOT EXISTS wechat_user ( + -- ========== 主键和基础字段(来自BaseEntity)========== + id BIGSERIAL PRIMARY KEY, -- 主键ID,自增 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + + -- ========== 关联字段 ========== + member_id BIGINT NOT NULL, -- 会员ID(外键) + + -- ========== 微信标识字段 ========== + union_id VARCHAR(100), -- 微信UnionID + miniapp_openid VARCHAR(100), -- 小程序OpenID + mp_openid VARCHAR(100), -- 服务号OpenID + + -- ========== 关注状态字段 ========== + is_subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号 + subscribe_time TIMESTAMP, -- 首次关注时间 + unsubscribe_time TIMESTAMP -- 最后一次取消关注时间 +); + +-- Step 3: 创建外键约束 +ALTER TABLE wechat_user + ADD CONSTRAINT fk_wechat_user_member + FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE CASCADE; + +-- Step 4: 创建索引 +-- UnionID索引(加速跨平台用户查找) +CREATE INDEX IF NOT EXISTS idx_wechat_user_union_id ON wechat_user(union_id); + +-- 小程序OpenID索引(加速小程序登录查询) +CREATE INDEX IF NOT EXISTS idx_wechat_user_miniapp_openid ON wechat_user(miniapp_openid); + +-- 服务号OpenID索引(加速服务号事件处理) +CREATE INDEX IF NOT EXISTS idx_wechat_user_mp_openid ON wechat_user(mp_openid); + +-- 会员ID索引(加速关联查询) +CREATE INDEX IF NOT EXISTS idx_wechat_user_member_id ON wechat_user(member_id); + +-- Step 5: 添加注释 +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 '最后一次取消关注时间'; diff --git a/gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java b/gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java new file mode 100644 index 0000000..799f865 --- /dev/null +++ b/gym-manage-api/gym-member/src/test/java/cn/novalon/gym/manage/member/MemberModuleTest.java @@ -0,0 +1,16 @@ +package cn.novalon.gym.manage.member; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * 会员模块测试类 + */ +@SpringBootTest +public class MemberModuleTest { + + @Test + void contextLoads() { + // 测试Spring上下文是否能正常加载 + } +} diff --git a/gym-manage-api/gym-member/test-member-apis.bat b/gym-manage-api/gym-member/test-member-apis.bat new file mode 100644 index 0000000..97a6060 --- /dev/null +++ b/gym-manage-api/gym-member/test-member-apis.bat @@ -0,0 +1,56 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 会员模块接口测试 +echo ======================================== +echo. + +set BASE_URL=http://localhost:8084 +set MEMBER_ID=26 + +echo [测试 1] 获取会员信息 +echo 请求: GET %BASE_URL%/api/member/info +curl -X GET "%BASE_URL%/api/member/info" -H "X-Member-Id: %MEMBER_ID%" -s | jq . +echo. +echo. + +echo [测试 2] 查询关注状态 +echo 请求: GET %BASE_URL%/api/member/subscribe/status +curl -X GET "%BASE_URL%/api/member/subscribe/status" -H "X-Member-Id: %MEMBER_ID%" -s +echo. +echo. + +echo [测试 3] 更新会员信息 +echo 请求: PUT %BASE_URL%/api/member/info?nickname=TestUser^&gender=1^&birthday=1990-01-01^&address=TestAddress +curl -X PUT "%BASE_URL%/api/member/info?nickname=TestUser&gender=1&birthday=1990-01-01&address=TestAddress" -H "X-Member-Id: %MEMBER_ID%" -s +echo. +echo. + +echo [测试 4] 管理端录入手机号 +echo 请求: POST %BASE_URL%/api/admin/members/%MEMBER_ID%/phone +curl -X POST "%BASE_URL%/api/admin/members/%MEMBER_ID%/phone" -H "Content-Type: application/json" -d "{\"phone\":\"13800138000\"}" -s +echo. +echo. + +echo [测试 5] 验证手机号已更新 +echo 请求: GET %BASE_URL%/api/member/info +curl -X GET "%BASE_URL%/api/member/info" -H "X-Member-Id: %MEMBER_ID%" -s | jq . +echo. +echo. + +echo [测试 6] 服务号回调验证签名 +echo 请求: GET %BASE_URL%/api/member/auth/mp/callback?signature=test^×tamp=123^&nonce=test^&echostr=test +curl -X GET "%BASE_URL%/api/member/auth/mp/callback?signature=test×tamp=123&nonce=test&echostr=test" -s +echo. +echo. + +echo ======================================== +echo 测试完成 +echo ======================================== +echo. +echo 提示: +echo 1. 小程序登录需要提供真实的微信code +echo 2. 服务号回调需要微信服务器推送的真实事件 +echo 3. 管理端录入手机号需要配置正确的AES密钥 +echo. +pause diff --git a/gym-manage-api/gym-member/test-member-apis.ps1 b/gym-manage-api/gym-member/test-member-apis.ps1 new file mode 100644 index 0000000..c4315ab --- /dev/null +++ b/gym-manage-api/gym-member/test-member-apis.ps1 @@ -0,0 +1,140 @@ +# 会员模块接口测试脚本 +# 使用前请确保应用已启动在 http://localhost:8084 + +$BASE_URL = "http://localhost:8084" +$TEST_MEMBER_ID = 26 # 测试环境会员ID + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " 会员模块接口测试" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +# ======================================== +# 1. 测试获取会员信息 +# ======================================== +Write-Host "[测试 1] 获取会员信息" -ForegroundColor Yellow +Write-Host "请求: GET $BASE_URL/api/member/info" -ForegroundColor Gray +Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray +try { + $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/info" -Method Get -Headers @{ + "X-Member-Id" = "$TEST_MEMBER_ID" + } + Write-Host "✅ 成功" -ForegroundColor Green + Write-Host "响应:" -ForegroundColor Cyan + $response | ConvertTo-Json -Depth 10 +} catch { + Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# ======================================== +# 2. 测试查询关注状态 +# ======================================== +Write-Host "[测试 2] 查询关注状态" -ForegroundColor Yellow +Write-Host "请求: GET $BASE_URL/api/member/subscribe/status" -ForegroundColor Gray +Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray +try { + $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/subscribe/status" -Method Get -Headers @{ + "X-Member-Id" = "$TEST_MEMBER_ID" + } + Write-Host "✅ 成功" -ForegroundColor Green + Write-Host "响应: $response" -ForegroundColor Cyan +} catch { + Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# ======================================== +# 3. 测试更新会员信息 +# ======================================== +Write-Host "[测试 3] 更新会员信息" -ForegroundColor Yellow +Write-Host "请求: PUT $BASE_URL/api/member/info?nickname=测试用户&gender=1&birthday=1990-01-01&address=测试地址" -ForegroundColor Gray +Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray +try { + $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/info?nickname=测试用户&gender=1&birthday=1990-01-01&address=测试地址" -Method Put -Headers @{ + "X-Member-Id" = "$TEST_MEMBER_ID" + } + Write-Host "✅ 成功" -ForegroundColor Green + Write-Host "响应: $response" -ForegroundColor Cyan +} catch { + Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# ======================================== +# 4. 测试管理端录入手机号 +# ======================================== +Write-Host "[测试 4] 管理端录入手机号" -ForegroundColor Yellow +Write-Host "请求: POST $BASE_URL/api/admin/members/$TEST_MEMBER_ID/phone" -ForegroundColor Gray +Write-Host "Body: { `"phone`": `"13800138000`" }" -ForegroundColor Gray +try { + $body = @{ phone = "13800138000" } | ConvertTo-Json + $response = Invoke-RestMethod -Uri "$BASE_URL/api/admin/members/$TEST_MEMBER_ID/phone" -Method Post -Body $body -ContentType "application/json" + Write-Host "✅ 成功" -ForegroundColor Green + Write-Host "响应: $response" -ForegroundColor Cyan +} catch { + Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# ======================================== +# 5. 再次获取会员信息(验证手机号已更新) +# ======================================== +Write-Host "[测试 5] 验证手机号已更新" -ForegroundColor Yellow +Write-Host "请求: GET $BASE_URL/api/member/info" -ForegroundColor Gray +Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray +try { + $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/info" -Method Get -Headers @{ + "X-Member-Id" = "$TEST_MEMBER_ID" + } + Write-Host "✅ 成功" -ForegroundColor Green + Write-Host "响应:" -ForegroundColor Cyan + $response | ConvertTo-Json -Depth 10 + + if ($response.hasPhone) { + Write-Host "✅ 手机号已成功绑定" -ForegroundColor Green + } else { + Write-Host "⚠️ 手机号未绑定" -ForegroundColor Yellow + } +} catch { + Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# ======================================== +# 6. 测试小程序登录(需要有效的code) +# ======================================== +Write-Host "[测试 6] 小程序登录(需要提供有效的微信code)" -ForegroundColor Yellow +Write-Host "注意: 此测试需要从小程序获取真实的code,跳过自动测试" -ForegroundColor Gray +Write-Host "手动测试命令:" -ForegroundColor Cyan +Write-Host "curl -X POST $BASE_URL/api/member/auth/miniapp/login \" -ForegroundColor Gray +Write-Host " -H `"Content-Type: application/json`" \" -ForegroundColor Gray +Write-Host " -d `"{`"code`":`"YOUR_CODE_HERE`"}"`" -ForegroundColor Gray +Write-Host "" + +# ======================================== +# 7. 测试服务号回调验证签名 +# ======================================== +Write-Host "[测试 7] 服务号回调验证签名" -ForegroundColor Yellow +Write-Host "请求: GET $BASE_URL/api/member/auth/mp/callback?signature=test×tamp=123&nonce=test&echostr=test" -ForegroundColor Gray +try { + $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/auth/mp/callback?signature=test×tamp=123&nonce=test&echostr=test" -Method Get + Write-Host "✅ 成功(返回echostr表示验证通过)" -ForegroundColor Green + Write-Host "响应: $response" -ForegroundColor Cyan +} catch { + Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red +} +Write-Host "" + +# ======================================== +# 总结 +# ======================================== +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " 测试完成" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "提示:" -ForegroundColor Yellow +Write-Host "1. 小程序登录需要提供真实的微信code" -ForegroundColor Gray +Write-Host "2. 服务号回调需要微信服务器推送的真实事件" -ForegroundColor Gray +Write-Host "3. 管理端录入手机号需要配置正确的AES密钥" -ForegroundColor Gray +Write-Host "" -- 2.52.0 From 923d147574f36e7240d56345ca165e7182f2e524 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Sun, 10 May 2026 18:34:11 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD=E8=B7=AF=E7=94=B1=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gym/manage/app/config/SystemRouter.java | 13 ++++++ .../filter/JwtAuthenticationFilter.java | 7 +++- .../src/main/resources/application.yml | 42 +++++++++---------- 3 files changed, 40 insertions(+), 22 deletions(-) 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 c869da3..546603b 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 @@ -192,6 +192,19 @@ public class SystemRouter { .POST("/api/permissions", permissionHandler::createPermission) .PUT("/api/permissions/{id}", permissionHandler::updatePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission) + + // ========== 会员模块路由 - 微信认证 ========== + .POST("/api/member/auth/miniapp/login", wechatAuthHandler::miniappLogin) + .GET("/api/member/auth/mp/callback", wechatAuthHandler::verifyMpSignature) + .POST("/api/member/auth/mp/callback", wechatAuthHandler::mpCallback) + + // ========== 会员模块路由 - 会员信息 ========== + .GET("/api/member/info", memberHandler::getMemberInfo) + .POST("/api/member/phone/bind", memberHandler::bindPhone) + .GET("/api/member/subscribe/status", memberHandler::checkSubscribeStatus) + + // ========== 会员模块路由 - 管理端 ========== + .POST("/api/admin/members/{id}/phone", memberHandler::adminUpdatePhone) .build(); } 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 cc54fbd..749702c 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 @@ -47,6 +47,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory Date: Sun, 10 May 2026 20:14:13 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/manage-app/pom.xml | 5 +++++ .../java/cn/novalon/gym/manage/app/ManageApplication.java | 2 +- .../java/cn/novalon/gym/manage/app/config/SystemRouter.java | 4 ++++ .../gym/manage/gateway/filter/JwtAuthenticationFilter.java | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 45fcc60..ced508a 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -38,6 +38,11 @@ manage-db ${project.version} + + cn.novalon.gym.manage + gym-member + ${project.version} + org.springframework.boot spring-boot-starter-webflux diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index f74c2f7..cb9a6db 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -18,7 +18,7 @@ import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) @EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", - "cn.novalon.gym.manage.sys.audit.repository" }) + "cn.novalon.gym.manage.sys.audit.repository", "cn.novalon.gym.manage.member.repository" }) public class ManageApplication { private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); 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 546603b..f5b549e 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 @@ -15,6 +15,8 @@ import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; import cn.novalon.gym.manage.file.handler.SysFileHandler; +import cn.novalon.gym.manage.member.handler.WechatAuthHandler; +import cn.novalon.gym.manage.member.handler.MemberHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.server.RouterFunction; @@ -51,6 +53,8 @@ public class SystemRouter { SysUserMessageHandler messageHandler, SysFileHandler fileHandler, SysPermissionHandler permissionHandler, + MemberHandler memberHandler, + WechatAuthHandler wechatAuthHandler, PasswordDiagnosticHandler passwordDiagnosticHandler) { return route() 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 749702c..c42d328 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 @@ -62,7 +62,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory Date: Sun, 10 May 2026 20:40:25 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=E5=B0=86=E6=9F=A5=E8=AF=A2=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=AA=E4=BA=BA=E4=BF=A1=E6=81=AF=E4=B8=8E=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=87=B3gym-member=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/dto/UpdateMemberInfoDto.java | 30 ++++++++++++++++ .../manage/member/handler/MemberHandler.java | 30 ++++++++++++++++ .../manage/member/service/MemberService.java | 10 ++++++ .../service/impl/MemberServiceImpl.java | 35 +++++++++++++++++++ .../gym/manage/app/config/SystemRouter.java | 1 + .../src/main/resources/application-dev.yml | 4 +++ .../src/main/resources/application.yml | 10 +++--- 7 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java new file mode 100644 index 0000000..835347c --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java @@ -0,0 +1,30 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 更新会员信息Dto + * + * @author 付嘉 + * @date 2026-05-10 + */ +@Data +public class UpdateMemberInfoDto { + + // 昵称 + private String nickname; + + // 性别 + private Integer gender; + + // 生日 + private Date birthday; + + // 头像 + private String avatar; + + // 地址 + private String address; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 271d499..a54c8eb 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -1,6 +1,7 @@ package cn.novalon.gym.manage.member.handler; import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto; +import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; @@ -52,6 +53,35 @@ public class MemberHandler { }); } + /** + * 更新会员信息 + * + * PUT /api/member/info + * Header: X-Member-Id: 123 + * Body: { + * "nickname": "新昵称", + * "gender": 1, + * "birthday": "2000-01-01", + * "avatar": "https://example.com/avatar.jpg", + * "address": "北京市朝阳区" + * } + */ + public Mono updateMemberInfo(ServerRequest request) { + + String memberIdStr = request.headers().firstHeader("X-Member-Id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + + if (memberId <= 0) throw new IllegalArgumentException("更新会员信息失败: memberId 无效"); + + log.info("更新会员信息, memberId: {}", memberId); + + return request.bodyToMono(UpdateMemberInfoDto.class) + .flatMap(updateDto -> memberService.updateMemberInfo(memberId, updateDto)) + .flatMap(info -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(info)); + } + /** * 绑定手机号(微信小程序) * diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java index 23a2ea8..aa4ea33 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service; +import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.vo.MemberInfoVO; import reactor.core.publisher.Mono; @@ -19,6 +20,15 @@ public interface MemberService { */ Mono getMemberInfo(Long memberId); + /** + * 会员更新个人信息 + * + * @param memberId 会员ID + * @param updateDto 更新信息DTO + * @return 更新后的会员信息 + */ + Mono updateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto); + /** * 管理端更新会员手机号 * diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index 6509616..d9c3e4c 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.vo.MemberInfoVO; import cn.novalon.gym.manage.member.entity.Member; import cn.novalon.gym.manage.member.repository.MemberRepository; @@ -40,6 +41,40 @@ public class MemberServiceImpl implements MemberService { .map(this::buildMemberInfoResponse); } + @Override + public Mono updateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) { + log.info("会员更新个人信息, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .switchIfEmpty(Mono.defer(() -> { + log.error("会员不存在: memberId={}", memberId); + return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "会员不存在")); + })) + .flatMap(member -> { + if (updateDto.getNickname() != null) { + member.setNickname(updateDto.getNickname()); + } + if (updateDto.getGender() != null) { + member.setGender(updateDto.getGender()); + } + if (updateDto.getBirthday() != null) { + member.setBirthday(updateDto.getBirthday()); + } + if (updateDto.getAvatar() != null) { + member.setAvatar(updateDto.getAvatar()); + } + if (updateDto.getAddress() != null) { + member.setAddress(updateDto.getAddress()); + } + + return memberRepository.save(member); + }) + .map(savedMember -> { + log.info("会员信息更新成功, memberId: {}", savedMember.getId()); + return buildMemberInfoResponse(savedMember); + }); + } + // 会员信息响应 private MemberInfoVO buildMemberInfoResponse(Member member) { String phone = member.getPhone(); 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 f5b549e..12d6cd0 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 @@ -204,6 +204,7 @@ public class SystemRouter { // ========== 会员模块路由 - 会员信息 ========== .GET("/api/member/info", memberHandler::getMemberInfo) + .PUT("/api/member/info", memberHandler::updateMemberInfo) .POST("/api/member/phone/bind", memberHandler::bindPhone) .GET("/api/member/subscribe/status", memberHandler::checkSubscribeStatus) diff --git a/gym-manage-api/manage-app/src/main/resources/application-dev.yml b/gym-manage-api/manage-app/src/main/resources/application-dev.yml index 8f5e8dc..33e428e 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-dev.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-dev.yml @@ -12,11 +12,15 @@ spring: max-life-time: 30m acquire-timeout: 3s flyway: + url: jdbc:postgresql://localhost:55432/manage_system + user: novalon + password: novalon123 enabled: true locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true + jwt: secret: novalon-gym-manage-jwt-secret-key-for-development-only-2026 expiration: 86400000 diff --git a/gym-manage-api/manage-app/src/main/resources/application.yml b/gym-manage-api/manage-app/src/main/resources/application.yml index 9305b16..807415e 100644 --- a/gym-manage-api/manage-app/src/main/resources/application.yml +++ b/gym-manage-api/manage-app/src/main/resources/application.yml @@ -15,8 +15,8 @@ spring: - org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration r2dbc: url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} - username: ${DB_USERNAME:postgres} - password: ${DB_PASSWORD:postgres} + username: ${DB_USERNAME:novalon} + password: ${DB_PASSWORD:novalon123} pool: initial-size: 10 max-size: 50 @@ -25,8 +25,8 @@ spring: acquire-timeout: 5s datasource: url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} - username: ${DB_USERNAME:postgres} - password: ${DB_PASSWORD:postgres} + username: ${DB_USERNAME:novalon} + password: ${DB_PASSWORD:novalon123} driver-class-name: org.postgresql.Driver flyway: enabled: true @@ -38,6 +38,8 @@ spring: user: name: disabled password: disabled + profiles: + active: dev management: endpoints: -- 2.52.0 From 8b8920a53d2cc3a5ea5693f3c407fbde40271070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E8=88=9F=E5=B9=B4?= <3147056268@qq.com> Date: Thu, 21 May 2026 12:34:11 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E5=8D=A1=E6=A8=A1=E5=9D=97=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-member-card/pom.xml | 84 +++++++++++++++++++ .../GymMemberCardApplication.java | 16 ++++ .../src/main/resources/application.properties | 1 + .../GymMemberCardApplicationTests.java | 13 +++ 4 files changed, 114 insertions(+) create mode 100644 gym-manage-api/gym-member-card/pom.xml create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java create mode 100644 gym-manage-api/gym-member-card/src/main/resources/application.properties create mode 100644 gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java diff --git a/gym-manage-api/gym-member-card/pom.xml b/gym-manage-api/gym-member-card/pom.xml new file mode 100644 index 0000000..e5940b4 --- /dev/null +++ b/gym-manage-api/gym-member-card/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.13 + + + cn.novalon.gym.manage + gym-member-card + 0.0.1-SNAPSHOT + gym-member-card + gym-member-card + + + 21 + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.postgresql + postgresql + runtime + + + + + cn.novalon.gym.manage + manage-db + 1.0.0 + compile + + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + cn.hutool + hutool-all + 5.8.22 + + + + + org.springframework + spring-context + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + exec + + + + + \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java new file mode 100644 index 0000000..c231709 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java @@ -0,0 +1,16 @@ +package cn.novalon.gym.manage.gymmembercard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; + +@SpringBootApplication +@EnableR2dbcRepositories(basePackages = "cn.novalon.gym.manage.db.dao") +public class GymMemberCardApplication { + + public static void main(String[] args) { + SpringApplication.run(GymMemberCardApplication.class, args); + } + +} diff --git a/gym-manage-api/gym-member-card/src/main/resources/application.properties b/gym-manage-api/gym-member-card/src/main/resources/application.properties new file mode 100644 index 0000000..81b7aa0 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=gym-member-card diff --git a/gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java b/gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java new file mode 100644 index 0000000..56e47d9 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/test/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplicationTests.java @@ -0,0 +1,13 @@ +package cn.novalon.gym.manage.gymmembercard; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GymMemberCardApplicationTests { + + @Test + void contextLoads() { + } + +} -- 2.52.0 From 0afd1cc865ee61bac77c9fccda31ad528fd6e446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E8=88=9F=E5=B9=B4?= <3147056268@qq.com> Date: Thu, 21 May 2026 14:51:29 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E5=8D=A1=E6=A8=A1=E5=9D=97=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gymmembercard/dao/MemberCardDao.java | 130 +++++++++++++ .../dao/MemberCardRecordDao.java | 148 +++++++++++++++ .../dao/MemberCardTransactionsDao.java | 159 ++++++++++++++++ .../gymmembercard/domain/MemberCard.java | 49 +++++ .../domain/MemberCardRecord.java | 50 +++++ .../domain/MemberCardTransactions.java | 59 ++++++ .../entity/MemberCardEntity.java | 52 +++++ .../entity/MemberCardRecordEntity.java | 53 ++++++ .../entity/MemberCardTransactionsEntity.java | 62 ++++++ .../enums/MemberCardRecordStatus.java | 36 ++++ .../enums/MemberCardTransactionsAction.java | 40 ++++ .../enums/MemberCardTransactionsType.java | 32 ++++ .../handler/MemberCardHandler.java | 114 +++++++++++ .../handler/MemberCardRecordHandler.java | 179 ++++++++++++++++++ .../handler/MemberCardTransactionHandler.java | 150 +++++++++++++++ .../IMemberCardRecordRepository.java | 83 ++++++++ .../repository/IMemberCardRepository.java | 78 ++++++++ .../IMemberCardTransactionsRepository.java | 91 +++++++++ .../impl/MemberCardRecordRepositoryImpl.java | 141 ++++++++++++++ .../impl/MemberCardRepositoryImpl.java | 146 ++++++++++++++ .../MemberCardTransactionsRepositoryImpl.java | 147 ++++++++++++++ .../sevice/IMemberCardRecordService.java | 83 ++++++++ .../sevice/IMemberCardService.java | 78 ++++++++ .../IMemberCardTransactionsService.java | 91 +++++++++ .../impl/MemberCardRecordServiceImpl.java | 67 +++++++ .../sevice/impl/MemberCardServiceImpl.java | 65 +++++++ .../MemberCardTransactionsServiceImpl.java | 69 +++++++ .../gymmembercard/util/BeanConvertUtil.java | 49 +++++ gym-manage-api/manage-app/pom.xml | 6 + .../gym/manage/app/ManageApplication.java | 3 +- .../gym/manage/app/config/SystemRouter.java | 56 +++++- gym-manage-api/pom.xml | 1 + gym-manage-web/pnpm-lock.yaml | 95 ++++++++-- 33 files changed, 2646 insertions(+), 16 deletions(-) create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java new file mode 100644 index 0000000..8877d8d --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardDao.java @@ -0,0 +1,130 @@ +package cn.novalon.gym.manage.gymmembercard.dao; + +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Modifying; +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 MemberCardDao extends R2dbcRepository { + + /** + * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) + * @param memberCardId 会员卡ID + * @return 会员卡完整信息,如果不存在或已删除则返回空 + */ + Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); + + /** + * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) + * 注意:模糊查询使用前后缀通配符,若数据量较大可能影响索引效率,建议后期引入全文索引或改用后缀模糊 + * @param status 会员卡状态(上架/下架) + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + @Query("SELECT * FROM member_card WHERE deleted_at IS NULL " + + "AND (:status IS NULL OR member_card_status = :status) " + + "AND (:name IS NULL OR member_card_name LIKE CONCAT('%', :name, '%')) " + + "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}") + Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable); + + /** + * 统计符合条件的会员卡总数(配合列表查询使用) + * @param status 会员卡状态 + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @return 符合条件的会员卡数量 + */ + @Query("SELECT COUNT(*) FROM member_card WHERE deleted_at IS NULL " + + "AND (:status IS NULL OR member_card_status = :status) " + + "AND (:name IS NULL OR member_card_name LIKE CONCAT('%', :name, '%')) " + + "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)") + Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice); + + /** + * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) + * @param status 会员卡状态(通常传上架状态) + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); + + /** + * 检查会员卡是否已被购买(用于删除前的校验) + * 注意:此查询关联到 member_card_record 表,建议后续独立至 MemberCardRecordDao + * @param memberCardId 会员卡ID + * @return 如果存在关联的会员记录则返回true,否则返回false + */ + @Query("SELECT EXISTS(SELECT 1 FROM member_card_record WHERE member_card_id = :memberCardId AND deleted_at IS NULL LIMIT 1)") + Mono existsPurchasedRecord(Long memberCardId); + + /** + * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) + * @param memberCardId 会员卡ID + * @return 受影响的行数 + */ + @Modifying + @Query("UPDATE member_card SET deleted_at = NOW() WHERE member_card_id = :memberCardId AND deleted_at IS NULL") + Mono logicalDelete(Long memberCardId); + + /** + * 【新增】安全更新会员卡信息(仅允许修改业务相关字段,防止覆盖敏感字段) + * @param memberCardId 会员卡ID + * @param name 卡种名称 + * @param price 价格 + * @param durationDays 有效天数 + * @param totalCount 总次数 + * @param denomination 面额 + * @param status 状态 + * @return 受影响的行数 + */ + @Modifying + @Query("UPDATE member_card SET " + + "member_card_name = COALESCE(:name, member_card_name), " + + "member_card_price = COALESCE(:price, member_card_price), " + + "duration_days = COALESCE(:durationDays, duration_days), " + + "total_count = COALESCE(:totalCount, total_count), " + + "denomination = COALESCE(:denomination, denomination), " + + "member_card_status = COALESCE(:status, member_card_status), " + + "updated_at = NOW() " + + "WHERE member_card_id = :memberCardId AND deleted_at IS NULL") + Mono updateSafe(Long memberCardId, String name, Double price, + Integer durationDays, Integer totalCount, + Double denomination, Integer status); + + /** + * 保存卡种信息(新增或更新) + * - 新增:entity.memberCardId 为 null 时,插入新记录 + * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 + * 注意:直接 save 会更新所有字段,如需安全更新请调用 updateSafe 方法 + * @param entity 卡种信息 + * @return 保存后的实体对象 + */ + @Override + Mono save(S entity); + + /** + * 批量查询上架的会员卡(用于小程序端展示) + * @param status 上架状态值 + * @return 上架的会员卡列表 + */ + @Query("SELECT * FROM member_card WHERE deleted_at IS NULL AND member_card_status = :status ORDER BY member_card_price ASC") + Flux findActiveCards(Integer status); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java new file mode 100644 index 0000000..e9c6bb1 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java @@ -0,0 +1,148 @@ +package cn.novalon.gym.manage.gymmembercard.dao; + +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardRecordEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Modifying; +import org.springframework.data.r2dbc.repository.Query; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Repository +public interface MemberCardRecordDao extends R2dbcRepository { + + /** + * 会员购卡/后台发卡 + * 支付成功后插入一条记录,状态为ACTIVE,设置有效期和初始剩余次数/余额 + * @param memberId 会员ID + * @param memberCardId 会员卡类型ID + * @param expireTime 到期时间 + * @param remainingTimes 剩余次数 + * @param remainingAmount 剩余余额 + * @param sourceOrderId 来源订单ID + * @return 插入的记录 + */ + @Modifying + @Query("INSERT INTO member_card_record (member_id, member_card_id, status, expire_time, remaining_times, remaining_amount, source_order_id, purchase_time, created_at, updated_at) " + + "VALUES (:memberId, :memberCardId, 'ACTIVE', :expireTime, :remainingTimes, :remainingAmount, :sourceOrderId, NOW(), NOW(), NOW()) " + + "RETURNING *") + Mono insertActiveRecord(@Param("memberId") Long memberId, + @Param("memberCardId") Long memberCardId, + @Param("expireTime") LocalDateTime expireTime, + @Param("remainingTimes") Integer remainingTimes, + @Param("remainingAmount") Double remainingAmount, + @Param("sourceOrderId") Long sourceOrderId); + + /** + * 扣次/扣费(含防超扣校验) + * 预约团课或私教成功后,更新 remaining_times 减1 或 remaining_amount 减课程价格 + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + @Modifying + @Query("UPDATE member_card_record SET " + + "remaining_times = remaining_times - :deductTimes, " + + "remaining_amount = remaining_amount - :deductAmount, " + + "updated_at = NOW() " + + "WHERE member_card_record_id = :recordId " + + "AND deleted_at IS NULL " + + "AND remaining_times >= :deductTimes " + + "AND remaining_amount >= :deductAmount") + Mono deductUsage(@Param("recordId") Long recordId, + @Param("deductTimes") Integer deductTimes, + @Param("deductAmount") Double deductAmount); + + /** + * 续费 + * 累加剩余次数/余额,顺延到期日期 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + @Modifying + @Query("UPDATE member_card_record SET remaining_times = remaining_times + :addTimes, " + + "remaining_amount = remaining_amount + :addAmount, expire_time = :newExpireTime, updated_at = NOW() " + + "WHERE member_card_record_id = :recordId AND deleted_at IS NULL") + Mono renewCard(@Param("recordId") Long recordId, + @Param("addTimes") Integer addTimes, + @Param("addAmount") Double addAmount, + @Param("newExpireTime") LocalDateTime newExpireTime); + + /** + * 状态变更 + * 过期定时任务将状态改为EXPIRED;退款后改为REFUNDED + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + @Modifying + @Query("UPDATE member_card_record SET status = :status, updated_at = NOW() " + + "WHERE member_card_record_id = :recordId AND deleted_at IS NULL") + Mono updateStatus(@Param("recordId") Long recordId, + @Param("status") MemberCardRecordStatus status); + + /** + * 会员端"我的卡包" + * 根据会员ID查询所有卡,过滤状态为ACTIVE的,展示剩余次数/天数/余额 + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + @Query("SELECT * FROM member_card_record WHERE member_id = :memberId AND status = 'ACTIVE' AND deleted_at IS NULL ORDER BY expire_time ASC") + Flux findActiveCardsByMemberId(@Param("memberId") Long memberId); + + /** + * 前台/店长查会员卡 + * 输入会员手机号或姓名,查出该会员持有的所有卡的信息 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + @Query("SELECT mcr.* FROM member_card_record mcr " + + "INNER JOIN member m ON mcr.member_id = m.member_id " + + "WHERE mcr.member_id = :memberId AND mcr.deleted_at IS NULL " + + "ORDER BY mcr.purchase_time DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findByMemberId(@Param("memberId") Long memberId, Pageable pageable); + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + + "AND status = 'ACTIVE' AND deleted_at IS NULL " + + "AND expire_time > NOW() " + + "AND remaining_times >= :requiredTimes") + Mono validateCountCard(@Param("recordId") Long recordId, + @Param("requiredTimes") Integer requiredTimes); + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + + "AND status = 'ACTIVE' AND deleted_at IS NULL " + + "AND expire_time > NOW() " + + "AND remaining_amount >= :requiredAmount") + Mono validateStoredCard(@Param("recordId") Long recordId, + @Param("requiredAmount") Double requiredAmount); + + /** + * 到期扫描(分批处理,避免内存压力) + * 定时任务:查询 status=ACTIVE 且 expire_time < 当前时间 的记录,用于批量过期处理 + * @return 已过期的会员卡记录列表(最多500条) + */ + @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND expire_time < NOW() AND deleted_at IS NULL LIMIT 500") + Flux findExpiredCards(); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java new file mode 100644 index 0000000..87836bb --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java @@ -0,0 +1,159 @@ +package cn.novalon.gym.manage.gymmembercard.dao; + +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardTransactionsEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Modifying; +import org.springframework.data.r2dbc.repository.Query; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Repository +public interface MemberCardTransactionsDao extends R2dbcRepository { + /** + * 记录每一次变动 + * 购卡、扣次、续费、退款、过期,均插入一条流水,记录变动前后快照 + * 注意:返回值依赖 PostgreSQL 的 RETURNING 语法,若使用 MySQL 请删除 RETURNING * 并改用 save() + * @param memberCardId 会员卡ID + * @param memberId 会员ID + * @param operationType 操作类型 + * @param changeAmount 变动次数(次卡) + * @param changeBalance 变动金额(储值卡) + * @param afterRemainingCount 变动后剩余次数 + * @param afterRemainingBalance 变动后剩余余额 + * @param relatedBizType 关联业务类型 + * @param remark 备注 + * @return 插入的流水记录 + */ + @Modifying + @Query("INSERT INTO member_card_transactions (member_card_id, member_id, operation_type, change_amount, " + + "change_balance, after_remaining_count, after_remaining_balance, related_biz_type, remark, created_at, updated_at) " + + "VALUES (:memberCardId, :memberId, :operationType, :changeAmount, :changeBalance, " + + ":afterRemainingCount, :afterRemainingBalance, :relatedBizType, :remark, NOW(), NOW()) " + + "RETURNING *") + Mono insertTransaction(@Param("memberCardId") Long memberCardId, + @Param("memberId") Long memberId, + @Param("operationType") MemberCardTransactionsAction operationType, + @Param("changeAmount") Integer changeAmount, + @Param("changeBalance") Double changeBalance, + @Param("afterRemainingCount") Integer afterRemainingCount, + @Param("afterRemainingBalance") Double afterRemainingBalance, + @Param("relatedBizType") MemberCardTransactionsType relatedBizType, + @Param("remark") String remark); + + /** + * 会员端"使用记录" + * 按会员ID和时间范围查询,按时间倒序,显示每次变动明细 + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_id = :memberId " + + "AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL " + + "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findByMemberIdAndTimeRange(@Param("memberId") Long memberId, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + Pageable pageable); + + /** + * 后台"使用记录查询" + * 按会员、卡号、操作类型、时间范围等条件组合查询 + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + @Query("SELECT * FROM member_card_transactions WHERE deleted_at IS NULL " + + "AND (:memberId IS NULL OR member_id = :memberId) " + + "AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " + + "AND (:operationType IS NULL OR operation_type = :operationType) " + + "AND (:startTime IS NULL OR created_at >= :startTime) " + + "AND (:endTime IS NULL OR created_at <= :endTime) " + + "ORDER BY created_at DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") + Flux findWithConditions(@Param("memberId") Long memberId, + @Param("memberCardId") Long memberCardId, + @Param("operationType") MemberCardTransactionsAction operationType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + Pageable pageable); + + /** + * 统计符合条件的流水总数 + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 流水记录数量 + */ + @Query("SELECT COUNT(*) FROM member_card_transactions WHERE deleted_at IS NULL " + + "AND (:memberId IS NULL OR member_id = :memberId) " + + "AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " + + "AND (:operationType IS NULL OR operation_type = :operationType) " + + "AND (:startTime IS NULL OR created_at >= :startTime) " + + "AND (:endTime IS NULL OR created_at <= :endTime)") + Mono countWithConditions(@Param("memberId") Long memberId, + @Param("memberCardId") Long memberCardId, + @Param("operationType") MemberCardTransactionsAction operationType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 按会员卡ID查询所有流水记录(补充常用方法) + * @param memberCardId 会员卡ID + * @return 该卡的所有流水记录,按时间倒序 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_card_id = :memberCardId AND deleted_at IS NULL ORDER BY created_at DESC") + Flux findByMemberCardId(@Param("memberCardId") Long memberCardId); + + /** + * 数据统计 - 统计某卡种的总扣次数 + * @param memberCardId 会员卡ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 总扣次数 + */ + @Query("SELECT COALESCE(SUM(change_amount), 0) FROM member_card_transactions " + + "WHERE member_card_id = :memberCardId AND operation_type = 'DEDUCT' " + + "AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL") + Mono sumDeductCountByCardId(@Param("memberCardId") Long memberCardId, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 数据统计 - 统计某时间段的续费总金额 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 续费总金额 + */ + @Query("SELECT COALESCE(SUM(change_balance), 0) FROM member_card_transactions " + + "WHERE operation_type = 'RENEW' AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL") + Mono sumRenewAmountByTimeRange(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 数据统计 - 统计某会员的购卡总金额 + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 购卡总金额 + */ + @Query("SELECT COALESCE(SUM(change_balance), 0) FROM member_card_transactions " + + "WHERE member_id = :memberId AND operation_type = 'PURCHASE' " + + "AND created_at BETWEEN :startTime AND :endTime AND deleted_at IS NULL") + Mono sumPurchaseAmountByMemberId(@Param("memberId") Long memberId, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java new file mode 100644 index 0000000..c5cf0e6 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java @@ -0,0 +1,49 @@ +package cn.novalon.gym.manage.gymmembercard.domain; + +import cn.novalon.gym.manage.sys.core.domain.BaseDomain; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Schema(description = "会员卡类型表") +public class MemberCard extends BaseDomain { + //会员卡id + @Schema(description = "会员卡Id",example = "1") + private Long memberCardId; + + //会员卡名称 + @Schema(description = "会员卡名称",example = "月卡") + private String memberCardName; + + //会员卡类型 + @Schema(description = "会员卡类型",example = "TIME_CARD") + private String memberCardType; + + //会员卡价格 + @Schema(description = "会员卡价格",example = "199.0") + private Double memberCardPrice; + + //会员卡有效天数(时长卡用) + @Schema(description = "会员卡有效天数",example = "30") + private Integer memberCardValidityDays; + + //会员卡总次数(次卡用) + @Schema(description = "会员卡总次数",example = "10") + private Integer memberCardTotalTimes; + + //会员卡面额(储值卡用) + @Schema(description = "会员卡面额",example = "500.0") + private Double memberCardAmount; + + //会员卡状态:0-正常,1-禁用 + @Schema(description = "会员卡状态",example = "0") + private Integer memberCardStatus; + + //会员卡创建时间 + @Schema(description = "会员卡创建时间",example = "2026-05-10 05:22:47") + private LocalDateTime memberCardCreateTime; +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java new file mode 100644 index 0000000..7e04215 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java @@ -0,0 +1,50 @@ +package cn.novalon.gym.manage.gymmembercard.domain; + +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.sys.core.domain.BaseDomain; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Schema(description = "会员卡记录",example = "member_card_record") +public class MemberCardRecord extends BaseDomain { + //会员持有卡id + @Schema(description = "会员持有卡Id",example = "1") + private Long memberCardRecordId; + + //会员Id + @Schema(description = "会员Id",example = "1") + private Long memberId; + + //关联会员卡Id + @Schema(description = "关联会员卡Id",example = "1") + private Long memberCardId; + + //状态:ACTIVE(有效) / USED_UP(用完) / EXPIRED(过期) / REFUNDED(已退款) + @Schema(description = "状态",example = "ACTIVE") + private MemberCardRecordStatus status; + + //剩余次数 + @Schema(description = "剩余次数",example = "1") + private Integer remainingTimes; + + //剩余余额 + @Schema(description = "剩余余额",example = "1") + private Double remainingAmount; + + //到期时间 + @Schema(description = "到期时间",example = "2026-05-10 05:22:47") + private LocalDateTime expireTime; + + //购买订单Id + @Schema(description = "购买订单Id",example = "1") + private Long sourceOrderId; + + //购买时间 + @Schema(description = "购买时间",example = "2026-05-10 05:22:47") + private LocalDateTime purchaseTime; +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java new file mode 100644 index 0000000..410ddac --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java @@ -0,0 +1,59 @@ +package cn.novalon.gym.manage.gymmembercard.domain; + +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; +import cn.novalon.gym.manage.sys.core.domain.BaseDomain; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Schema(description = "会员卡流水") +public class MemberCardTransactions extends BaseDomain { + //会员卡流水Id + @Schema(description = "会员卡流水Id",example = "1") + private Long memberCardTransactionsId; + + //会员卡Id + @Schema(description = "会员卡Id",example = "1") + private Long memberCardId; + + //会员Id + @Schema(description = "会员Id",example = "1") + private Long memberId; + + //操作类型:PURCHASE(购买) / DEDUCT(扣次/扣费) / RENEW(续费) / REFUND(退款) / EXPIRE(过期) + @Schema(description = "操作类型",example = "PURCHASE") + private MemberCardTransactionsAction operationType; + + //变动次数(次卡用) + @Schema(description = "变动次数(次卡用)",example = "1") + private Integer changeAmount; + + //变动金额(储值卡用) + @Schema(description = "变动金额(储值卡用)",example = "1") + private Double changeBalance; + + //变动后剩余次数 + @Schema(description = "变动后剩余次数",example = "1") + private Integer afterRemainingCount; + + //变动后剩余金额 + @Schema(description = "变动后剩余金额",example = "500.0") + private Double afterRemainingBalance; + + //关联业务类型 + @Schema(description = "关联业务类型",example = "GROUP_CLASS") + private MemberCardTransactionsType relatedBizType; + + //备注 + @Schema(description = "备注",example = "预约团课:瑜伽课扣1次") + private String remark; + + //创建时间 + @Schema(description = "创建时间",example = "2026-05-10 05:22:47") + private LocalDateTime createdAt; +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java new file mode 100644 index 0000000..06ff025 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java @@ -0,0 +1,52 @@ +package cn.novalon.gym.manage.gymmembercard.entity; + +import cn.novalon.gym.manage.db.entity.BaseEntity; +import lombok.Data; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Data +@Table("member_card") +public class MemberCardEntity extends BaseEntity { + //会员卡id + @Column("member_card_id") + private Long memberCardId; + + //会员卡名称 + @Column("member_card_name") + private String memberCardName; + + //会员卡类型 + @Column("member_card_type") + private String memberCardType; + + //会员卡价格 + @Column("member_card_price") + private Double memberCardPrice; + + //会员卡有效天数(时长卡用) + @Column("member_card_validity_days") + private Integer memberCardValidityDays; + + //会员卡总次数(次卡用) + @Column("member_card_total_times") + private Integer memberCardTotalTimes; + + //会员卡面额(储值卡用) + @Column("member_card_amount") + private Double memberCardAmount; + + //会员卡状态:0-正常,1-禁用 + @Column("member_card_status") + private Integer memberCardStatus; + + //会员卡创建时间 + @Column("member_card_create_time") + private LocalDateTime memberCardCreateTime; +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java new file mode 100644 index 0000000..2c1e1c6 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java @@ -0,0 +1,53 @@ +package cn.novalon.gym.manage.gymmembercard.entity; + +import cn.novalon.gym.manage.db.entity.BaseEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import lombok.Data; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Data +@Table("member_card_record") +public class MemberCardRecordEntity extends BaseEntity { + //会员持有卡id + @Column("member_card_record_id") + private Long memberCardRecordId; + + //会员Id + @Column("member_id") + private Long memberId; + + //关联会员卡Id + @Column("member_card_id") + private Long memberCardId; + + //状态:ACTIVE(有效) / USED_UP(用完) / EXPIRED(过期) / REFUNDED(已退款) + @Column("status") + private MemberCardRecordStatus status; + + //剩余次数 + @Column("remaining_times") + private Integer remainingTimes; + + //剩余余额 + @Column("remaining_amount") + private Double remainingAmount; + + //到期时间 + @Column("expire_time") + private LocalDateTime expireTime; + + //购买订单Id + @Column("source_order_id") + private Long sourceOrderId; + + //购买时间 + @Column("purchase_time") + private LocalDateTime purchaseTime; +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java new file mode 100644 index 0000000..b988065 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java @@ -0,0 +1,62 @@ +package cn.novalon.gym.manage.gymmembercard.entity; + +import cn.novalon.gym.manage.db.entity.BaseEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; +import lombok.Data; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 22:47:31 + */ +@Data +@Table("member_card_transactions") +public class MemberCardTransactionsEntity extends BaseEntity { + //会员卡流水Id + @Column("member_card_transactions_id") + private Long memberCardTransactionsId; + + //会员卡Id + @Column("member_card_id") + private Long memberCardId; + + //会员Id + @Column("member_id") + private Long memberId; + + //操作类型:PURCHASE(购买) / DEDUCT(扣次/扣费) / RENEW(续费) / REFUND(退款) / EXPIRE(过期) + @Column("operation_type") + private MemberCardTransactionsAction operationType; + + //变动次数(次卡用) + @Column("change_amount") + private Integer changeAmount; + + //变动金额(储值卡用) + @Column("change_balance") + private Double changeBalance; + + //变动后剩余次数 + @Column("after_remaining_count") + private Integer afterRemainingCount; + + //变动后剩余金额 + @Column("after_remaining_balance") + private Double afterRemainingBalance; + + //关联业务类型 + @Column("related_biz_type") + private MemberCardTransactionsType relatedBizType; + + //备注 + @Column("remark") + private String remark; + + //创建时间 + @Column("created_at") + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java new file mode 100644 index 0000000..b4fb006 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardRecordStatus.java @@ -0,0 +1,36 @@ +package cn.novalon.gym.manage.gymmembercard.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 23:59:29 + */ +@Schema(description = "会员卡状态枚举") +public enum MemberCardRecordStatus { + //有效 + @Schema(description = "有效") + ACTIVE("有效"), + + //用完 + @Schema(description = "用完") + USED_UP("用完"), + + //过期 + @Schema(description = "过期") + EXPIRED("过期"), + + //已退款 + @Schema(description = "已退款") + REFUNDED("已退款"); + + private final String desc; + + MemberCardRecordStatus(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java new file mode 100644 index 0000000..6be4fc0 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsAction.java @@ -0,0 +1,40 @@ +package cn.novalon.gym.manage.gymmembercard.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 23:59:29 + */ +@Schema(description = "会员卡流水操作枚举") +public enum MemberCardTransactionsAction { + //购买 + @Schema(description = "购买") + PURCHASE("购买"), + + //扣次/扣费 + @Schema(description = "扣次/扣费") + DEDUCT("扣次/扣费"), + + //续费 + @Schema(description = "续费") + RENEW("续费"), + + //退款 + @Schema(description = "退款") + REFUND("退款"), + + //过期 + @Schema(description = "过期") + EXPIRE("过期"); + + private final String desc; + + MemberCardTransactionsAction(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java new file mode 100644 index 0000000..ae55fe9 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardTransactionsType.java @@ -0,0 +1,32 @@ +package cn.novalon.gym.manage.gymmembercard.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +/* + *@Author:shizhounian + *@Date:2026/5/10-05 23:59:29 + */ +@Schema(description = "会员卡流水关联业务类型枚举") +public enum MemberCardTransactionsType { + //团课 + @Schema(description = "团课") + GROUP_CLASS("团课"), + + //私教 + @Schema(description = "私教") + PT_CLASS("私教"), + + //签到 + @Schema(description = "签到") + CHECK_IN("签到"); + + private final String desc; + + MemberCardTransactionsType(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java new file mode 100644 index 0000000..eaff5f0 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java @@ -0,0 +1,114 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.hutool.db.PageResult; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +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.util.List; + +/* + *@Author:shizhounian + *@Date:2026/5/17-05 20:10:22 + */ +@Component +@Tag(name = "会员卡类型管理", description = "会员卡类型相关操作") +public class MemberCardHandler { + private final IMemberCardService memberCardService; + private final Validator validator; + + public MemberCardHandler(IMemberCardService memberCardService, Validator validator) { + this.memberCardService = memberCardService; + this.validator = validator; + } + + /** + * 根据会员卡ID查询会员卡详情 + */ + @Operation(summary = "根据会员卡ID查询会员卡详情", description = "用于编辑前回显或小程序端展示") + public Mono getMemberCardById(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("memberCardId")); + return memberCardService.findByMemberCardIdAndDeletedAtIsNull(memberCardId) + .flatMap(card -> ServerResponse.ok().bodyValue(card)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + /** + * 条件查询会员卡列表(分页) + */ + @Operation(summary = "条件查询会员卡列表", description = "后台卡种列表展示,支持多条件组合+分页+排序") + public Mono getMemberCardList(ServerRequest request) { + Integer status = request.queryParam("status").map(Integer::parseInt).orElse(null); + String name = request.queryParam("name").orElse(null); + String type = request.queryParam("type").orElse(null); + Double minPrice = request.queryParam("minPrice").map(Double::parseDouble).orElse(null); + Double maxPrice = request.queryParam("maxPrice").map(Double::parseDouble).orElse(null); + + int page = request.queryParam("page").map(Integer::parseInt).orElse(0); + int size = request.queryParam("size").map(Integer::parseInt).orElse(10); + String sortField = request.queryParam("sortField").orElse("created_at"); + String sortOrder = request.queryParam("sortOrder").orElse("DESC"); + + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(sortOrder), sortField)); + + Mono countMono = memberCardService.countWithConditions(status, name, type, minPrice, maxPrice); + Flux cardsFlux = memberCardService.findWithConditions(status, name, type, minPrice, maxPrice, pageable); + + return Mono.zip(countMono, cardsFlux.collectList()) + .flatMap(tuple -> { + long total = tuple.getT1(); + List list = tuple.getT2(); + + // 使用 PageResult(int page, int pageSize, int total) 构造, + // 它会自动计算 totalPage + PageResult result = new PageResult<>(page, size, (int) total); + result.addAll(list); // 将查询结果填充进去 + + return ServerResponse.ok().bodyValue(result); + }); + } + + /** + * 保存卡种信息(新增或更新) + */ + @Operation(summary = "保存卡种信息", description = "新增或更新会员卡类型") + public Mono saveMemberCard(ServerRequest request) { + return request.bodyToMono(MemberCard.class) + .flatMap(memberCardService::save) + .flatMap(card -> ServerResponse.ok().bodyValue(card)); + } + + /** + * 逻辑删除会员卡(下架) + */ + @Operation(summary = "逻辑删除会员卡", description = "下架卡种,防止已购会员数据异常") + public Mono deleteMemberCard(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("memberCardId")); + return memberCardService.logicalDelete(memberCardId) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.noContent().build(); + } else { + return ServerResponse.notFound().build(); + } + }); + } + + /** + * 批量查询上架的会员卡 + */ + @Operation(summary = "批量查询上架的会员卡", description = "用于小程序端展示") + public Mono getActiveCards(ServerRequest request) { + Integer status = request.queryParam("status").map(Integer::parseInt).orElse(0); + return ServerResponse.ok().body(memberCardService.findActiveCards(status), MemberCard.class); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java new file mode 100644 index 0000000..3485d5b --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java @@ -0,0 +1,179 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import lombok.Data; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +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.Mono; + +import java.time.LocalDateTime; + +/* + *@Author:shizhounian + *@Date:2026/5/17-05 20:10:22 + */ +@Component +@Tag(name = "会员卡记录管理", description = "会员卡记录相关操作") +public class MemberCardRecordHandler { + + private final IMemberCardRecordService memberCardRecordService; + private final Validator validator; + + public MemberCardRecordHandler(IMemberCardRecordService memberCardRecordService, Validator validator) { + this.memberCardRecordService = memberCardRecordService; + this.validator = validator; + } + + /** + * 会员购卡/后台发卡 + */ + @Operation(summary = "会员购卡/后台发卡", description = "支付成功后插入一条记录,状态为ACTIVE") + public Mono insertActiveRecord(ServerRequest request) { + return request.bodyToMono(MemberCardRecord.class) + .flatMap(memberCardRecordService::insertActiveRecord) + .flatMap(record -> ServerResponse.ok().bodyValue(record)); + } + + /** + * 扣次/扣费 + */ + @Operation(summary = "扣次/扣费", description = "预约团课或私教成功后扣减次数或余额") + public Mono deductUsage(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + return request.bodyToMono(DeductRequest.class) + .flatMap(body -> memberCardRecordService.deductUsage(recordId, + body.getDeductTimes(), + body.getDeductAmount())) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.ok().build(); + } else { + return ServerResponse.badRequest().bodyValue("余额不足或卡无效"); + } + }); + } + + /** + * 续费 + */ + @Operation(summary = "续费", description = "累加剩余次数/余额,顺延到期日期") + public Mono renewCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + return request.bodyToMono(RenewRequest.class) + .flatMap(body -> memberCardRecordService.renewCard(recordId, + body.getAddTimes(), + body.getAddAmount(), + body.getNewExpireTime())) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.ok().build(); + } else { + return ServerResponse.notFound().build(); + } + }); + } + + /** + * 状态变更(过期、退款) + */ + @Operation(summary = "状态变更", description = "将卡状态改为EXPIRED或REFUNDED") + public Mono updateStatus(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + return request.queryParam("status") + .map(s -> { + try { + return MemberCardRecordStatus.valueOf(s.toUpperCase()); + } catch (IllegalArgumentException e) { + return null; + } + }) + .map(status -> memberCardRecordService.updateStatus(recordId, status) + .flatMap(rows -> { + if (rows > 0) { + return ServerResponse.ok().build(); + } else { + return ServerResponse.notFound().build(); + } + })) + .orElse(ServerResponse.badRequest().bodyValue("status 参数缺失或无效")); + } + + /** + * 会员端“我的卡包” + */ + @Operation(summary = "会员我的卡包", description = "查询当前登录会员的所有有效卡") + public Mono getMyCards(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + return ServerResponse.ok().body(memberCardRecordService.findActiveCardsByMemberId(memberId), + MemberCardRecord.class); + } + + /** + * 前台/店长查会员卡(分页) + */ + @Operation(summary = "管理端查询会员卡", description = "按会员ID分页查询所有会员卡记录") + public Mono getMemberCardRecords(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + int page = request.queryParam("page").map(Integer::parseInt).orElse(0); + int size = request.queryParam("size").map(Integer::parseInt).orElse(10); + Pageable pageable = PageRequest.of(page, size, Sort.by("purchase_time").descending()); + return ServerResponse.ok().body(memberCardRecordService.findByMemberId(memberId, pageable), + MemberCardRecord.class); + } + + /** + * 验证次卡是否可用 + */ + @Operation(summary = "验证次卡", description = "校验次卡剩余次数是否足够") + public Mono validateCountCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + Integer requiredTimes = request.queryParam("times").map(Integer::parseInt).orElse(1); + return memberCardRecordService.validateCountCard(recordId, requiredTimes) + .flatMap(card -> ServerResponse.ok().bodyValue(card)) + .switchIfEmpty(ServerResponse.badRequest().bodyValue("次卡不可用")); + } + + /** + * 验证储值卡是否可用 + */ + @Operation(summary = "验证储值卡", description = "校验储值卡余额是否足够") + public Mono validateStoredCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("id")); + Double requiredAmount = request.queryParam("amount").map(Double::parseDouble).orElse(0.0); + return memberCardRecordService.validateStoredCard(recordId, requiredAmount) + .flatMap(card -> ServerResponse.ok().bodyValue(card)) + .switchIfEmpty(ServerResponse.badRequest().bodyValue("储值卡不可用")); + } + + /** + * 到期扫描(管理端触发) + */ + @Operation(summary = "到期扫描", description = "扫描并返回已过期的会员卡(最多500条)") + public Mono getExpiredCards(ServerRequest request) { + return ServerResponse.ok().body(memberCardRecordService.findExpiredCards(), MemberCardRecord.class); + } + + // ==================== 内部请求体 DTO ==================== + + @Data + public static class DeductRequest { + private Integer deductTimes; + private Double deductAmount; + } + + @Data + public static class RenewRequest { + private Integer addTimes; + private Double addAmount; + private LocalDateTime newExpireTime; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java new file mode 100644 index 0000000..ead4283 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardTransactionHandler.java @@ -0,0 +1,150 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.hutool.db.PageResult; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Validator; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +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.List; + +/* + *@Author:shizhounian + *@Date:2026/5/17-05 20:10:22 + */ +@Component +@Tag(name = "会员卡流水管理", description = "会员卡流水相关操作") +public class MemberCardTransactionHandler { + + private final IMemberCardTransactionsService memberCardTransactionsService; + private final Validator validator; + + public MemberCardTransactionHandler(IMemberCardTransactionsService memberCardTransactionsService, + Validator validator) { + this.memberCardTransactionsService = memberCardTransactionsService; + this.validator = validator; + } + + /** + * 记录每一次变动 + */ + @Operation(summary = "插入流水记录", description = "购卡、扣次、续费、退款、过期时插入流水") + public Mono insertTransaction(ServerRequest request) { + return request.bodyToMono(MemberCardTransactions.class) + .flatMap(memberCardTransactionsService::insertTransaction) + .flatMap(record -> ServerResponse.ok().bodyValue(record)); + } + + /** + * 会员端"使用记录" + */ + @Operation(summary = "会员查询使用记录", description = "按会员ID和时间范围查询流水,支持分页") + public Mono getMemberTransactions(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + LocalDateTime startTime = request.queryParam("startTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); + LocalDateTime endTime = request.queryParam("endTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now()); + int page = request.queryParam("page").map(Integer::parseInt).orElse(0); + int size = request.queryParam("size").map(Integer::parseInt).orElse(10); + Pageable pageable = PageRequest.of(page, size, Sort.by("created_at").descending()); + + return ServerResponse.ok() + .body(memberCardTransactionsService.findByMemberIdAndTimeRange( + memberId, startTime, endTime, pageable), MemberCardTransactions.class); + } + + /** + * 后台"使用记录查询"(条件分页) + */ + @Operation(summary = "管理端流水查询", description = "按会员、卡号、操作类型、时间等条件分页查询流水") + public Mono getTransactionsWithConditions(ServerRequest request) { + Long memberId = request.queryParam("memberId").map(Long::parseLong).orElse(null); + Long memberCardId = request.queryParam("memberCardId").map(Long::parseLong).orElse(null); + MemberCardTransactionsAction operationType = request.queryParam("operationType") + .map(s -> MemberCardTransactionsAction.valueOf(s.toUpperCase())).orElse(null); + LocalDateTime startTime = request.queryParam("startTime").map(LocalDateTime::parse).orElse(null); + LocalDateTime endTime = request.queryParam("endTime").map(LocalDateTime::parse).orElse(null); + int page = request.queryParam("page").map(Integer::parseInt).orElse(0); + int size = request.queryParam("size").map(Integer::parseInt).orElse(10); + Pageable pageable = PageRequest.of(page, size, Sort.by("created_at").descending()); + + Mono countMono = memberCardTransactionsService.countWithConditions( + memberId, memberCardId, operationType, startTime, endTime); + Flux flux = memberCardTransactionsService.findWithConditions( + memberId, memberCardId, operationType, startTime, endTime, pageable); + + return Mono.zip(countMono, flux.collectList()) + .flatMap(tuple -> { + Long total = tuple.getT1(); + List list = tuple.getT2(); + // 构造 PageResult,内部自动计算总页数 + PageResult result = new PageResult<>(page, size, total.intValue()); + result.addAll(list); + return ServerResponse.ok().bodyValue(result); + }); + } + + /** + * 按卡ID查询流水 + */ + @Operation(summary = "按卡ID查询流水", description = "查看某张卡的所有流水记录") + public Mono getTransactionsByCardId(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("cardId")); + return ServerResponse.ok() + .body(memberCardTransactionsService.findByMemberCardId(memberCardId), + MemberCardTransactions.class); + } + + /** + * 统计某卡种的总扣次数 + */ + @Operation(summary = "统计卡种总扣次数", description = "按卡种ID和时间范围统计扣次总数") + public Mono getDeductCountByCardId(ServerRequest request) { + Long memberCardId = Long.parseLong(request.pathVariable("cardId")); + LocalDateTime startTime = request.queryParam("startTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); + LocalDateTime endTime = request.queryParam("endTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now()); + return memberCardTransactionsService.sumDeductCountByCardId(memberCardId, startTime, endTime) + .flatMap(count -> ServerResponse.ok().bodyValue(count)); + } + + /** + * 统计某时间段的续费总金额 + */ + @Operation(summary = "统计续费总金额", description = "按时间段统计续费总金额") + public Mono getRenewAmountByTimeRange(ServerRequest request) { + LocalDateTime startTime = request.queryParam("startTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); + LocalDateTime endTime = request.queryParam("endTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now()); + return memberCardTransactionsService.sumRenewAmountByTimeRange(startTime, endTime) + .flatMap(amount -> ServerResponse.ok().bodyValue(amount)); + } + + /** + * 统计某会员的购卡总金额 + */ + @Operation(summary = "统计会员购卡总金额", description = "按会员ID和时间段统计购卡总金额") + public Mono getPurchaseAmountByMember(ServerRequest request) { + Long memberId = Long.parseLong(request.pathVariable("memberId")); + LocalDateTime startTime = request.queryParam("startTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now().minusMonths(1)); + LocalDateTime endTime = request.queryParam("endTime") + .map(LocalDateTime::parse).orElse(LocalDateTime.now()); + return memberCardTransactionsService.sumPurchaseAmountByMemberId(memberId, startTime, endTime) + .flatMap(amount -> ServerResponse.ok().bodyValue(amount)); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java new file mode 100644 index 0000000..237b440 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java @@ -0,0 +1,83 @@ +package cn.novalon.gym.manage.gymmembercard.repository; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +public interface IMemberCardRecordRepository { + + /** + * 会员购卡/后台发卡 + * @param record 会员卡记录 + * @return 插入的记录 + */ + Mono insertActiveRecord(MemberCardRecord record); + + /** + * 扣次/扣费(含防超扣校验) + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + + /** + * 续费 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); + + /** + * 状态变更 + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + Mono updateStatus(Long recordId, MemberCardRecordStatus status); + + /** + * 会员端"我的卡包" + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + Flux findActiveCardsByMemberId(Long memberId); + + /** + * 前台/店长查会员卡 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + Flux findByMemberId(Long memberId, Pageable pageable); + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateCountCard(Long recordId, Integer requiredTimes); + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateStoredCard(Long recordId, Double requiredAmount); + + /** + * 到期扫描(分批处理,避免内存压力) + * @return 已过期的会员卡记录列表(最多500条) + */ + Flux findExpiredCards(); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java new file mode 100644 index 0000000..8b24397 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRepository.java @@ -0,0 +1,78 @@ +package cn.novalon.gym.manage.gymmembercard.repository; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface IMemberCardRepository { + /** + * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) + * @param memberCardId 会员卡ID + * @return 会员卡完整信息,如果不存在或已删除则返回空 + */ + Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); + + /** + * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) + * @param status 会员卡状态(上架/下架) + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable); + + /** + * 统计符合条件的会员卡总数(配合列表查询使用) + * @param status 会员卡状态 + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @return 符合条件的会员卡数量 + */ + Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice); + + /** + * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) + * @param status 会员卡状态(通常传上架状态) + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); + + /** + * 检查会员卡是否已被购买(用于删除前的校验) + * @param memberCardId 会员卡ID + * @return 如果存在关联的会员记录则返回true,否则返回false + */ + Mono existsPurchasedRecord(Long memberCardId); + + /** + * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) + * @param memberCardId 会员卡ID + * @return 受影响的行数 + */ + Mono logicalDelete(Long memberCardId); + + /** + * 保存卡种信息(新增或更新) + * - 新增:entity.memberCardId 为 null 时,插入新记录 + * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 + * @param entity 卡种信息 + * @return 保存后的实体对象 + */ + Mono save(MemberCard entity); + + /** + * 批量查询上架的会员卡(用于小程序端展示) + * @param status 上架状态值 + * @return 上架的会员卡列表 + */ + Flux findActiveCards(Integer status); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java new file mode 100644 index 0000000..010556a --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java @@ -0,0 +1,91 @@ +package cn.novalon.gym.manage.gymmembercard.repository; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +public interface IMemberCardTransactionsRepository { + + /** + * 记录每一次变动 + * @param transactions 流水记录 + * @return 插入的流水记录 + */ + Mono insertTransaction(MemberCardTransactions transactions); + + /** + * 会员端"使用记录" + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable); + + /** + * 后台"使用记录查询" + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + Flux findWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable); + + /** + * 统计符合条件的流水总数 + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 流水记录数量 + */ + Mono countWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime); + + /** + * 按会员卡ID查询所有流水记录 + * @param memberCardId 会员卡ID + * @return 该卡的所有流水记录,按时间倒序 + */ + Flux findByMemberCardId(Long memberCardId); + + /** + * 数据统计 - 统计某卡种的总扣次数 + * @param memberCardId 会员卡ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 总扣次数 + */ + Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 数据统计 - 统计某时间段的续费总金额 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 续费总金额 + */ + Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 数据统计 - 统计某会员的购卡总金额 + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 购卡总金额 + */ + Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java new file mode 100644 index 0000000..9148112 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java @@ -0,0 +1,141 @@ +package cn.novalon.gym.manage.gymmembercard.repository.impl; + +import cn.novalon.gym.manage.gymmembercard.dao.MemberCardRecordDao; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardRecordEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; +import cn.novalon.gym.manage.gymmembercard.util.BeanConvertUtil; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Repository +public class MemberCardRecordRepositoryImpl implements IMemberCardRecordRepository { + private final MemberCardRecordDao memberCardRecordDao; + private final BeanConvertUtil beanConvertUtil; + private final R2dbcEntityTemplate r2dbcEntityTemplate; + + public MemberCardRecordRepositoryImpl(MemberCardRecordDao memberCardRecordDao, + BeanConvertUtil beanConvertUtil, + R2dbcEntityTemplate r2dbcEntityTemplate) { + this.memberCardRecordDao = memberCardRecordDao; + this.beanConvertUtil = beanConvertUtil; + this.r2dbcEntityTemplate = r2dbcEntityTemplate; + } + + /** + * 会员购卡/后台发卡 + * @param record 会员卡记录 + * @return 插入的记录 + */ + @Override + public Mono insertActiveRecord(MemberCardRecord record) { + MemberCardRecordEntity entity = BeanConvertUtil.toBean(record, MemberCardRecordEntity.class); + return memberCardRecordDao.insertActiveRecord( + entity.getMemberId(), + entity.getMemberCardId(), + entity.getExpireTime(), + entity.getRemainingTimes(), + entity.getRemainingAmount(), + entity.getSourceOrderId()) + .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class)); + } + + /** + * 扣次/扣费(含防超扣校验) + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + @Override + public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { + return memberCardRecordDao.deductUsage(recordId, deductTimes, deductAmount); + } + + /** + * 续费 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + @Override + public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { + return memberCardRecordDao.renewCard(recordId, addTimes, addAmount, newExpireTime); + } + + /** + * 状态变更 + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + @Override + public Mono updateStatus(Long recordId, MemberCardRecordStatus status) { + return memberCardRecordDao.updateStatus(recordId, status); + } + + /** + * 会员端"我的卡包" + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + @Override + public Flux findActiveCardsByMemberId(Long memberId) { + return memberCardRecordDao.findActiveCardsByMemberId(memberId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 前台/店长查会员卡 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + @Override + public Flux findByMemberId(Long memberId, Pageable pageable) { + return memberCardRecordDao.findByMemberId(memberId, pageable) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + @Override + public Mono validateCountCard(Long recordId, Integer requiredTimes) { + return memberCardRecordDao.validateCountCard(recordId, requiredTimes) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + @Override + public Mono validateStoredCard(Long recordId, Double requiredAmount) { + return memberCardRecordDao.validateStoredCard(recordId, requiredAmount) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + /** + * 到期扫描(分批处理,避免内存压力) + * @return 已过期的会员卡记录列表(最多500条) + */ + @Override + public Flux findExpiredCards() { + return memberCardRecordDao.findExpiredCards() + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java new file mode 100644 index 0000000..afc3f97 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRepositoryImpl.java @@ -0,0 +1,146 @@ +package cn.novalon.gym.manage.gymmembercard.repository.impl; + +import cn.novalon.gym.manage.gymmembercard.dao.MemberCardDao; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardEntity; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRepository; +import cn.novalon.gym.manage.gymmembercard.util.BeanConvertUtil; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Repository +public class MemberCardRepositoryImpl implements IMemberCardRepository { + private final MemberCardDao memberCardDao; + private final BeanConvertUtil beanConvertUtil; + private final R2dbcEntityTemplate r2dbcEntityTemplate; + //构造函数,初始化 + public MemberCardRepositoryImpl(MemberCardDao memberCardDao, BeanConvertUtil beanConvertUtil, R2dbcEntityTemplate r2dbcEntityTemplate) { + this.memberCardDao = memberCardDao; + this.beanConvertUtil = beanConvertUtil; + this.r2dbcEntityTemplate = r2dbcEntityTemplate; + } + + /** + * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) + * @param memberCardId 会员卡ID + * @return 会员卡完整信息,如果不存在或已删除则返回空 + */ + @Override + public Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { + return memberCardDao.findByMemberCardIdAndDeletedAtIsNull(memberCardId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); + } + + /** + * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) + * @param status 会员卡状态(上架/下架) + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + @Override + public Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable) { + return memberCardDao.findWithConditions(status, name, type, minPrice, maxPrice, pageable) + .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); + } + + /** + * 统计符合条件的会员卡总数(配合列表查询使用) + * @param status 会员卡状态 + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @return 符合条件的会员卡数量 + */ + @Override + public Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice) { + return memberCardDao.countWithConditions(status, name, type, minPrice, maxPrice); + } + + /** + * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) + * @param status 会员卡状态(通常传上架状态) + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + @Override + public Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable) { + return memberCardDao.findByMemberCardStatusAndDeletedAtIsNull(status, pageable) + .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); + } + + /** + * 检查会员卡是否已被购买(用于删除前的校验) + * @param memberCardId 会员卡ID + * @return 如果存在关联的会员记录则返回true,否则返回false + */ + @Override + public Mono existsPurchasedRecord(Long memberCardId) { + return memberCardDao.existsPurchasedRecord(memberCardId); + } + + /** + * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) + * @param memberCardId 会员卡ID + * @return 受影响的行数 + */ + @Override + public Mono logicalDelete(Long memberCardId) { + return memberCardDao.logicalDelete(memberCardId); + } + + /** + * 安全更新会员卡信息(不覆盖不允许修改的字段) + * @param memberCardId 会员卡ID + * @param updateData 需要更新的卡种信息 + * @return 受影响的行数 + */ + public Mono updateSafe(Long memberCardId, MemberCard updateData) { + MemberCardEntity memberCardEntity = beanConvertUtil.toBean(updateData, MemberCardEntity.class); + return memberCardDao.updateSafe( + memberCardId, + memberCardEntity.getMemberCardName(), + memberCardEntity.getMemberCardPrice(), + memberCardEntity.getMemberCardValidityDays(), + memberCardEntity.getMemberCardTotalTimes(), + memberCardEntity.getMemberCardAmount(), + memberCardEntity.getMemberCardStatus() + ); + } + + + /** + * 保存卡种信息(新增或更新) + * - 新增:entity.memberCardId 为 null 时,插入新记录 + * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 + * 建议:更新时优先使用 updateSafe 方法避免全字段覆盖 + * @param entity 卡种信息 + * @return 保存后的实体对象 + */ + @Override + public Mono save(MemberCard entity) { + MemberCardEntity cardEntity = beanConvertUtil.toBean(entity, MemberCardEntity.class); + return memberCardDao.save(cardEntity) + .map(savedEntity -> beanConvertUtil.toBean(savedEntity, MemberCard.class)); + } + + /** + * 批量查询上架的会员卡(用于小程序端展示) + * @param status 上架状态值 + * @return 上架的会员卡列表 + */ + @Override + public Flux findActiveCards(Integer status) { + return memberCardDao.findActiveCards(status) + .map(entity -> beanConvertUtil.toBean(entity, MemberCard.class)); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java new file mode 100644 index 0000000..0704716 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java @@ -0,0 +1,147 @@ +package cn.novalon.gym.manage.gymmembercard.repository.impl; + +import cn.novalon.gym.manage.gymmembercard.dao.MemberCardTransactionsDao; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.entity.MemberCardTransactionsEntity; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardTransactionsRepository; +import cn.novalon.gym.manage.gymmembercard.util.BeanConvertUtil; +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Repository +public class MemberCardTransactionsRepositoryImpl implements IMemberCardTransactionsRepository { + private final MemberCardTransactionsDao memberCardTransactionsDao; + private final BeanConvertUtil beanConvertUtil; + private final R2dbcEntityTemplate r2dbcEntityTemplate; + + public MemberCardTransactionsRepositoryImpl(MemberCardTransactionsDao memberCardTransactionsDao, BeanConvertUtil beanConvertUtil, R2dbcEntityTemplate r2dbcEntityTemplate) { + this.memberCardTransactionsDao = memberCardTransactionsDao; + this.beanConvertUtil = beanConvertUtil; + this.r2dbcEntityTemplate = r2dbcEntityTemplate; + } + + /** + * 记录每一次变动 + * @param transactions 流水记录 + * @return 插入的流水记录 + */ + @Override + public Mono insertTransaction(MemberCardTransactions transactions) { + MemberCardTransactionsEntity entity = beanConvertUtil.toBean(transactions, MemberCardTransactionsEntity.class); + return memberCardTransactionsDao.insertTransaction( + entity.getMemberCardId(), + entity.getMemberId(), + entity.getOperationType(), + entity.getChangeAmount(), + entity.getChangeBalance(), + entity.getAfterRemainingCount(), + entity.getAfterRemainingBalance(), + entity.getRelatedBizType(), + entity.getRemark()) + .map(e -> beanConvertUtil.toBean(e, MemberCardTransactions.class)); + } + + /** + * 会员端"使用记录" + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + @Override + public Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable) { + return memberCardTransactionsDao.findByMemberIdAndTimeRange(memberId, startTime, endTime, pageable) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); + } + + /** + * 后台"使用记录查询" + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + @Override + public Flux findWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable) { + return memberCardTransactionsDao.findWithConditions(memberId, memberCardId, operationType, + startTime, endTime, pageable) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); + } + + /** + * 统计符合条件的流水总数 + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 流水记录数量 + */ + @Override + public Mono countWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsDao.countWithConditions(memberId, memberCardId, + operationType, startTime, endTime); + } + + /** + * 按会员卡ID查询所有流水记录 + * @param memberCardId 会员卡ID + * @return 该卡的所有流水记录,按时间倒序 + */ + @Override + public Flux findByMemberCardId(Long memberCardId) { + return memberCardTransactionsDao.findByMemberCardId(memberCardId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); + } + + /** + * 数据统计 - 统计某卡种的总扣次数 + * @param memberCardId 会员卡ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 总扣次数 + */ + @Override + public Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsDao.sumDeductCountByCardId(memberCardId, startTime, endTime); + } + + /** + * 数据统计 - 统计某时间段的续费总金额 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 续费总金额 + */ + @Override + public Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsDao.sumRenewAmountByTimeRange(startTime, endTime); + } + + /** + * 数据统计 - 统计某会员的购卡总金额 + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 购卡总金额 + */ + @Override + public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsDao.sumPurchaseAmountByMemberId(memberId, startTime, endTime); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java new file mode 100644 index 0000000..5773df8 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java @@ -0,0 +1,83 @@ +package cn.novalon.gym.manage.gymmembercard.sevice; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +public interface IMemberCardRecordService { + + /** + * 会员购卡/后台发卡 + * @param record 会员卡记录 + * @return 插入的记录 + */ + Mono insertActiveRecord(MemberCardRecord record); + + /** + * 扣次/扣费(含防超扣校验) + * @param recordId 会员卡记录ID + * @param deductTimes 扣除次数 + * @param deductAmount 扣除金额 + * @return 受影响的行数(0表示余额不足,扣费失败) + */ + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + + /** + * 续费 + * @param recordId 会员卡记录ID + * @param addTimes 增加次数 + * @param addAmount 增加金额 + * @param newExpireTime 新的到期时间 + * @return 受影响的行数 + */ + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); + + /** + * 状态变更 + * @param recordId 会员卡记录ID + * @param status 新状态 + * @return 受影响的行数 + */ + Mono updateStatus(Long recordId, MemberCardRecordStatus status); + + /** + * 会员端"我的卡包" + * @param memberId 会员ID + * @return 有效会员卡列表 + */ + Flux findActiveCardsByMemberId(Long memberId); + + /** + * 前台/店长查会员卡 + * @param memberId 会员ID + * @param pageable 分页参数 + * @return 会员卡列表 + */ + Flux findByMemberId(Long memberId, Pageable pageable); + + /** + * 验证次卡是否可用(仅检验次数和过期时间) + * @param recordId 会员卡记录ID + * @param requiredTimes 需要的次数 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateCountCard(Long recordId, Integer requiredTimes); + + /** + * 验证储值卡是否可用(仅检验余额和过期时间) + * @param recordId 会员卡记录ID + * @param requiredAmount 需要的金额 + * @return 符合条件的记录,空表示不可用 + */ + Mono validateStoredCard(Long recordId, Double requiredAmount); + + /** + * 到期扫描(分批处理,避免内存压力) + * @return 已过期的会员卡记录列表(最多500条) + */ + Flux findExpiredCards(); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java new file mode 100644 index 0000000..b1b744d --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java @@ -0,0 +1,78 @@ +package cn.novalon.gym.manage.gymmembercard.sevice; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface IMemberCardService { + /** + * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) + * @param memberCardId 会员卡ID + * @return 会员卡完整信息,如果不存在或已删除则返回空 + */ + Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); + + /** + * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) + * @param status 会员卡状态(上架/下架) + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable); + + /** + * 统计符合条件的会员卡总数(配合列表查询使用) + * @param status 会员卡状态 + * @param name 会员卡名称(模糊查询) + * @param type 会员卡类型 + * @param minPrice 最低价格 + * @param maxPrice 最高价格 + * @return 符合条件的会员卡数量 + */ + Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice); + + /** + * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) + * @param status 会员卡状态(通常传上架状态) + * @param pageable 分页和排序参数 + * @return 符合条件的会员卡列表 + */ + Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); + + /** + * 检查会员卡是否已被购买(用于删除前的校验) + * @param memberCardId 会员卡ID + * @return 如果存在关联的会员记录则返回true,否则返回false + */ + Mono existsPurchasedRecord(Long memberCardId); + + /** + * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) + * @param memberCardId 会员卡ID + * @return 受影响的行数 + */ + Mono logicalDelete(Long memberCardId); + + /** + * 保存卡种信息(新增或更新) + * - 新增:entity.memberCardId 为 null 时,插入新记录 + * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 + * @param entity 卡种信息 + * @return 保存后的实体对象 + */ + Mono save(MemberCard entity); + + /** + * 批量查询上架的会员卡(用于小程序端展示) + * @param status 上架状态值 + * @return 上架的会员卡列表 + */ + Flux findActiveCards(Integer status); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java new file mode 100644 index 0000000..526bcb8 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java @@ -0,0 +1,91 @@ +package cn.novalon.gym.manage.gymmembercard.sevice; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +public interface IMemberCardTransactionsService { + + /** + * 记录每一次变动 + * @param transactions 流水记录 + * @return 插入的流水记录 + */ + Mono insertTransaction(MemberCardTransactions transactions); + + /** + * 会员端"使用记录" + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable); + + /** + * 后台"使用记录查询" + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param pageable 分页参数 + * @return 流水记录列表 + */ + Flux findWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable); + + /** + * 统计符合条件的流水总数 + * @param memberId 会员ID + * @param memberCardId 会员卡ID + * @param operationType 操作类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 流水记录数量 + */ + Mono countWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime); + + /** + * 按会员卡ID查询所有流水记录 + * @param memberCardId 会员卡ID + * @return 该卡的所有流水记录,按时间倒序 + */ + Flux findByMemberCardId(Long memberCardId); + + /** + * 数据统计 - 统计某卡种的总扣次数 + * @param memberCardId 会员卡ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 总扣次数 + */ + Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 数据统计 - 统计某时间段的续费总金额 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 续费总金额 + */ + Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 数据统计 - 统计某会员的购卡总金额 + * @param memberId 会员ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 购卡总金额 + */ + Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java new file mode 100644 index 0000000..95b8ebe --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java @@ -0,0 +1,67 @@ +package cn.novalon.gym.manage.gymmembercard.sevice.impl; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Service +public class MemberCardRecordServiceImpl implements IMemberCardRecordService { + private final IMemberCardRecordRepository memberCardRecordRepository; + + public MemberCardRecordServiceImpl(IMemberCardRecordRepository memberCardRecordRepository) { + this.memberCardRecordRepository = memberCardRecordRepository; + } + + @Override + public Mono insertActiveRecord(MemberCardRecord record) { + return memberCardRecordRepository.insertActiveRecord(record); + } + + @Override + public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { + return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount); + } + + @Override + public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { + return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime); + } + + @Override + public Mono updateStatus(Long recordId, MemberCardRecordStatus status) { + return memberCardRecordRepository.updateStatus(recordId, status); + } + + @Override + public Flux findActiveCardsByMemberId(Long memberId) { + return memberCardRecordRepository.findActiveCardsByMemberId(memberId); + } + + @Override + public Flux findByMemberId(Long memberId, Pageable pageable) { + return memberCardRecordRepository.findByMemberId(memberId, pageable); + } + + @Override + public Mono validateCountCard(Long recordId, Integer requiredTimes) { + return memberCardRecordRepository.validateCountCard(recordId, requiredTimes); + } + + @Override + public Mono validateStoredCard(Long recordId, Double requiredAmount) { + return memberCardRecordRepository.validateStoredCard(recordId, requiredAmount); + } + + @Override + public Flux findExpiredCards() { + return memberCardRecordRepository.findExpiredCards(); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java new file mode 100644 index 0000000..0ec900c --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java @@ -0,0 +1,65 @@ +package cn.novalon.gym.manage.gymmembercard.sevice.impl; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRepository; +import cn.novalon.gym.manage.gymmembercard.repository.impl.MemberCardRepositoryImpl; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Service +public class MemberCardServiceImpl implements IMemberCardService { + private final IMemberCardRepository memberCardRepository; + + public MemberCardServiceImpl(IMemberCardRepository memberCardRepository) { + this.memberCardRepository = memberCardRepository; + } + + @Override + public Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { + return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId); + } + + @Override + public Flux findWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice, Pageable pageable) { + return memberCardRepository.findWithConditions(status, name, type, minPrice, maxPrice, pageable); + } + + @Override + public Mono countWithConditions(Integer status, String name, String type, + Double minPrice, Double maxPrice) { + return memberCardRepository.countWithConditions(status, name, type, minPrice, maxPrice); + } + + @Override + public Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable) { + return memberCardRepository.findByMemberCardStatusAndDeletedAtIsNull(status, pageable); + } + + @Override + public Mono existsPurchasedRecord(Long memberCardId) { + return memberCardRepository.existsPurchasedRecord(memberCardId); + } + + @Override + public Mono logicalDelete(Long memberCardId) { + return memberCardRepository.logicalDelete(memberCardId); + } + + public Mono updateSafe(Long memberCardId, MemberCard updateData) { + return ((MemberCardRepositoryImpl) memberCardRepository).updateSafe(memberCardId, updateData); + } + + @Override + public Mono save(MemberCard entity) { + return memberCardRepository.save(entity); + } + + @Override + public Flux findActiveCards(Integer status) { + return memberCardRepository.findActiveCards(status); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java new file mode 100644 index 0000000..eddccea --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java @@ -0,0 +1,69 @@ +package cn.novalon.gym.manage.gymmembercard.sevice.impl; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardTransactionsRepository; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Service +public class MemberCardTransactionsServiceImpl implements IMemberCardTransactionsService { + private final IMemberCardTransactionsRepository memberCardTransactionsRepository; + + public MemberCardTransactionsServiceImpl(IMemberCardTransactionsRepository memberCardTransactionsRepository) { + this.memberCardTransactionsRepository = memberCardTransactionsRepository; + } + + @Override + public Mono insertTransaction(MemberCardTransactions transactions) { + return memberCardTransactionsRepository.insertTransaction(transactions); + } + + @Override + public Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable) { + return memberCardTransactionsRepository.findByMemberIdAndTimeRange(memberId, startTime, endTime, pageable); + } + + @Override + public Flux findWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable) { + return memberCardTransactionsRepository.findWithConditions(memberId, memberCardId, operationType, + startTime, endTime, pageable); + } + + @Override + public Mono countWithConditions(Long memberId, Long memberCardId, + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsRepository.countWithConditions(memberId, memberCardId, + operationType, startTime, endTime); + } + + @Override + public Flux findByMemberCardId(Long memberCardId) { + return memberCardTransactionsRepository.findByMemberCardId(memberCardId); + } + + @Override + public Mono sumDeductCountByCardId(Long memberCardId, LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsRepository.sumDeductCountByCardId(memberCardId, startTime, endTime); + } + + @Override + public Mono sumRenewAmountByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsRepository.sumRenewAmountByTimeRange(startTime, endTime); + } + + @Override + public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) { + return memberCardTransactionsRepository.sumPurchaseAmountByMemberId(memberId, startTime, endTime); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java new file mode 100644 index 0000000..f9f7e00 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/util/BeanConvertUtil.java @@ -0,0 +1,49 @@ +package cn.novalon.gym.manage.gymmembercard.util; +/* + *@Author:shizhounian + *@Date:2026/5/11-05 21:19:04 + */ + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Entity、Domain、VO、DTO转换工具类 + */ +@Component +public class BeanConvertUtil { + + /** + * 单个对象泛型转换 + * @param source 源对象 + * @param targetClass 目标类Class + * @return 转换后的目标对象 + */ + public static T toBean(S source, Class targetClass) { + if (source == null) { + return null; + } + return BeanUtil.copyProperties(source, targetClass); + } + + /** + * 集合批量泛型转换 + * @param sourceList 源对象集合 + * @param targetClass 目标类Class + * @return 转换后的目标对象集合 + */ + public static List toBeanList(List sourceList, Class targetClass) { + if (sourceList == null || sourceList.isEmpty()) { + return List.of(); + } + + List targetList = new ArrayList<>(); + for (S source : sourceList) { + targetList.add(toBean(source, targetClass)); + } + return targetList; + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 45fcc60..bc92fbd 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -133,6 +133,12 @@ org.springdoc springdoc-openapi-starter-webflux-ui + + cn.novalon.gym.manage + gym-member-card + 0.0.1-SNAPSHOT + compile + diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index f74c2f7..0278f53 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -18,7 +18,8 @@ import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) @EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", - "cn.novalon.gym.manage.sys.audit.repository" }) + "cn.novalon.gym.manage.sys.audit.repository" , + "cn.novalon.gym.manage.gymmembercard.dao"}) public class ManageApplication { private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); 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 c869da3..bf571d8 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 @@ -1,5 +1,8 @@ package cn.novalon.gym.manage.app.config; +import cn.novalon.gym.manage.gymmembercard.handler.MemberCardHandler; +import cn.novalon.gym.manage.gymmembercard.handler.MemberCardRecordHandler; +import cn.novalon.gym.manage.gymmembercard.handler.MemberCardTransactionHandler; import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; @@ -51,7 +54,10 @@ public class SystemRouter { SysUserMessageHandler messageHandler, SysFileHandler fileHandler, SysPermissionHandler permissionHandler, - PasswordDiagnosticHandler passwordDiagnosticHandler) { + PasswordDiagnosticHandler passwordDiagnosticHandler, + MemberCardHandler memberCardHandler, + MemberCardRecordHandler memberCardRecordHandler, + MemberCardTransactionHandler memberCardTransactionHandler) { return route() // ========== 诊断路由 ========== @@ -192,7 +198,53 @@ public class SystemRouter { .POST("/api/permissions", permissionHandler::createPermission) .PUT("/api/permissions/{id}", permissionHandler::updatePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission) - + + // ========== 会员卡管理路由 ========== + // 会员卡类型 + // 1. 获取所有会员卡类型 + .GET("/api/memberCard/active", memberCardHandler::getActiveCards) + // 2. 获取会员卡详情 + .GET("/api/memberCard/{memberCardId}", memberCardHandler::getMemberCardById) + // 3. 条件查询会员卡列表 + .GET("/api/memberCard", memberCardHandler::getMemberCardList) + // 4. 新增/更新会员卡 + .POST("/api/memberCard", memberCardHandler::saveMemberCard) + // 5. 逻辑删除会员卡 + .DELETE("/api/memberCard/{memberCardId}", memberCardHandler::deleteMemberCard) + // 会员卡持卡 + // 1. 会员购卡/发卡 + .POST("/api/memberCardRecord", memberCardRecordHandler::insertActiveRecord) + // 2. 会员端“我的卡包” - 按会员ID获取有效卡 + .GET("/api/memberCardRecord/member/{memberId}/active", memberCardRecordHandler::getMyCards) + // 3. 管理端按会员ID分页查所有卡记录 + .GET("/api/memberCardRecord/member/{memberId}", memberCardRecordHandler::getMemberCardRecords) + // 4. 到期扫描 + .GET("/api/memberCardRecord/expired", memberCardRecordHandler::getExpiredCards) + // 5. 扣次/扣费 + .POST("/api/memberCardRecord/{id}/deduct", memberCardRecordHandler::deductUsage) + // 6. 续费 + .POST("/api/memberCardRecord/{id}/renew", memberCardRecordHandler::renewCard) + // 7. 状态变更(过期/退款) + .PUT("/api/memberCardRecord/{id}/status", memberCardRecordHandler::updateStatus) + // 8. 验证次卡 + .GET("/api/memberCardRecord/{id}/validate/count", memberCardRecordHandler::validateCountCard) + // 9. 验证储值卡 + .GET("/api/memberCardRecord/{id}/validate/stored", memberCardRecordHandler::validateStoredCard) + // 会员卡交易 + // 1. 插入流水记录 + .POST("/api/transactions", memberCardTransactionHandler::insertTransaction) + // 2. 后台条件分页查询流水(带多个查询参数) + .GET("/api/transactions", memberCardTransactionHandler::getTransactionsWithConditions) + // 3. 按会员ID查询使用记录(分页 + 时间范围) + .GET("/api/transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions) + // 4. 按卡ID查询所有流水记录 + .GET("/api/transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId) + // 5. 统计某卡种总扣次数 + .GET("/api/transactions/statistics/deduct/card/{cardId}", memberCardTransactionHandler::getDeductCountByCardId) + // 6. 统计某时间段续费总金额 + .GET("/api/transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange) + // 7. 统计某会员购卡总金额 + .GET("/api/transactions/statistics/purchase/member/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember) .build(); } } diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index df2e4c2..3f820b0 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -36,6 +36,7 @@ manage-sys manage-gateway + gym-member-card manage-app manage-common manage-db diff --git a/gym-manage-web/pnpm-lock.yaml b/gym-manage-web/pnpm-lock.yaml index 63bd44e..cc01eea 100644 --- a/gym-manage-web/pnpm-lock.yaml +++ b/gym-manage-web/pnpm-lock.yaml @@ -59,7 +59,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) '@vitejs/plugin-vue': specifier: ^6.0.3 - version: 6.0.5(vite@7.3.1(@types/node@20.19.37))(vue@3.5.30(typescript@5.9.3)) + version: 6.0.5(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2))(vue@3.5.30(typescript@5.9.3)) '@vitest/coverage-v8': specifier: ^4.1.1 version: 4.1.2(vitest@4.1.0) @@ -81,15 +81,18 @@ importers: prettier: specifier: ^3.1.1 version: 3.8.1 + terser: + specifier: ^5.46.1 + version: 5.46.2 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@20.19.37) + version: 7.3.1(@types/node@20.19.37)(terser@5.46.2) vitest: specifier: ^4.0.16 - version: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + version: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) vue-tsc: specifier: ^3.2.2 version: 3.2.5(typescript@5.9.3) @@ -390,10 +393,16 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -464,66 +473,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -858,6 +880,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -889,6 +914,9 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1631,6 +1659,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -1672,6 +1707,11 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + terser@5.46.2: + resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==} + engines: {node: '>=10'} + hasBin: true + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2138,8 +2178,18 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -2366,10 +2416,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@20.19.37))(vue@3.5.30(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2))(vue@3.5.30(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.46.2) vue: 3.5.30(typescript@5.9.3) '@vitest/coverage-v8@4.1.2(vitest@4.1.0)': @@ -2384,7 +2434,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) '@vitest/expect@4.1.0': dependencies: @@ -2395,13 +2445,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@20.19.37))': + '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.46.2) '@vitest/pretty-format@4.1.0': dependencies: @@ -2434,7 +2484,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) '@vitest/utils@4.1.0': dependencies: @@ -2642,6 +2692,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-from@1.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2668,6 +2720,8 @@ snapshots: commander@10.0.1: {} + commander@2.20.3: {} + concat-map@0.0.1: {} config-chain@1.1.13: @@ -3456,6 +3510,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + speakingurl@14.0.1: {} stackback@0.0.2: {} @@ -3494,6 +3555,13 @@ snapshots: symbol-tree@3.2.4: {} + terser@5.46.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -3547,7 +3615,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@7.3.1(@types/node@20.19.37): + vite@7.3.1(@types/node@20.19.37)(terser@5.46.2): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.3) @@ -3558,11 +3626,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.37 fsevents: 2.3.3 + terser: 5.46.2 - vitest@4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)): + vitest@4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@20.19.37)) + '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@20.19.37)(terser@5.46.2)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -3579,7 +3648,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.46.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.37 -- 2.52.0 From 2325c66c37ea74bde4c0499eb7b69946cac1baec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E8=88=9F=E5=B9=B4?= <3147056268@qq.com> Date: Thu, 21 May 2026 14:53:53 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E5=8D=A1=E6=A8=A1=E5=9D=97=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gym/manage/gymmembercard/GymMemberCardApplication.java | 1 - 1 file changed, 1 deletion(-) diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java index c231709..c3e170d 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java @@ -2,7 +2,6 @@ package cn.novalon.gym.manage.gymmembercard; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; @SpringBootApplication -- 2.52.0 From cd44caee57dbca0d5444fc17985a50066a8f7780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E8=88=9F=E5=B9=B4?= <3147056268@qq.com> Date: Sun, 24 May 2026 00:57:22 +0800 Subject: [PATCH 08/16] =?UTF-8?q?=E4=BC=9A=E5=91=98=E5=8D=A1=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-member-card/pom.xml | 28 ++ .../GymMemberCardApplication.java | 4 +- .../dao/MemberCardRecordDao.java | 69 +---- .../dao/MemberCardTransactionsDao.java | 18 +- .../dao/RefundApplicationDao.java | 64 +++++ .../gymmembercard/domain/MemberCard.java | 72 +++++ .../domain/MemberCardRecord.java | 107 +++++-- .../domain/MemberCardTransactions.java | 100 +++++++ .../domain/RefundApplication.java | 127 +++++++++ .../entity/MemberCardEntity.java | 73 ++++- .../entity/MemberCardRecordEntity.java | 73 ++++- .../entity/MemberCardTransactionsEntity.java | 136 +++++++-- .../entity/RefundApplicationEntity.java | 127 +++++++++ .../gymmembercard/enums/MemberCardEvent.java | 34 +++ .../gymmembercard/enums/MemberCardType.java | 25 ++ .../handler/DistributedLockService.java | 50 ++++ .../handler/ExpirationReminderService.java | 151 ++++++++++ .../handler/MemberCardHandler.java | 163 ++++++----- .../handler/MemberCardRecordHandler.java | 195 +++++-------- .../handler/MemberCardScheduledHandler.java | 99 +++++++ .../handler/MemberCardStateMachine.java | 85 ++++++ .../handler/RefundSagaHandler.java | 126 +++++++++ .../IMemberCardRecordRepository.java | 93 ++---- .../IMemberCardTransactionsRepository.java | 21 ++ .../IRefundApplicationRepository.java | 81 ++++++ .../impl/MemberCardRecordRepositoryImpl.java | 132 ++++----- .../MemberCardTransactionsRepositoryImpl.java | 23 ++ .../impl/RefundApplicationRepositoryImpl.java | 125 ++++++++ .../sevice/IMemberCardRecordService.java | 88 ++---- .../sevice/IMemberCardService.java | 82 ++---- .../IMemberCardTransactionsService.java | 31 +- .../sevice/IRefundApplicationService.java | 33 +++ .../impl/MemberCardRecordServiceImpl.java | 5 + .../sevice/impl/MemberCardServiceImpl.java | 266 +++++++++++++++++- .../MemberCardTransactionsServiceImpl.java | 20 ++ .../impl/RefundApplicationServiceImpl.java | 85 ++++++ .../src/main/resources/application.properties | 1 - .../gym-member-card/src/main/resources/sql | 132 +++++++++ .../gym/manage/app/config/SystemRouter.java | 103 +++---- 39 files changed, 2570 insertions(+), 677 deletions(-) create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java create mode 100644 gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java delete mode 100644 gym-manage-api/gym-member-card/src/main/resources/application.properties create mode 100644 gym-manage-api/gym-member-card/src/main/resources/sql diff --git a/gym-manage-api/gym-member-card/pom.xml b/gym-manage-api/gym-member-card/pom.xml index e5940b4..1801b55 100644 --- a/gym-manage-api/gym-member-card/pom.xml +++ b/gym-manage-api/gym-member-card/pom.xml @@ -61,12 +61,40 @@ spring-context + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-starter-json + + + org.springframework.boot spring-boot-starter-test test + + diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java index c3e170d..00c02f6 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/GymMemberCardApplication.java @@ -3,9 +3,11 @@ package cn.novalon.gym.manage.gymmembercard; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableR2dbcRepositories(basePackages = "cn.novalon.gym.manage.db.dao") +@EnableR2dbcRepositories(basePackages = "cn.novalon.gym.manage.gymmembercard.dao") +@EnableScheduling public class GymMemberCardApplication { public static void main(String[] args) { diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java index e9c6bb1..728d82c 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardRecordDao.java @@ -16,17 +16,6 @@ import java.time.LocalDateTime; @Repository public interface MemberCardRecordDao extends R2dbcRepository { - /** - * 会员购卡/后台发卡 - * 支付成功后插入一条记录,状态为ACTIVE,设置有效期和初始剩余次数/余额 - * @param memberId 会员ID - * @param memberCardId 会员卡类型ID - * @param expireTime 到期时间 - * @param remainingTimes 剩余次数 - * @param remainingAmount 剩余余额 - * @param sourceOrderId 来源订单ID - * @return 插入的记录 - */ @Modifying @Query("INSERT INTO member_card_record (member_id, member_card_id, status, expire_time, remaining_times, remaining_amount, source_order_id, purchase_time, created_at, updated_at) " + "VALUES (:memberId, :memberCardId, 'ACTIVE', :expireTime, :remainingTimes, :remainingAmount, :sourceOrderId, NOW(), NOW(), NOW()) " + @@ -38,14 +27,6 @@ public interface MemberCardRecordDao extends R2dbcRepository updateStatus(@Param("recordId") Long recordId, @Param("status") MemberCardRecordStatus status); - /** - * 会员端"我的卡包" - * 根据会员ID查询所有卡,过滤状态为ACTIVE的,展示剩余次数/天数/余额 - * @param memberId 会员ID - * @return 有效会员卡列表 - */ @Query("SELECT * FROM member_card_record WHERE member_id = :memberId AND status = 'ACTIVE' AND deleted_at IS NULL ORDER BY expire_time ASC") Flux findActiveCardsByMemberId(@Param("memberId") Long memberId); - /** - * 前台/店长查会员卡 - * 输入会员手机号或姓名,查出该会员持有的所有卡的信息 - * @param memberId 会员ID - * @param pageable 分页参数 - * @return 会员卡列表 - */ @Query("SELECT mcr.* FROM member_card_record mcr " + "INNER JOIN member m ON mcr.member_id = m.member_id " + "WHERE mcr.member_id = :memberId AND mcr.deleted_at IS NULL " + "ORDER BY mcr.purchase_time DESC LIMIT :#{#pageable.pageSize} OFFSET :#{#pageable.offset}") Flux findByMemberId(@Param("memberId") Long memberId, Pageable pageable); - /** - * 验证次卡是否可用(仅检验次数和过期时间) - * @param recordId 会员卡记录ID - * @param requiredTimes 需要的次数 - * @return 符合条件的记录,空表示不可用 - */ @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + "AND status = 'ACTIVE' AND deleted_at IS NULL " + "AND expire_time > NOW() " + @@ -125,12 +71,6 @@ public interface MemberCardRecordDao extends R2dbcRepository validateCountCard(@Param("recordId") Long recordId, @Param("requiredTimes") Integer requiredTimes); - /** - * 验证储值卡是否可用(仅检验余额和过期时间) - * @param recordId 会员卡记录ID - * @param requiredAmount 需要的金额 - * @return 符合条件的记录,空表示不可用 - */ @Query("SELECT * FROM member_card_record WHERE member_card_record_id = :recordId " + "AND status = 'ACTIVE' AND deleted_at IS NULL " + "AND expire_time > NOW() " + @@ -138,11 +78,10 @@ public interface MemberCardRecordDao extends R2dbcRepository validateStoredCard(@Param("recordId") Long recordId, @Param("requiredAmount") Double requiredAmount); - /** - * 到期扫描(分批处理,避免内存压力) - * 定时任务:查询 status=ACTIVE 且 expire_time < 当前时间 的记录,用于批量过期处理 - * @return 已过期的会员卡记录列表(最多500条) - */ @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND expire_time < NOW() AND deleted_at IS NULL LIMIT 500") Flux findExpiredCards(); + + @Query("SELECT * FROM member_card_record WHERE status = 'ACTIVE' AND deleted_at IS NULL") + Flux findActiveRecords(); + } \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java index 87836bb..bb3473e 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/MemberCardTransactionsDao.java @@ -153,7 +153,21 @@ public interface MemberCardTransactionsDao extends R2dbcRepository sumPurchaseAmountByMemberId(@Param("memberId") Long memberId, - @Param("startTime") LocalDateTime startTime, + Mono sumPurchaseAmountByMemberId(@Param("memberId") Long memberId, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); + /** + * 按会员ID查询所有流水记录 + * @param memberId 会员ID + * @return 该会员的所有流水记录,按时间倒序 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_id = :memberId ORDER BY created_at DESC") + Flux findByMemberId(Long memberId); + /** + * 按会员卡记录ID查询所有流水记录 + * @param recordId 会员卡记录ID + * @return 该会员卡的所有流水记录,按时间倒序 + */ + @Query("SELECT * FROM member_card_transactions WHERE member_card_record_id = :recordId ORDER BY created_at DESC") + Flux findByRecordId(Long recordId); + } \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java new file mode 100644 index 0000000..3dca6d4 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/dao/RefundApplicationDao.java @@ -0,0 +1,64 @@ +package cn.novalon.gym.manage.gymmembercard.dao; + +import cn.novalon.gym.manage.gymmembercard.entity.RefundApplicationEntity; +import org.springframework.data.r2dbc.repository.Query; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 退款申请数据访问对象 + * + * @author shizhounian + * @date 2026-05-23 + */ +public interface RefundApplicationDao extends R2dbcRepository { + + /** + * 根据会员卡记录ID查询退款申请 + * + * @param recordId 会员卡记录ID + * @return 退款申请实体 + */ + @Query("SELECT * FROM refund_application WHERE record_id = :recordId AND deleted_at IS NULL LIMIT 1") + Mono findByRecordId(Long recordId); + + /** + * 根据会员ID查询退款申请列表 + * + * @param memberId 会员ID + * @return 退款申请列表 + */ + @Query("SELECT * FROM refund_application WHERE member_id = :memberId AND deleted_at IS NULL ORDER BY created_at DESC") + Flux findByMemberId(Long memberId); + + /** + * 根据状态查询退款申请列表 + * + * @param status 状态 + * @return 退款申请列表 + */ + @Query("SELECT * FROM refund_application WHERE status = :status AND deleted_at IS NULL ORDER BY created_at DESC") + Flux findByStatus(String status); + + /** + * 审核退款申请(更新状态、审核人、审核时间、备注) + * + * @param id 退款申请ID + * @param status 审核状态 + * @param auditorId 审核人ID + * @param auditRemark 审核备注 + * @return 受影响的行数 + */ + @Query("UPDATE refund_application SET status = :status, auditor_id = :auditorId, audit_time = NOW(), audit_remark = :auditRemark, updated_at = NOW() WHERE id = :id") + Mono approve(Long id, String status, Long auditorId, String auditRemark); + + /** + * 逻辑删除退款申请 + * + * @param id 退款申请ID + * @return 受影响的行数 + */ + @Query("UPDATE refund_application SET deleted_at = NOW() WHERE id = :id") + Mono logicalDelete(Long id); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java index c5cf0e6..6a79ba5 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCard.java @@ -46,4 +46,76 @@ public class MemberCard extends BaseDomain { //会员卡创建时间 @Schema(description = "会员卡创建时间",example = "2026-05-10 05:22:47") private LocalDateTime memberCardCreateTime; + + public Long getMemberCardId() { + return memberCardId; + } + + public void setMemberCardId(Long memberCardId) { + this.memberCardId = memberCardId; + } + + public String getMemberCardName() { + return memberCardName; + } + + public void setMemberCardName(String memberCardName) { + this.memberCardName = memberCardName; + } + + public String getMemberCardType() { + return memberCardType; + } + + public void setMemberCardType(String memberCardType) { + this.memberCardType = memberCardType; + } + + public Double getMemberCardPrice() { + return memberCardPrice; + } + + public void setMemberCardPrice(Double memberCardPrice) { + this.memberCardPrice = memberCardPrice; + } + + public Integer getMemberCardValidityDays() { + return memberCardValidityDays; + } + + public void setMemberCardValidityDays(Integer memberCardValidityDays) { + this.memberCardValidityDays = memberCardValidityDays; + } + + public Integer getMemberCardTotalTimes() { + return memberCardTotalTimes; + } + + public void setMemberCardTotalTimes(Integer memberCardTotalTimes) { + this.memberCardTotalTimes = memberCardTotalTimes; + } + + public Double getMemberCardAmount() { + return memberCardAmount; + } + + public void setMemberCardAmount(Double memberCardAmount) { + this.memberCardAmount = memberCardAmount; + } + + public Integer getMemberCardStatus() { + return memberCardStatus; + } + + public void setMemberCardStatus(Integer memberCardStatus) { + this.memberCardStatus = memberCardStatus; + } + + public LocalDateTime getMemberCardCreateTime() { + return memberCardCreateTime; + } + + public void setMemberCardCreateTime(LocalDateTime memberCardCreateTime) { + this.memberCardCreateTime = memberCardCreateTime; + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java index 7e04215..a837546 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardRecord.java @@ -6,45 +6,104 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; -/* - *@Author:shizhounian - *@Date:2026/5/10-05 22:47:31 - */ -@Schema(description = "会员卡记录",example = "member_card_record") +@Schema(description = "会员卡记录") public class MemberCardRecord extends BaseDomain { - //会员持有卡id - @Schema(description = "会员持有卡Id",example = "1") + @Schema(description = "会员持有卡ID", example = "1") private Long memberCardRecordId; - //会员Id - @Schema(description = "会员Id",example = "1") + @Schema(description = "会员ID", example = "1001") private Long memberId; - //关联会员卡Id - @Schema(description = "关联会员卡Id",example = "1") + @Schema(description = "关联会员卡ID", example = "1") private Long memberCardId; - //状态:ACTIVE(有效) / USED_UP(用完) / EXPIRED(过期) / REFUNDED(已退款) - @Schema(description = "状态",example = "ACTIVE") + @Schema(description = "状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款", example = "ACTIVE") private MemberCardRecordStatus status; - //剩余次数 - @Schema(description = "剩余次数",example = "1") + @Schema(description = "剩余次数", example = "10") private Integer remainingTimes; - //剩余余额 - @Schema(description = "剩余余额",example = "1") + @Schema(description = "剩余余额", example = "500.0") private Double remainingAmount; - //到期时间 - @Schema(description = "到期时间",example = "2026-05-10 05:22:47") + @Schema(description = "到期时间", example = "2026-06-23 10:00:00") private LocalDateTime expireTime; - //购买订单Id - @Schema(description = "购买订单Id",example = "1") + @Schema(description = "购买订单ID", example = "10001") private Long sourceOrderId; - //购买时间 - @Schema(description = "购买时间",example = "2026-05-10 05:22:47") + @Schema(description = "购买时间", example = "2026-05-23 10:00:00") private LocalDateTime purchaseTime; -} + + public Long getMemberCardRecordId() { + return memberCardRecordId; + } + + public void setMemberCardRecordId(Long memberCardRecordId) { + this.memberCardRecordId = memberCardRecordId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public Long getMemberCardId() { + return memberCardId; + } + + public void setMemberCardId(Long memberCardId) { + this.memberCardId = memberCardId; + } + + public MemberCardRecordStatus getStatus() { + return status; + } + + public void setStatus(MemberCardRecordStatus status) { + this.status = status; + } + + public Integer getRemainingTimes() { + return remainingTimes; + } + + public void setRemainingTimes(Integer remainingTimes) { + this.remainingTimes = remainingTimes; + } + + public Double getRemainingAmount() { + return remainingAmount; + } + + public void setRemainingAmount(Double remainingAmount) { + this.remainingAmount = remainingAmount; + } + + public LocalDateTime getExpireTime() { + return expireTime; + } + + public void setExpireTime(LocalDateTime expireTime) { + this.expireTime = expireTime; + } + + public Long getSourceOrderId() { + return sourceOrderId; + } + + public void setSourceOrderId(Long sourceOrderId) { + this.sourceOrderId = sourceOrderId; + } + + public LocalDateTime getPurchaseTime() { + return purchaseTime; + } + + public void setPurchaseTime(LocalDateTime purchaseTime) { + this.purchaseTime = purchaseTime; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java index 410ddac..aa7928f 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/MemberCardTransactions.java @@ -53,7 +53,107 @@ public class MemberCardTransactions extends BaseDomain { @Schema(description = "备注",example = "预约团课:瑜伽课扣1次") private String remark; + //关联订单ID + @Schema(description = "关联订单ID",example = "1") + private Long sourceOrderId; + //创建时间 @Schema(description = "创建时间",example = "2026-05-10 05:22:47") private LocalDateTime createdAt; + + public Long getMemberCardTransactionsId() { + return memberCardTransactionsId; + } + + public void setMemberCardTransactionsId(Long memberCardTransactionsId) { + this.memberCardTransactionsId = memberCardTransactionsId; + } + + public Long getMemberCardId() { + return memberCardId; + } + + public void setMemberCardId(Long memberCardId) { + this.memberCardId = memberCardId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public MemberCardTransactionsAction getOperationType() { + return operationType; + } + + public void setOperationType(MemberCardTransactionsAction operationType) { + this.operationType = operationType; + } + + public Integer getChangeAmount() { + return changeAmount; + } + + public void setChangeAmount(Integer changeAmount) { + this.changeAmount = changeAmount; + } + + public Double getChangeBalance() { + return changeBalance; + } + + public void setChangeBalance(Double changeBalance) { + this.changeBalance = changeBalance; + } + + public Integer getAfterRemainingCount() { + return afterRemainingCount; + } + + public void setAfterRemainingCount(Integer afterRemainingCount) { + this.afterRemainingCount = afterRemainingCount; + } + + public Double getAfterRemainingBalance() { + return afterRemainingBalance; + } + + public void setAfterRemainingBalance(Double afterRemainingBalance) { + this.afterRemainingBalance = afterRemainingBalance; + } + + public MemberCardTransactionsType getRelatedBizType() { + return relatedBizType; + } + + public void setRelatedBizType(MemberCardTransactionsType relatedBizType) { + this.relatedBizType = relatedBizType; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public Long getSourceOrderId() { + return sourceOrderId; + } + + public void setSourceOrderId(Long sourceOrderId) { + this.sourceOrderId = sourceOrderId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java new file mode 100644 index 0000000..01b5f05 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/domain/RefundApplication.java @@ -0,0 +1,127 @@ +package cn.novalon.gym.manage.gymmembercard.domain; + +import cn.novalon.gym.manage.sys.core.domain.BaseDomain; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 退款申请领域对象 + * + * @author shizhounian + * @date 2026-05-23 21:11:46 + */ +@Schema(description = "退款申请", example = "refund_application") +public class RefundApplication extends BaseDomain { + + @Schema(description = "退款申请ID", example = "1") + private Long id; + + @Schema(description = "会员卡记录ID", example = "1") + private Long recordId; + + @Schema(description = "会员ID", example = "1") + private Long memberId; + + @Schema(description = "状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败", example = "PENDING") + private String status; + + @Schema(description = "退款原因", example = "个人原因申请退款") + private String reason; + + @Schema(description = "申请时间", example = "2026-05-23 21:09:14") + private LocalDateTime applyTime; + + @Schema(description = "审核时间", example = "2026-05-24 10:00:00") + private LocalDateTime auditTime; + + @Schema(description = "审核人ID", example = "1") + private Long auditorId; + + @Schema(description = "审核备注", example = "同意退款") + private String auditRemark; + + @Schema(description = "退款金额", example = "500.00") + private BigDecimal refundAmount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getRecordId() { + return recordId; + } + + public void setRecordId(Long recordId) { + this.recordId = recordId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public LocalDateTime getApplyTime() { + return applyTime; + } + + public void setApplyTime(LocalDateTime applyTime) { + this.applyTime = applyTime; + } + + public LocalDateTime getAuditTime() { + return auditTime; + } + + public void setAuditTime(LocalDateTime auditTime) { + this.auditTime = auditTime; + } + + public Long getAuditorId() { + return auditorId; + } + + public void setAuditorId(Long auditorId) { + this.auditorId = auditorId; + } + + public String getAuditRemark() { + return auditRemark; + } + + public void setAuditRemark(String auditRemark) { + this.auditRemark = auditRemark; + } + + public BigDecimal getRefundAmount() { + return refundAmount; + } + + public void setRefundAmount(BigDecimal refundAmount) { + this.refundAmount = refundAmount; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java index 06ff025..d57641e 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardEntity.java @@ -11,7 +11,6 @@ import java.time.LocalDateTime; *@Author:shizhounian *@Date:2026/5/10-05 22:47:31 */ -@Data @Table("member_card") public class MemberCardEntity extends BaseEntity { //会员卡id @@ -49,4 +48,76 @@ public class MemberCardEntity extends BaseEntity { //会员卡创建时间 @Column("member_card_create_time") private LocalDateTime memberCardCreateTime; + + public Long getMemberCardId() { + return memberCardId; + } + + public void setMemberCardId(Long memberCardId) { + this.memberCardId = memberCardId; + } + + public String getMemberCardName() { + return memberCardName; + } + + public void setMemberCardName(String memberCardName) { + this.memberCardName = memberCardName; + } + + public String getMemberCardType() { + return memberCardType; + } + + public void setMemberCardType(String memberCardType) { + this.memberCardType = memberCardType; + } + + public Double getMemberCardPrice() { + return memberCardPrice; + } + + public void setMemberCardPrice(Double memberCardPrice) { + this.memberCardPrice = memberCardPrice; + } + + public Integer getMemberCardValidityDays() { + return memberCardValidityDays; + } + + public void setMemberCardValidityDays(Integer memberCardValidityDays) { + this.memberCardValidityDays = memberCardValidityDays; + } + + public Integer getMemberCardTotalTimes() { + return memberCardTotalTimes; + } + + public void setMemberCardTotalTimes(Integer memberCardTotalTimes) { + this.memberCardTotalTimes = memberCardTotalTimes; + } + + public Double getMemberCardAmount() { + return memberCardAmount; + } + + public void setMemberCardAmount(Double memberCardAmount) { + this.memberCardAmount = memberCardAmount; + } + + public Integer getMemberCardStatus() { + return memberCardStatus; + } + + public void setMemberCardStatus(Integer memberCardStatus) { + this.memberCardStatus = memberCardStatus; + } + + public LocalDateTime getMemberCardCreateTime() { + return memberCardCreateTime; + } + + public void setMemberCardCreateTime(LocalDateTime memberCardCreateTime) { + this.memberCardCreateTime = memberCardCreateTime; + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java index 2c1e1c6..a601f43 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardRecordEntity.java @@ -12,7 +12,6 @@ import java.time.LocalDateTime; *@Author:shizhounian *@Date:2026/5/10-05 22:47:31 */ -@Data @Table("member_card_record") public class MemberCardRecordEntity extends BaseEntity { //会员持有卡id @@ -50,4 +49,76 @@ public class MemberCardRecordEntity extends BaseEntity { //购买时间 @Column("purchase_time") private LocalDateTime purchaseTime; + + public Long getMemberCardRecordId() { + return memberCardRecordId; + } + + public void setMemberCardRecordId(Long memberCardRecordId) { + this.memberCardRecordId = memberCardRecordId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public Long getMemberCardId() { + return memberCardId; + } + + public void setMemberCardId(Long memberCardId) { + this.memberCardId = memberCardId; + } + + public MemberCardRecordStatus getStatus() { + return status; + } + + public void setStatus(MemberCardRecordStatus status) { + this.status = status; + } + + public Integer getRemainingTimes() { + return remainingTimes; + } + + public void setRemainingTimes(Integer remainingTimes) { + this.remainingTimes = remainingTimes; + } + + public Double getRemainingAmount() { + return remainingAmount; + } + + public void setRemainingAmount(Double remainingAmount) { + this.remainingAmount = remainingAmount; + } + + public LocalDateTime getExpireTime() { + return expireTime; + } + + public void setExpireTime(LocalDateTime expireTime) { + this.expireTime = expireTime; + } + + public Long getSourceOrderId() { + return sourceOrderId; + } + + public void setSourceOrderId(Long sourceOrderId) { + this.sourceOrderId = sourceOrderId; + } + + public LocalDateTime getPurchaseTime() { + return purchaseTime; + } + + public void setPurchaseTime(LocalDateTime purchaseTime) { + this.purchaseTime = purchaseTime; + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java index b988065..1aedaea 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/MemberCardTransactionsEntity.java @@ -3,7 +3,6 @@ package cn.novalon.gym.manage.gymmembercard.entity; import cn.novalon.gym.manage.db.entity.BaseEntity; import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsType; -import lombok.Data; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @@ -13,50 +12,149 @@ import java.time.LocalDateTime; *@Author:shizhounian *@Date:2026/5/10-05 22:47:31 */ -@Data @Table("member_card_transactions") -public class MemberCardTransactionsEntity extends BaseEntity { - //会员卡流水Id - @Column("member_card_transactions_id") - private Long memberCardTransactionsId; +public class MemberCardTransactionsEntity extends BaseEntity{ - //会员卡Id - @Column("member_card_id") - private Long memberCardId; + @Column("id") + private Long id; + + @Column("member_card_record_id") + private Long memberCardRecordId; - //会员Id @Column("member_id") private Long memberId; - //操作类型:PURCHASE(购买) / DEDUCT(扣次/扣费) / RENEW(续费) / REFUND(退款) / EXPIRE(过期) + @Column("member_card_id") + private Long memberCardId; + @Column("operation_type") private MemberCardTransactionsAction operationType; - //变动次数(次卡用) @Column("change_amount") private Integer changeAmount; - //变动金额(储值卡用) @Column("change_balance") private Double changeBalance; - //变动后剩余次数 @Column("after_remaining_count") private Integer afterRemainingCount; - //变动后剩余金额 @Column("after_remaining_balance") private Double afterRemainingBalance; - //关联业务类型 @Column("related_biz_type") private MemberCardTransactionsType relatedBizType; - //备注 + @Column("source_order_id") + private Long sourceOrderId; + @Column("remark") private String remark; - //创建时间 @Column("created_at") private LocalDateTime createdAt; -} \ No newline at end of file + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getMemberCardRecordId() { + return memberCardRecordId; + } + + public void setMemberCardRecordId(Long memberCardRecordId) { + this.memberCardRecordId = memberCardRecordId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public Long getMemberCardId() { + return memberCardId; + } + + public void setMemberCardId(Long memberCardId) { + this.memberCardId = memberCardId; + } + + public MemberCardTransactionsAction getOperationType() { + return operationType; + } + + public void setOperationType(MemberCardTransactionsAction operationType) { + this.operationType = operationType; + } + + public Integer getChangeAmount() { + return changeAmount; + } + + public void setChangeAmount(Integer changeAmount) { + this.changeAmount = changeAmount; + } + + public Double getChangeBalance() { + return changeBalance; + } + + public void setChangeBalance(Double changeBalance) { + this.changeBalance = changeBalance; + } + + public Integer getAfterRemainingCount() { + return afterRemainingCount; + } + + public void setAfterRemainingCount(Integer afterRemainingCount) { + this.afterRemainingCount = afterRemainingCount; + } + + public Double getAfterRemainingBalance() { + return afterRemainingBalance; + } + + public void setAfterRemainingBalance(Double afterRemainingBalance) { + this.afterRemainingBalance = afterRemainingBalance; + } + + public MemberCardTransactionsType getRelatedBizType() { + return relatedBizType; + } + + public void setRelatedBizType(MemberCardTransactionsType relatedBizType) { + this.relatedBizType = relatedBizType; + } + + public Long getSourceOrderId() { + return sourceOrderId; + } + + public void setSourceOrderId(Long sourceOrderId) { + this.sourceOrderId = sourceOrderId; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java new file mode 100644 index 0000000..49fd781 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/entity/RefundApplicationEntity.java @@ -0,0 +1,127 @@ +package cn.novalon.gym.manage.gymmembercard.entity; + +import cn.novalon.gym.manage.db.entity.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 退款申请实体类 + * + * @author shizhounian + * @date 2026-05-23 21:09:14 + */ +@Schema(description = "退款申请实体", example = "refund_application") +public class RefundApplicationEntity extends BaseEntity { + + @Schema(description = "退款申请ID", example = "1") + private Long id; + + @Schema(description = "会员卡记录ID", example = "1") + private Long recordId; + + @Schema(description = "会员ID", example = "1") + private Long memberId; + + @Schema(description = "状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败", example = "PENDING") + private String status; + + @Schema(description = "退款原因", example = "个人原因申请退款") + private String reason; + + @Schema(description = "申请时间", example = "2026-05-23 21:09:14") + private LocalDateTime applyTime; + + @Schema(description = "审核时间", example = "2026-05-24 10:00:00") + private LocalDateTime auditTime; + + @Schema(description = "审核人ID", example = "1") + private Long auditorId; + + @Schema(description = "审核备注", example = "同意退款") + private String auditRemark; + + @Schema(description = "退款金额", example = "500.00") + private BigDecimal refundAmount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getRecordId() { + return recordId; + } + + public void setRecordId(Long recordId) { + this.recordId = recordId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public LocalDateTime getApplyTime() { + return applyTime; + } + + public void setApplyTime(LocalDateTime applyTime) { + this.applyTime = applyTime; + } + + public LocalDateTime getAuditTime() { + return auditTime; + } + + public void setAuditTime(LocalDateTime auditTime) { + this.auditTime = auditTime; + } + + public Long getAuditorId() { + return auditorId; + } + + public void setAuditorId(Long auditorId) { + this.auditorId = auditorId; + } + + public String getAuditRemark() { + return auditRemark; + } + + public void setAuditRemark(String auditRemark) { + this.auditRemark = auditRemark; + } + + public BigDecimal getRefundAmount() { + return refundAmount; + } + + public void setRefundAmount(BigDecimal refundAmount) { + this.refundAmount = refundAmount; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java new file mode 100644 index 0000000..42e8094 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardEvent.java @@ -0,0 +1,34 @@ +package cn.novalon.gym.manage.gymmembercard.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "会员卡状态机事件") +public enum MemberCardEvent { + @Schema(description = "激活卡片") + ACTIVATE("激活卡片"), + + @Schema(description = "使用卡片") + USE("使用卡片"), + + @Schema(description = "续费") + RENEW("续费"), + + @Schema(description = "过期") + EXPIRE("过期"), + + @Schema(description = "退款") + REFUND("退款"), + + @Schema(description = "禁用") + DISABLE("禁用"); + + private final String desc; + + MemberCardEvent(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java new file mode 100644 index 0000000..decb62a --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/enums/MemberCardType.java @@ -0,0 +1,25 @@ +package cn.novalon.gym.manage.gymmembercard.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "会员卡类型枚举") +public enum MemberCardType { + @Schema(description = "时长卡") + TIME_CARD("时长卡"), + + @Schema(description = "次卡") + COUNT_CARD("次卡"), + + @Schema(description = "储值卡") + STORED_VALUE_CARD("储值卡"); + + private final String desc; + + MemberCardType(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java new file mode 100644 index 0000000..08f57ae --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/DistributedLockService.java @@ -0,0 +1,50 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 分布式锁服务(简化版,使用本地锁) + * + * @author shizhounian + * @date 2026-05-23 + */ +@Component +public class DistributedLockService { + + private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); + + /** + * 执行带锁的操作(业务操作) + */ + public Mono executeWithLock(String userId, String cardType, Mono operation) { + String lockKey = "lock:member:card:operation:" + userId + ":" + cardType; + ReentrantLock lock = locks.computeIfAbsent(lockKey, k -> new ReentrantLock()); + + lock.lock(); + try { + return operation.doFinally(signalType -> lock.unlock()); + } catch (Exception e) { + lock.unlock(); + return Mono.error(e); + } + } + + /** + * 执行带锁的操作(通用/定时任务) + */ + public Mono executeWithLock(String lockKey, Mono operation) { + ReentrantLock lock = locks.computeIfAbsent(lockKey, k -> new ReentrantLock()); + + lock.lock(); + try { + return operation.doFinally(signalType -> lock.unlock()); + } catch (Exception e) { + lock.unlock(); + return Mono.error(e); + } + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java new file mode 100644 index 0000000..3050327 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/ExpirationReminderService.java @@ -0,0 +1,151 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.core.ReactiveStringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ExpirationReminderService { + + private final ReactiveStringRedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + private static final String REMINDER_QUEUE = "queue:member_card_expiration"; + private static final String DEAD_LETTER_QUEUE = "queue:member_card_expiration_dead_letter"; + private static final int REMINDER_DAYS_BEFORE = 7; + private static final long MAX_DELAY_MILLIS = Duration.ofDays(365).toMillis(); + + /** + * 设置到期提醒(购卡/续费时调用) + */ + public Mono scheduleExpirationReminder(MemberCardRecord record) { + if (record.getExpireTime() == null) { + return Mono.empty(); + } + + LocalDateTime now = LocalDateTime.now(); + + Flux reminderFlux = Flux.range(1, REMINDER_DAYS_BEFORE) + .flatMap(daysBefore -> { + LocalDateTime reminderTime = record.getExpireTime().minusDays(daysBefore); + + if (reminderTime.isBefore(now)) { + log.debug("会员卡记录ID={} 的{}天前提醒时间已过,跳过", + record.getMemberCardRecordId(), daysBefore); + return Mono.empty(); + } + + long delayMillis = Duration.between(now, reminderTime).toMillis(); + + if (delayMillis > MAX_DELAY_MILLIS) { + log.warn("会员卡记录ID={} 的{}天后提醒时间超过1年,跳过", + record.getMemberCardRecordId(), daysBefore); + return Mono.empty(); + } + + try { + String messageId = UUID.randomUUID().toString(); + String message = objectMapper.writeValueAsString(new ExpirationMessage( + messageId, + record.getMemberCardRecordId(), + record.getMemberId(), + record.getExpireTime(), + daysBefore + )); + + double executeTime = System.currentTimeMillis() + delayMillis; + + return redisTemplate.opsForZSet() + .add(REMINDER_QUEUE, message, executeTime) + .doOnSuccess(v -> log.info("设置会员卡到期提醒: recordId={}, daysBefore={}, expireTime={}, executeTime={}", + record.getMemberCardRecordId(), daysBefore, record.getExpireTime(), executeTime)) + .then(); + } catch (Exception e) { + log.error("设置会员卡到期提醒失败: recordId={}, daysBefore={}", + record.getMemberCardRecordId(), daysBefore, e); + return Mono.error(e); + } + }); + + return reminderFlux.then(); + } + + /** + * 定时任务:每分钟扫描到期的提醒并发送 + */ + @Scheduled(fixedRate = 60000) + public void processDueReminders() { + double now = System.currentTimeMillis(); + + redisTemplate.opsForZSet() + .rangeByScoreWithScores( + REMINDER_QUEUE, + Range.from(Range.Bound.inclusive(0.0)) + .to(Range.Bound.inclusive(now)), + RedisZSetCommands.Limit.limit().count(100) + ) + .flatMap(tuple -> { + String message = tuple.getValue(); + if (message == null) { + return Mono.empty(); + } + + return Mono.fromCallable(() -> objectMapper.readValue(message, ExpirationMessage.class)) + .flatMap(expirationMessage -> { + log.info("处理到期提醒: messageId={}, memberId={}, expireTime={}, daysBefore={}", + expirationMessage.messageId(), + expirationMessage.memberId(), + expirationMessage.expireTime(), + expirationMessage.daysBefore()); + + // TODO: 集成微信/短信通知服务 + sendNotification(expirationMessage); + + return redisTemplate.opsForZSet() + .remove(REMINDER_QUEUE, message) + .doOnSuccess(removed -> { + if (removed > 0) { + log.info("成功删除已处理的提醒消息"); + } + }); + }) + .onErrorResume(e -> { + log.error("解析到期提醒消息失败,移至死信队列: message={}", message, e); + return redisTemplate.opsForZSet() + .add(DEAD_LETTER_QUEUE, message, System.currentTimeMillis()) + .then(Mono.empty()); + }); + }) + .then() + .subscribe(); + } + + + private void sendNotification(ExpirationMessage reminder) { + // TODO: 实际项目中调用微信模板消息或短信API + log.info("[模拟发送] 会员卡到期提醒 - 会员ID: {}, 到期时间: {}, 提前天数: {}", + reminder.memberId(), reminder.expireTime(), reminder.daysBefore()); + } + + public record ExpirationMessage( + String messageId, + Long recordId, + Long memberId, + LocalDateTime expireTime, + int daysBefore + ) {} +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java index eaff5f0..1744819 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardHandler.java @@ -1,114 +1,133 @@ package cn.novalon.gym.manage.gymmembercard.handler; -import cn.hutool.db.PageResult; import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; 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.util.List; -/* - *@Author:shizhounian - *@Date:2026/5/17-05 20:10:22 +/** + * 会员卡管理处理器 + * + * @author shizhounian + * @date 2026-05-23 */ +@Slf4j @Component -@Tag(name = "会员卡类型管理", description = "会员卡类型相关操作") +@Tag(name = "会员卡管理", description = "会员卡类型管理和会员持卡管理") public class MemberCardHandler { - private final IMemberCardService memberCardService; - private final Validator validator; - public MemberCardHandler(IMemberCardService memberCardService, Validator validator) { + private final IMemberCardService memberCardService; + + public MemberCardHandler(IMemberCardService memberCardService) { this.memberCardService = memberCardService; - this.validator = validator; } - /** - * 根据会员卡ID查询会员卡详情 - */ - @Operation(summary = "根据会员卡ID查询会员卡详情", description = "用于编辑前回显或小程序端展示") + @Operation(summary = "根据ID查询会员卡类型", description = "查询指定ID的会员卡类型详情") public Mono getMemberCardById(ServerRequest request) { - Long memberCardId = Long.parseLong(request.pathVariable("memberCardId")); - return memberCardService.findByMemberCardIdAndDeletedAtIsNull(memberCardId) + Long id = Long.valueOf(request.pathVariable("id")); + return memberCardService.findByMemberCardIdAndDeletedAtIsNull(id) .flatMap(card -> ServerResponse.ok().bodyValue(card)) .switchIfEmpty(ServerResponse.notFound().build()); } - /** - * 条件查询会员卡列表(分页) - */ - @Operation(summary = "条件查询会员卡列表", description = "后台卡种列表展示,支持多条件组合+分页+排序") - public Mono getMemberCardList(ServerRequest request) { - Integer status = request.queryParam("status").map(Integer::parseInt).orElse(null); + @Operation(summary = "查询会员卡类型列表", description = "支持分页和条件查询") + public Mono listMemberCards(ServerRequest request) { + Integer status = request.queryParam("status").map(Integer::valueOf).orElse(null); String name = request.queryParam("name").orElse(null); String type = request.queryParam("type").orElse(null); - Double minPrice = request.queryParam("minPrice").map(Double::parseDouble).orElse(null); - Double maxPrice = request.queryParam("maxPrice").map(Double::parseDouble).orElse(null); + Double minPrice = request.queryParam("minPrice").map(Double::valueOf).orElse(null); + Double maxPrice = request.queryParam("maxPrice").map(Double::valueOf).orElse(null); + int page = request.queryParam("page").map(Integer::valueOf).orElse(0); + int size = request.queryParam("size").map(Integer::valueOf).orElse(10); - int page = request.queryParam("page").map(Integer::parseInt).orElse(0); - int size = request.queryParam("size").map(Integer::parseInt).orElse(10); - String sortField = request.queryParam("sortField").orElse("created_at"); - String sortOrder = request.queryParam("sortOrder").orElse("DESC"); - - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.fromString(sortOrder), sortField)); - - Mono countMono = memberCardService.countWithConditions(status, name, type, minPrice, maxPrice); - Flux cardsFlux = memberCardService.findWithConditions(status, name, type, minPrice, maxPrice, pageable); - - return Mono.zip(countMono, cardsFlux.collectList()) - .flatMap(tuple -> { - long total = tuple.getT1(); - List list = tuple.getT2(); - - // 使用 PageResult(int page, int pageSize, int total) 构造, - // 它会自动计算 totalPage - PageResult result = new PageResult<>(page, size, (int) total); - result.addAll(list); // 将查询结果填充进去 - - return ServerResponse.ok().bodyValue(result); - }); + var pageable = PageRequest.of(page, size); + return ServerResponse.ok() + .body(memberCardService.findWithConditions(status, name, type, minPrice, maxPrice, pageable), + MemberCard.class); } - /** - * 保存卡种信息(新增或更新) - */ - @Operation(summary = "保存卡种信息", description = "新增或更新会员卡类型") - public Mono saveMemberCard(ServerRequest request) { + @Operation(summary = "创建会员卡类型", description = "创建新的会员卡类型(时长卡、次卡或储值卡)") + public Mono createMemberCard(ServerRequest request) { return request.bodyToMono(MemberCard.class) .flatMap(memberCardService::save) - .flatMap(card -> ServerResponse.ok().bodyValue(card)); + .flatMap(card -> ServerResponse.status(HttpStatus.CREATED).bodyValue(card)); } - /** - * 逻辑删除会员卡(下架) - */ - @Operation(summary = "逻辑删除会员卡", description = "下架卡种,防止已购会员数据异常") + @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); + }) + .flatMap(updated -> ServerResponse.ok().bodyValue(updated)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + @Operation(summary = "删除会员卡类型", description = "逻辑删除会员卡类型") public Mono deleteMemberCard(ServerRequest request) { - Long memberCardId = Long.parseLong(request.pathVariable("memberCardId")); - return memberCardService.logicalDelete(memberCardId) + Long id = Long.valueOf(request.pathVariable("id")); + return memberCardService.logicalDelete(id) .flatMap(rows -> { if (rows > 0) { return ServerResponse.noContent().build(); - } else { - return ServerResponse.notFound().build(); } + return ServerResponse.notFound().build(); }); } - /** - * 批量查询上架的会员卡 - */ - @Operation(summary = "批量查询上架的会员卡", description = "用于小程序端展示") - public Mono getActiveCards(ServerRequest request) { - Integer status = request.queryParam("status").map(Integer::parseInt).orElse(0); - return ServerResponse.ok().body(memberCardService.findActiveCards(status), MemberCard.class); + @Operation(summary = "购买会员卡", description = "会员购买会员卡,生成会员卡记录") + public Mono purchaseCard(ServerRequest request) { + Long memberId = Long.valueOf(request.queryParam("memberId").orElseThrow()); + Long memberCardId = Long.valueOf(request.queryParam("memberCardId").orElseThrow()); + Long sourceOrderId = request.queryParam("sourceOrderId").map(Long::valueOf).orElse(null); + + return memberCardService.purchaseCard(memberId, memberCardId, sourceOrderId) + .flatMap(record -> ServerResponse.status(HttpStatus.CREATED).bodyValue(record)); } -} \ No newline at end of file + + @Operation(summary = "续费会员卡", description = "为已有会员卡续费") + public Mono renewCard(ServerRequest request) { + Long recordId = Long.valueOf(request.queryParam("recordId").orElseThrow()); + Integer addTimes = request.queryParam("addTimes").map(Integer::valueOf).orElse(null); + Double addAmount = request.queryParam("addAmount").map(Double::valueOf).orElse(null); + Integer addDays = request.queryParam("addDays").map(Integer::valueOf).orElse(null); + Long sourceOrderId = request.queryParam("sourceOrderId").map(Long::valueOf).orElse(null); + + return memberCardService.renewCard(recordId, addTimes, addAmount, addDays, sourceOrderId) + .flatMap(record -> ServerResponse.ok().bodyValue(record)); + } + + @Operation(summary = "使用会员卡", description = "扣减会员卡次数或余额") + public Mono useCard(ServerRequest request) { + Long recordId = Long.valueOf(request.queryParam("recordId").orElseThrow()); + Integer deductTimes = request.queryParam("deductTimes").map(Integer::valueOf).orElse(null); + Double deductAmount = request.queryParam("deductAmount").map(Double::valueOf).orElse(null); + + return memberCardService.useCard(recordId, deductTimes, deductAmount) + .flatMap(record -> ServerResponse.ok().bodyValue(record)); + } + + @Operation(summary = "退款会员卡", description = "申请会员卡退款") + public Mono refundCard(ServerRequest request) { + Long recordId = Long.valueOf(request.queryParam("recordId").orElseThrow()); + return memberCardService.refundCard(recordId) + .then(ServerResponse.noContent().build()); + } + + @Operation(summary = "查询有效会员卡", description = "查询指定状态的会员卡类型") + public Mono getActiveCards(ServerRequest request) { + Integer status = request.queryParam("status").map(Integer::valueOf).orElse(1); + return ServerResponse.ok() + .body(memberCardService.findActiveCards(status), MemberCard.class); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java index 3485d5b..77ee607 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardRecordHandler.java @@ -1,171 +1,103 @@ package cn.novalon.gym.manage.gymmembercard.handler; import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardRecordService; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Validator; import lombok.Data; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; 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.Mono; -import java.time.LocalDateTime; - -/* - *@Author:shizhounian - *@Date:2026/5/17-05 20:10:22 - */ @Component -@Tag(name = "会员卡记录管理", description = "会员卡记录相关操作") +@Tag(name = "会员卡记录管理", description = "会员卡购买、续费、使用、退款等核心业务") public class MemberCardRecordHandler { + private final IMemberCardService memberCardService; private final IMemberCardRecordService memberCardRecordService; - private final Validator validator; - public MemberCardRecordHandler(IMemberCardRecordService memberCardRecordService, Validator validator) { + public MemberCardRecordHandler(IMemberCardService memberCardService, + IMemberCardRecordService memberCardRecordService) { + this.memberCardService = memberCardService; this.memberCardRecordService = memberCardRecordService; - this.validator = validator; } - /** - * 会员购卡/后台发卡 - */ - @Operation(summary = "会员购卡/后台发卡", description = "支付成功后插入一条记录,状态为ACTIVE") - public Mono insertActiveRecord(ServerRequest request) { - return request.bodyToMono(MemberCardRecord.class) - .flatMap(memberCardRecordService::insertActiveRecord) - .flatMap(record -> ServerResponse.ok().bodyValue(record)); + @Operation(summary = "购买会员卡", description = "支持时长卡、次卡、储值卡,自动设置到期提醒") + public Mono purchaseCard(ServerRequest request) { + return request.bodyToMono(PurchaseRequest.class) + .flatMap(body -> memberCardService.purchaseCard( + body.getMemberId(), + body.getMemberCardId(), + body.getSourceOrderId())) + .flatMap(record -> ServerResponse.ok().bodyValue(record)) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue("购买失败: " + e.getMessage())); } - /** - * 扣次/扣费 - */ - @Operation(summary = "扣次/扣费", description = "预约团课或私教成功后扣减次数或余额") - public Mono deductUsage(ServerRequest request) { - Long recordId = Long.parseLong(request.pathVariable("id")); - return request.bodyToMono(DeductRequest.class) - .flatMap(body -> memberCardRecordService.deductUsage(recordId, - body.getDeductTimes(), - body.getDeductAmount())) - .flatMap(rows -> { - if (rows > 0) { - return ServerResponse.ok().build(); - } else { - return ServerResponse.badRequest().bodyValue("余额不足或卡无效"); - } - }); - } - - /** - * 续费 - */ - @Operation(summary = "续费", description = "累加剩余次数/余额,顺延到期日期") + @Operation(summary = "续费会员卡", description = "累加剩余次数/余额,顺延到期日期,权益立即生效") public Mono renewCard(ServerRequest request) { - Long recordId = Long.parseLong(request.pathVariable("id")); + Long recordId = Long.parseLong(request.pathVariable("recordId")); return request.bodyToMono(RenewRequest.class) - .flatMap(body -> memberCardRecordService.renewCard(recordId, + .flatMap(body -> memberCardService.renewCard(recordId, body.getAddTimes(), body.getAddAmount(), - body.getNewExpireTime())) - .flatMap(rows -> { - if (rows > 0) { - return ServerResponse.ok().build(); - } else { - return ServerResponse.notFound().build(); - } - }); + body.getAddDays(), + body.getSourceOrderId())) + .flatMap(record -> ServerResponse.ok().bodyValue(record)) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue("续费失败: " + e.getMessage())); } - /** - * 状态变更(过期、退款) - */ - @Operation(summary = "状态变更", description = "将卡状态改为EXPIRED或REFUNDED") - public Mono updateStatus(ServerRequest request) { - Long recordId = Long.parseLong(request.pathVariable("id")); - return request.queryParam("status") - .map(s -> { - try { - return MemberCardRecordStatus.valueOf(s.toUpperCase()); - } catch (IllegalArgumentException e) { - return null; - } - }) - .map(status -> memberCardRecordService.updateStatus(recordId, status) - .flatMap(rows -> { - if (rows > 0) { - return ServerResponse.ok().build(); - } else { - return ServerResponse.notFound().build(); - } - })) - .orElse(ServerResponse.badRequest().bodyValue("status 参数缺失或无效")); + @Operation(summary = "使用会员卡", description = "预约团课或私教成功后扣减次数或余额") + public Mono useCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("recordId")); + return request.bodyToMono(UseCardRequest.class) + .flatMap(body -> memberCardService.useCard(recordId, + body.getDeductTimes(), + body.getDeductAmount())) + .flatMap(record -> ServerResponse.ok().bodyValue(record)) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue("使用失败: " + e.getMessage())); } - /** - * 会员端“我的卡包” - */ - @Operation(summary = "会员我的卡包", description = "查询当前登录会员的所有有效卡") + @Operation(summary = "退款会员卡", description = "使用Saga模式执行退款流程,保证事务一致性") + public Mono refundCard(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("recordId")); + return memberCardService.refundCard(recordId) + .then(ServerResponse.ok().bodyValue("退款成功")) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue("退款失败: " + e.getMessage())); + } + + @Operation(summary = "查询会员卡记录详情", description = "根据记录ID查询详细信息") + public Mono getMemberCardRecordById(ServerRequest request) { + Long recordId = Long.parseLong(request.pathVariable("recordId")); + return memberCardRecordService.findById(recordId) + .flatMap(record -> ServerResponse.ok().bodyValue(record)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + @Operation(summary = "会员我的卡包", description = "查询当前会员的所有有效卡") public Mono getMyCards(ServerRequest request) { Long memberId = Long.parseLong(request.pathVariable("memberId")); - return ServerResponse.ok().body(memberCardRecordService.findActiveCardsByMemberId(memberId), + return ServerResponse.ok().body( + memberCardRecordService.findActiveCardsByMemberId(memberId), MemberCardRecord.class); } - /** - * 前台/店长查会员卡(分页) - */ - @Operation(summary = "管理端查询会员卡", description = "按会员ID分页查询所有会员卡记录") - public Mono getMemberCardRecords(ServerRequest request) { - Long memberId = Long.parseLong(request.pathVariable("memberId")); - int page = request.queryParam("page").map(Integer::parseInt).orElse(0); - int size = request.queryParam("size").map(Integer::parseInt).orElse(10); - Pageable pageable = PageRequest.of(page, size, Sort.by("purchase_time").descending()); - return ServerResponse.ok().body(memberCardRecordService.findByMemberId(memberId, pageable), - MemberCardRecord.class); + @Operation(summary = "处理过期会员卡", description = "定时任务调用,扫描并更新过期卡状态") + public Mono processExpiredCards(ServerRequest request) { + return memberCardService.processExpiredCards() + .flatMap(count -> ServerResponse.ok().bodyValue("处理完成,共处理" + count + "条")); } - /** - * 验证次卡是否可用 - */ - @Operation(summary = "验证次卡", description = "校验次卡剩余次数是否足够") - public Mono validateCountCard(ServerRequest request) { - Long recordId = Long.parseLong(request.pathVariable("id")); - Integer requiredTimes = request.queryParam("times").map(Integer::parseInt).orElse(1); - return memberCardRecordService.validateCountCard(recordId, requiredTimes) - .flatMap(card -> ServerResponse.ok().bodyValue(card)) - .switchIfEmpty(ServerResponse.badRequest().bodyValue("次卡不可用")); - } - - /** - * 验证储值卡是否可用 - */ - @Operation(summary = "验证储值卡", description = "校验储值卡余额是否足够") - public Mono validateStoredCard(ServerRequest request) { - Long recordId = Long.parseLong(request.pathVariable("id")); - Double requiredAmount = request.queryParam("amount").map(Double::parseDouble).orElse(0.0); - return memberCardRecordService.validateStoredCard(recordId, requiredAmount) - .flatMap(card -> ServerResponse.ok().bodyValue(card)) - .switchIfEmpty(ServerResponse.badRequest().bodyValue("储值卡不可用")); - } - - /** - * 到期扫描(管理端触发) - */ - @Operation(summary = "到期扫描", description = "扫描并返回已过期的会员卡(最多500条)") - public Mono getExpiredCards(ServerRequest request) { - return ServerResponse.ok().body(memberCardRecordService.findExpiredCards(), MemberCardRecord.class); - } - - // ==================== 内部请求体 DTO ==================== - @Data - public static class DeductRequest { + public static class PurchaseRequest { + private Long memberId; + private Long memberCardId; + private Long sourceOrderId; + } + + @Data + public static class UseCardRequest { private Integer deductTimes; private Double deductAmount; } @@ -174,6 +106,7 @@ public class MemberCardRecordHandler { public static class RenewRequest { private Integer addTimes; private Double addAmount; - private LocalDateTime newExpireTime; + private Integer addDays; + private Long sourceOrderId; } } \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java new file mode 100644 index 0000000..de6108b --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardScheduledHandler.java @@ -0,0 +1,99 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MemberCardScheduledHandler { + + private final IMemberCardRecordRepository recordRepository; + private final ExpirationReminderService expirationReminderService; + private final MemberCardStateMachine stateMachine; + private final DistributedLockService distributedLockService; + + /** + * 每日凌晨2点检查过期会员卡 + */ + @Scheduled(cron = "0 0 2 * * ?") + public void checkExpiredCards() { + String lockKey = "scheduled:check_expired_cards"; + + distributedLockService.executeWithLock("SYSTEM", "EXPIRE_CHECK", + Mono.fromRunnable(() -> { + log.info("开始执行会员卡过期检查任务"); + + LocalDateTime now = LocalDateTime.now(); + + recordRepository.findActiveRecords() + .filter(record -> record.getExpireTime() != null && record.getExpireTime().isBefore(now)) + .flatMap(record -> + stateMachine.transition(record.getStatus(), MemberCardEvent.EXPIRE) + .flatMap(newState -> { + record.setStatus(newState); + return recordRepository.save(record); + }) + .doOnSuccess(r -> log.info("会员卡记录ID={} 已标记为过期", r.getMemberCardRecordId())) + .onErrorResume(e -> { + log.error("处理会员卡过期失败: recordId={}", record.getMemberCardRecordId(), e); + return Mono.empty(); + }) + ) + .then() + .subscribe(); + }) + ).subscribe(); + } + + /** + * 每日凌晨3点检查是否有遗漏的到期提醒(兜底机制) + * 主要依赖购卡/续费时的主动调用和每分钟扫描任务,此任务仅用于异常恢复 + */ + @Scheduled(cron = "0 0 3 * * ?") + public void checkAndSendExpirationReminders() { + String lockKey = "scheduled:expiration_reminder"; + + distributedLockService.executeWithLock("SYSTEM", "REMINDER_CHECK", + Mono.fromRunnable(() -> { + log.info("开始执行到期提醒兜底检查任务"); + + LocalDateTime now = LocalDateTime.now(); + + // 查询所有活跃的会员卡 + recordRepository.findActiveRecords() + .filter(record -> record.getExpireTime() != null) + .flatMap(record -> { + try { + // 计算距离到期还有几天 + long daysBetween = java.time.Duration.between(now, record.getExpireTime()).toDays(); + + // 如果到期时间在1-7天范围内,记录日志供人工检查 + if (daysBetween >= 1 && daysBetween <= 7) { + log.warn("发现到期前{}天的会员卡记录ID={},请确认是否已发送提醒", + daysBetween, record.getMemberCardRecordId()); + } + + return Mono.empty(); + } catch (Exception e) { + log.error("检查到期提醒失败: recordId={}", record.getMemberCardRecordId(), e); + return Mono.empty(); + } + }) + .then() + .subscribe(); + }) + ).subscribe(); + + log.info("到期提醒兜底检查任务完成"); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java new file mode 100644 index 0000000..e48386e --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/MemberCardStateMachine.java @@ -0,0 +1,85 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +public class MemberCardStateMachine { + + private final Map> stateTransitionMap; + + public MemberCardStateMachine() { + this.stateTransitionMap = buildStateTransitionMap(); + } + + private Map> buildStateTransitionMap() { + Map> map = new HashMap<>(); + + // ACTIVE 状态可以转换的事件 + Map activeTransitions = new HashMap<>(); + activeTransitions.put(MemberCardEvent.USE, MemberCardRecordStatus.ACTIVE); + activeTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE); + activeTransitions.put(MemberCardEvent.EXPIRE, MemberCardRecordStatus.EXPIRED); + activeTransitions.put(MemberCardEvent.REFUND, MemberCardRecordStatus.REFUNDED); + map.put(MemberCardRecordStatus.ACTIVE, activeTransitions); + + // USED_UP 状态可以转换的事件 + Map usedUpTransitions = new HashMap<>(); + usedUpTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE); + usedUpTransitions.put(MemberCardEvent.REFUND, MemberCardRecordStatus.REFUNDED); + map.put(MemberCardRecordStatus.USED_UP, usedUpTransitions); + + // EXPIRED 状态可以转换的事件 + Map expiredTransitions = new HashMap<>(); + expiredTransitions.put(MemberCardEvent.RENEW, MemberCardRecordStatus.ACTIVE); + map.put(MemberCardRecordStatus.EXPIRED, expiredTransitions); + + // REFUNDED 状态是终态,不允许任何转换 + + return map; + } + + public Mono canTransition(MemberCardRecordStatus currentState, MemberCardEvent event) { + return Mono.fromSupplier(() -> { + Map transitions = stateTransitionMap.get(currentState); + if (transitions == null) { + return false; + } + return transitions.containsKey(event); + }); + } + + public Mono transition(MemberCardRecordStatus currentState, MemberCardEvent event) { + return Mono.fromSupplier(() -> { + Map transitions = stateTransitionMap.get(currentState); + if (transitions == null || !transitions.containsKey(event)) { + log.error("Invalid state transition: currentState={}, event={}", currentState, event); + throw new IllegalStateException( + String.format("不允许的状态转换: 当前状态=%s, 事件=%s", currentState, event)); + } + MemberCardRecordStatus newState = transitions.get(event); + log.info("State transition: {} --({})--> {}", currentState, event, newState); + return newState; + }); + } + + public Mono validateTransition(MemberCardRecord card, MemberCardEvent event) { + return canTransition(card.getStatus(), event) + .flatMap(canTransition -> { + if (!canTransition) { + return Mono.error(new IllegalStateException( + String.format("会员卡记录ID=%d 不允许的状态转换: 当前状态=%s, 事件=%s", + card.getMemberCardRecordId(), card.getStatus(), event))); + } + return Mono.empty(); + }); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java new file mode 100644 index 0000000..32c0ca1 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/handler/RefundSagaHandler.java @@ -0,0 +1,126 @@ +package cn.novalon.gym.manage.gymmembercard.handler; + +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RefundSagaHandler { + + private final IMemberCardRecordRepository recordRepository; + private final IMemberCardTransactionsService transactionsService; + private final MemberCardStateMachine stateMachine; + + public Mono executeRefund(Long recordId) { + return recordRepository.findById(recordId) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) + .flatMap(record -> stateMachine.validateTransition(record, MemberCardEvent.REFUND) + .then(Mono.defer(() -> doExecuteRefund(recordId, record)))); + } + + private Mono doExecuteRefund(Long recordId, MemberCardRecord record) { + List steps = new ArrayList<>(); + List rollbackSteps = new ArrayList<>(); + + SagaStep step1 = new SagaStep( + "更新会员卡状态为已退款", + updateCardStatus(recordId, MemberCardRecordStatus.REFUNDED), + Mono.defer(() -> updateCardStatus(recordId, record.getStatus())) + ); + steps.add(step1); + rollbackSteps.add(0, step1); + + SagaStep step2 = new SagaStep( + "记录退款流水", + createRefundTransaction(record), + createReversalTransaction(record) + ); + steps.add(step2); + rollbackSteps.add(0, step2); + + return executeSaga(steps, rollbackSteps); + } + + private Mono updateCardStatus(Long recordId, MemberCardRecordStatus status) { + return recordRepository.updateStatus(recordId, status) + .flatMap(rows -> { + if (rows == 0) { + return Mono.error(new RuntimeException("更新会员卡状态失败")); + } + return Mono.empty(); + }); + } + + private Mono createRefundTransaction(MemberCardRecord record) { + MemberCardTransactions transaction = new MemberCardTransactions(); + transaction.setMemberId(record.getMemberId()); + transaction.setMemberCardId(record.getMemberCardId()); + transaction.setOperationType(MemberCardTransactionsAction.REFUND); + transaction.setChangeAmount(-record.getRemainingTimes()); + transaction.setChangeBalance(-record.getRemainingAmount()); + transaction.setAfterRemainingCount(0); + transaction.setAfterRemainingBalance(0.0); + transaction.setRemark("会员卡退款"); + + return transactionsService.createTransaction(transaction); + } + + private Mono createReversalTransaction(MemberCardRecord record) { + MemberCardTransactions reversal = new MemberCardTransactions(); + reversal.setMemberId(record.getMemberId()); + reversal.setMemberCardId(record.getMemberCardId()); + reversal.setOperationType(MemberCardTransactionsAction.REFUND); + reversal.setChangeAmount(record.getRemainingTimes()); + reversal.setChangeBalance(record.getRemainingAmount()); + reversal.setRemark("退款冲正"); + + return transactionsService.createTransaction(reversal); + } + + private Mono executeSaga(List steps, List rollbackSteps) { + return executeStep(steps, 0, rollbackSteps); + } + + private Mono executeStep(List steps, int index, List rollbackSteps) { + if (index >= steps.size()) { + return Mono.empty(); + } + + SagaStep currentStep = steps.get(index); + + return currentStep.operation() + .then(Mono.defer(() -> executeStep(steps, index + 1, rollbackSteps))) + .onErrorResume(error -> { + log.error("Saga步骤执行失败: step={}, error={}", currentStep.description(), error.getMessage()); + return rollback(rollbackSteps, 0).then(Mono.error(error)); + }); + } + + private Mono rollback(List rollbackSteps, int index) { + if (index >= rollbackSteps.size()) { + return Mono.empty(); + } + + SagaStep currentStep = rollbackSteps.get(index); + + return currentStep.rollbackOperation() + .then(Mono.defer(() -> rollback(rollbackSteps, index + 1))) + .doOnError(error -> log.error("Saga回滚失败: step={}, error={}", currentStep.description(), error.getMessage())) + .onErrorResume(e -> Mono.empty()); + } + + private record SagaStep(String description, Mono operation, Mono rollbackOperation) {} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java index 237b440..6cafcde 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardRecordRepository.java @@ -6,78 +6,29 @@ import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.LocalDateTime; - public interface IMemberCardRecordRepository { - - /** - * 会员购卡/后台发卡 - * @param record 会员卡记录 - * @return 插入的记录 - */ + + Mono findById(Long id); + + Mono save(MemberCardRecord record); + Mono insertActiveRecord(MemberCardRecord record); - - /** - * 扣次/扣费(含防超扣校验) - * @param recordId 会员卡记录ID - * @param deductTimes 扣除次数 - * @param deductAmount 扣除金额 - * @return 受影响的行数(0表示余额不足,扣费失败) - */ - Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); - - /** - * 续费 - * @param recordId 会员卡记录ID - * @param addTimes 增加次数 - * @param addAmount 增加金额 - * @param newExpireTime 新的到期时间 - * @return 受影响的行数 - */ - Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); - - /** - * 状态变更 - * @param recordId 会员卡记录ID - * @param status 新状态 - * @return 受影响的行数 - */ - Mono updateStatus(Long recordId, MemberCardRecordStatus status); - - /** - * 会员端"我的卡包" - * @param memberId 会员ID - * @return 有效会员卡列表 - */ - Flux findActiveCardsByMemberId(Long memberId); - - /** - * 前台/店长查会员卡 - * @param memberId 会员ID - * @param pageable 分页参数 - * @return 会员卡列表 - */ + Flux findByMemberId(Long memberId, Pageable pageable); - - /** - * 验证次卡是否可用(仅检验次数和过期时间) - * @param recordId 会员卡记录ID - * @param requiredTimes 需要的次数 - * @return 符合条件的记录,空表示不可用 - */ - Mono validateCountCard(Long recordId, Integer requiredTimes); - - /** - * 验证储值卡是否可用(仅检验余额和过期时间) - * @param recordId 会员卡记录ID - * @param requiredAmount 需要的金额 - * @return 符合条件的记录,空表示不可用 - */ - Mono validateStoredCard(Long recordId, Double requiredAmount); - - /** - * 到期扫描(分批处理,避免内存压力) - * @return 已过期的会员卡记录列表(最多500条) - */ + + Flux findActiveCardsByMemberId(Long memberId); + + Flux findActiveRecords(); + + Mono updateStatus(Long id, MemberCardRecordStatus status); + + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, java.time.LocalDateTime newExpireTime); + Flux findExpiredCards(); -} + + Mono validateCountCard(Long recordId, Integer requiredTimes); + + Mono validateStoredCard(Long recordId, Double requiredAmount); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java index 010556a..46ff531 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IMemberCardTransactionsRepository.java @@ -88,4 +88,25 @@ public interface IMemberCardTransactionsRepository { * @return 购卡总金额 */ Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 保存流水记录 + * @param transaction 流水记录 + * @return 保存后的流水记录 + */ + Mono save(MemberCardTransactions transaction); + + /** + * 按会员ID查询所有流水记录 + * @param memberId 会员ID + * @return 该会员的所有流水记录,按时间倒序 + */ + Flux findByMemberId(Long memberId); + + /** + * 按流水ID查询流水记录 + * @param recordId 流水ID + * @return 该流水记录 + */ + Flux findByRecordId(Long recordId); } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java new file mode 100644 index 0000000..931b1fc --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/IRefundApplicationRepository.java @@ -0,0 +1,81 @@ +package cn.novalon.gym.manage.gymmembercard.repository; + +import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * 退款申请仓储接口 + * + * @author shizhounian + * @date 2026-05-23 + */ +public interface IRefundApplicationRepository { + + /** + * 创建退款申请 + * + * @param application 退款申请对象 + * @return 创建后的退款申请 + */ + Mono create(RefundApplication application); + + /** + * 根据ID查询退款申请 + * + * @param id 退款申请ID + * @return 退款申请对象 + */ + Mono findById(Long id); + + /** + * 根据会员卡记录ID查询退款申请 + * + * @param recordId 会员卡记录ID + * @return 退款申请对象 + */ + Mono findByRecordId(Long recordId); + + /** + * 根据会员ID查询退款申请列表 + * + * @param memberId 会员ID + * @return 退款申请列表 + */ + Flux findByMemberId(Long memberId); + + /** + * 根据状态查询退款申请列表 + * + * @param status 状态 + * @return 退款申请列表 + */ + Flux findByStatus(String status); + + /** + * 更新退款申请 + * + * @param application 退款申请对象 + * @return 更新后的退款申请 + */ + Mono update(RefundApplication application); + + /** + * 审核退款申请 + * + * @param id 退款申请ID + * @param status 审核状态(APPROVED/REJECTED) + * @param auditorId 审核人ID + * @param auditRemark 审核备注 + * @return 更新后的退款申请 + */ + Mono approve(Long id, String status, Long auditorId, String auditRemark); + + /** + * 删除退款申请(逻辑删除) + * + * @param id 退款申请ID + * @return 受影响的行数 + */ + Mono delete(Long id); +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java index 9148112..30406e5 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardRecordRepositoryImpl.java @@ -12,8 +12,6 @@ import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.LocalDateTime; - @Repository public class MemberCardRecordRepositoryImpl implements IMemberCardRecordRepository { private final MemberCardRecordDao memberCardRecordDao; @@ -28,14 +26,22 @@ public class MemberCardRecordRepositoryImpl implements IMemberCardRecordReposito this.r2dbcEntityTemplate = r2dbcEntityTemplate; } - /** - * 会员购卡/后台发卡 - * @param record 会员卡记录 - * @return 插入的记录 - */ + @Override + public Mono findById(Long id) { + return memberCardRecordDao.findById(id) + .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class)); + } + + @Override + public Mono save(MemberCardRecord record) { + MemberCardRecordEntity entity = beanConvertUtil.toBean(record, MemberCardRecordEntity.class); + return memberCardRecordDao.save(entity) + .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class)); + } + @Override public Mono insertActiveRecord(MemberCardRecord record) { - MemberCardRecordEntity entity = BeanConvertUtil.toBean(record, MemberCardRecordEntity.class); + MemberCardRecordEntity entity = beanConvertUtil.toBean(record, MemberCardRecordEntity.class); return memberCardRecordDao.insertActiveRecord( entity.getMemberId(), entity.getMemberCardId(), @@ -46,96 +52,54 @@ public class MemberCardRecordRepositoryImpl implements IMemberCardRecordReposito .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class)); } - /** - * 扣次/扣费(含防超扣校验) - * @param recordId 会员卡记录ID - * @param deductTimes 扣除次数 - * @param deductAmount 扣除金额 - * @return 受影响的行数(0表示余额不足,扣费失败) - */ - @Override - public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { - return memberCardRecordDao.deductUsage(recordId, deductTimes, deductAmount); - } - - /** - * 续费 - * @param recordId 会员卡记录ID - * @param addTimes 增加次数 - * @param addAmount 增加金额 - * @param newExpireTime 新的到期时间 - * @return 受影响的行数 - */ - @Override - public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { - return memberCardRecordDao.renewCard(recordId, addTimes, addAmount, newExpireTime); - } - - /** - * 状态变更 - * @param recordId 会员卡记录ID - * @param status 新状态 - * @return 受影响的行数 - */ - @Override - public Mono updateStatus(Long recordId, MemberCardRecordStatus status) { - return memberCardRecordDao.updateStatus(recordId, status); - } - - /** - * 会员端"我的卡包" - * @param memberId 会员ID - * @return 有效会员卡列表 - */ - @Override - public Flux findActiveCardsByMemberId(Long memberId) { - return memberCardRecordDao.findActiveCardsByMemberId(memberId) - .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); - } - - /** - * 前台/店长查会员卡 - * @param memberId 会员ID - * @param pageable 分页参数 - * @return 会员卡列表 - */ @Override public Flux findByMemberId(Long memberId, Pageable pageable) { return memberCardRecordDao.findByMemberId(memberId, pageable) .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); } - /** - * 验证次卡是否可用(仅检验次数和过期时间) - * @param recordId 会员卡记录ID - * @param requiredTimes 需要的次数 - * @return 符合条件的记录,空表示不可用 - */ + @Override + public Flux findActiveCardsByMemberId(Long memberId) { + return memberCardRecordDao.findActiveCardsByMemberId(memberId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + @Override + public Flux findActiveRecords() { + return memberCardRecordDao.findActiveRecords() + .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); + } + + @Override + public Mono updateStatus(Long id, MemberCardRecordStatus status) { + return memberCardRecordDao.updateStatus(id, status); + } + + @Override + public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { + return memberCardRecordDao.deductUsage(recordId, deductTimes, deductAmount); + } + + @Override + public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, java.time.LocalDateTime newExpireTime) { + return memberCardRecordDao.renewCard(recordId, addTimes, addAmount, newExpireTime); + } + + @Override + public Flux findExpiredCards() { + return memberCardRecordDao.findExpiredCards() + .map(e -> beanConvertUtil.toBean(e, MemberCardRecord.class)); + } + @Override public Mono validateCountCard(Long recordId, Integer requiredTimes) { return memberCardRecordDao.validateCountCard(recordId, requiredTimes) .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); } - /** - * 验证储值卡是否可用(仅检验余额和过期时间) - * @param recordId 会员卡记录ID - * @param requiredAmount 需要的金额 - * @return 符合条件的记录,空表示不可用 - */ @Override public Mono validateStoredCard(Long recordId, Double requiredAmount) { return memberCardRecordDao.validateStoredCard(recordId, requiredAmount) .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); } - - /** - * 到期扫描(分批处理,避免内存压力) - * @return 已过期的会员卡记录列表(最多500条) - */ - @Override - public Flux findExpiredCards() { - return memberCardRecordDao.findExpiredCards() - .map(entity -> beanConvertUtil.toBean(entity, MemberCardRecord.class)); - } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java index 0704716..4afecbd 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/MemberCardTransactionsRepositoryImpl.java @@ -144,4 +144,27 @@ public class MemberCardTransactionsRepositoryImpl implements IMemberCardTransact public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) { return memberCardTransactionsDao.sumPurchaseAmountByMemberId(memberId, startTime, endTime); } + /** + * 保存流水记录 + * @param transaction 流水记录 + * @return 保存后的流水记录 + */ + @Override + public Mono save(MemberCardTransactions transaction) { + MemberCardTransactionsEntity entity = beanConvertUtil.toBean(transaction, MemberCardTransactionsEntity.class); + return memberCardTransactionsDao.save(entity) + .map(savedEntity -> beanConvertUtil.toBean(savedEntity, MemberCardTransactions.class)); + } + + @Override + public Flux findByMemberId(Long memberId) { + return memberCardTransactionsDao.findByMemberId(memberId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); + } + + @Override + public Flux findByRecordId(Long recordId) { + return memberCardTransactionsDao.findByRecordId(recordId) + .map(entity -> beanConvertUtil.toBean(entity, MemberCardTransactions.class)); + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java new file mode 100644 index 0000000..ca85d61 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/repository/impl/RefundApplicationRepositoryImpl.java @@ -0,0 +1,125 @@ +package cn.novalon.gym.manage.gymmembercard.repository.impl; + +import cn.novalon.gym.manage.gymmembercard.dao.RefundApplicationDao; +import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication; +import cn.novalon.gym.manage.gymmembercard.entity.RefundApplicationEntity; +import cn.novalon.gym.manage.gymmembercard.repository.IRefundApplicationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * 退款申请仓储实现类 + * + * @author shizhounian + * @date 2026-05-23 21:16:36 + */ +@Repository +@RequiredArgsConstructor +public class RefundApplicationRepositoryImpl implements IRefundApplicationRepository { + + private final RefundApplicationDao refundApplicationDao; + + @Override + public Mono create(RefundApplication application) { + RefundApplicationEntity entity = new RefundApplicationEntity(); + entity.setRecordId(application.getRecordId()); + entity.setMemberId(application.getMemberId()); + entity.setStatus(application.getStatus() != null ? application.getStatus() : "PENDING"); + entity.setReason(application.getReason()); + entity.setApplyTime(application.getApplyTime() != null ? application.getApplyTime() : LocalDateTime.now()); + entity.setRefundAmount(application.getRefundAmount()); + + return refundApplicationDao.save(entity) + .map(this::convertToDomain); + } + + @Override + public Mono findById(Long id) { + return refundApplicationDao.findById(id) + .map(this::convertToDomain); + } + + @Override + public Mono findByRecordId(Long recordId) { + return refundApplicationDao.findByRecordId(recordId) + .map(this::convertToDomain); + } + + @Override + public Flux findByMemberId(Long memberId) { + return refundApplicationDao.findByMemberId(memberId) + .map(this::convertToDomain); + } + + @Override + public Flux findByStatus(String status) { + return refundApplicationDao.findByStatus(status) + .map(this::convertToDomain); + } + + @Override + public Mono update(RefundApplication application) { + return refundApplicationDao.findById(application.getId()) + .flatMap(entity -> { + if (application.getStatus() != null) { + entity.setStatus(application.getStatus()); + } + if (application.getReason() != null) { + entity.setReason(application.getReason()); + } + if (application.getAuditTime() != null) { + entity.setAuditTime(application.getAuditTime()); + } + if (application.getAuditorId() != null) { + entity.setAuditorId(application.getAuditorId()); + } + if (application.getAuditRemark() != null) { + entity.setAuditRemark(application.getAuditRemark()); + } + if (application.getRefundAmount() != null) { + entity.setRefundAmount(application.getRefundAmount()); + } + return refundApplicationDao.save(entity); + }) + .map(this::convertToDomain); + } + + @Override + public Mono approve(Long id, String status, Long auditorId, String auditRemark) { + return refundApplicationDao.approve(id, status, auditorId, auditRemark) + .flatMap(rows -> { + if (rows > 0) { + return refundApplicationDao.findById(id) + .map(this::convertToDomain); + } + return Mono.empty(); + }); + } + + @Override + public Mono delete(Long id) { + return refundApplicationDao.logicalDelete(id); + } + + /** + * Entity转Domain + */ + private RefundApplication convertToDomain(RefundApplicationEntity entity) { + RefundApplication domain = new RefundApplication(); + domain.setId(entity.getId()); + domain.setRecordId(entity.getRecordId()); + domain.setMemberId(entity.getMemberId()); + domain.setStatus(entity.getStatus()); + domain.setReason(entity.getReason()); + domain.setApplyTime(entity.getApplyTime()); + domain.setAuditTime(entity.getAuditTime()); + domain.setAuditorId(entity.getAuditorId()); + domain.setAuditRemark(entity.getAuditRemark()); + domain.setRefundAmount(entity.getRefundAmount()); + return domain; + } +} diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java index 5773df8..06e1cdd 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardRecordService.java @@ -1,83 +1,29 @@ package cn.novalon.gym.manage.gymmembercard.sevice; import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; -import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.LocalDateTime; - public interface IMemberCardRecordService { - - /** - * 会员购卡/后台发卡 - * @param record 会员卡记录 - * @return 插入的记录 - */ - Mono insertActiveRecord(MemberCardRecord record); - - /** - * 扣次/扣费(含防超扣校验) - * @param recordId 会员卡记录ID - * @param deductTimes 扣除次数 - * @param deductAmount 扣除金额 - * @return 受影响的行数(0表示余额不足,扣费失败) - */ - Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); - - /** - * 续费 - * @param recordId 会员卡记录ID - * @param addTimes 增加次数 - * @param addAmount 增加金额 - * @param newExpireTime 新的到期时间 - * @return 受影响的行数 - */ - Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime); - - /** - * 状态变更 - * @param recordId 会员卡记录ID - * @param status 新状态 - * @return 受影响的行数 - */ - Mono updateStatus(Long recordId, MemberCardRecordStatus status); - - /** - * 会员端"我的卡包" - * @param memberId 会员ID - * @return 有效会员卡列表 - */ - Flux findActiveCardsByMemberId(Long memberId); - - /** - * 前台/店长查会员卡 - * @param memberId 会员ID - * @param pageable 分页参数 - * @return 会员卡列表 - */ + + Mono findById(Long id); + Flux findByMemberId(Long memberId, Pageable pageable); - - /** - * 验证次卡是否可用(仅检验次数和过期时间) - * @param recordId 会员卡记录ID - * @param requiredTimes 需要的次数 - * @return 符合条件的记录,空表示不可用 - */ + + Flux findActiveCardsByMemberId(Long memberId); + + Mono insertActiveRecord(MemberCardRecord record); + + Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount); + + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, java.time.LocalDateTime newExpireTime); + + Mono updateStatus(Long recordId, cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus status); + Mono validateCountCard(Long recordId, Integer requiredTimes); - - /** - * 验证储值卡是否可用(仅检验余额和过期时间) - * @param recordId 会员卡记录ID - * @param requiredAmount 需要的金额 - * @return 符合条件的记录,空表示不可用 - */ + Mono validateStoredCard(Long recordId, Double requiredAmount); - - /** - * 到期扫描(分批处理,避免内存压力) - * @return 已过期的会员卡记录列表(最多500条) - */ + Flux findExpiredCards(); -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java index b1b744d..255dfe7 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardService.java @@ -1,78 +1,38 @@ package cn.novalon.gym.manage.gymmembercard.sevice; import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface IMemberCardService { - /** - * 根据会员卡ID查询会员卡详情(用于编辑前回显或小程序端展示) - * @param memberCardId 会员卡ID - * @return 会员卡完整信息,如果不存在或已删除则返回空 - */ + Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId); - - /** - * 条件查询会员卡列表(后台卡种列表展示,支持多条件组合+分页+排序) - * @param status 会员卡状态(上架/下架) - * @param name 会员卡名称(模糊查询) - * @param type 会员卡类型 - * @param minPrice 最低价格 - * @param maxPrice 最高价格 - * @param pageable 分页和排序参数 - * @return 符合条件的会员卡列表 - */ + Flux findWithConditions(Integer status, String name, String type, Double minPrice, Double maxPrice, Pageable pageable); - - /** - * 统计符合条件的会员卡总数(配合列表查询使用) - * @param status 会员卡状态 - * @param name 会员卡名称(模糊查询) - * @param type 会员卡类型 - * @param minPrice 最低价格 - * @param maxPrice 最高价格 - * @return 符合条件的会员卡数量 - */ + Mono countWithConditions(Integer status, String name, String type, Double minPrice, Double maxPrice); - - /** - * 按状态查询会员卡列表(用于小程序端展示可购买卡列表,只展示上架的卡) - * @param status 会员卡状态(通常传上架状态) - * @param pageable 分页和排序参数 - * @return 符合条件的会员卡列表 - */ + Flux findByMemberCardStatusAndDeletedAtIsNull(Integer status, Pageable pageable); - - /** - * 检查会员卡是否已被购买(用于删除前的校验) - * @param memberCardId 会员卡ID - * @return 如果存在关联的会员记录则返回true,否则返回false - */ + Mono existsPurchasedRecord(Long memberCardId); - - /** - * 逻辑删除会员卡(下架卡种,防止已购会员数据异常) - * @param memberCardId 会员卡ID - * @return 受影响的行数 - */ + Mono logicalDelete(Long memberCardId); - - /** - * 保存卡种信息(新增或更新) - * - 新增:entity.memberCardId 为 null 时,插入新记录 - * - 更新:entity.memberCardId 不为 null 时,根据ID更新现有记录 - * @param entity 卡种信息 - * @return 保存后的实体对象 - */ - Mono save(MemberCard entity); - - /** - * 批量查询上架的会员卡(用于小程序端展示) - * @param status 上架状态值 - * @return 上架的会员卡列表 - */ + Flux findActiveCards(Integer status); -} + + Mono save(MemberCard entity); + + Mono purchaseCard(Long memberId, Long memberCardId, Long sourceOrderId); + + Mono renewCard(Long recordId, Integer addTimes, Double addAmount, Integer addDays, Long sourceOrderId); + + Mono useCard(Long recordId, Integer deductTimes, Double deductAmount); + + Mono refundCard(Long recordId); + + Mono processExpiredCards(); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java index 526bcb8..06b1bf2 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IMemberCardTransactionsService.java @@ -26,7 +26,7 @@ public interface IMemberCardTransactionsService { * @return 流水记录列表 */ Flux findByMemberIdAndTimeRange(Long memberId, LocalDateTime startTime, - LocalDateTime endTime, Pageable pageable); + LocalDateTime endTime, Pageable pageable); /** * 后台"使用记录查询" @@ -39,9 +39,9 @@ public interface IMemberCardTransactionsService { * @return 流水记录列表 */ Flux findWithConditions(Long memberId, Long memberCardId, - MemberCardTransactionsAction operationType, - LocalDateTime startTime, LocalDateTime endTime, - Pageable pageable); + MemberCardTransactionsAction operationType, + LocalDateTime startTime, LocalDateTime endTime, + Pageable pageable); /** * 统计符合条件的流水总数 @@ -88,4 +88,25 @@ public interface IMemberCardTransactionsService { * @return 购卡总金额 */ Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime); -} + + /** + * 创建交易记录 + * @param transaction 交易记录 + * @return 创建的交易记录 + */ + Mono createTransaction(MemberCardTransactions transaction); + + /** + * 查询会员的交易记录 + * @param memberId 会员ID + * @return 交易记录列表 + */ + Flux findByMemberId(Long memberId); + + /** + * 查询会员卡记录的交易历史 + * @param recordId 会员卡记录ID + * @return 交易记录列表 + */ + Flux findByRecordId(Long recordId); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java new file mode 100644 index 0000000..56fa066 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/IRefundApplicationService.java @@ -0,0 +1,33 @@ +package cn.novalon.gym.manage.gymmembercard.sevice; + +import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication; +import reactor.core.publisher.Mono; + +/** + * 退款申请服务 + * + * @author shizhounian + * @date 2026-05-23 + */ +public interface IRefundApplicationService { + + /** + * 创建退款申请 + */ + Mono create(Long recordId, String reason); + + /** + * 审核退款申请 + */ + Mono approve(Long applicationId, Long auditorId, String remark); + + /** + * 拒绝退款申请 + */ + Mono reject(Long applicationId, Long auditorId, String remark); + + /** + * 根据记录ID查询申请 + */ + Mono findByRecordId(Long recordId); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java index 95b8ebe..598c435 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardRecordServiceImpl.java @@ -64,4 +64,9 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { public Flux findExpiredCards() { return memberCardRecordRepository.findExpiredCards(); } + + @Override + public Mono findById(Long recordId) { + return memberCardRecordRepository.findById(recordId); + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java index 0ec900c..379dded 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardServiceImpl.java @@ -1,20 +1,53 @@ package cn.novalon.gym.manage.gymmembercard.sevice.impl; import cn.novalon.gym.manage.gymmembercard.domain.MemberCard; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardRecord; +import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardEvent; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; +import cn.novalon.gym.manage.gymmembercard.enums.MemberCardType; +import cn.novalon.gym.manage.gymmembercard.handler.DistributedLockService; +import cn.novalon.gym.manage.gymmembercard.handler.ExpirationReminderService; +import cn.novalon.gym.manage.gymmembercard.handler.MemberCardStateMachine; +import cn.novalon.gym.manage.gymmembercard.handler.RefundSagaHandler; +import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRecordRepository; import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardRepository; -import cn.novalon.gym.manage.gymmembercard.repository.impl.MemberCardRepositoryImpl; import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardService; +import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.time.LocalDateTime; + +@Slf4j @Service public class MemberCardServiceImpl implements IMemberCardService { private final IMemberCardRepository memberCardRepository; + private final IMemberCardRecordRepository recordRepository; + private final IMemberCardTransactionsService transactionsService; + private final MemberCardStateMachine stateMachine; + private final DistributedLockService distributedLockService; + private final ExpirationReminderService expirationReminderService; + private final RefundSagaHandler refundSagaHandler; - public MemberCardServiceImpl(IMemberCardRepository memberCardRepository) { + public MemberCardServiceImpl(IMemberCardRepository memberCardRepository, + IMemberCardRecordRepository recordRepository, + IMemberCardTransactionsService transactionsService, + MemberCardStateMachine stateMachine, + DistributedLockService distributedLockService, + ExpirationReminderService expirationReminderService, + RefundSagaHandler refundSagaHandler) { this.memberCardRepository = memberCardRepository; + this.recordRepository = recordRepository; + this.transactionsService = transactionsService; + this.stateMachine = stateMachine; + this.distributedLockService = distributedLockService; + this.expirationReminderService = expirationReminderService; + this.refundSagaHandler = refundSagaHandler; } @Override @@ -49,8 +82,9 @@ public class MemberCardServiceImpl implements IMemberCardService { return memberCardRepository.logicalDelete(memberCardId); } - public Mono updateSafe(Long memberCardId, MemberCard updateData) { - return ((MemberCardRepositoryImpl) memberCardRepository).updateSafe(memberCardId, updateData); + @Override + public Flux findActiveCards(Integer status) { + return memberCardRepository.findActiveCards(status); } @Override @@ -59,7 +93,225 @@ public class MemberCardServiceImpl implements IMemberCardService { } @Override - public Flux findActiveCards(Integer status) { - return memberCardRepository.findActiveCards(status); + public Mono purchaseCard(Long memberId, Long memberCardId, Long sourceOrderId) { + return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在"))) + .flatMap(card -> { + if (card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) { + return Mono.error(new RuntimeException("该会员卡已禁用")); + } + + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + + return distributedLockService.executeWithLock( + memberId.toString(), + cardType.name(), + Mono.defer(() -> createCardRecord(memberId, memberCardId, sourceOrderId, card)) + ); + }) + .flatMap(record -> createTransaction(record, MemberCardTransactionsAction.PURCHASE, "购买会员卡") + .thenReturn(record)) + .flatMap(record -> expirationReminderService.scheduleExpirationReminder(record) + .then(Mono.just(record))); } -} + + private Mono createCardRecord(Long memberId, Long memberCardId, + Long sourceOrderId, MemberCard card) { + return Mono.defer(() -> { + MemberCardRecord record = new MemberCardRecord(); + record.setMemberId(memberId); + record.setMemberCardId(memberCardId); + record.setSourceOrderId(sourceOrderId); + record.setPurchaseTime(LocalDateTime.now()); + record.setStatus(MemberCardRecordStatus.ACTIVE); + + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + LocalDateTime now = LocalDateTime.now(); + + switch (cardType) { + case TIME_CARD: + record.setExpireTime(now.plusDays(card.getMemberCardValidityDays())); + record.setRemainingTimes(0); + record.setRemainingAmount(0.0); + break; + case COUNT_CARD: + record.setExpireTime(now.plusDays(card.getMemberCardValidityDays())); + record.setRemainingTimes(card.getMemberCardTotalTimes()); + record.setRemainingAmount(0.0); + break; + case STORED_VALUE_CARD: + record.setExpireTime(now.plusYears(1)); + record.setRemainingTimes(0); + record.setRemainingAmount(card.getMemberCardAmount()); + break; + default: + return Mono.error(new RuntimeException("不支持的会员卡类型")); + } + + return recordRepository.insertActiveRecord(record); + }); + } + + @Override + public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, + Integer addDays, Long sourceOrderId) { + return recordRepository.findById(recordId) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) + .flatMap(originalRecord -> stateMachine.validateTransition(originalRecord, MemberCardEvent.RENEW) + .then(Mono.just(originalRecord))) + .flatMap(originalRecord -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(originalRecord.getMemberCardId()) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在"))) + .flatMap(card -> { + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + + return distributedLockService.executeWithLock( + originalRecord.getMemberId().toString(), + cardType.name(), + Mono.defer(() -> doRenewCard(originalRecord, card, addTimes, addAmount, addDays)) + ); + })); + } + + private Mono doRenewCard(MemberCardRecord record, MemberCard card, + Integer addTimes, Double addAmount, Integer addDays) { + LocalDateTime now = LocalDateTime.now(); + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + + switch (cardType) { + case TIME_CARD: + LocalDateTime currentExpire = record.getExpireTime(); + LocalDateTime baseTime = (currentExpire != null && currentExpire.isAfter(now)) ? currentExpire : now; + int daysToAdd = addDays != null ? addDays : card.getMemberCardValidityDays(); + record.setExpireTime(baseTime.plusDays(daysToAdd)); + break; + case COUNT_CARD: + int currentTimes = record.getRemainingTimes() != null ? record.getRemainingTimes() : 0; + int timesToAdd = addTimes != null ? addTimes : card.getMemberCardTotalTimes(); + record.setRemainingTimes(currentTimes + timesToAdd); + if (record.getStatus() == MemberCardRecordStatus.USED_UP) { + record.setStatus(MemberCardRecordStatus.ACTIVE); + } + break; + case STORED_VALUE_CARD: + double currentAmount = record.getRemainingAmount() != null ? record.getRemainingAmount() : 0.0; + double amountToAdd = addAmount != null ? addAmount : card.getMemberCardAmount(); + record.setRemainingAmount(currentAmount + amountToAdd); + if (record.getStatus() == MemberCardRecordStatus.USED_UP) { + record.setStatus(MemberCardRecordStatus.ACTIVE); + } + break; + default: + return Mono.error(new RuntimeException("不支持的会员卡类型")); + } + + return recordRepository.save(record) + .flatMap(updatedRecord -> createTransaction(updatedRecord, MemberCardTransactionsAction.RENEW, "续费会员卡") + .then(expirationReminderService.scheduleExpirationReminder(updatedRecord)) + .thenReturn(updatedRecord)); + } + + @Override + public Mono useCard(Long recordId, Integer deductTimes, Double deductAmount) { + return recordRepository.findById(recordId) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) + .flatMap(record -> stateMachine.validateTransition(record, MemberCardEvent.USE) + .then(Mono.just(record))) + .flatMap(record -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(record.getMemberCardId()) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在"))) + .flatMap(card -> { + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + + return distributedLockService.executeWithLock( + record.getMemberId().toString(), + cardType.name(), + Mono.defer(() -> doUseCard(record, card, deductTimes, deductAmount)) + ); + })); + } + + private Mono doUseCard(MemberCardRecord record, MemberCard card, + Integer deductTimes, Double deductAmount) { + if (record.getStatus() != MemberCardRecordStatus.ACTIVE) { + return Mono.error(new RuntimeException("会员卡状态不正确")); + } + + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + LocalDateTime now = LocalDateTime.now(); + + switch (cardType) { + case TIME_CARD: + if (record.getExpireTime() != null && record.getExpireTime().isBefore(now)) { + return Mono.error(new RuntimeException("会员卡已过期")); + } + break; + case COUNT_CARD: + int currentTimes = record.getRemainingTimes() != null ? record.getRemainingTimes() : 0; + int timesToDeduct = deductTimes != null ? deductTimes : 1; + if (currentTimes < timesToDeduct) { + return Mono.error(new RuntimeException("剩余次数不足")); + } + record.setRemainingTimes(currentTimes - timesToDeduct); + if (record.getRemainingTimes() == 0) { + record.setStatus(MemberCardRecordStatus.USED_UP); + } + break; + case STORED_VALUE_CARD: + double currentAmount = record.getRemainingAmount() != null ? record.getRemainingAmount() : 0.0; + double amountToDeduct = deductAmount != null ? deductAmount : 0.0; + if (currentAmount < amountToDeduct) { + return Mono.error(new RuntimeException("余额不足")); + } + record.setRemainingAmount(currentAmount - amountToDeduct); + if (record.getRemainingAmount() == 0) { + record.setStatus(MemberCardRecordStatus.USED_UP); + } + break; + default: + return Mono.error(new RuntimeException("不支持的会员卡类型")); + } + + return recordRepository.save(record) + .flatMap(updatedRecord -> createTransaction(updatedRecord, MemberCardTransactionsAction.DEDUCT, "使用会员卡") + .thenReturn(updatedRecord)); + } + + @Override + public Mono refundCard(Long recordId) { + return recordRepository.findById(recordId) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡记录不存在"))) + .flatMap(record -> memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(record.getMemberCardId()) + .switchIfEmpty(Mono.error(new RuntimeException("会员卡类型不存在"))) + .flatMap(card -> { + MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); + return distributedLockService.executeWithLock( + record.getMemberId().toString(), + cardType.name(), + refundSagaHandler.executeRefund(recordId) + ); + })); + } + + @Override + public Mono processExpiredCards() { + return recordRepository.findExpiredCards() + .flatMap(record -> stateMachine.transition(record.getStatus(), MemberCardEvent.EXPIRE) + .flatMap(newState -> recordRepository.updateStatus( + record.getMemberCardRecordId(), newState))) + .reduce(0, Integer::sum) + .doOnSuccess(count -> log.info("处理过期会员卡完成,共处理{}条", count)); + } + + private Mono createTransaction(MemberCardRecord record, MemberCardTransactionsAction action, String remark) { + MemberCardTransactions transaction = new MemberCardTransactions(); + transaction.setMemberId(record.getMemberId()); + transaction.setMemberCardId(record.getMemberCardId()); + transaction.setOperationType(action); + transaction.setChangeAmount(record.getRemainingTimes()); + transaction.setChangeBalance(record.getRemainingAmount()); + transaction.setAfterRemainingCount(record.getRemainingTimes()); + transaction.setAfterRemainingBalance(record.getRemainingAmount()); + transaction.setRemark(remark); + + return transactionsService.createTransaction(transaction); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java index eddccea..119c4bf 100644 --- a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/MemberCardTransactionsServiceImpl.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.gymmembercard.domain.MemberCardTransactions; import cn.novalon.gym.manage.gymmembercard.enums.MemberCardTransactionsAction; import cn.novalon.gym.manage.gymmembercard.repository.IMemberCardTransactionsRepository; import cn.novalon.gym.manage.gymmembercard.sevice.IMemberCardTransactionsService; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -11,6 +12,7 @@ import reactor.core.publisher.Mono; import java.time.LocalDateTime; +@Slf4j @Service public class MemberCardTransactionsServiceImpl implements IMemberCardTransactionsService { private final IMemberCardTransactionsRepository memberCardTransactionsRepository; @@ -66,4 +68,22 @@ public class MemberCardTransactionsServiceImpl implements IMemberCardTransaction public Mono sumPurchaseAmountByMemberId(Long memberId, LocalDateTime startTime, LocalDateTime endTime) { return memberCardTransactionsRepository.sumPurchaseAmountByMemberId(memberId, startTime, endTime); } + + @Override + public Mono createTransaction(MemberCardTransactions transaction) { + return memberCardTransactionsRepository.save(transaction) + .then() + .doOnSuccess(v -> log.info("创建会员卡交易记录: memberId={}, cardId={}, type={}", + transaction.getMemberId(), transaction.getMemberCardId(), transaction.getOperationType())); + } + + @Override + public Flux findByMemberId(Long memberId) { + return memberCardTransactionsRepository.findByMemberId(memberId); + } + + @Override + public Flux findByRecordId(Long recordId) { + return memberCardTransactionsRepository.findByRecordId(recordId); + } } diff --git a/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java new file mode 100644 index 0000000..4c29ac7 --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/java/cn/novalon/gym/manage/gymmembercard/sevice/impl/RefundApplicationServiceImpl.java @@ -0,0 +1,85 @@ +package cn.novalon.gym.manage.gymmembercard.sevice.impl; + +import cn.novalon.gym.manage.gymmembercard.domain.RefundApplication; +import cn.novalon.gym.manage.gymmembercard.repository.IRefundApplicationRepository; +import cn.novalon.gym.manage.gymmembercard.sevice.IRefundApplicationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * 退款申请服务实现类 + * + * @author shizhounian + * @date 2026-05-23 21:18:33 + */ +@Slf4j +@Service +public class RefundApplicationServiceImpl implements IRefundApplicationService { + + private final IRefundApplicationRepository refundApplicationRepository; + + public RefundApplicationServiceImpl(IRefundApplicationRepository refundApplicationRepository) { + this.refundApplicationRepository = refundApplicationRepository; + } + + @Override + public Mono create(Long recordId, String reason) { + return refundApplicationRepository.findByRecordId(recordId) + .flatMap(existing -> { + if (existing != null) { + return Mono.error(new RuntimeException("该会员卡记录已有退款申请")); + } + return Mono.empty(); + }) + .then(Mono.defer(() -> { + RefundApplication application = new RefundApplication(); + application.setRecordId(recordId); + application.setReason(reason); + application.setStatus("PENDING"); + application.setApplyTime(LocalDateTime.now()); + + return refundApplicationRepository.create(application) + .doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}", + app.getId(), recordId)); + })); + } + + @Override + public Mono approve(Long applicationId, Long auditorId, String remark) { + return refundApplicationRepository.findById(applicationId) + .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在"))) + .flatMap(application -> { + if (!"PENDING".equals(application.getStatus())) { + return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); + } + + return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark) + .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", + applicationId, auditorId)); + }); + } + + @Override + public Mono reject(Long applicationId, Long auditorId, String remark) { + return refundApplicationRepository.findById(applicationId) + .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在"))) + .flatMap(application -> { + if (!"PENDING".equals(application.getStatus())) { + return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); + } + + return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark) + .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", + applicationId, auditorId)); + }); + } + + @Override + public Mono findByRecordId(Long recordId) { + return refundApplicationRepository.findByRecordId(recordId); + } +} diff --git a/gym-manage-api/gym-member-card/src/main/resources/application.properties b/gym-manage-api/gym-member-card/src/main/resources/application.properties deleted file mode 100644 index 81b7aa0..0000000 --- a/gym-manage-api/gym-member-card/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=gym-member-card diff --git a/gym-manage-api/gym-member-card/src/main/resources/sql b/gym-manage-api/gym-member-card/src/main/resources/sql new file mode 100644 index 0000000..509e01f --- /dev/null +++ b/gym-manage-api/gym-member-card/src/main/resources/sql @@ -0,0 +1,132 @@ +-- ============================================ +-- 会员卡类型表 +-- ============================================ +CREATE TABLE IF NOT EXISTS member_card ( + member_card_id BIGSERIAL PRIMARY KEY, + 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 JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +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格式,用于未来组合卡等)'; + +-- ============================================ +-- 会员卡记录表(会员持有的卡) +-- ============================================ +CREATE TABLE IF NOT EXISTS member_card_record ( + member_card_record_id BIGSERIAL PRIMARY KEY, + 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 TIMESTAMPTZ, + source_order_id BIGINT, + purchase_time TIMESTAMPTZ DEFAULT NOW(), + version INTEGER DEFAULT 0 NOT NULL, + card_composition JSONB DEFAULT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ + -- 移除外键约束,改用应用层验证 +); + +-- 索引优化 +CREATE INDEX idx_member_card_record_member_id ON member_card_record(member_id); +CREATE INDEX idx_member_card_record_status ON member_card_record(status); +CREATE INDEX idx_member_card_record_expire_time ON member_card_record(expire_time); +CREATE INDEX idx_member_card_record_member_status ON member_card_record(member_id, status); +CREATE INDEX 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格式,用于组合卡)'; + +-- ============================================ +-- 会员卡交易流水表 +-- ============================================ +CREATE TABLE IF NOT EXISTS member_card_transactions ( + id BIGSERIAL PRIMARY KEY, + 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 NOT NULL, + archived_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() + -- 移除外键约束,改用应用层验证 +); + +-- 退款申请表 +CREATE TABLE IF NOT EXISTS refund_application ( + id BIGSERIAL PRIMARY KEY, + record_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING/APPROVED/REJECTED/PROCESSING/SUCCESS/FAILED + reason VARCHAR(500), + apply_time TIMESTAMPTZ DEFAULT NOW(), + audit_time TIMESTAMPTZ, + auditor_id BIGINT, + audit_remark VARCHAR(500), + refund_amount DECIMAL(10, 2), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_refund_application_record_id ON refund_application(record_id); +CREATE INDEX 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-失败'; + + +-- 索引优化 +CREATE INDEX idx_member_card_transactions_member_id ON member_card_transactions(member_id); +CREATE INDEX idx_member_card_transactions_record_id ON member_card_transactions(member_card_record_id); +CREATE INDEX idx_member_card_transactions_created_at ON member_card_transactions(created_at); +CREATE INDEX 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 '归档时间'; 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 bf571d8..114ffd4 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 @@ -31,7 +31,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r * 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法 * 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由 * 算法:使用RouterFunctions.route()构建函数式路由规则 - * + * * @author 张翔 * @date 2026-03-13 */ @@ -58,11 +58,11 @@ public class SystemRouter { MemberCardHandler memberCardHandler, MemberCardRecordHandler memberCardRecordHandler, MemberCardTransactionHandler memberCardTransactionHandler) { - + return route() // ========== 诊断路由 ========== .GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose) - + // ========== 字典路由 ========== .GET("/api/dictionaries", dictionaryHandler::getAllDictionaries) .GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById) @@ -71,7 +71,7 @@ public class SystemRouter { .POST("/api/dictionaries", dictionaryHandler::createDictionary) .PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary) .DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary) - + // ========== 用户路由 ========== .GET("/api/users", userHandler::getAllUsers) .GET("/api/users/page", userHandler::getUsersByPage) @@ -90,7 +90,7 @@ public class SystemRouter { .POST("/api/users/{id}/action/restore", userHandler::restoreUser) .GET("/api/users/{id}/roles", userHandler::getUserRoles) .POST("/api/users/{id}/roles", userHandler::assignRoles) - + // ========== 菜单路由 ========== .GET("/api/menus", menuHandler::getAllMenus) .GET("/api/menus/tree", menuHandler::getMenuTree) @@ -98,7 +98,7 @@ public class SystemRouter { .POST("/api/menus", menuHandler::createMenu) .PUT("/api/menus/{id}", menuHandler::updateMenu) .DELETE("/api/menus/{id}", menuHandler::deleteMenu) - + // ========== 角色路由 ========== .GET("/api/roles", roleHandler::getAllRoles) .GET("/api/roles/page", roleHandler::getRolesByPage) @@ -112,7 +112,7 @@ public class SystemRouter { .POST("/api/roles/{id}/restore", roleHandler::restoreRole) .GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId) .POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole) - + // ========== 配置路由 ========== .GET("/api/config", configHandler::getAllConfigs) .GET("/api/config/{id}", configHandler::getConfigById) @@ -120,7 +120,7 @@ public class SystemRouter { .POST("/api/config", configHandler::createConfig) .PUT("/api/config/{id}", configHandler::updateConfig) .DELETE("/api/config/{id}", configHandler::deleteConfig) - + // ========== 日志路由 ========== .GET("/api/logs/login", logHandler::getAllLoginLogs) .GET("/api/logs/login/page", logHandler::getLoginLogsByPage) @@ -140,15 +140,15 @@ public class SystemRouter { .GET("/api/logs/operation/count", operationLogHandler::getOperationLogCount) .GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById) .POST("/api/logs/operation", operationLogHandler::createOperationLog) - + // ========== 认证路由 ========== .POST("/api/auth/login", authHandler::login) .POST("/api/auth/register", authHandler::register) .POST("/api/auth/logout", authHandler::logout) - + // ========== 统计路由 ========== .GET("/api/stats/overview", statsHandler::getOverview) - + // ========== 数据字典路由 ========== .GET("/api/dict/types", dictHandler::getAllDictTypes) .GET("/api/dict/types/{id}", dictHandler::getDictTypeById) @@ -162,7 +162,7 @@ public class SystemRouter { .POST("/api/dict/data", dictHandler::createDictData) .PUT("/api/dict/data/{id}", dictHandler::updateDictData) .DELETE("/api/dict/data/{id}", dictHandler::deleteDictData) - + // ========== 公告路由 ========== .GET("/api/notices", noticeHandler::getAllNotices) .GET("/api/notices/{id}", noticeHandler::getNoticeById) @@ -170,7 +170,7 @@ public class SystemRouter { .POST("/api/notices", noticeHandler::createNotice) .PUT("/api/notices/{id}", noticeHandler::updateNotice) .DELETE("/api/notices/{id}", noticeHandler::deleteNotice) - + // ========== 消息路由 ========== .GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser) .GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount) @@ -178,7 +178,7 @@ public class SystemRouter { .POST("/api/messages", messageHandler::createMessage) .PUT("/api/messages/{id}/read", messageHandler::markAsRead) .DELETE("/api/messages/{id}", messageHandler::deleteMessage) - + // ========== 文件路由 ========== .GET("/api/files", fileHandler::getAllFiles) .GET("/api/files/{id}", fileHandler::getFileById) @@ -188,7 +188,7 @@ public class SystemRouter { .GET("/api/files/{id}/preview", fileHandler::previewFile) .GET("/api/files/preview/{fileName}", fileHandler::previewFileByName) .DELETE("/api/files/{id}", fileHandler::deleteFile) - + // ========== 权限路由 ========== .GET("/api/permissions", permissionHandler::getAllPermissions) .GET("/api/permissions/{id}", permissionHandler::getPermissionById) @@ -199,52 +199,33 @@ public class SystemRouter { .PUT("/api/permissions/{id}", permissionHandler::updatePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission) - // ========== 会员卡管理路由 ========== - // 会员卡类型 - // 1. 获取所有会员卡类型 - .GET("/api/memberCard/active", memberCardHandler::getActiveCards) - // 2. 获取会员卡详情 - .GET("/api/memberCard/{memberCardId}", memberCardHandler::getMemberCardById) - // 3. 条件查询会员卡列表 - .GET("/api/memberCard", memberCardHandler::getMemberCardList) - // 4. 新增/更新会员卡 - .POST("/api/memberCard", memberCardHandler::saveMemberCard) - // 5. 逻辑删除会员卡 - .DELETE("/api/memberCard/{memberCardId}", memberCardHandler::deleteMemberCard) - // 会员卡持卡 - // 1. 会员购卡/发卡 - .POST("/api/memberCardRecord", memberCardRecordHandler::insertActiveRecord) - // 2. 会员端“我的卡包” - 按会员ID获取有效卡 - .GET("/api/memberCardRecord/member/{memberId}/active", memberCardRecordHandler::getMyCards) - // 3. 管理端按会员ID分页查所有卡记录 - .GET("/api/memberCardRecord/member/{memberId}", memberCardRecordHandler::getMemberCardRecords) - // 4. 到期扫描 - .GET("/api/memberCardRecord/expired", memberCardRecordHandler::getExpiredCards) - // 5. 扣次/扣费 - .POST("/api/memberCardRecord/{id}/deduct", memberCardRecordHandler::deductUsage) - // 6. 续费 - .POST("/api/memberCardRecord/{id}/renew", memberCardRecordHandler::renewCard) - // 7. 状态变更(过期/退款) - .PUT("/api/memberCardRecord/{id}/status", memberCardRecordHandler::updateStatus) - // 8. 验证次卡 - .GET("/api/memberCardRecord/{id}/validate/count", memberCardRecordHandler::validateCountCard) - // 9. 验证储值卡 - .GET("/api/memberCardRecord/{id}/validate/stored", memberCardRecordHandler::validateStoredCard) - // 会员卡交易 - // 1. 插入流水记录 - .POST("/api/transactions", memberCardTransactionHandler::insertTransaction) - // 2. 后台条件分页查询流水(带多个查询参数) - .GET("/api/transactions", memberCardTransactionHandler::getTransactionsWithConditions) - // 3. 按会员ID查询使用记录(分页 + 时间范围) - .GET("/api/transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions) - // 4. 按卡ID查询所有流水记录 - .GET("/api/transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId) - // 5. 统计某卡种总扣次数 - .GET("/api/transactions/statistics/deduct/card/{cardId}", memberCardTransactionHandler::getDeductCountByCardId) - // 6. 统计某时间段续费总金额 - .GET("/api/transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange) - // 7. 统计某会员购卡总金额 - .GET("/api/transactions/statistics/purchase/member/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember) + // ======================================== + // ========== 会员卡管理路由 ============== + // ======================================== + + // ===== 会员卡类型管理 ===== + .GET("/api/member-cards/active", memberCardHandler::getActiveCards) + .GET("/api/member-cards/{memberCardId}", memberCardHandler::getMemberCardById) + .POST("/api/member-cards", memberCardHandler::createMemberCard) + + // ===== 会员卡记录管理(核心业务)===== + .POST("/api/member-card-records/purchase", memberCardRecordHandler::purchaseCard) + .POST("/api/member-card-records/{recordId}/renew", memberCardRecordHandler::renewCard) + .POST("/api/member-card-records/{recordId}/use", memberCardRecordHandler::useCard) + .POST("/api/member-card-records/{recordId}/refund", memberCardRecordHandler::refundCard) + .GET("/api/member-card-records/my-cards/{memberId}", memberCardRecordHandler::getMyCards) + .GET("/api/member-card-records/{recordId}", memberCardRecordHandler::getMemberCardRecordById) + .POST("/api/member-card-records/process-expired", memberCardRecordHandler::processExpiredCards) + + // ===== 会员卡交易流水管理 ===== + .POST("/api/member-card-transactions", memberCardTransactionHandler::insertTransaction) + .GET("/api/member-card-transactions", memberCardTransactionHandler::getTransactionsWithConditions) + .GET("/api/member-card-transactions/member/{memberId}", memberCardTransactionHandler::getMemberTransactions) + .GET("/api/member-card-transactions/card/{cardId}", memberCardTransactionHandler::getTransactionsByCardId) + .GET("/api/member-card-transactions/statistics/deduct/{cardId}", memberCardTransactionHandler::getDeductCountByCardId) + .GET("/api/member-card-transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange) + .GET("/api/member-card-transactions/statistics/purchase/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember) + .build(); } } -- 2.52.0 From 85ed6f91966736ba90de573d5073178eed0ba2b1 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 26 May 2026 23:50:50 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-member/pom.xml | 12 +- .../member/config/WechatProperties.java | 3 - .../manage/member/dto/SearchMemberDto.java | 23 ++ .../manage/member/entity/SignInRecord.java | 38 ++++ .../gym/manage/member/es/entity/MemberES.java | 35 +++ .../es/repository/MemberESRepository.java | 20 ++ .../manage/member/handler/MemberHandler.java | 209 +++++++++++++++--- .../member/handler/WechatAuthHandler.java | 6 +- .../handler/WechatOfficialEventHandler.java | 94 ++++---- ...Repository.java => IMemberRepository.java} | 10 +- .../manage/member/service/MemberService.java | 21 ++ .../service/impl/MemberServiceImpl.java | 69 +++++- .../service/impl/WechatAuthServiceImpl.java | 32 ++- .../impl/WechatOfficialServiceImpl.java | 37 +++- .../gym/manage/member/util/EsSyncUtils.java | 159 +++++++++++++ .../{application.yml => member-config.yml} | 8 +- .../gym/manage/app/ManageApplication.java | 11 +- .../gym/manage/app/config/SystemRouter.java | 7 +- .../src/main/resources/application-dev.yml | 2 +- .../src/main/resources/application-local.yml | 2 +- .../src/main/resources/application-test.yml | 2 +- .../src/main/resources/application.yml | 6 +- .../filter/JwtAuthenticationFilter.java | 1 - .../gym/manage/sys/config/SecurityConfig.java | 3 +- .../sys/dto/request/UserUpdateRequest.java | 2 - gym-manage-api/pom.xml | 1 + gym-manage-web/pnpm-lock.yaml | 95 ++++++-- 27 files changed, 774 insertions(+), 134 deletions(-) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/{MemberRepository.java => IMemberRepository.java} (66%) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java rename gym-manage-api/gym-member/src/main/resources/{application.yml => member-config.yml} (74%) diff --git a/gym-manage-api/gym-member/pom.xml b/gym-manage-api/gym-member/pom.xml index 00d2673..c74a4c3 100644 --- a/gym-manage-api/gym-member/pom.xml +++ b/gym-manage-api/gym-member/pom.xml @@ -108,8 +108,7 @@ com.github.binarywang weixin-java-miniapp 4.6.0 - - + com.github.binarywang weixin-java-mp 4.6.0 @@ -129,6 +128,15 @@ jjwt-jackson runtime + + cn.hutool + hutool-all + 5.8.25 + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java index 9527e6b..ec32292 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/WechatProperties.java @@ -2,7 +2,6 @@ package cn.novalon.gym.manage.member.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; /** @@ -15,8 +14,6 @@ import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "wechat") -// 指定属性文件位置(当前在gym-member模块的application.yml) -@PropertySource(value = "classpath:application.yml", ignoreResourceNotFound = true) public class WechatProperties { // 小程序配置 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java new file mode 100644 index 0000000..bcd528d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java @@ -0,0 +1,23 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SearchMemberDto { + + // 搜索字段 - 包括 会员号、昵称、手机号 + private String searchValue; + + // 排序 + private String filter; + + // 页码 + private Integer pageNum = 1; + + // 页大小 + private Integer pageSize = 10; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java new file mode 100644 index 0000000..83dae3a --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java @@ -0,0 +1,38 @@ +package cn.novalon.gym.manage.member.entity; + +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +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.LocalDate; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table("sign_in_record") +public class SignInRecord { + + @Id + private Long id; + + // 会员ID + @Column("member_id") + private Long memberId; + + // 签到日期 + @Column("sign_in_date") + private LocalDate signInDate; + + // 签到时间 + @Column("sign_in_time") + private LocalDateTime signInTime; + + // 创建时间 + @CreatedDate + @Column("created_at") + private LocalDateTime createdAt; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java new file mode 100644 index 0000000..b479ce5 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java @@ -0,0 +1,35 @@ +package cn.novalon.gym.manage.member.es.entity; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Data +@Document(indexName = "gym_members") +public class MemberES { + + @Id + private String id; + + // 会员号 - 需要搜索(精确匹配) + @Field(type = FieldType.Keyword) + private String memberNo; + + // 昵称 - 需要搜索(模糊搜索) + @Field(type = FieldType.Text) + private String nickname; + + // 手机号 - 需要搜索(精确匹配) + @Field(type = FieldType.Keyword) + private String phone; + + // 性别 - 用于筛选 + @Field(type = FieldType.Integer) + private Integer gender; + + // 头像 - 列表展示 + @Field(type = FieldType.Keyword) + private String avatar; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java new file mode 100644 index 0000000..a233097 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java @@ -0,0 +1,20 @@ +package cn.novalon.gym.manage.member.es.repository; + +import cn.novalon.gym.manage.member.es.entity.MemberES; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +/** + * ES 会员数据访问层 + */ +@Repository +public interface MemberESRepository extends ReactiveElasticsearchRepository { + + /** + * 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配) + */ + Flux findByMemberNoOrPhoneOrNicknameContainingAndGender( + String memberNo, String phone, String nickname,String gender, Pageable pageable); +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index a54c8eb..fbd6e99 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -1,13 +1,19 @@ package cn.novalon.gym.manage.member.handler; +import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto; +import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; +import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; @@ -16,7 +22,7 @@ import reactor.core.publisher.Mono; /** * 会员信息处理器 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -29,22 +35,24 @@ public class MemberHandler { private final MemberService memberService; private final WechatAuthService wechatAuthService; private final WechatOfficialService wechatOfficialService; + private final JwtTokenProvider jwtTokenProvider; + private final WechatProperties wechatProperties; /** * 获取会员信息 - * + * * GET /api/member/info * Header: X-Member-Id: 123 */ public Mono getMemberInfo(ServerRequest request) { - + String memberIdStr = request.headers().firstHeader("X-Member-Id"); long memberId = NumberUtils.toLong(memberIdStr,0L); if (memberId <= 0) throw new IllegalArgumentException("获取会员信息失败: memberId 无效"); - + log.info("获取会员信息, memberId: {}", memberId); - + return memberService.getMemberInfo(memberId) .flatMap(info -> { return ServerResponse.ok() @@ -55,7 +63,7 @@ public class MemberHandler { /** * 更新会员信息 - * + * * PUT /api/member/info * Header: X-Member-Id: 123 * Body: { @@ -67,14 +75,14 @@ public class MemberHandler { * } */ public Mono updateMemberInfo(ServerRequest request) { - + String memberIdStr = request.headers().firstHeader("X-Member-Id"); long memberId = NumberUtils.toLong(memberIdStr, 0L); if (memberId <= 0) throw new IllegalArgumentException("更新会员信息失败: memberId 无效"); - + log.info("更新会员信息, memberId: {}", memberId); - + return request.bodyToMono(UpdateMemberInfoDto.class) .flatMap(updateDto -> memberService.updateMemberInfo(memberId, updateDto)) .flatMap(info -> ServerResponse.ok() @@ -84,23 +92,23 @@ public class MemberHandler { /** * 绑定手机号(微信小程序) - * + * * POST /api/member/phone/bind?code=PHONE_CODE * Header: X-Member-Id: 123 */ public Mono bindPhone(ServerRequest request) { - + String memberIdStr = request.headers().firstHeader("X-Member-Id"); Long memberId = NumberUtils.toLong(memberIdStr, 0L); - + if (memberId <= 0) throw new IllegalArgumentException("绑定手机号失败: memberId 无效"); - + String phoneCode = request.queryParam("phoneCode").orElse(""); - + if (phoneCode == null || phoneCode.trim().isEmpty()) throw new IllegalArgumentException("手机号code不能为空"); - + log.info("收到绑定手机号请求, memberId: {}, phoneCode: {}", memberId, phoneCode); - + return wechatAuthService.bindPhone(memberId, phoneCode) .flatMap(success -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) @@ -109,20 +117,20 @@ public class MemberHandler { /** * 查询服务号关注状态 - * + * * GET /api/member/subscribe/status * Header: X-Member-Id: 123 - * + * */ public Mono checkSubscribeStatus(ServerRequest request) { - + String memberIdStr = request.headers().firstHeader("X-Member-Id"); long memberId = NumberUtils.toLong(memberIdStr,0L); if (memberId <= 0) throw new IllegalArgumentException("查询服务号关注状态失败: memberId 无效"); - + log.info("查询服务号关注状态, memberId: {}", memberId); - + return wechatOfficialService.checkSubscribeStatus(memberId) .flatMap(subscribed -> { return ServerResponse.ok() @@ -133,30 +141,30 @@ public class MemberHandler { /** * 管理员更新手机号 - * - * POST /api/admin/members/123/phone + * + * POST /api/admin/member/123/phone * Body: { "phone": "13800138000" } - * + * */ public Mono adminUpdatePhone(ServerRequest request) { - + String memberIdStr = request.pathVariable("id"); long memberId = NumberUtils.toLong(memberIdStr, 0L); - + if (memberId <= 0) throw new IllegalArgumentException("更新手机号失败: memberId 无效"); - + log.info("收到更新手机号请求, memberId: {}", memberId); - + return request.bodyToMono(AdminUpdatePhoneDto.class) .flatMap(body -> { String phone = body.getPhone(); - + if (phone == null || phone.isEmpty()) return Mono.error(new IllegalArgumentException("手机号不能为空")); - + if (!phone.matches("^1[3-9]\\d{9}$")) return Mono.error(new IllegalArgumentException("手机号格式不正确")); log.info("开始更新手机号, memberId: {}, phone: {}", memberId, phone); - + return memberService.adminUpdatePhone(memberId, phone); }) .flatMap(success -> { @@ -167,4 +175,143 @@ public class MemberHandler { }); } + /** + * 前台查看会员信息 + * + * GET /api/admin/member/{id} + * header: { "Authorization": "xxx" } + * + */ + public Mono adminGetMemberInfo(ServerRequest request) { + + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) throw new IllegalArgumentException("无权访问"); + authorization = authorization.substring(7); + // 验证token并获取memberId + if (!jwtTokenProvider.validateToken(authorization)) throw new IllegalArgumentException("Authorization 无效"); + + String memberIdStr = request.pathVariable("id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); + + Long adminId = jwtTokenProvider.getUserIdFromToken(authorization); + + // TODO 多表查询:会员信息、团课信息、会员卡信息 + + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue("成功"); + } + + /** + * 前台编辑会员信息 + * + * PUT /api/admin/member/{id} + * header: { "Authorization": "xxx" } + * Body:{"字段","值"} + */ + public Mono adminUpdateMemberInfo(ServerRequest request) { + + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) throw new IllegalArgumentException("无权访问"); + authorization = authorization.substring(7); + // 验证token并获取memberId + if (!jwtTokenProvider.validateToken(authorization)) throw new IllegalArgumentException("Authorization 无效"); + + String memberIdStr = request.pathVariable("id"); + long memberId = NumberUtils.toLong(memberIdStr, 0L); + if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); + + Long adminId = jwtTokenProvider.getUserIdFromToken(authorization); + + // TODO 多表查询:会员信息、团课信息、会员卡信息 + + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue("成功"); + } + + /** + * 前台搜索会员列表 + * + * GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10 + * header: { "Authorization": "Bearer xxx" } + */ + public Mono searchMembers(ServerRequest request) { + + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("无权访问"); + } + authorization = authorization.substring(7); + if (!jwtTokenProvider.validateToken(authorization)) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("Authorization 无效"); + } + + String keyword = request.queryParam("searchValue").orElse(null); + String filter = request.queryParam("filter").orElse(null); + int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); + int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + + return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize)) + .map(member -> { + // 解密手机号 + if (member.getPhone() != null && !member.getPhone().isEmpty()) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); + member.setPhone(decryptedPhone); + } catch (Exception e) { + log.error("手机号解密失败, memberId: {}", member.getId(), e); + member.setPhone(null); + } + } + return member; + }) + .collectList() + .flatMap(list -> ServerResponse.ok().bodyValue(list)); + } + + + /** + * 前台查看会员列表 + * + * GET /api/admin/members/all?pageNum=1&pageSize=10 + * header: { "Authorization": "Bearer xxx" } + */ + public Mono getAllMembers(ServerRequest request) { + + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("无权访问"); + } + authorization = authorization.substring(7); + if (!jwtTokenProvider.validateToken(authorization)) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("Authorization 无效"); + } + + int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); + int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + + return memberService.findAll(pageNum, pageSize) + .map(member -> { + // 解密手机号 + if (member.getPhone() != null && !member.getPhone().isEmpty()) { + try { + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); + member.setPhone(decryptedPhone); + } catch (Exception e) { + log.error("手机号解密失败, memberId: {}", member.getId(), e); + member.setPhone(null); + } + } + return member; + }) + .collectList() + .flatMap(list -> ServerResponse.ok().bodyValue(list)); + } + } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java index 7ba7fa8..13cfd53 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java @@ -11,7 +11,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; /** - * 微信֤ + * 微信认证 * * @author 付嘉 * @date 2026-05-01 @@ -51,7 +51,7 @@ public class WechatAuthHandler { } /** - * 更新ص + * 公众号回调 * * POST /api/member/auth/mp/callback * Body: subscribeopenid @@ -61,7 +61,7 @@ public class WechatAuthHandler { return wechatOfficialEventHandler.handleEvent(request); } - // ֤微信ŷǩ + // 验证微信公众号签名 public Mono verifyMpSignature(ServerRequest request) { return wechatOfficialEventHandler.verifySignature(request); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java index d35ef87..d01e989 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java @@ -15,7 +15,7 @@ import java.security.MessageDigest; import java.util.Arrays; /** - * 微信ŷ更新 + * 微信公众号事件处理器 * * @author 付嘉 * @date 2026-05-01 @@ -30,30 +30,30 @@ public class WechatOfficialEventHandler { private final WechatProperties wechatProperties; /** - * 微信ŷ͵更新 + * 处理微信公众号事件 * - * ʽXML - * Ӧʽsuccess 头像地址 + * 请求格式:XML + * 响应格式:success 或 回复消息内容 */ public Mono handleEvent(ServerRequest request) { return request.bodyToMono(String.class) .flatMap(xmlBody -> { - log.info("收到微信ŷ {}", xmlBody); + log.info("收到微信公众号事件 {}", xmlBody); - // TODO: XML为WechatOfficialEventDto - // Ŀǰ򻯴ֱ获取openidevent + // TODO: 将XML解析为WechatOfficialEventDto + // 目前简化处理直接获取openId和event String openId = extractOpenId(xmlBody); String event = extractEvent(xmlBody); if (openId == null || event == null) { - log.error("޷微信更新"); + log.error("无法解析微信公众号事件"); return ServerResponse.badRequest().bodyValue("error"); } - log.info(" openId={}, event={}", openId, event); + log.info("处理事件 openId={}, event={}", openId, event); - // 更新ʹ + // 根据事件类型处理 if ("subscribe".equals(event)) { return wechatOfficialService.handleSubscribeEvent(openId) .then(ServerResponse.ok() @@ -65,24 +65,24 @@ public class WechatOfficialEventHandler { .contentType(MediaType.TEXT_PLAIN) .bodyValue("success")); } else { - log.warn("δ֪更新: {}", event); + log.warn("未知事件类型: {}", event); return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) .bodyValue("success"); } }) .onErrorResume(e -> { - log.error("微信更新失败", e); + log.error("处理微信公众号事件失败", e); return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) - .bodyValue("success"); // ʹ失败Ҳsuccess微信 + .bodyValue("success"); // 即使处理失败也返回success避免微信重试 }); } /** - * ֤微信ŷǩ + * 验证微信公众号签名 * - * GET֤头像地址 + * GET请求用于验证服务器地址 */ public Mono verifySignature(ServerRequest request) { String signature = request.queryParam("signature").orElse(""); @@ -90,27 +90,27 @@ public class WechatOfficialEventHandler { String nonce = request.queryParam("nonce").orElse(""); String echostr = request.queryParam("echostr").orElse(""); - log.info("========== 微信ǩ֤=========="); - log.info("收到IJ:"); + log.info("========== 微信公众号签名验证 =========="); + log.info("收到的参数:"); log.info(" signature: {}", signature); log.info(" timestamp: {}", timestamp); log.info(" nonce: {}", nonce); log.info(" echostr: {}", echostr); - // 获取õToken + // 获取配置的Token String token = wechatProperties.getMp().getToken(); - log.info("õToken: {}", token); + log.info("配置的Token: {}", token); - // ֤ǩ + // 验证签名 if (checkSignature(signature, timestamp, nonce, token)) { - log.info("ǩ֤成功echostr: {}", echostr); - log.info("========== 微信ǩ֤ =========="); + log.info("签名验证成功,返回echostr: {}", echostr); + log.info("========== 微信公众号签名验证结束 =========="); return ServerResponse.ok() .contentType(MediaType.TEXT_PLAIN) .bodyValue(echostr); } else { - log.warn("ǩ֤失败"); - log.info("========== 微信ǩ֤ =========="); + log.warn("签名验证失败"); + log.info("========== 微信公众号签名验证结束 =========="); return ServerResponse.badRequest() .contentType(MediaType.TEXT_PLAIN) .bodyValue("error"); @@ -118,38 +118,38 @@ public class WechatOfficialEventHandler { } /** - * ֤ǩ + * 验证签名 * - * @param signature 微信żǩ - * @param timestamp 创建时间 - * @param nonce + * @param signature 微信加密签名 + * @param timestamp 时间戳 + * @param nonce 随机数 * @param token Token - * @return Ƿ֤通过 + * @return 是否验证通过 */ private boolean checkSignature(String signature, String timestamp, String nonce, String token) { - // 1. tokentimestampnonceֵ + // 1. 将token、timestamp、nonce三个参数进行字典序排序 String[] arr = new String[]{token, timestamp, nonce}; Arrays.sort(arr); - // 2. 头像地址ƴӳһ头像地址 + // 2. 将三个参数字符串拼接成一个字符串 StringBuilder sb = new StringBuilder(); for (String str : arr) { sb.append(str); } - // 3. ƴӺ头像地址sha1 + // 3. 将拼接后的字符串进行sha1加密 String encrypted = sha1(sb.toString()); - log.debug("õǩ {}", encrypted); + log.debug("计算的签名 {}", encrypted); - // 4. ܺ头像地址signature会员 + // 4. 将加密后的字符串与signature对比 return encrypted != null && encrypted.equalsIgnoreCase(signature); } /** - * SHA1 + * SHA1加密 * - * @param str 头像地址 - * @return ܺ头像地址 + * @param str 待加密字符串 + * @return 加密后字符串 */ private String sha1(String str) { try { @@ -165,51 +165,51 @@ public class WechatOfficialEventHandler { } return hexString.toString(); } catch (Exception e) { - log.error("SHA1失败", e); + log.error("SHA1加密失败", e); return null; } } /** - * XML OpenID + * 从XML中提取OpenID */ private String extractOpenId(String xml) { int start = xml.indexOf(""); int end = xml.indexOf(""); if (start != -1 && end != -1) { String value = xml.substring(start + 14, end); - // ȥ CDATA + // 去除 CDATA 标记 return cleanCdata(value); } return null; } /** - * XML 获取更新 + * 从XML中获取事件类型 */ private String extractEvent(String xml) { int start = xml.indexOf(""); int end = xml.indexOf(""); if (start != -1 && end != -1) { String value = xml.substring(start + 7, end); - // ȥ CDATA + // 去除 CDATA 标记 return cleanCdata(value); } return null; } /** - * CDATA - * : -> subscribe + * 清理 CDATA 标记 + * 例如: -> subscribe */ private String cleanCdata(String value) { if (value == null) { return null; } - // ȥǰհ + // 去除前后空白 value = value.trim(); - // CDATA获取м - // ʽ: + // 提取 CDATA 中间内容 + // 格式: if (value.startsWith("")) { return value.substring(9, value.length() - 3); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java similarity index 66% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java index 1e248cf..e746aea 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java @@ -1,8 +1,10 @@ package cn.novalon.gym.manage.member.repository; import cn.novalon.gym.manage.member.entity.Member; +import org.springframework.data.domain.Pageable; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -12,7 +14,7 @@ import reactor.core.publisher.Mono; */ @Repository -public interface MemberRepository extends R2dbcRepository { +public interface IMemberRepository extends R2dbcRepository { // UnionID查询会员 Mono findByUnionId(String unionId); @@ -25,4 +27,10 @@ public interface MemberRepository extends R2dbcRepository { // 手机号查询 Mono findByPhone(String phone); + + /** + * 分页查询所有会员 + * 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有 + */ + Flux findAllBy(Pageable pageable); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java index aa4ea33..c659f1b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java @@ -1,7 +1,11 @@ package cn.novalon.gym.manage.member.service; +import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.es.entity.MemberES; import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -37,4 +41,21 @@ public interface MemberService { * @return 是否成功 */ Mono adminUpdatePhone(Long memberId, String phone); + + /** + * 管理端查询会员(es查询) + * + * @param searchMemberDto 会员信息dto + * @return 会员信息 + */ + Flux searchMember(SearchMemberDto searchMemberDto); + + /** + * 管理端查询所有会员 + * + * @param pageNum 页码 + * @param pageSize 页大小 + * @return 所有会员信息 + */ + Flux findAll(Integer pageNum, Integer pageSize); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index d9c3e4c..cfd1517 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -1,17 +1,27 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; -import cn.novalon.gym.manage.member.vo.MemberInfoVO; import cn.novalon.gym.manage.member.entity.Member; -import cn.novalon.gym.manage.member.repository.MemberRepository; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; @@ -27,7 +37,17 @@ import java.time.LocalDateTime; @RequiredArgsConstructor public class MemberServiceImpl implements MemberService { - private final MemberRepository memberRepository; + private final IMemberRepository memberRepository; + private final MemberESRepository memberESRepository; + private final EsSyncUtils esSyncUtils; + private final WechatProperties wechatProperties; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } @Value("${wechat.aes.secret-key:}") private String aesSecretKey; @@ -69,6 +89,7 @@ public class MemberServiceImpl implements MemberService { return memberRepository.save(member); }) + .doOnSuccess(memberSyncer::sync) .map(savedMember -> { log.info("会员信息更新成功, memberId: {}", savedMember.getId()); return buildMemberInfoResponse(savedMember); @@ -130,7 +151,46 @@ public class MemberServiceImpl implements MemberService { return updateMemberPhone(memberId, encryptedPhone); })); } - + + @Override + public Flux searchMember(SearchMemberDto searchMemberDto) { + + String searchValue = searchMemberDto.getSearchValue(); + + // 1. 处理手机号加密 + if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + searchValue = AesUtil.encrypt(searchValue,secretKey,iv); + } + + // 2. 分页参数 + Pageable pageable = PageRequest.of( + searchMemberDto.getPageNum() - 1, + searchMemberDto.getPageSize(), + Sort.by(Sort.Direction.DESC, "update_at") + ); + + // 3. 调用 Repository 查询 + return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender( + searchValue, + searchValue, + searchValue, + searchMemberDto.getFilter() , + pageable + ); + } + + @Override + public Flux findAll(Integer pageNum, Integer pageSize) { + Pageable pageable = PageRequest.of( + pageNum - 1, + pageSize + ); + + return memberRepository.findAllBy(pageable); + } + // 更新会员手机号 private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) @@ -139,6 +199,7 @@ public class MemberServiceImpl implements MemberService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .map(savedMember -> { log.info("手机号录入成功, memberId: {}", savedMember.getId()); return true; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 3d5fe99..1a858d7 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -3,16 +3,20 @@ package cn.novalon.gym.manage.member.service.impl; import cn.novalon.gym.manage.common.config.JwtProperties; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.WechatLoginDto; -import cn.novalon.gym.manage.member.vo.WechatLoginVO; import cn.novalon.gym.manage.member.entity.Member; -import cn.novalon.gym.manage.member.repository.MemberRepository; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.service.WechatApiService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.MemberNoGenerator; import cn.novalon.gym.manage.member.util.WechatPhoneUtil; +import cn.novalon.gym.manage.member.vo.WechatLoginVO; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -38,10 +42,20 @@ import java.util.Map; public class WechatAuthServiceImpl implements WechatAuthService { private final WechatApiService wechatApiService; - private final MemberRepository memberRepository; + private final IMemberRepository memberRepository; private final JwtProperties jwtProperties; private final WechatProperties wechatProperties; private final WechatPhoneUtil wechatPhoneUtil; + private final MemberESRepository memberESRepository; + private final EsSyncUtils esSyncUtils; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } + /** * 小程序登录 - 通过微信 code 完成登录 @@ -72,6 +86,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -81,6 +96,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -96,6 +112,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -114,6 +131,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -182,6 +200,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setPhone(encryptedPhone); member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .map(savedMember -> { log.info("更新会员手机号成功, memberId: {}", savedMember.getId()); return true; @@ -263,9 +282,9 @@ public class WechatAuthServiceImpl implements WechatAuthService { // Step 3: 保存 Member return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { log.info("保存 Member 成功, id: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo()); - // Step 4: 如果有 phoneCode,尝试获取手机号 if (phoneCode != null && !phoneCode.isEmpty()) { log.info("检测到 phoneCode,尝试获取手机号"); @@ -278,7 +297,10 @@ public class WechatAuthServiceImpl implements WechatAuthService { // 为新会员绑定手机号 savedMember.setPhone(encryptedPhone); return memberRepository.save(savedMember) - .doOnSuccess(m -> log.info("新用户手机号绑定成功")) + .doOnSuccess(memberSyncer::sync) + .doOnSuccess(m -> { + log.info("新用户手机号绑定成功"); + }) .thenReturn(buildLoginResponse(savedMember, true, sessionKey)); } else { log.warn("未获取到手机号"); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index 4e3a3d0..018c200 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -1,12 +1,16 @@ package cn.novalon.gym.manage.member.service.impl; import cn.novalon.gym.manage.member.config.WechatProperties; -import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; import cn.novalon.gym.manage.member.entity.Member; -import cn.novalon.gym.manage.member.repository.MemberRepository; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.service.WechatOfficialService; +import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -30,12 +34,22 @@ import java.util.Map; @RequiredArgsConstructor public class WechatOfficialServiceImpl implements WechatOfficialService { - private final MemberRepository memberRepository; + private final IMemberRepository memberRepository; private final WechatProperties wechatProperties; private final WebClient webClient; + private final MemberESRepository memberESRepository; private final ObjectMapper objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private final EsSyncUtils esSyncUtils; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } + /** * 处理关注事件 */ @@ -68,6 +82,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { } return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) .then(sendWelcomeMessage(openId)); } else { log.info("老用户关注服务号: memberId={}", existingMember.getId()); @@ -75,6 +90,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) .then(sendWelcomeMessage(openId)); } }) @@ -88,6 +104,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) .then(sendWelcomeMessage(openId)); }) .switchIfEmpty(Mono.defer(() -> { @@ -105,6 +122,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(existingMember) + .doOnSuccess(memberSyncer::sync) .then(sendWelcomeMessage(openId)); }) .switchIfEmpty(Mono.defer(() -> { @@ -129,7 +147,9 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { log.info("找到会员,更新为未关注状态, memberId: {}", member.getId()); member.setSubscribed(false); member.setLastLoginAt(LocalDateTime.now()); - return memberRepository.save(member); + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .then(); }) .then() .switchIfEmpty(Mono.defer(() -> { @@ -192,8 +212,8 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { if (officialOpenId != null && !officialOpenId.isEmpty()) { member.setOfficialOpenId(officialOpenId); } - return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) .map(savedMember -> { log.info("关联成功, memberId: {}", savedMember.getId()); return true; @@ -234,10 +254,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { .build(); log.info("新用户关注服务号,仅保存标识信息(UnionID和OpenID)"); - return memberRepository.save(member) - .doOnSuccess(savedMember -> - log.info("从服务号创建新会员成功, memberId: {}", savedMember.getId())) + .doOnSuccess(memberSyncer::sync) + .doOnSuccess(savedMember -> { + log.info("从服务号创建新会员成功, memberId: {}", savedMember.getId()); + }) .then(); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java new file mode 100644 index 0000000..e4db37d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/EsSyncUtils.java @@ -0,0 +1,159 @@ +package cn.novalon.gym.manage.member.util; + +import cn.hutool.core.bean.BeanUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 通用 ES 同步工具类 + * + * 使用方式: + * + * 1. 注入工具类 + * @Autowired private EsSyncUtils esSyncUtils; + * + * 2. 同步数据到 ES(不返回结果,适合 doOnSuccess) + * esSyncUtils.sync(Member.class, MemberES.class, member, memberESRepository); + * + * 3. 同步数据到 ES(返回 Mono,适合链式调用) + * esSyncUtils.syncToES(Member.class, MemberES.class, member, memberESRepository).subscribe(); + * + * 4. 如果 Repository 是单例,可以先绑定 + * var syncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + * syncer.sync(member); + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class EsSyncUtils { + + /** + * 同步实体到 ES(不返回结果,适合 doOnSuccess) + * + * @param sourceClass 源实体类(如 Member.class) + * @param targetClass 目标ES实体类(如 MemberES.class) + * @param source 源实体对象 + * @param repository ES Repository + * @param 源实体类型 + * @param ES实体类型 + * @param ID类型 + */ + public void sync(Class sourceClass, Class targetClass, + S source, ReactiveElasticsearchRepository repository) { + if (source == null) { + log.warn("同步 ES 失败:源实体为空"); + return; + } + + try { + T target = BeanUtil.toBean(source, targetClass); + repository.save(target).subscribe( + success -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()), + error -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), error) + ); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + } + } + + /** + * 同步实体到 ES(返回 Mono,适合链式调用) + * + * @param sourceClass 源实体类 + * @param targetClass 目标ES实体类 + * @param source 源实体对象 + * @param repository ES Repository + * @return Mono + */ + public Mono syncToES(Class sourceClass, Class targetClass, + S source, ReactiveElasticsearchRepository repository) { + if (source == null) { + log.warn("同步 ES 失败:源实体为空"); + return Mono.empty(); + } + + try { + T target = BeanUtil.toBean(source, targetClass); + return repository.save(target) + .doOnSuccess(t -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName())) + .doOnError(e -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), e)) + .then(); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + return Mono.empty(); + } + } + + /** + * 绑定 Repository,返回一个针对特定实体类型的同步器 + * + * @param sourceClass 源实体类 + * @param targetClass 目标ES实体类 + * @param repository ES Repository + * @return 实体同步器 + */ + public EntitySyncer bind(Class sourceClass, Class targetClass, + ReactiveElasticsearchRepository repository) { + return new EntitySyncer<>(sourceClass, targetClass, repository); + } + + /** + * 实体同步器(绑定特定类型的同步器,避免重复传 Class) + * + * @param 源实体类型 + * @param ES实体类型 + * @param ID类型 + */ + public static class EntitySyncer { + private final Class sourceClass; + private final Class targetClass; + private final ReactiveElasticsearchRepository repository; + + public EntitySyncer(Class sourceClass, Class targetClass, + ReactiveElasticsearchRepository repository) { + this.sourceClass = sourceClass; + this.targetClass = targetClass; + this.repository = repository; + } + + /** + * 同步(不返回结果) + */ + public void sync(S source) { + if (source == null) return; + try { + T target = BeanUtil.toBean(source, targetClass); + repository.save(target).subscribe( + success -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()), + error -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), error) + ); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + } + } + + /** + * 同步(返回 Mono) + */ + public Mono syncMono(S source) { + if (source == null) return Mono.empty(); + try { + T target = BeanUtil.toBean(source, targetClass); + return repository.save(target) + .doOnSuccess(t -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName())) + .doOnError(e -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), e)) + .then(); + } catch (Exception e) { + log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}", + sourceClass.getSimpleName(), targetClass.getSimpleName(), e); + return Mono.empty(); + } + } + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/resources/application.yml b/gym-manage-api/gym-member/src/main/resources/member-config.yml similarity index 74% rename from gym-manage-api/gym-member/src/main/resources/application.yml rename to gym-manage-api/gym-member/src/main/resources/member-config.yml index 789379f..8c457f6 100644 --- a/gym-manage-api/gym-member/src/main/resources/application.yml +++ b/gym-manage-api/gym-member/src/main/resources/member-config.yml @@ -14,5 +14,9 @@ wechat: callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback # 手机号加密配置 phone-encryption: - secret-key: dGVzdF9zZWNyZXRfa2V5X2Zvcl9waG9uZV9lbmNyeXB0aW9uMTI= - iv: dGVzdF9pdl9mb3JfcGhvbmU= + secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY= + iv: LMpG6Ih9mmfEAALOCeIJBw== + +spring: + elasticsearch: + uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔) diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index cb9a6db..f2daa1f 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -7,9 +7,8 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.web.server.WebFilter; @@ -17,8 +16,12 @@ import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) -@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", - "cn.novalon.gym.manage.sys.audit.repository", "cn.novalon.gym.manage.member.repository" }) +@EnableR2dbcRepositories(basePackages = { + "cn.novalon.gym.manage.db.dao", + "cn.novalon.gym.manage.sys.audit.repository", + "cn.novalon.gym.manage.member.repository" +}) +@EnableReactiveElasticsearchRepositories("cn.novalon.gym.manage.member.es.repository") public class ManageApplication { private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); 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 12d6cd0..6947791 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 @@ -209,8 +209,11 @@ public class SystemRouter { .GET("/api/member/subscribe/status", memberHandler::checkSubscribeStatus) // ========== 会员模块路由 - 管理端 ========== - .POST("/api/admin/members/{id}/phone", memberHandler::adminUpdatePhone) - + .POST("/api/admin/member/{id}/phone", memberHandler::adminUpdatePhone) + .GET("/api/admin/member/{id}", memberHandler::adminGetMemberInfo) + .PUT("/api/admin/member/{id}", memberHandler::adminUpdateMemberInfo) + .GET("/api/admin/members", memberHandler::searchMembers) + .GET("/api/admin/members/all", memberHandler::getAllMembers) .build(); } } diff --git a/gym-manage-api/manage-app/src/main/resources/application-dev.yml b/gym-manage-api/manage-app/src/main/resources/application-dev.yml index 33e428e..443a17e 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-dev.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-dev.yml @@ -15,7 +15,7 @@ spring: url: jdbc:postgresql://localhost:55432/manage_system user: novalon password: novalon123 - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true diff --git a/gym-manage-api/manage-app/src/main/resources/application-local.yml b/gym-manage-api/manage-app/src/main/resources/application-local.yml index 2e9c4f6..73b95b3 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-local.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-local.yml @@ -19,7 +19,7 @@ spring: password: 123456 driver-class-name: org.postgresql.Driver flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true baseline-version: 0 diff --git a/gym-manage-api/manage-app/src/main/resources/application-test.yml b/gym-manage-api/manage-app/src/main/resources/application-test.yml index 5a55a80..0b1fba2 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-test.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-test.yml @@ -15,7 +15,7 @@ spring: max-life-time: 1h acquire-timeout: 5s flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true diff --git a/gym-manage-api/manage-app/src/main/resources/application.yml b/gym-manage-api/manage-app/src/main/resources/application.yml index 807415e..a8e0ec4 100644 --- a/gym-manage-api/manage-app/src/main/resources/application.yml +++ b/gym-manage-api/manage-app/src/main/resources/application.yml @@ -24,12 +24,12 @@ spring: max-life-time: 1h acquire-timeout: 5s datasource: - url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system} username: ${DB_USERNAME:novalon} password: ${DB_PASSWORD:novalon123} driver-class-name: org.postgresql.Driver flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true baseline-version: 0 @@ -40,6 +40,8 @@ spring: password: disabled profiles: active: dev + config: + import: classpath:member-config.yml management: endpoints: 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 c42d328..ea4b245 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 @@ -58,7 +58,6 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactorymanage-audit manage-notify manage-file + gym-member diff --git a/gym-manage-web/pnpm-lock.yaml b/gym-manage-web/pnpm-lock.yaml index 63bd44e..c3a82f2 100644 --- a/gym-manage-web/pnpm-lock.yaml +++ b/gym-manage-web/pnpm-lock.yaml @@ -59,7 +59,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) '@vitejs/plugin-vue': specifier: ^6.0.3 - version: 6.0.5(vite@7.3.1(@types/node@20.19.37))(vue@3.5.30(typescript@5.9.3)) + version: 6.0.5(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0))(vue@3.5.30(typescript@5.9.3)) '@vitest/coverage-v8': specifier: ^4.1.1 version: 4.1.2(vitest@4.1.0) @@ -81,15 +81,18 @@ importers: prettier: specifier: ^3.1.1 version: 3.8.1 + terser: + specifier: ^5.46.1 + version: 5.48.0 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@20.19.37) + version: 7.3.1(@types/node@20.19.37)(terser@5.48.0) vitest: specifier: ^4.0.16 - version: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + version: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0)) vue-tsc: specifier: ^3.2.2 version: 3.2.5(typescript@5.9.3) @@ -390,10 +393,16 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -464,66 +473,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -858,6 +880,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -889,6 +914,9 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1631,6 +1659,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -1672,6 +1707,11 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2138,8 +2178,18 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -2366,10 +2416,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@20.19.37))(vue@3.5.30(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0))(vue@3.5.30(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.48.0) vue: 3.5.30(typescript@5.9.3) '@vitest/coverage-v8@4.1.2(vitest@4.1.0)': @@ -2384,7 +2434,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0)) '@vitest/expect@4.1.0': dependencies: @@ -2395,13 +2445,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@20.19.37))': + '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.48.0) '@vitest/pretty-format@4.1.0': dependencies: @@ -2434,7 +2484,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)) + vitest: 4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0)) '@vitest/utils@4.1.0': dependencies: @@ -2642,6 +2692,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-from@1.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2668,6 +2720,8 @@ snapshots: commander@10.0.1: {} + commander@2.20.3: {} + concat-map@0.0.1: {} config-chain@1.1.13: @@ -3456,6 +3510,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + speakingurl@14.0.1: {} stackback@0.0.2: {} @@ -3494,6 +3555,13 @@ snapshots: symbol-tree@3.2.4: {} + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -3547,7 +3615,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@7.3.1(@types/node@20.19.37): + vite@7.3.1(@types/node@20.19.37)(terser@5.48.0): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.3) @@ -3558,11 +3626,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.37 fsevents: 2.3.3 + terser: 5.48.0 - vitest@4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)): + vitest@4.1.0(@types/node@20.19.37)(@vitest/ui@4.1.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@20.19.37)) + '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@20.19.37)(terser@5.48.0)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -3579,7 +3648,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@20.19.37) + vite: 7.3.1(@types/node@20.19.37)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.37 -- 2.52.0 From e673d96f6f6856b96fa669597d35517fbdec5a9b Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 27 May 2026 02:13:50 +0800 Subject: [PATCH 10/16] =?UTF-8?q?=E5=88=A0=E9=99=A4=20gym-member=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E4=B8=AD=E7=9A=84=E6=97=A0=E7=94=A8=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=92=8C=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-member/merge_comments.py | 102 ------------- .../gym-member/spotbugs-exclude.xml | 18 --- .../gym-member/test-member-apis.bat | 56 ------- .../gym-member/test-member-apis.ps1 | 140 ------------------ 4 files changed, 316 deletions(-) delete mode 100644 gym-manage-api/gym-member/merge_comments.py delete mode 100644 gym-manage-api/gym-member/spotbugs-exclude.xml delete mode 100644 gym-manage-api/gym-member/test-member-apis.bat delete mode 100644 gym-manage-api/gym-member/test-member-apis.ps1 diff --git a/gym-manage-api/gym-member/merge_comments.py b/gym-manage-api/gym-member/merge_comments.py deleted file mode 100644 index 3c1000c..0000000 --- a/gym-manage-api/gym-member/merge_comments.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import re - -base_path = r"c:\Users\13603\Desktop\健身房项目\gym-manage\gym-manage-api\gym-member\src\main\java\cn\novalon\gym\manage\member" - -count = 0 -for root, dirs, files in os.walk(base_path): - for file in files: - if file.endswith('.java'): - filepath = os.path.join(root, file) - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - - # 匹配模式:两个连续的注释块(类文档 + 作者注释) - pattern = r'(/\*\*[\s\S]*?\*/)\s*\n(/\*\*\s*\n\s*\*\s*@author\s+.+\s*\n\s*\*\s*@date\s+\d{4}-\d{2}-\d{2}\s*\n\s*\*/)' - - match = re.search(pattern, content) - if match: - class_doc = match.group(1) - author_doc = match.group(2) - - # 提取作者信息 - author_match = re.search(r'@author\s+(.+)', author_doc) - date_match = re.search(r'@date\s+(\d{4}-\d{2}-\d{2})', author_doc) - - if author_match and date_match: - author = author_match.group(1).strip() - date = date_match.group(1).strip() - - # 提取类文档的内容(去掉 /** 和 */) - class_doc_content = re.sub(r'/\*\*\s*\n|\s*\*/', '', class_doc).strip() - # 清理每行开头的多余星号和空格 - class_doc_lines = class_doc_content.split('\n') - cleaned_lines = [] - for line in class_doc_lines: - cleaned_line = re.sub(r'^\s*\*\s?', '', line).strip() - if cleaned_line: - cleaned_lines.append(cleaned_line) - class_doc_content = '\n * '.join(cleaned_lines) - - # 构建合并后的注释 - merged_doc = f"""/** - * {class_doc_content} - * - * @author {author} - * @date {date} - */""" - - # 替换原文 - new_content = content[:match.start()] + merged_doc + content[match.end():] - - with open(filepath, 'w', encoding='utf-8') as f: - f.write(new_content) - - count += 1 - print(f"已合并: {file}") - else: - # 检查是否有单独的作者注释在类文档之前 - pattern2 = r'/\*\*\s*\n\s*\*\s*@author\s+.+\s*\n\s*\*\s*@date\s+\d{4}-\d{2}-\d{2}\s*\n\s*\*/\s*\n(/\*\*[\s\S]*?\*/)' - match2 = re.search(pattern2, content) - if match2: - author_doc = match2.group(0).split('\n/**')[0] + '\n/**' - class_doc = '/**' + match2.group(1) - - # 提取作者信息 - author_match = re.search(r'@author\s+(.+)', author_doc) - date_match = re.search(r'@date\s+(\d{4}-\d{2}-\d{2})', author_doc) - - if author_match and date_match: - author = author_match.group(1).strip() - date = date_match.group(1).strip() - - # 提取类文档的内容 - class_doc_content = re.sub(r'/\*\*\s*\n|\s*\*/', '', class_doc).strip() - # 清理每行开头的多余星号和空格 - class_doc_lines = class_doc_content.split('\n') - cleaned_lines = [] - for line in class_doc_lines: - cleaned_line = re.sub(r'^\s*\*\s?', '', line).strip() - if cleaned_line: - cleaned_lines.append(cleaned_line) - class_doc_content = '\n * '.join(cleaned_lines) - - # 构建合并后的注释 - merged_doc = f"""/** - * {class_doc_content} - * - * @author {author} - * @date {date} - */""" - - # 替换原文 - new_content = content[:match2.start()] + merged_doc + content[match2.end():] - - with open(filepath, 'w', encoding='utf-8') as f: - f.write(new_content) - - count += 1 - print(f"已合并: {file}") - -print(f"\n完成!共合并 {count} 个文件") diff --git a/gym-manage-api/gym-member/spotbugs-exclude.xml b/gym-manage-api/gym-member/spotbugs-exclude.xml deleted file mode 100644 index fd9cd91..0000000 --- a/gym-manage-api/gym-member/spotbugs-exclude.xml +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/gym-manage-api/gym-member/test-member-apis.bat b/gym-manage-api/gym-member/test-member-apis.bat deleted file mode 100644 index 97a6060..0000000 --- a/gym-manage-api/gym-member/test-member-apis.bat +++ /dev/null @@ -1,56 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 会员模块接口测试 -echo ======================================== -echo. - -set BASE_URL=http://localhost:8084 -set MEMBER_ID=26 - -echo [测试 1] 获取会员信息 -echo 请求: GET %BASE_URL%/api/member/info -curl -X GET "%BASE_URL%/api/member/info" -H "X-Member-Id: %MEMBER_ID%" -s | jq . -echo. -echo. - -echo [测试 2] 查询关注状态 -echo 请求: GET %BASE_URL%/api/member/subscribe/status -curl -X GET "%BASE_URL%/api/member/subscribe/status" -H "X-Member-Id: %MEMBER_ID%" -s -echo. -echo. - -echo [测试 3] 更新会员信息 -echo 请求: PUT %BASE_URL%/api/member/info?nickname=TestUser^&gender=1^&birthday=1990-01-01^&address=TestAddress -curl -X PUT "%BASE_URL%/api/member/info?nickname=TestUser&gender=1&birthday=1990-01-01&address=TestAddress" -H "X-Member-Id: %MEMBER_ID%" -s -echo. -echo. - -echo [测试 4] 管理端录入手机号 -echo 请求: POST %BASE_URL%/api/admin/members/%MEMBER_ID%/phone -curl -X POST "%BASE_URL%/api/admin/members/%MEMBER_ID%/phone" -H "Content-Type: application/json" -d "{\"phone\":\"13800138000\"}" -s -echo. -echo. - -echo [测试 5] 验证手机号已更新 -echo 请求: GET %BASE_URL%/api/member/info -curl -X GET "%BASE_URL%/api/member/info" -H "X-Member-Id: %MEMBER_ID%" -s | jq . -echo. -echo. - -echo [测试 6] 服务号回调验证签名 -echo 请求: GET %BASE_URL%/api/member/auth/mp/callback?signature=test^×tamp=123^&nonce=test^&echostr=test -curl -X GET "%BASE_URL%/api/member/auth/mp/callback?signature=test×tamp=123&nonce=test&echostr=test" -s -echo. -echo. - -echo ======================================== -echo 测试完成 -echo ======================================== -echo. -echo 提示: -echo 1. 小程序登录需要提供真实的微信code -echo 2. 服务号回调需要微信服务器推送的真实事件 -echo 3. 管理端录入手机号需要配置正确的AES密钥 -echo. -pause diff --git a/gym-manage-api/gym-member/test-member-apis.ps1 b/gym-manage-api/gym-member/test-member-apis.ps1 deleted file mode 100644 index c4315ab..0000000 --- a/gym-manage-api/gym-member/test-member-apis.ps1 +++ /dev/null @@ -1,140 +0,0 @@ -# 会员模块接口测试脚本 -# 使用前请确保应用已启动在 http://localhost:8084 - -$BASE_URL = "http://localhost:8084" -$TEST_MEMBER_ID = 26 # 测试环境会员ID - -Write-Host "========================================" -ForegroundColor Cyan -Write-Host " 会员模块接口测试" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" - -# ======================================== -# 1. 测试获取会员信息 -# ======================================== -Write-Host "[测试 1] 获取会员信息" -ForegroundColor Yellow -Write-Host "请求: GET $BASE_URL/api/member/info" -ForegroundColor Gray -Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray -try { - $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/info" -Method Get -Headers @{ - "X-Member-Id" = "$TEST_MEMBER_ID" - } - Write-Host "✅ 成功" -ForegroundColor Green - Write-Host "响应:" -ForegroundColor Cyan - $response | ConvertTo-Json -Depth 10 -} catch { - Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red -} -Write-Host "" - -# ======================================== -# 2. 测试查询关注状态 -# ======================================== -Write-Host "[测试 2] 查询关注状态" -ForegroundColor Yellow -Write-Host "请求: GET $BASE_URL/api/member/subscribe/status" -ForegroundColor Gray -Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray -try { - $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/subscribe/status" -Method Get -Headers @{ - "X-Member-Id" = "$TEST_MEMBER_ID" - } - Write-Host "✅ 成功" -ForegroundColor Green - Write-Host "响应: $response" -ForegroundColor Cyan -} catch { - Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red -} -Write-Host "" - -# ======================================== -# 3. 测试更新会员信息 -# ======================================== -Write-Host "[测试 3] 更新会员信息" -ForegroundColor Yellow -Write-Host "请求: PUT $BASE_URL/api/member/info?nickname=测试用户&gender=1&birthday=1990-01-01&address=测试地址" -ForegroundColor Gray -Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray -try { - $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/info?nickname=测试用户&gender=1&birthday=1990-01-01&address=测试地址" -Method Put -Headers @{ - "X-Member-Id" = "$TEST_MEMBER_ID" - } - Write-Host "✅ 成功" -ForegroundColor Green - Write-Host "响应: $response" -ForegroundColor Cyan -} catch { - Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red -} -Write-Host "" - -# ======================================== -# 4. 测试管理端录入手机号 -# ======================================== -Write-Host "[测试 4] 管理端录入手机号" -ForegroundColor Yellow -Write-Host "请求: POST $BASE_URL/api/admin/members/$TEST_MEMBER_ID/phone" -ForegroundColor Gray -Write-Host "Body: { `"phone`": `"13800138000`" }" -ForegroundColor Gray -try { - $body = @{ phone = "13800138000" } | ConvertTo-Json - $response = Invoke-RestMethod -Uri "$BASE_URL/api/admin/members/$TEST_MEMBER_ID/phone" -Method Post -Body $body -ContentType "application/json" - Write-Host "✅ 成功" -ForegroundColor Green - Write-Host "响应: $response" -ForegroundColor Cyan -} catch { - Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red -} -Write-Host "" - -# ======================================== -# 5. 再次获取会员信息(验证手机号已更新) -# ======================================== -Write-Host "[测试 5] 验证手机号已更新" -ForegroundColor Yellow -Write-Host "请求: GET $BASE_URL/api/member/info" -ForegroundColor Gray -Write-Host "Header: X-Member-Id: $TEST_MEMBER_ID" -ForegroundColor Gray -try { - $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/info" -Method Get -Headers @{ - "X-Member-Id" = "$TEST_MEMBER_ID" - } - Write-Host "✅ 成功" -ForegroundColor Green - Write-Host "响应:" -ForegroundColor Cyan - $response | ConvertTo-Json -Depth 10 - - if ($response.hasPhone) { - Write-Host "✅ 手机号已成功绑定" -ForegroundColor Green - } else { - Write-Host "⚠️ 手机号未绑定" -ForegroundColor Yellow - } -} catch { - Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red -} -Write-Host "" - -# ======================================== -# 6. 测试小程序登录(需要有效的code) -# ======================================== -Write-Host "[测试 6] 小程序登录(需要提供有效的微信code)" -ForegroundColor Yellow -Write-Host "注意: 此测试需要从小程序获取真实的code,跳过自动测试" -ForegroundColor Gray -Write-Host "手动测试命令:" -ForegroundColor Cyan -Write-Host "curl -X POST $BASE_URL/api/member/auth/miniapp/login \" -ForegroundColor Gray -Write-Host " -H `"Content-Type: application/json`" \" -ForegroundColor Gray -Write-Host " -d `"{`"code`":`"YOUR_CODE_HERE`"}"`" -ForegroundColor Gray -Write-Host "" - -# ======================================== -# 7. 测试服务号回调验证签名 -# ======================================== -Write-Host "[测试 7] 服务号回调验证签名" -ForegroundColor Yellow -Write-Host "请求: GET $BASE_URL/api/member/auth/mp/callback?signature=test×tamp=123&nonce=test&echostr=test" -ForegroundColor Gray -try { - $response = Invoke-RestMethod -Uri "$BASE_URL/api/member/auth/mp/callback?signature=test×tamp=123&nonce=test&echostr=test" -Method Get - Write-Host "✅ 成功(返回echostr表示验证通过)" -ForegroundColor Green - Write-Host "响应: $response" -ForegroundColor Cyan -} catch { - Write-Host "❌ 失败: $($_.Exception.Message)" -ForegroundColor Red -} -Write-Host "" - -# ======================================== -# 总结 -# ======================================== -Write-Host "========================================" -ForegroundColor Cyan -Write-Host " 测试完成" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" -Write-Host "提示:" -ForegroundColor Yellow -Write-Host "1. 小程序登录需要提供真实的微信code" -ForegroundColor Gray -Write-Host "2. 服务号回调需要微信服务器推送的真实事件" -ForegroundColor Gray -Write-Host "3. 管理端录入手机号需要配置正确的AES密钥" -ForegroundColor Gray -Write-Host "" -- 2.52.0 From ddb77db6054fbe26fbbb46bf7a178b14513084e2 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 27 May 2026 03:21:38 +0800 Subject: [PATCH 11/16] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=92=8C=E7=B3=BB=E7=BB=9F=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-member/pom.xml | 23 ++----- .../manage/member/handler/MemberHandler.java | 64 ++++-------------- .../service/impl/WechatAuthServiceImpl.java | 66 ++++--------------- .../gym/manage/member/util/AuthUtil.java | 32 +++++++++ gym-manage-api/manage-app/pom.xml | 5 ++ .../gym/manage/sys/config/SecurityConfig.java | 3 +- gym-manage-api/pom.xml | 3 +- 7 files changed, 73 insertions(+), 123 deletions(-) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java diff --git a/gym-manage-api/gym-member/pom.xml b/gym-manage-api/gym-member/pom.xml index c74a4c3..2f57396 100644 --- a/gym-manage-api/gym-member/pom.xml +++ b/gym-manage-api/gym-member/pom.xml @@ -27,6 +27,11 @@ manage-db ${project.version} + + cn.novalon.gym.manage + manage-sys + ${project.version} + org.springframework.boot spring-boot-starter-webflux @@ -108,26 +113,12 @@ com.github.binarywang weixin-java-miniapp 4.6.0 - + + com.github.binarywang weixin-java-mp 4.6.0 - - - io.jsonwebtoken - jjwt-api - - - io.jsonwebtoken - jjwt-impl - runtime - - - io.jsonwebtoken - jjwt-jackson - runtime - cn.hutool hutool-all diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index fbd6e99..4d66c7a 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -8,12 +8,11 @@ import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.AuthUtil; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; @@ -37,6 +36,7 @@ public class MemberHandler { private final WechatOfficialService wechatOfficialService; private final JwtTokenProvider jwtTokenProvider; private final WechatProperties wechatProperties; + private final AuthUtil authUtil; /** * 获取会员信息 @@ -46,10 +46,7 @@ public class MemberHandler { */ public Mono getMemberInfo(ServerRequest request) { - String memberIdStr = request.headers().firstHeader("X-Member-Id"); - long memberId = NumberUtils.toLong(memberIdStr,0L); - - if (memberId <= 0) throw new IllegalArgumentException("获取会员信息失败: memberId 无效"); + Long memberId = authUtil.getMemberIdOrThrow(request); log.info("获取会员信息, memberId: {}", memberId); @@ -76,10 +73,7 @@ public class MemberHandler { */ public Mono updateMemberInfo(ServerRequest request) { - String memberIdStr = request.headers().firstHeader("X-Member-Id"); - long memberId = NumberUtils.toLong(memberIdStr, 0L); - - if (memberId <= 0) throw new IllegalArgumentException("更新会员信息失败: memberId 无效"); + Long memberId = authUtil.getMemberIdOrThrow(request); log.info("更新会员信息, memberId: {}", memberId); @@ -98,14 +92,11 @@ public class MemberHandler { */ public Mono bindPhone(ServerRequest request) { - String memberIdStr = request.headers().firstHeader("X-Member-Id"); - Long memberId = NumberUtils.toLong(memberIdStr, 0L); - - if (memberId <= 0) throw new IllegalArgumentException("绑定手机号失败: memberId 无效"); + Long memberId = authUtil.getMemberIdOrThrow(request); String phoneCode = request.queryParam("phoneCode").orElse(""); - if (phoneCode == null || phoneCode.trim().isEmpty()) throw new IllegalArgumentException("手机号code不能为空"); + if (phoneCode.trim().isEmpty()) throw new IllegalArgumentException("手机号code不能为空"); log.info("收到绑定手机号请求, memberId: {}, phoneCode: {}", memberId, phoneCode); @@ -124,10 +115,7 @@ public class MemberHandler { */ public Mono checkSubscribeStatus(ServerRequest request) { - String memberIdStr = request.headers().firstHeader("X-Member-Id"); - long memberId = NumberUtils.toLong(memberIdStr,0L); - - if (memberId <= 0) throw new IllegalArgumentException("查询服务号关注状态失败: memberId 无效"); + Long memberId = authUtil.getMemberIdOrThrow(request); log.info("查询服务号关注状态, memberId: {}", memberId); @@ -148,6 +136,8 @@ public class MemberHandler { */ public Mono adminUpdatePhone(ServerRequest request) { + Long adminId = authUtil.getMemberIdOrThrow(request); + String memberIdStr = request.pathVariable("id"); long memberId = NumberUtils.toLong(memberIdStr, 0L); @@ -184,18 +174,12 @@ public class MemberHandler { */ public Mono adminGetMemberInfo(ServerRequest request) { - String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); - if (authorization == null || !authorization.startsWith("Bearer ")) throw new IllegalArgumentException("无权访问"); - authorization = authorization.substring(7); - // 验证token并获取memberId - if (!jwtTokenProvider.validateToken(authorization)) throw new IllegalArgumentException("Authorization 无效"); + Long adminId = authUtil.getMemberIdOrThrow(request); String memberIdStr = request.pathVariable("id"); long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); - Long adminId = jwtTokenProvider.getUserIdFromToken(authorization); - // TODO 多表查询:会员信息、团课信息、会员卡信息 return ServerResponse.ok() @@ -212,17 +196,11 @@ public class MemberHandler { */ public Mono adminUpdateMemberInfo(ServerRequest request) { - String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); - if (authorization == null || !authorization.startsWith("Bearer ")) throw new IllegalArgumentException("无权访问"); - authorization = authorization.substring(7); - // 验证token并获取memberId - if (!jwtTokenProvider.validateToken(authorization)) throw new IllegalArgumentException("Authorization 无效"); + Long adminId = authUtil.getMemberIdOrThrow(request); String memberIdStr = request.pathVariable("id"); long memberId = NumberUtils.toLong(memberIdStr, 0L); - if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); - - Long adminId = jwtTokenProvider.getUserIdFromToken(authorization); + if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误"); // TODO 多表查询:会员信息、团课信息、会员卡信息 @@ -239,14 +217,7 @@ public class MemberHandler { */ public Mono searchMembers(ServerRequest request) { - String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); - if (authorization == null || !authorization.startsWith("Bearer ")) { - return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("无权访问"); - } - authorization = authorization.substring(7); - if (!jwtTokenProvider.validateToken(authorization)) { - return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("Authorization 无效"); - } + Long adminId = authUtil.getMemberIdOrThrow(request); String keyword = request.queryParam("searchValue").orElse(null); String filter = request.queryParam("filter").orElse(null); @@ -282,14 +253,7 @@ public class MemberHandler { */ public Mono getAllMembers(ServerRequest request) { - String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); - if (authorization == null || !authorization.startsWith("Bearer ")) { - return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("无权访问"); - } - authorization = authorization.substring(7); - if (!jwtTokenProvider.validateToken(authorization)) { - return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("Authorization 无效"); - } + Long adminId = authUtil.getMemberIdOrThrow(request); int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 1a858d7..5d7904f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -1,6 +1,5 @@ package cn.novalon.gym.manage.member.service.impl; -import cn.novalon.gym.manage.common.config.JwtProperties; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.WechatLoginDto; import cn.novalon.gym.manage.member.entity.Member; @@ -14,20 +13,16 @@ import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.MemberNoGenerator; import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.member.vo.WechatLoginVO; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; /** * 微信认证服务实现 @@ -43,11 +38,11 @@ public class WechatAuthServiceImpl implements WechatAuthService { private final WechatApiService wechatApiService; private final IMemberRepository memberRepository; - private final JwtProperties jwtProperties; private final WechatProperties wechatProperties; private final WechatPhoneUtil wechatPhoneUtil; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; + private final JwtTokenProvider jwtTokenProvider; private EsSyncUtils.EntitySyncer memberSyncer; @@ -332,60 +327,23 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.getPhone() != null ? "已绑定" : "未绑定"); } - // 生成 Access Token(使用 Gateway 相同的方式) - String accessToken = generateJwtToken(member.getId(), "access"); - // 生成 Refresh Token(有效期更长) - String refreshToken = generateJwtToken(member.getId(), "refresh", 7L * 24 * 60 * 60 * 1000); + // 统一使用 JwtTokenProvider 生成 Token + // 使用空的角色列表,会员不需要角色 + List roles = new ArrayList<>(); + String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles); log.info("JWT Token 生成成功, memberId: {}", member.getId()); - // 计算过期时间(秒) - long expiresIn = jwtProperties.getExpiration() / 1000; + // 使用 JwtTokenProvider 默认过期时间(86400 秒) + int expiresIn = 86400; return WechatLoginVO.builder() .memberId(member.getId()) .accessToken(accessToken) - .refreshToken(refreshToken) - .expiresIn((int) expiresIn) + .refreshToken(accessToken) + .expiresIn(expiresIn) .isNewUser(isNewUser) .needCompleteInfo(needCompleteInfo) .build(); } - - /** - * 生成 JWT Token(与 Gateway 一致) - * - * @param memberId 会员 ID - * @param tokenType Token 类型(access/refresh) - * @return JWT Token 字符串 - */ - private String generateJwtToken(Long memberId, String tokenType) { - return generateJwtToken(memberId, tokenType, jwtProperties.getExpiration()); - } - - /** - * 生成 JWT Token(自定义过期时间) - * - * @param memberId 会员 ID - * @param tokenType Token 类型(access/refresh) - * @param expirationMs 过期时间(毫秒) - * @return JWT Token 字符串 - */ - private String generateJwtToken(Long memberId, String tokenType, long expirationMs) { - SecretKey key = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + expirationMs); - - Map claims = new HashMap<>(); - claims.put("memberId", memberId); - claims.put("tokenType", tokenType); - - return Jwts.builder() - .setClaims(claims) - .setSubject(String.valueOf(memberId)) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(key) - .compact(); - } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java new file mode 100644 index 0000000..c2a9ea6 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java @@ -0,0 +1,32 @@ +package cn.novalon.gym.manage.member.util; + +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ResponseStatusException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthUtil { + + private final JwtTokenProvider jwtTokenProvider; + + public String extractToken(ServerRequest request) { + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) return null; + return authorization.substring(7); + } + + public Long getMemberIdOrThrow(ServerRequest request) { + String token = extractToken(request); + if (token == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "缺少 Token"); + if (!jwtTokenProvider.validateToken(token)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token 无效或已过期"); + if (jwtTokenProvider.getUserIdFromToken(token) <= 0L) throw new IllegalArgumentException("ID无效"); + return jwtTokenProvider.getUserIdFromToken(token); + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index ced508a..5eecb9d 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -43,6 +43,11 @@ gym-member ${project.version} + + cn.novalon.gym.manage + gym-checkIn + ${project.version} + org.springframework.boot spring-boot-starter-webflux 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 03ca331..4df2a4b 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 @@ -50,8 +50,7 @@ public class SecurityConfig { spec.pathMatchers("/api/auth/**").permitAll() .pathMatchers("/api/public/**").permitAll() .pathMatchers("/ws/**").permitAll() - .pathMatchers("/actuator/**").permitAll() - .pathMatchers("/api/member/checkIn").permitAll(); + .pathMatchers("/actuator/**").permitAll(); if (isDevOrTest) { spec.pathMatchers("/swagger-ui.html").permitAll() diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 9681760..70df0c1 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -43,7 +43,8 @@ manage-notify manage-file gym-member - + gym-checkIn + -- 2.52.0 From 8dbd507dd2e727deff8d54922bc0a41140a01607 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 27 May 2026 13:26:46 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AuthUtil=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB=EF=BC=8C=E4=BC=98=E5=8C=96=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=8F=8A=E4=BC=9A=E5=91=98=E6=9C=8D=E5=8A=A1=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage/member/handler/MemberHandler.java | 26 ++-- .../service/impl/MemberServiceImpl.java | 79 +++++----- .../service/impl/WechatApiServiceImpl.java | 123 +++++---------- .../service/impl/WechatAuthServiceImpl.java | 145 ++++++------------ .../impl/WechatOfficialServiceImpl.java | 6 +- .../gym/manage/app/config/SystemRouter.java | 38 +++-- .../novalon/gym/manage/sys/util/AuthUtil.java | 32 ++++ 7 files changed, 202 insertions(+), 247 deletions(-) create mode 100644 gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 4d66c7a..7571053 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -8,7 +8,7 @@ import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.util.AesUtil; -import cn.novalon.gym.manage.member.util.AuthUtil; +import cn.novalon.gym.manage.sys.util.AuthUtil; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -42,7 +42,7 @@ public class MemberHandler { * 获取会员信息 * * GET /api/member/info - * Header: X-Member-Id: 123 + * header: { "Authorization": "Bearer xxx" } */ public Mono getMemberInfo(ServerRequest request) { @@ -51,18 +51,16 @@ public class MemberHandler { log.info("获取会员信息, memberId: {}", memberId); return memberService.getMemberInfo(memberId) - .flatMap(info -> { - return ServerResponse.ok() + .flatMap(info -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) - .bodyValue(info); - }); + .bodyValue(info)); } /** * 更新会员信息 * * PUT /api/member/info - * Header: X-Member-Id: 123 + * header: { "Authorization": "Bearer xxx" } * Body: { * "nickname": "新昵称", * "gender": 1, @@ -86,9 +84,8 @@ public class MemberHandler { /** * 绑定手机号(微信小程序) - * + * header: { "Authorization": "Bearer xxx" } * POST /api/member/phone/bind?code=PHONE_CODE - * Header: X-Member-Id: 123 */ public Mono bindPhone(ServerRequest request) { @@ -110,7 +107,6 @@ public class MemberHandler { * 查询服务号关注状态 * * GET /api/member/subscribe/status - * Header: X-Member-Id: 123 * */ public Mono checkSubscribeStatus(ServerRequest request) { @@ -131,6 +127,7 @@ public class MemberHandler { * 管理员更新手机号 * * POST /api/admin/member/123/phone + * header: { "Authorization": "Bearer xxx" } * Body: { "phone": "13800138000" } * */ @@ -180,6 +177,8 @@ public class MemberHandler { long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); + log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId); + // TODO 多表查询:会员信息、团课信息、会员卡信息 return ServerResponse.ok() @@ -202,6 +201,8 @@ public class MemberHandler { long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误"); + log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId); + // TODO 多表查询:会员信息、团课信息、会员卡信息 return ServerResponse.ok() @@ -224,6 +225,9 @@ public class MemberHandler { int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", + adminId, keyword, filter, pageNum, pageSize); + return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize)) .map(member -> { // 解密手机号 @@ -258,6 +262,8 @@ public class MemberHandler { int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize); + return memberService.findAll(pageNum, pageSize) .map(member -> { // 解密手机号 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index cfd1517..d1b6c44 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -1,5 +1,9 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.exception.ConflictException; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.NotFoundException; +import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; @@ -14,13 +18,10 @@ import cn.novalon.gym.manage.member.vo.MemberInfoVO; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +29,7 @@ import java.time.LocalDateTime; /** * 会员服务实现 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -48,28 +49,22 @@ public class MemberServiceImpl implements MemberService { public void init() { this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); } - - @Value("${wechat.aes.secret-key:}") - private String aesSecretKey; - - @Value("${wechat.aes.iv:}") - private String aesIv; @Override public Mono getMemberInfo(Long memberId) { return memberRepository.findById(memberId) - .map(this::buildMemberInfoResponse); + .map(this::buildMemberInfoResponse) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); } @Override public Mono updateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) { log.info("会员更新个人信息, memberId: {}", memberId); - + return memberRepository.findById(memberId) - .switchIfEmpty(Mono.defer(() -> { - log.error("会员不存在: memberId={}", memberId); - return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "会员不存在")); - })) .flatMap(member -> { if (updateDto.getNickname() != null) { member.setNickname(updateDto.getNickname()); @@ -86,17 +81,20 @@ public class MemberServiceImpl implements MemberService { if (updateDto.getAddress() != null) { member.setAddress(updateDto.getAddress()); } - + return memberRepository.save(member); }) .doOnSuccess(memberSyncer::sync) .map(savedMember -> { log.info("会员信息更新成功, memberId: {}", savedMember.getId()); return buildMemberInfoResponse(savedMember); - }); + }) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); } - // 会员信息响应 private MemberInfoVO buildMemberInfoResponse(Member member) { String phone = member.getPhone(); String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null; @@ -116,34 +114,27 @@ public class MemberServiceImpl implements MemberService { @Override public Mono adminUpdatePhone(Long memberId, String phone) { log.info("管理端录入手机号, memberId: {}, phone: {}", memberId, phone); - + String encryptedPhone; try { - encryptedPhone = AesUtil.encrypt(phone, aesSecretKey, aesIv); + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + encryptedPhone = AesUtil.encrypt(phone, secretKey, iv); log.info("手机号加密成功"); } catch (Exception e) { log.error("手机号加密失败", e); - return Mono.error(new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, - "手机号加密失败: " + e.getMessage() - )); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败: " + e.getMessage()); } - + return memberRepository.findByPhone(encryptedPhone) .flatMap(existingMember -> { if (existingMember.getId().equals(memberId)) { log.warn("手机号已是当前用户的: memberId={}", memberId); - return Mono.error(new ResponseStatusException( - HttpStatus.CONFLICT, - "重复绑定" - )); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "重复绑定"); } else { - log.warn("手机号已被其他用户绑定: memberId={}, existingMemberId={}", + log.warn("手机号已被其他用户绑定: memberId={}, existingMemberId={}", memberId, existingMember.getId()); - return Mono.error(new ResponseStatusException( - HttpStatus.CONFLICT, - "该手机号已被其他会员绑定" - )); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "该手机号已被其他会员绑定"); } }) .switchIfEmpty(Mono.defer(() -> { @@ -154,24 +145,27 @@ public class MemberServiceImpl implements MemberService { @Override public Flux searchMember(SearchMemberDto searchMemberDto) { + log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", + searchMemberDto.getSearchValue(), + searchMemberDto.getFilter(), + searchMemberDto.getPageNum(), + searchMemberDto.getPageSize()); String searchValue = searchMemberDto.getSearchValue(); - // 1. 处理手机号加密 if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ + log.debug("搜索值为手机号格式,进行加密处理"); String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); String iv = wechatProperties.getPhoneEncryption().getIv(); searchValue = AesUtil.encrypt(searchValue,secretKey,iv); } - // 2. 分页参数 Pageable pageable = PageRequest.of( searchMemberDto.getPageNum() - 1, searchMemberDto.getPageSize(), Sort.by(Sort.Direction.DESC, "update_at") ); - // 3. 调用 Repository 查询 return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender( searchValue, searchValue, @@ -183,6 +177,8 @@ public class MemberServiceImpl implements MemberService { @Override public Flux findAll(Integer pageNum, Integer pageSize) { + log.info("查询所有会员列表, pageNum: {}, pageSize: {}", pageNum, pageSize); + Pageable pageable = PageRequest.of( pageNum - 1, pageSize @@ -191,13 +187,12 @@ public class MemberServiceImpl implements MemberService { return memberRepository.findAllBy(pageable); } - // 更新会员手机号 private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) .flatMap(member -> { member.setPhone(encryptedPhone); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .map(savedMember -> { @@ -205,9 +200,9 @@ public class MemberServiceImpl implements MemberService { return true; }); }) - .switchIfEmpty(Mono.defer(() -> { + .switchIfEmpty(Mono.error(() -> { log.error("会员不存在: memberId={}", memberId); - return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "会员不存在")); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java index 8b818f1..f760ae4 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.service.WechatApiService; import lombok.RequiredArgsConstructor; @@ -17,7 +19,7 @@ import java.util.Map; /** * 微信API服务实现 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -28,33 +30,22 @@ import java.util.Map; public class WechatApiServiceImpl implements WechatApiService { private final WechatProperties wechatProperties; - - /** - * WebClient实例 - 用于发送HTTP请求 - * 最大内存大小为10MB - */ + private final WebClient webClient = WebClient.builder() .baseUrl("https://api.weixin.qq.com") .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) .build(); - /** - * 小程序 - * - * @param code 小程序登录的code - * @return Mono> session_keyopenidunionid响应格式 - */ @Override public Mono> jsCode2Session(String code) { log.info("微信jsCode2Session API"); - log.info("信息 - AppID: {}, AppSecret {}", + log.info("信息 - AppID: {}, AppSecret {}", wechatProperties.getMiniapp().getAppId(), - wechatProperties.getMiniapp().getAppSecret() != null ? + wechatProperties.getMiniapp().getAppSecret() != null ? wechatProperties.getMiniapp().getAppSecret().substring(0, Math.min(4, wechatProperties.getMiniapp().getAppSecret().length())) + "***" : "null"); log.info(" - code: {}", code); - + return webClient.get() - // 构建URI .uri(uriBuilder -> uriBuilder .path("/sns/jscode2session") .queryParam("appid", wechatProperties.getMiniapp().getAppId()) @@ -62,44 +53,37 @@ public class WechatApiServiceImpl implements WechatApiService { .queryParam("js_code", code) .queryParam("grant_type", "authorization_code") .build()) - // 获取响应 .retrieve() .bodyToMono(String.class) - // 处理响应 .map(responseBody -> { log.info("微信API响应: {}", responseBody); - - // 解析JSON响应 + Map response; try { - // 使用Jackson解析JSON响应 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); response = mapper.readValue(responseBody, Map.class); } catch (Exception e) { log.error("微信API响应失败", e); - throw new RuntimeException("微信API响应失败: " + e.getMessage()); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API响应失败: " + e.getMessage()); } - + Map result = new HashMap<>(); - - // 检查错误码 + if (response.containsKey("errcode")) { Integer errcode = (Integer) response.get("errcode"); String errmsg = (String) response.get("errmsg"); log.error("微信API失败, errcode: {}, errmsg: {}", errcode, errmsg); - throw new RuntimeException("微信API失败 [" + errcode + "]: " + errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API失败 [" + errcode + "]: " + errmsg); } - - // 获取session_keyopenidunionid + result.put("session_key", (String) response.get("session_key")); result.put("openid", (String) response.get("openid")); result.put("unionid", (String) response.get("unionid")); - - log.info("微信API响应成功, openid: {}, unionid: {}", + + log.info("微信API响应成功, openid: {}, unionid: {}", result.get("openid"), result.get("unionid")); return result; }) - // 异常处理 .onErrorResume(e -> { log.error("微信API响应异常 - URL: https://api.weixin.qq.com/sns/jscode2session"); log.error("异常: {}", e.getClass().getName()); @@ -107,26 +91,23 @@ public class WechatApiServiceImpl implements WechatApiService { if (e.getCause() != null) { log.error("异常原因: {}", e.getCause().getMessage()); } - return Mono.error(new RuntimeException("微信API响应异常 " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API响应异常 " + e.getMessage()); }); } - /** - * 获取手机号 - * - * @param code 手机号或获取手机号的code - * @return Mono 手机号 - */ @Override public Mono getPhoneNumber(String code) { log.debug("微信getPhoneNumber API, code: {}", code); - + return getAccessToken("miniapp") .flatMap(accessToken -> { Map requestBody = new HashMap<>(); requestBody.put("code", code); - + return webClient.post() .uri(uriBuilder -> uriBuilder .path("/wxa/business/getuserphonenumber") @@ -137,43 +118,35 @@ public class WechatApiServiceImpl implements WechatApiService { .retrieve() .bodyToMono(Map.class) .map(response -> { - if (response.containsKey("errcode") && + if (response.containsKey("errcode") && (Integer) response.get("errcode") == 0) { - - Map phoneInfo = + + Map phoneInfo = (Map) response.get("phone_info"); String phoneNumber = (String) phoneInfo.get("purePhoneNumber"); - + log.info("获取手机号成功{}", phoneNumber); return phoneNumber; } else { String errmsg = (String) response.get("errmsg"); log.error("获取手机号失败 {}", errmsg); - throw new RuntimeException("获取手机号失败 " + errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取手机号失败 " + errmsg); } }); }) - // .onErrorResume(e -> { log.error("获取手机号失败", e); - return Mono.error(new RuntimeException("获取手机号失败 " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取手机号失败 " + e.getMessage()); }); } - /** - * 获取Access Token - * - * @param appType 应用类型 - * @return Mono access_token - */ @Override public Mono getAccessToken(String appType) { log.debug("获取access_token, appType: {}", appType); - - // TODO: 实现缓存逻辑 - // 使用 Caffeine 或 Redis 缓存 - // 目前直接调用微信API - + String appId, appSecret; if ("miniapp".equals(appType)) { appId = wechatProperties.getMiniapp().getAppId(); @@ -182,7 +155,7 @@ public class WechatApiServiceImpl implements WechatApiService { appId = wechatProperties.getMp().getAppId(); appSecret = wechatProperties.getMp().getAppSecret(); } - + return webClient.get() .uri(uriBuilder -> uriBuilder .path("/cgi-bin/token") @@ -197,46 +170,34 @@ public class WechatApiServiceImpl implements WechatApiService { String accessToken = (String) response.get("access_token"); Integer expiresIn = (Integer) response.get("expires_in"); log.info("获取access_token成功, expires_in: {}s", expiresIn); - - // TODO: 加入缓存 - // cache.put("wechat:token:" + appType, accessToken, expiresIn - 200, TimeUnit.SECONDS); - return accessToken; } else { String errmsg = (String) response.get("errmsg"); log.error("获取access_token失败: {}", errmsg); - throw new RuntimeException("获取access_token失败: " + errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg); } }); } - /** - * 验证微信消息签名 - * - * @param signature 微信消息签名 - * @param timestamp 创建时间戳 - * @param nonce 随机字符串 - * @return boolean true-签名有效false-签名无效 - */ @Override public boolean checkSignature(String signature, String timestamp, String nonce) { - log.debug("验证微信消息签名, signature: {}, timestamp: {}, nonce: {}", + log.debug("验证微信消息签名, signature: {}, timestamp: {}, nonce: {}", signature, timestamp, nonce); - + try { String token = wechatProperties.getMp().getToken(); - + String[] arr = new String[]{token, timestamp, nonce}; Arrays.sort(arr); - + StringBuilder content = new StringBuilder(); for (String s : arr) { content.append(s); } - + MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] digest = md.digest(content.toString().getBytes(StandardCharsets.UTF_8)); - + StringBuilder hexString = new StringBuilder(); for (byte b : digest) { String hex = Integer.toHexString(0xff & b); @@ -245,12 +206,12 @@ public class WechatApiServiceImpl implements WechatApiService { } hexString.append(hex); } - + String calculatedSignature = hexString.toString(); - + boolean isValid = calculatedSignature.equals(signature); log.debug("验证微信消息签名结果: {}", isValid ? "通过" : "失败"); - + return isValid; } catch (Exception e) { log.error("验证微信消息签名异常", e); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 5d7904f..5e7f299 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -1,5 +1,9 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.exception.ConflictException; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.NotFoundException; +import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.WechatLoginDto; import cn.novalon.gym.manage.member.entity.Member; @@ -26,7 +30,7 @@ import java.util.List; /** * 微信认证服务实现 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -52,34 +56,28 @@ public class WechatAuthServiceImpl implements WechatAuthService { } - /** - * 小程序登录 - 通过微信 code 完成登录 - * - * @param request 微信登录请求 - * @return Mono 微信登录响应 - */ @Override public Mono miniappLogin(WechatLoginDto request) { log.info("开始小程序登录"); - + return wechatApiService.jsCode2Session(request.getCode()) .flatMap(sessionData -> { String openid = sessionData.get("openid"); String unionId = sessionData.get("unionid"); String sessionKey = sessionData.get("session_key"); - + log.info("微信 API 返回: openid={}, unionid={}", openid, unionId); - + if (unionId != null && !unionId.isEmpty()) { return memberRepository.findByUnionId(unionId) .flatMap(member -> { log.info("找到会员, memberId: {}", member.getId()); - + if (member.getMiniappOpenId() == null || member.getMiniappOpenId().isEmpty()) { log.info("用户已有 UnionID,补充小程序 OpenID, memberId: {}", member.getId()); member.setMiniappOpenId(openid); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -89,7 +87,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { } else { log.info("老用户登录,更新最后登录时间, memberId: {}", member.getId()); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -105,7 +103,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { log.info("找到会员, memberId: {}", member.getId()); member.setUnionId(unionId); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -124,7 +122,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { .flatMap(member -> { log.info("找到会员, memberId: {}", member.getId()); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -140,32 +138,28 @@ public class WechatAuthServiceImpl implements WechatAuthService { }) .onErrorResume(e -> { log.error("小程序登录失败", e); - return Mono.error(new RuntimeException("登录失败: " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "登录失败: " + e.getMessage())); }); } - /** - * 绑定手机号 - 通过微信 code 获取并绑定手机号 - * - * @param memberId 会员 ID - * @param code 微信手机号 code - * @return Mono - */ @Override public Mono bindPhone(Long memberId, String code) { log.info("开始绑定手机号, memberId: {}", memberId); - + return wechatApiService.getPhoneNumber(code) .flatMap(phoneNumber -> { log.info("获取手机号: {}", phoneNumber); String encryptedPhone = encryptPhone(phoneNumber); - + return memberRepository.findByPhone(encryptedPhone) .flatMap(existingMember -> { if (!existingMember.getId().equals(memberId)) { log.warn("手机号已被其他会员绑定, currentMemberId={}, existingMemberId={}", memberId, existingMember.getId()); - return Mono.error(new RuntimeException("手机号已被其他会员绑定")); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "手机号已被其他会员绑定"); } else { log.info("更新会员手机号, memberId: {}", memberId); return updateMemberPhone(memberId, encryptedPhone); @@ -178,17 +172,13 @@ public class WechatAuthServiceImpl implements WechatAuthService { }) .onErrorResume(e -> { log.error("绑定手机号失败", e); - return Mono.error(new RuntimeException("绑定失败: " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "绑定失败: " + e.getMessage())); }); } - - /** - * 更新会员手机号 - * - * @param memberId 会员 ID - * @param encryptedPhone 加密后的手机号(Base64 编码) - * @return Mono 是否更新成功 - */ + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) .flatMap(member -> { @@ -201,95 +191,68 @@ public class WechatAuthServiceImpl implements WechatAuthService { return true; }); }) - .switchIfEmpty(Mono.defer(() -> { + .switchIfEmpty(Mono.error(() -> { log.error("会员不存在, memberId={}", memberId); - return Mono.error(new RuntimeException("会员不存在")); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } - - /** - * 手机号加密 - * - * @param phoneNumber 明文手机号 - * @return 加密后的手机号(Base64 编码) - */ + private String encryptPhone(String phoneNumber) { try { String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); String iv = wechatProperties.getPhoneEncryption().getIv(); - + String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv); - + log.debug("手机号加密成功"); return encryptedPhone; } catch (Exception e) { log.error("手机号加密失败", e); - throw new RuntimeException("手机号加密失败 " + e.getMessage()); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败 " + e.getMessage()); } } - /** - * AES 解密手机号 - * - * @param encryptedPhone 加密后的手机号(Base64 编码) - * @return 明文手机号 - */ public String decryptPhone(String encryptedPhone) { try { String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); String iv = wechatProperties.getPhoneEncryption().getIv(); - + String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv); - + log.debug("手机号解密成功"); return phoneNumber; } catch (Exception e) { log.error("手机号解密失败", e); - throw new RuntimeException("手机号解密失败 " + e.getMessage()); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号解密失败 " + e.getMessage()); } } - /** - * 创建新会员(首次登录) - * - * @param unionId 微信 UnionID - * @param openid 小程序 OpenID - * @param sessionKey 会话密钥 - * @param phoneCode 手机号 code(可选,前端调用 wx.getPhoneNumber()) - * @return Mono 登录响应 - */ private Mono createNewMember(String unionId, String openid, String sessionKey, String phoneCode) { log.info("开始创建新会员, unionId: {}, openid: {}", unionId, openid); - - // Step 1: 生成会员号 + String memberNo = MemberNoGenerator.generate(); log.info("生成会员号: {}", memberNo); - - // Step 2: 构建 Member 实体(仅保存标识信息) + Member member = Member.builder() - .memberNo(memberNo) // 小程序注册时自动生成会员号 - .unionId(unionId) // 存储 UnionID 用于统一身份 - .miniappOpenId(openid) // 存储小程序 OpenID - .lastLoginAt(LocalDateTime.now()) // 记录首次登录时间 + .memberNo(memberNo) + .unionId(unionId) + .miniappOpenId(openid) + .lastLoginAt(LocalDateTime.now()) .build(); - + log.info("用户未注册,创建新会员(仅保存标识信息)"); - - // Step 3: 保存 Member + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { log.info("保存 Member 成功, id: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo()); - // Step 4: 如果有 phoneCode,尝试获取手机号 if (phoneCode != null && !phoneCode.isEmpty()) { log.info("检测到 phoneCode,尝试获取手机号"); return wechatPhoneUtil.getPhoneNumber(phoneCode) .flatMap(phoneNumber -> { if (phoneNumber != null && !phoneNumber.isEmpty()) { log.info("获取到手机号: {}", phoneNumber); - // 加密手机号 String encryptedPhone = encryptPhone(phoneNumber); - // 为新会员绑定手机号 savedMember.setPhone(encryptedPhone); return memberRepository.save(savedMember) .doOnSuccess(memberSyncer::sync) @@ -303,40 +266,28 @@ public class WechatAuthServiceImpl implements WechatAuthService { } }); } else { - // 没有 phoneCode,直接返回 return Mono.just(buildLoginResponse(savedMember, true, sessionKey)); } }); } - /** - * 构建登录响应 - 封装返回数据(前端调用) - * - * @param member 会员实体 - * @param isNewUser 是否为新用户 - * @param sessionKey 会话密钥(后续可用于解密) - * @return WechatLoginVO 登录响应 - */ private WechatLoginVO buildLoginResponse(Member member, boolean isNewUser, String sessionKey) { log.debug("构建登录响应, memberId: {}, isNewUser: {}", member.getId(), isNewUser); - + boolean needCompleteInfo = member.getNickname() == null || member.getPhone() == null; if (needCompleteInfo) { - log.info("用户需要补全信息: nickname={}, phone={}", + log.info("用户需要补全信息: nickname={}, phone={}", member.getNickname() != null ? "已有" : "未设置", member.getPhone() != null ? "已绑定" : "未绑定"); } - - // 统一使用 JwtTokenProvider 生成 Token - // 使用空的角色列表,会员不需要角色 + List roles = new ArrayList<>(); String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles); - + log.info("JWT Token 生成成功, memberId: {}", member.getId()); - - // 使用 JwtTokenProvider 默认过期时间(86400 秒) + int expiresIn = 86400; - + return WechatLoginVO.builder() .memberId(member.getId()) .accessToken(accessToken) diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index 018c200..b5b863a 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -234,10 +234,14 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { */ @Override public Mono checkSubscribeStatus(Long memberId) { + log.info("查询用户关注状态, memberId: {}", memberId); + return memberRepository.findById(memberId) .map(member -> { Boolean subscribed = member.getSubscribed(); - return subscribed != null && subscribed; + boolean result = subscribed != null && subscribed; + log.info("查询用户关注状态结果, memberId: {}, subscribed: {}", memberId, result); + return result; }) .defaultIfEmpty(false); } 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 6947791..406ea19 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 @@ -1,22 +1,23 @@ package cn.novalon.gym.manage.app.config; -import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; -import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; -import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; -import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; -import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; -import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; -import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; -import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; -import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler; -import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler; -import cn.novalon.gym.manage.sys.handler.stats.StatsHandler; -import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; +import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; +import cn.novalon.gym.manage.file.handler.SysFileHandler; +import cn.novalon.gym.manage.member.handler.MemberHandler; +import cn.novalon.gym.manage.member.handler.WechatAuthHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; -import cn.novalon.gym.manage.file.handler.SysFileHandler; -import cn.novalon.gym.manage.member.handler.WechatAuthHandler; -import cn.novalon.gym.manage.member.handler.MemberHandler; +import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; +import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; +import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; +import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; +import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; +import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; +import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; +import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; +import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler; +import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler; +import cn.novalon.gym.manage.sys.handler.stats.StatsHandler; +import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.server.RouterFunction; @@ -55,7 +56,8 @@ public class SystemRouter { SysPermissionHandler permissionHandler, MemberHandler memberHandler, WechatAuthHandler wechatAuthHandler, - PasswordDiagnosticHandler passwordDiagnosticHandler) { + PasswordDiagnosticHandler passwordDiagnosticHandler, + CheckInHandler checkInHandler) { return route() // ========== 诊断路由 ========== @@ -214,6 +216,10 @@ public class SystemRouter { .PUT("/api/admin/member/{id}", memberHandler::adminUpdateMemberInfo) .GET("/api/admin/members", memberHandler::searchMembers) .GET("/api/admin/members/all", memberHandler::getAllMembers) + + // ========== 签到模块路由 ========== + .POST("/api/checkin", checkInHandler::checkIn) + .GET("/api/QRCode", checkInHandler::getQRCode) .build(); } } diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java new file mode 100644 index 0000000..14c5531 --- /dev/null +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java @@ -0,0 +1,32 @@ +package cn.novalon.gym.manage.sys.util; + +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ResponseStatusException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthUtil { + + private final JwtTokenProvider jwtTokenProvider; + + public String extractToken(ServerRequest request) { + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) return null; + return authorization.substring(7); + } + + public Long getMemberIdOrThrow(ServerRequest request) { + String token = extractToken(request); + if (token == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "缺少 Token"); + if (!jwtTokenProvider.validateToken(token)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token 无效或已过期"); + if (jwtTokenProvider.getUserIdFromToken(token) <= 0L) throw new IllegalArgumentException("ID无效"); + return jwtTokenProvider.getUserIdFromToken(token); + } +} \ No newline at end of file -- 2.52.0 From 574021d47cfaff45b1323a7e4148a0a161215ec2 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 27 May 2026 13:42:05 +0800 Subject: [PATCH 13/16] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=97=E4=BD=99=20Au?= =?UTF-8?q?thUtil=EF=BC=8C=E6=9B=B4=E6=96=B0=20manage-app=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gym/manage/member/util/AuthUtil.java | 32 ------------------- gym-manage-api/manage-app/pom.xml | 6 +--- .../gym/manage/app/config/SystemRouter.java | 8 ++--- .../src/main/resources/application.yml | 10 +++--- 4 files changed, 8 insertions(+), 48 deletions(-) delete mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java deleted file mode 100644 index c2a9ea6..0000000 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AuthUtil.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.novalon.gym.manage.member.util; - -import cn.novalon.gym.manage.sys.security.JwtTokenProvider; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.server.ResponseStatusException; - -@Slf4j -@Component -@RequiredArgsConstructor -public class AuthUtil { - - private final JwtTokenProvider jwtTokenProvider; - - public String extractToken(ServerRequest request) { - String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); - if (authorization == null || !authorization.startsWith("Bearer ")) return null; - return authorization.substring(7); - } - - public Long getMemberIdOrThrow(ServerRequest request) { - String token = extractToken(request); - if (token == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "缺少 Token"); - if (!jwtTokenProvider.validateToken(token)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token 无效或已过期"); - if (jwtTokenProvider.getUserIdFromToken(token) <= 0L) throw new IllegalArgumentException("ID无效"); - return jwtTokenProvider.getUserIdFromToken(token); - } -} \ No newline at end of file diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 5eecb9d..fb5f6f4 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -43,11 +43,7 @@ gym-member ${project.version} - - cn.novalon.gym.manage - gym-checkIn - ${project.version} - + org.springframework.boot spring-boot-starter-webflux 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 406ea19..665762e 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 @@ -1,6 +1,6 @@ package cn.novalon.gym.manage.app.config; -import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; + import cn.novalon.gym.manage.file.handler.SysFileHandler; import cn.novalon.gym.manage.member.handler.MemberHandler; import cn.novalon.gym.manage.member.handler.WechatAuthHandler; @@ -56,8 +56,7 @@ public class SystemRouter { SysPermissionHandler permissionHandler, MemberHandler memberHandler, WechatAuthHandler wechatAuthHandler, - PasswordDiagnosticHandler passwordDiagnosticHandler, - CheckInHandler checkInHandler) { + PasswordDiagnosticHandler passwordDiagnosticHandler) { return route() // ========== 诊断路由 ========== @@ -217,9 +216,6 @@ public class SystemRouter { .GET("/api/admin/members", memberHandler::searchMembers) .GET("/api/admin/members/all", memberHandler::getAllMembers) - // ========== 签到模块路由 ========== - .POST("/api/checkin", checkInHandler::checkIn) - .GET("/api/QRCode", checkInHandler::getQRCode) .build(); } } diff --git a/gym-manage-api/manage-app/src/main/resources/application.yml b/gym-manage-api/manage-app/src/main/resources/application.yml index a8e0ec4..9ff6e52 100644 --- a/gym-manage-api/manage-app/src/main/resources/application.yml +++ b/gym-manage-api/manage-app/src/main/resources/application.yml @@ -15,8 +15,8 @@ spring: - org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration r2dbc: url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} - username: ${DB_USERNAME:novalon} - password: ${DB_PASSWORD:novalon123} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:postgres} pool: initial-size: 10 max-size: 50 @@ -24,9 +24,9 @@ spring: max-life-time: 1h acquire-timeout: 5s datasource: - url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system} - username: ${DB_USERNAME:novalon} - password: ${DB_PASSWORD:novalon123} + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:postgres} driver-class-name: org.postgresql.Driver flyway: enabled: false -- 2.52.0 From 7c08c685d0c2b79f7196eda861229af1c9cbd099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=97=B6=E8=88=9F=E5=B9=B4?= <3147056268@qq.com> Date: Wed, 27 May 2026 16:51:05 +0800 Subject: [PATCH 14/16] =?UTF-8?q?membercard=E4=B8=8Emember-reguster?= =?UTF-8?q?=E5=90=88=E5=B9=B6=EF=BC=8C=E8=83=BD=E8=BF=9B=E8=A1=8C=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E5=92=8C=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/dto/CreateMemberCardRequest.java | 61 ++++++++++++++ .../member/dto/PurchaseCardRequest.java | 32 ++++++++ .../manage/member/dto/RefundCardRequest.java | 21 +++++ .../manage/member/dto/RenewCardRequest.java | 35 ++++++++ .../member/dto/UpdateMemberCardRequest.java | 60 ++++++++++++++ .../gym/manage/member/dto/UseCardRequest.java | 23 ++++++ .../member/{card => }/entity/MemberCard.java | 3 +- .../{card => }/entity/MemberCardRecord.java | 5 +- .../entity/MemberCardTransaction.java | 3 +- .../{card => }/entity/RefundApplication.java | 5 +- .../member/{card => }/enums/BizType.java | 2 +- .../member/{card => }/enums/CardEvent.java | 2 +- .../enums/MemberCardRecordStatus.java | 2 +- .../{card => }/enums/MemberCardType.java | 2 +- .../member/{card => }/enums/RefundStatus.java | 2 +- .../{card => }/enums/TransactionType.java | 2 +- .../handler/DistributedLockService.java | 2 +- .../handler/ExpirationReminderService.java | 4 +- .../{card => }/handler/MemberCardHandler.java | 7 +- .../handler/MemberCardRecordHandler.java | 8 +- .../handler/MemberCardScheduledHandler.java | 8 +- .../handler/MemberCardStateMachine.java | 8 +- .../handler/MemberCardTransactionHandler.java | 8 +- .../{card => }/handler/RefundSagaHandler.java | 16 ++-- .../MemberCardRecordRepository.java | 4 +- .../repository/MemberCardRepository.java | 4 +- .../MemberCardTransactionRepository.java | 4 +- .../RefundApplicationRepository.java | 4 +- .../service/IMemberCardRecordService.java | 4 +- .../service/IMemberCardService.java | 6 +- .../IMemberCardTransactionService.java | 6 +- .../service/IRefundApplicationService.java | 4 +- .../impl/MemberCardRecordServiceImpl.java | 8 +- .../service/impl/MemberCardServiceImpl.java | 32 ++++---- .../MemberCardTransactionServiceImpl.java | 10 +-- .../impl/RefundApplicationServiceImpl.java | 10 +-- .../{card => }/util/BeanConvertUtil.java | 2 +- .../manage/member/vo/MemberCardRecordVO.java | 71 ++++++++++++++++ .../member/vo/MemberCardTransactionVO.java | 81 +++++++++++++++++++ .../gym/manage/member/vo/MemberCardVO.java | 77 ++++++++++++++++++ .../manage/member/vo/RefundApplicationVO.java | 77 ++++++++++++++++++ .../gym/manage/app/config/SystemRouter.java | 6 +- 42 files changed, 631 insertions(+), 100 deletions(-) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/CreateMemberCardRequest.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/PurchaseCardRequest.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RefundCardRequest.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RenewCardRequest.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberCardRequest.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UseCardRequest.java rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/entity/MemberCard.java (93%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/entity/MemberCardRecord.java (90%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/entity/MemberCardTransaction.java (94%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/entity/RefundApplication.java (89%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/enums/BizType.java (87%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/enums/CardEvent.java (89%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/enums/MemberCardRecordStatus.java (88%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/enums/MemberCardType.java (87%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/enums/RefundStatus.java (89%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/enums/TransactionType.java (89%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/DistributedLockService.java (96%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/ExpirationReminderService.java (98%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/MemberCardHandler.java (96%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/MemberCardRecordHandler.java (95%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/MemberCardScheduledHandler.java (92%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/MemberCardStateMachine.java (93%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/MemberCardTransactionHandler.java (96%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/handler/RefundSagaHandler.java (89%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/repository/MemberCardRecordRepository.java (97%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/repository/MemberCardRepository.java (97%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/repository/MemberCardTransactionRepository.java (97%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/repository/RefundApplicationRepository.java (93%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/IMemberCardRecordService.java (89%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/IMemberCardService.java (88%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/IMemberCardTransactionService.java (93%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/IRefundApplicationService.java (84%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/impl/MemberCardRecordServiceImpl.java (90%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/impl/MemberCardServiceImpl.java (93%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/impl/MemberCardTransactionServiceImpl.java (92%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/service/impl/RefundApplicationServiceImpl.java (91%) rename gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/{card => }/util/BeanConvertUtil.java (96%) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardTransactionVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/RefundApplicationVO.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/CreateMemberCardRequest.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/CreateMemberCardRequest.java new file mode 100644 index 0000000..6acb34d --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/CreateMemberCardRequest.java @@ -0,0 +1,61 @@ +package cn.novalon.gym.manage.member.dto +; + +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * 创建会员卡类型请求 DTO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +public class CreateMemberCardRequest { + + /** + * 会员卡名称 + */ + @NotBlank(message = "会员卡名称不能为空") + private String memberCardName; + + /** + * 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡 + */ + @NotBlank(message = "会员卡类型不能为空") + private String memberCardType; + + /** + * 会员卡价格 + */ + @NotNull(message = "会员卡价格不能为空") + private BigDecimal memberCardPrice; + + /** + * 有效天数(时长卡用) + */ + private Integer memberCardValidityDays; + + /** + * 总次数(次卡用) + */ + private Integer memberCardTotalTimes; + + /** + * 面额(储值卡用) + */ + private BigDecimal memberCardAmount; + + /** + * 状态:0-下架, 1-上架 + */ + private Integer memberCardStatus = 1; + + /** + * 扩展配置(JSON格式) + */ + private String extraConfig; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/PurchaseCardRequest.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/PurchaseCardRequest.java new file mode 100644 index 0000000..fe6bdc6 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/PurchaseCardRequest.java @@ -0,0 +1,32 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 购买会员卡请求 DTO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +public class PurchaseCardRequest { + + /** + * 会员ID + */ + @NotNull(message = "会员ID不能为空") + private Long memberId; + + /** + * 会员卡类型ID + */ + @NotNull(message = "会员卡类型ID不能为空") + private Long memberCardId; + + /** + * 来源订单ID + */ + private Long sourceOrderId; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RefundCardRequest.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RefundCardRequest.java new file mode 100644 index 0000000..93a2045 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RefundCardRequest.java @@ -0,0 +1,21 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 退款申请请求 DTO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +public class RefundCardRequest { + + /** + * 退款原因 + */ + @NotBlank(message = "退款原因不能为空") + private String reason; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RenewCardRequest.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RenewCardRequest.java new file mode 100644 index 0000000..a56479b --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/RenewCardRequest.java @@ -0,0 +1,35 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotNull; + +/** + * 续费会员卡请求 DTO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +public class RenewCardRequest { + + /** + * 增加的次数 + */ + private Integer addTimes; + + /** + * 增加的金额 + */ + private Double addAmount; + + /** + * 增加的天数 + */ + private Integer addDays; + + /** + * 来源订单ID + */ + private Long sourceOrderId; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberCardRequest.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberCardRequest.java new file mode 100644 index 0000000..dc39d7f --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberCardRequest.java @@ -0,0 +1,60 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * 更新会员卡类型请求 DTO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +public class UpdateMemberCardRequest { + + /** + * 会员卡名称 + */ + @NotBlank(message = "会员卡名称不能为空") + private String memberCardName; + + /** + * 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡 + */ + @NotBlank(message = "会员卡类型不能为空") + private String memberCardType; + + /** + * 会员卡价格 + */ + @NotNull(message = "会员卡价格不能为空") + private BigDecimal memberCardPrice; + + /** + * 有效天数(时长卡用) + */ + private Integer memberCardValidityDays; + + /** + * 总次数(次卡用) + */ + private Integer memberCardTotalTimes; + + /** + * 面额(储值卡用) + */ + private BigDecimal memberCardAmount; + + /** + * 状态:0-下架, 1-上架 + */ + private Integer memberCardStatus; + + /** + * 扩展配置(JSON格式) + */ + private String extraConfig; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UseCardRequest.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UseCardRequest.java new file mode 100644 index 0000000..33df8dc --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UseCardRequest.java @@ -0,0 +1,23 @@ +package cn.novalon.gym.manage.member.dto; + +import lombok.Data; + +/** + * 使用会员卡请求 DTO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +public class UseCardRequest { + + /** + * 扣减的次数 + */ + private Integer deductTimes; + + /** + * 扣减的金额 + */ + private Double deductAmount; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCard.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCard.java similarity index 93% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCard.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCard.java index 684d401..6d2bd40 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCard.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCard.java @@ -1,6 +1,5 @@ -package cn.novalon.gym.manage.member.card.entity; +package cn.novalon.gym.manage.member.entity; -import cn.novalon.gym.manage.member.entity.BaseEntity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardRecord.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCardRecord.java similarity index 90% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardRecord.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCardRecord.java index 98d5e4b..06d00f3 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardRecord.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCardRecord.java @@ -1,7 +1,6 @@ -package cn.novalon.gym.manage.member.card.entity; +package cn.novalon.gym.manage.member.entity; -import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.member.entity.BaseEntity; +import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardTransaction.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCardTransaction.java similarity index 94% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardTransaction.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCardTransaction.java index 29abd08..debc15a 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/MemberCardTransaction.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/MemberCardTransaction.java @@ -1,6 +1,5 @@ -package cn.novalon.gym.manage.member.card.entity; +package cn.novalon.gym.manage.member.entity; -import cn.novalon.gym.manage.member.entity.BaseEntity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/RefundApplication.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/RefundApplication.java similarity index 89% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/RefundApplication.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/RefundApplication.java index 88547bc..74c9e83 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/entity/RefundApplication.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/RefundApplication.java @@ -1,7 +1,6 @@ -package cn.novalon.gym.manage.member.card.entity; +package cn.novalon.gym.manage.member.entity; -import cn.novalon.gym.manage.member.card.enums.RefundStatus; -import cn.novalon.gym.manage.member.entity.BaseEntity; +import cn.novalon.gym.manage.member.enums.RefundStatus; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/BizType.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/BizType.java similarity index 87% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/BizType.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/BizType.java index b53f839..492891f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/BizType.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/BizType.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.enums; +package cn.novalon.gym.manage.member.enums; /** * 会员卡流水关联业务类型枚举 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/CardEvent.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/CardEvent.java similarity index 89% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/CardEvent.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/CardEvent.java index db3f8ee..dcf94cc 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/CardEvent.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/CardEvent.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.enums; +package cn.novalon.gym.manage.member.enums; /** * 会员卡状态机事件枚举 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardRecordStatus.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/MemberCardRecordStatus.java similarity index 88% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardRecordStatus.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/MemberCardRecordStatus.java index fe27fe7..91bf7df 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardRecordStatus.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/MemberCardRecordStatus.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.enums; +package cn.novalon.gym.manage.member.enums; /** * 会员卡记录状态枚举 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardType.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/MemberCardType.java similarity index 87% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardType.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/MemberCardType.java index 207d1ff..7cf18e6 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/MemberCardType.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/MemberCardType.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.enums; +package cn.novalon.gym.manage.member.enums; /** * 会员卡类型枚举 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/RefundStatus.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/RefundStatus.java similarity index 89% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/RefundStatus.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/RefundStatus.java index c350f31..dbd56ea 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/RefundStatus.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/RefundStatus.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.enums; +package cn.novalon.gym.manage.member.enums; /** * 退款申请状态枚举 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/TransactionType.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/TransactionType.java similarity index 89% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/TransactionType.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/TransactionType.java index 5ae8c94..7345b80 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/enums/TransactionType.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/TransactionType.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.enums; +package cn.novalon.gym.manage.member.enums; /** * 会员卡流水操作类型枚举 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/DistributedLockService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/DistributedLockService.java similarity index 96% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/DistributedLockService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/DistributedLockService.java index c7098af..3fc995e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/DistributedLockService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/DistributedLockService.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/ExpirationReminderService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/ExpirationReminderService.java similarity index 98% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/ExpirationReminderService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/ExpirationReminderService.java index c063de9..ef6cead 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/ExpirationReminderService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/ExpirationReminderService.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java similarity index 96% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java index b697999..9d052e2 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardHandler.java @@ -1,8 +1,7 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; -import cn.novalon.gym.manage.member.card.entity.MemberCard; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.service.IMemberCardService; +import cn.novalon.gym.manage.member.entity.MemberCard; +import cn.novalon.gym.manage.member.service.IMemberCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardRecordHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java similarity index 95% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardRecordHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java index 7928a7c..6d5b091 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardRecordHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java @@ -1,8 +1,8 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.service.IMemberCardRecordService; -import cn.novalon.gym.manage.member.card.service.IMemberCardService; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.service.IMemberCardRecordService; +import cn.novalon.gym.manage.member.service.IMemberCardService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Data; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardScheduledHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardScheduledHandler.java similarity index 92% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardScheduledHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardScheduledHandler.java index 0c4181f..047afc3 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardScheduledHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardScheduledHandler.java @@ -1,9 +1,7 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.enums.CardEvent; -import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.enums.CardEvent; +import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardStateMachine.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardStateMachine.java similarity index 93% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardStateMachine.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardStateMachine.java index 94c8738..3547a80 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardStateMachine.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardStateMachine.java @@ -1,8 +1,8 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.enums.CardEvent; -import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.enums.CardEvent; +import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardTransactionHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardTransactionHandler.java similarity index 96% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardTransactionHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardTransactionHandler.java index 771248a..23f964e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/MemberCardTransactionHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardTransactionHandler.java @@ -1,9 +1,9 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; import cn.hutool.db.PageResult; -import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; -import cn.novalon.gym.manage.member.card.enums.TransactionType; -import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; +import cn.novalon.gym.manage.member.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.enums.TransactionType; +import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Validator; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/RefundSagaHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/RefundSagaHandler.java similarity index 89% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/RefundSagaHandler.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/RefundSagaHandler.java index fd42204..de914c7 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/handler/RefundSagaHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/RefundSagaHandler.java @@ -1,12 +1,12 @@ -package cn.novalon.gym.manage.member.card.handler; +package cn.novalon.gym.manage.member.handler; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; -import cn.novalon.gym.manage.member.card.enums.CardEvent; -import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.member.card.enums.TransactionType; -import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; -import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.enums.CardEvent; +import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.enums.TransactionType; +import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRecordRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java similarity index 97% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRecordRepository.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java index b18f53c..b3ce72e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRecordRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.repository; +package cn.novalon.gym.manage.member.repository; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; import org.springframework.data.domain.Pageable; import org.springframework.data.r2dbc.repository.Modifying; import org.springframework.data.r2dbc.repository.Query; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java similarity index 97% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRepository.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java index c45dd9e..b3820ae 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRepository.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.repository; +package cn.novalon.gym.manage.member.repository; -import cn.novalon.gym.manage.member.card.entity.MemberCard; +import cn.novalon.gym.manage.member.entity.MemberCard; import org.springframework.data.domain.Pageable; import org.springframework.data.r2dbc.repository.Modifying; import org.springframework.data.r2dbc.repository.Query; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardTransactionRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardTransactionRepository.java similarity index 97% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardTransactionRepository.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardTransactionRepository.java index 7f33e53..8f9d040 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/MemberCardTransactionRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardTransactionRepository.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.repository; +package cn.novalon.gym.manage.member.repository; -import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.entity.MemberCardTransaction; import org.springframework.data.domain.Pageable; import org.springframework.data.r2dbc.repository.Modifying; import org.springframework.data.r2dbc.repository.Query; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/RefundApplicationRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/RefundApplicationRepository.java similarity index 93% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/RefundApplicationRepository.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/RefundApplicationRepository.java index ae9f288..5c4e4c1 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/repository/RefundApplicationRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/RefundApplicationRepository.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.repository; +package cn.novalon.gym.manage.member.repository; -import cn.novalon.gym.manage.member.card.entity.RefundApplication; +import cn.novalon.gym.manage.member.entity.RefundApplication; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardRecordService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardRecordService.java similarity index 89% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardRecordService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardRecordService.java index 32348f5..52eb9fc 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardRecordService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardRecordService.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.service; +package cn.novalon.gym.manage.member.service; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardService.java similarity index 88% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardService.java index ec5a3c3..14517b4 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardService.java @@ -1,7 +1,7 @@ -package cn.novalon.gym.manage.member.card.service; +package cn.novalon.gym.manage.member.service; -import cn.novalon.gym.manage.member.card.entity.MemberCard; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.entity.MemberCard; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardTransactionService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardTransactionService.java similarity index 93% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardTransactionService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardTransactionService.java index 516b393..fd7ac94 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IMemberCardTransactionService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IMemberCardTransactionService.java @@ -1,7 +1,7 @@ -package cn.novalon.gym.manage.member.card.service; +package cn.novalon.gym.manage.member.service; -import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; -import cn.novalon.gym.manage.member.card.enums.TransactionType; +import cn.novalon.gym.manage.member.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.enums.TransactionType; import org.springframework.data.domain.Pageable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IRefundApplicationService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IRefundApplicationService.java similarity index 84% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IRefundApplicationService.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IRefundApplicationService.java index 669a42b..0c77335 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/IRefundApplicationService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/IRefundApplicationService.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.member.card.service; +package cn.novalon.gym.manage.member.service; -import cn.novalon.gym.manage.member.card.entity.RefundApplication; +import cn.novalon.gym.manage.member.entity.RefundApplication; import reactor.core.publisher.Mono; /** diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java similarity index 90% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardRecordServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java index 874a342..ce477f2 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardRecordServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java @@ -1,8 +1,8 @@ -package cn.novalon.gym.manage.member.card.service.impl; +package cn.novalon.gym.manage.member.service.impl; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; -import cn.novalon.gym.manage.member.card.service.IMemberCardRecordService; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.service.IMemberCardRecordService; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java similarity index 93% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java index 939704d..e7831ea 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java @@ -1,20 +1,20 @@ -package cn.novalon.gym.manage.member.card.service.impl; +package cn.novalon.gym.manage.member.service.impl; -import cn.novalon.gym.manage.member.card.entity.MemberCard; -import cn.novalon.gym.manage.member.card.entity.MemberCardRecord; -import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; -import cn.novalon.gym.manage.member.card.enums.CardEvent; -import cn.novalon.gym.manage.member.card.enums.MemberCardRecordStatus; -import cn.novalon.gym.manage.member.card.enums.MemberCardType; -import cn.novalon.gym.manage.member.card.enums.TransactionType; -import cn.novalon.gym.manage.member.card.handler.DistributedLockService; -import cn.novalon.gym.manage.member.card.handler.ExpirationReminderService; -import cn.novalon.gym.manage.member.card.handler.MemberCardStateMachine; -import cn.novalon.gym.manage.member.card.handler.RefundSagaHandler; -import cn.novalon.gym.manage.member.card.repository.MemberCardRecordRepository; -import cn.novalon.gym.manage.member.card.repository.MemberCardRepository; -import cn.novalon.gym.manage.member.card.service.IMemberCardService; -import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; +import cn.novalon.gym.manage.member.entity.MemberCard; +import cn.novalon.gym.manage.member.entity.MemberCardRecord; +import cn.novalon.gym.manage.member.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.enums.CardEvent; +import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus; +import cn.novalon.gym.manage.member.enums.MemberCardType; +import cn.novalon.gym.manage.member.enums.TransactionType; +import cn.novalon.gym.manage.member.handler.DistributedLockService; +import cn.novalon.gym.manage.member.handler.ExpirationReminderService; +import cn.novalon.gym.manage.member.handler.MemberCardStateMachine; +import cn.novalon.gym.manage.member.handler.RefundSagaHandler; +import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; +import cn.novalon.gym.manage.member.repository.MemberCardRepository; +import cn.novalon.gym.manage.member.service.IMemberCardService; +import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardTransactionServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardTransactionServiceImpl.java similarity index 92% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardTransactionServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardTransactionServiceImpl.java index 6b5bc21..987ed5a 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/MemberCardTransactionServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardTransactionServiceImpl.java @@ -1,9 +1,9 @@ -package cn.novalon.gym.manage.member.card.service.impl; +package cn.novalon.gym.manage.member.service.impl; -import cn.novalon.gym.manage.member.card.entity.MemberCardTransaction; -import cn.novalon.gym.manage.member.card.enums.TransactionType; -import cn.novalon.gym.manage.member.card.repository.MemberCardTransactionRepository; -import cn.novalon.gym.manage.member.card.service.IMemberCardTransactionService; +import cn.novalon.gym.manage.member.entity.MemberCardTransaction; +import cn.novalon.gym.manage.member.enums.TransactionType; +import cn.novalon.gym.manage.member.repository.MemberCardTransactionRepository; +import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java similarity index 91% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/RefundApplicationServiceImpl.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java index 48fb6ae..7097f3e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/service/impl/RefundApplicationServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java @@ -1,9 +1,9 @@ -package cn.novalon.gym.manage.member.card.service.impl; +package cn.novalon.gym.manage.member.service.impl; -import cn.novalon.gym.manage.member.card.entity.RefundApplication; -import cn.novalon.gym.manage.member.card.enums.RefundStatus; -import cn.novalon.gym.manage.member.card.repository.RefundApplicationRepository; -import cn.novalon.gym.manage.member.card.service.IRefundApplicationService; +import cn.novalon.gym.manage.member.entity.RefundApplication; +import cn.novalon.gym.manage.member.enums.RefundStatus; +import cn.novalon.gym.manage.member.repository.RefundApplicationRepository; +import cn.novalon.gym.manage.member.service.IRefundApplicationService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/BeanConvertUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/BeanConvertUtil.java similarity index 96% rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/BeanConvertUtil.java rename to gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/BeanConvertUtil.java index 493fa85..19d780e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/card/util/BeanConvertUtil.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/BeanConvertUtil.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.member.card.util; +package cn.novalon.gym.manage.member.util; import cn.hutool.core.bean.BeanUtil; import org.springframework.stereotype.Component; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java new file mode 100644 index 0000000..db4aed9 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java @@ -0,0 +1,71 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 会员卡记录响应 VO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberCardRecordVO { + + /** + * 会员卡记录ID + */ + private Long memberCardRecordId; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 会员卡类型ID + */ + private Long memberCardId; + + /** + * 会员卡名称 + */ + private String memberCardName; + + /** + * 状态:ACTIVE-有效, USED_UP-用完, EXPIRED-过期, REFUNDED-已退款 + */ + private String status; + + /** + * 剩余次数 + */ + private Integer remainingTimes; + + /** + * 剩余金额 + */ + private Double remainingAmount; + + /** + * 到期时间 + */ + private LocalDateTime expireTime; + + /** + * 购买时间 + */ + private LocalDateTime purchaseTime; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardTransactionVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardTransactionVO.java new file mode 100644 index 0000000..50d4038 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardTransactionVO.java @@ -0,0 +1,81 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 会员卡交易流水响应 VO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberCardTransactionVO { + + /** + * 交易ID + */ + private Long id; + + /** + * 会员卡记录ID + */ + private Long memberCardRecordId; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 会员卡类型ID + */ + private Long memberCardId; + + /** + * 操作类型:PURCHASE-购买, DEDUCT-扣次/扣费, RENEW-续费, REFUND-退款, EXPIRE-过期 + */ + private String operationType; + + /** + * 变动次数 + */ + private Integer changeAmount; + + /** + * 变动金额 + */ + private Double changeBalance; + + /** + * 变动后剩余次数 + */ + private Integer afterRemainingCount; + + /** + * 变动后剩余金额 + */ + private Double afterRemainingBalance; + + /** + * 关联业务类型 + */ + private String relatedBizType; + + /** + * 备注 + */ + private String remark; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardVO.java new file mode 100644 index 0000000..6165bf0 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardVO.java @@ -0,0 +1,77 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 会员卡类型响应 VO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberCardVO { + + /** + * 会员卡ID + */ + private Long memberCardId; + + /** + * 会员卡名称 + */ + private String memberCardName; + + /** + * 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡 + */ + private String memberCardType; + + /** + * 会员卡价格 + */ + private BigDecimal memberCardPrice; + + /** + * 有效天数(时长卡用) + */ + private Integer memberCardValidityDays; + + /** + * 总次数(次卡用) + */ + private Integer memberCardTotalTimes; + + /** + * 面额(储值卡用) + */ + private BigDecimal memberCardAmount; + + /** + * 状态:0-下架, 1-上架 + */ + private Integer memberCardStatus; + + /** + * 扩展配置(JSON格式) + */ + private String extraConfig; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/RefundApplicationVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/RefundApplicationVO.java new file mode 100644 index 0000000..9f6d99c --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/RefundApplicationVO.java @@ -0,0 +1,77 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 退款申请响应 VO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefundApplicationVO { + + /** + * 申请ID + */ + private Long id; + + /** + * 会员卡记录ID + */ + private Long recordId; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 状态:PENDING-待审核, APPROVED-已批准, REJECTED-已拒绝, PROCESSING-处理中, SUCCESS-成功, FAILED-失败 + */ + private String status; + + /** + * 退款原因 + */ + private String reason; + + /** + * 申请时间 + */ + private LocalDateTime applyTime; + + /** + * 审核时间 + */ + private LocalDateTime auditTime; + + /** + * 审核人ID + */ + private Long auditorId; + + /** + * 审核备注 + */ + private String auditRemark; + + /** + * 退款金额 + */ + private BigDecimal refundAmount; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; +} 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 5df39d9..996c1d1 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 @@ -2,9 +2,9 @@ package cn.novalon.gym.manage.app.config; import cn.novalon.gym.manage.file.handler.SysFileHandler; -import cn.novalon.gym.manage.member.card.handler.MemberCardHandler; -import cn.novalon.gym.manage.member.card.handler.MemberCardRecordHandler; -import cn.novalon.gym.manage.member.card.handler.MemberCardTransactionHandler; +import cn.novalon.gym.manage.member.handler.MemberCardHandler; +import cn.novalon.gym.manage.member.handler.MemberCardRecordHandler; +import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler; import cn.novalon.gym.manage.member.handler.MemberHandler; import cn.novalon.gym.manage.member.handler.WechatAuthHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; -- 2.52.0 From 29b73c1f674a846ac971b98187f412c7736c6d86 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Fri, 29 May 2026 14:25:17 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=89=8D=E5=8F=B0?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=98=B2XSS=E6=B3=A8=E5=85=A5=EF=BC=8C=E5=8A=A0=E5=85=A5ES?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage/member/dto/SearchMemberDto.java | 4 +- .../member/dto/UpdateMemberInfoDto.java | 20 +-- .../gym/manage/member/entity/BaseEntity.java | 3 + .../gym/manage/member/entity/Member.java | 9 +- .../manage/member/entity/SignInRecord.java | 38 ----- .../gym/manage/member/enums/GenderEnum.java | 37 +++++ .../gym/manage/member/es/entity/MemberES.java | 6 + .../es/repository/MemberESRepository.java | 5 +- .../manage/member/handler/MemberHandler.java | 53 ++++--- .../member/repository/IMemberRepository.java | 41 +++++- .../MemberCardRecordRepository.java | 2 +- .../manage/member/service/MemberService.java | 18 +++ .../service/impl/MemberServiceImpl.java | 132 +++++++++++++++--- .../impl/RefundApplicationServiceImpl.java | 7 +- .../service/impl/WechatAuthServiceImpl.java | 11 +- .../impl/WechatOfficialServiceImpl.java | 5 +- .../gym/manage/member/util/AesUtil.java | 41 ++++-- .../manage/member/util/WechatPhoneUtil.java | 2 +- .../manage/member/vo/MemberCardInfoVO.java | 92 ++++++++++++ .../manage/member/vo/MemberCardRecordVO.java | 4 +- .../gym/manage/member/vo/MemberDetailVO.java | 96 +++++++++++++ .../gym/manage/member/vo/MemberInfoVO.java | 13 +- .../src/main/resources/db/schema.sql | 2 +- .../gym/manage/app/ManageApplication.java | 10 +- .../manage/common/util/HtmlEscapeUtil.java | 117 ++++++++++++++++ 25 files changed, 635 insertions(+), 133 deletions(-) delete mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java create mode 100644 gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java index bcd528d..8b66ee0 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java @@ -12,8 +12,8 @@ public class SearchMemberDto { // 搜索字段 - 包括 会员号、昵称、手机号 private String searchValue; - // 排序 - private String filter; + // 性别排序 + private Integer gender; // 页码 private Integer pageNum = 1; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java index 835347c..fb633df 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/UpdateMemberInfoDto.java @@ -1,30 +1,32 @@ package cn.novalon.gym.manage.member.dto; +import cn.novalon.gym.manage.member.enums.GenderEnum; import lombok.Data; +import java.time.LocalDate; import java.util.Date; /** * 更新会员信息Dto - * + * * @author 付嘉 * @date 2026-05-10 */ @Data public class UpdateMemberInfoDto { - + // 昵称 private String nickname; - + // 性别 - private Integer gender; - + private GenderEnum gender; + // 生日 - private Date birthday; - + private LocalDate birthday; + // 头像 private String avatar; - + // 地址 private String address; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java index 1ad7597..865e4bb 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/BaseEntity.java @@ -33,6 +33,9 @@ public abstract class BaseEntity implements Persistable { @Column("updated_at") private LocalDateTime updatedAt; + @Column("deleted_at") + private LocalDateTime deletedAt; + // 判断当前实体是否是新建的 @Override public boolean isNew() { diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java index 36c7ba4..8983e20 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -1,13 +1,10 @@ package cn.novalon.gym.manage.member.entity; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Date; @@ -44,7 +41,7 @@ public class Member extends BaseEntity { //生日 @Column("birthday") - private Date birthday; + private LocalDate birthday; //地址 @Column("address") diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java deleted file mode 100644 index 83dae3a..0000000 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/SignInRecord.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.novalon.gym.manage.member.entity; - -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -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.LocalDate; -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Table("sign_in_record") -public class SignInRecord { - - @Id - private Long id; - - // 会员ID - @Column("member_id") - private Long memberId; - - // 签到日期 - @Column("sign_in_date") - private LocalDate signInDate; - - // 签到时间 - @Column("sign_in_time") - private LocalDateTime signInTime; - - // 创建时间 - @CreatedDate - @Column("created_at") - private LocalDateTime createdAt; -} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java new file mode 100644 index 0000000..9924cfb --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/enums/GenderEnum.java @@ -0,0 +1,37 @@ +package cn.novalon.gym.manage.member.enums; + +import lombok.Getter; + +/** + * 性别枚举 + * + * @author 付嘉 + * @date 2026-05-29 + */ +@Getter +public enum GenderEnum { + + UNKNOWN(0, "未知"), + MALE(1, "男"), + FEMALE(2, "女"); + + private final Integer code; + private final String desc; + + GenderEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + public static GenderEnum fromCode(Integer code) { + if (code == null) { + return UNKNOWN; + } + for (GenderEnum gender : values()) { + if (gender.code.equals(code)) { + return gender; + } + } + return UNKNOWN; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java index b479ce5..1e2e23b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/entity/MemberES.java @@ -1,12 +1,18 @@ package cn.novalon.gym.manage.member.es.entity; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import java.time.LocalDateTime; + @Data +@NoArgsConstructor +@AllArgsConstructor @Document(indexName = "gym_members") public class MemberES { diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java index a233097..80064ed 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/es/repository/MemberESRepository.java @@ -2,6 +2,7 @@ package cn.novalon.gym.manage.member.es.repository; import cn.novalon.gym.manage.member.es.entity.MemberES; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -15,6 +16,6 @@ public interface MemberESRepository extends ReactiveElasticsearchRepository findByMemberNoOrPhoneOrNicknameContainingAndGender( - String memberNo, String phone, String nickname,String gender, Pageable pageable); + Flux findByMemberNoOrPhoneOrNicknameContaining( + String memberNo, String phone, String nickname, Pageable pageable); } \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 7571053..6d1206b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -8,6 +8,7 @@ import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.sys.util.AuthUtil; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; @@ -34,8 +35,6 @@ public class MemberHandler { private final MemberService memberService; private final WechatAuthService wechatAuthService; private final WechatOfficialService wechatOfficialService; - private final JwtTokenProvider jwtTokenProvider; - private final WechatProperties wechatProperties; private final AuthUtil authUtil; /** @@ -179,11 +178,21 @@ public class MemberHandler { log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId); - // TODO 多表查询:会员信息、团课信息、会员卡信息 - - return ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue("成功"); + return memberService.getMemberDetail(memberId) + .flatMap(detail -> { + if (detail.getPhone() != null && !detail.getPhone().isEmpty()) { + try { + String decryptedPhone = AesUtil.decrypt(detail.getPhone()); + detail.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone)); + } catch (Exception e) { + log.error("手机号解密失败, memberId: {}", detail.getId(), e); + detail.setPhone(null); + } + } + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(detail); + }); } /** @@ -201,13 +210,14 @@ public class MemberHandler { long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误"); + // TODO: 补充签到记录 log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId); - // TODO 多表查询:会员信息、团课信息、会员卡信息 - - return ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue("成功"); + return request.bodyToMono(UpdateMemberInfoDto.class) + .flatMap(updateDto -> memberService.adminUpdateMemberInfo(memberId, updateDto)) + .flatMap(detail -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(detail)); } /** @@ -221,9 +231,9 @@ public class MemberHandler { Long adminId = authUtil.getMemberIdOrThrow(request); String keyword = request.queryParam("searchValue").orElse(null); - String filter = request.queryParam("filter").orElse(null); - int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); - int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + Integer filter = NumberUtils.toInt(request.queryParam("filter").orElse("-1"), -1); + Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); + Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", adminId, keyword, filter, pageNum, pageSize); @@ -233,10 +243,8 @@ public class MemberHandler { // 解密手机号 if (member.getPhone() != null && !member.getPhone().isEmpty()) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); - member.setPhone(decryptedPhone); + String decryptedPhone = AesUtil.decrypt(member.getPhone()); + member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone)); } catch (Exception e) { log.error("手机号解密失败, memberId: {}", member.getId(), e); member.setPhone(null); @@ -263,16 +271,15 @@ public class MemberHandler { int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize); + // TODO: 补充签到记录 return memberService.findAll(pageNum, pageSize) .map(member -> { // 解密手机号 if (member.getPhone() != null && !member.getPhone().isEmpty()) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv); - member.setPhone(decryptedPhone); + String decryptedPhone = AesUtil.decrypt(member.getPhone()); + member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone)); } catch (Exception e) { log.error("手机号解密失败, memberId: {}", member.getId(), e); member.setPhone(null); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java index e746aea..d6c868e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/IMemberRepository.java @@ -1,7 +1,9 @@ package cn.novalon.gym.manage.member.repository; import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.vo.MemberCardInfoVO; import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -30,7 +32,44 @@ public interface IMemberRepository extends R2dbcRepository { /** * 分页查询所有会员 - * 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有 */ Flux findAllBy(Pageable pageable); + + /** + * 查询会员的所有卡片 + */ + @Query("SELECT " + + " r.id, " + + " r.member_card_record_id, " + + " r.member_id, " + + " r.member_card_id, " + + " r.status, " + + " r.remaining_times, " + + " r.remaining_amount, " + + " r.expire_time, " + + " r.purchase_time, " + + " r.source_order_id, " + + " r.created_at, " + + " r.updated_at, " + + " r.version, " + + " r.card_composition, " + + " c.id AS card_id, " + + " c.member_card_id, " + + " c.member_card_name, " + + " c.member_card_type, " + + " c.member_card_price, " + + " c.member_card_validity_days, " + + " c.member_card_total_times, " + + " c.member_card_amount, " + + " c.member_card_status, " + + " c.extra_config, " + + " c.created_at AS card_created_at, " + + " c.updated_at AS card_updated_at " + + "FROM member_card_record r " + + "LEFT JOIN member_card c ON r.member_card_id = c.id " + + "WHERE r.member_id = :memberId " + + "AND r.deleted_at IS NULL " + + "AND c.deleted_at IS NULL " + + "ORDER BY r.created_at DESC") + Flux findCardRecordsWithCardInfoByMemberId(Long memberId); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java index b3ce72e..8c3d6f1 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/repository/MemberCardRecordRepository.java @@ -13,7 +13,7 @@ import java.time.LocalDateTime; /** * 会员卡记录 Repository(会员持有的卡) - * + * * @author 付嘉 * @date 2026-05-27 */ diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java index c659f1b..72e4b1f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/MemberService.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.entity.Member; import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -58,4 +59,21 @@ public interface MemberService { * @return 所有会员信息 */ Flux findAll(Integer pageNum, Integer pageSize); + + /** + * 前台管理端获取会员详情(含会员卡信息) + * + * @param memberId 会员ID + * @return 会员详情 + */ + Mono getMemberDetail(Long memberId); + + /** + * 前台管理端编辑会员信息 + * + * @param memberId 会员ID + * @param updateDto 更新信息DTO + * @return 更新后的会员详情 + */ + Mono adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index d1b6c44..e63e79d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -4,20 +4,31 @@ import cn.novalon.gym.manage.common.exception.ConflictException; import cn.novalon.gym.manage.common.exception.ErrorCode; import cn.novalon.gym.manage.common.exception.NotFoundException; import cn.novalon.gym.manage.common.exception.SystemException; -import cn.novalon.gym.manage.member.config.WechatProperties; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.enums.GenderEnum; +import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.es.entity.MemberES; import cn.novalon.gym.manage.member.es.repository.MemberESRepository; import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.util.AesUtil; +import cn.novalon.gym.manage.member.util.BeanConvertUtil; import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.vo.MemberCardInfoVO; +import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.IndexResponse; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -26,6 +37,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * 会员服务实现 @@ -41,7 +56,6 @@ public class MemberServiceImpl implements MemberService { private final IMemberRepository memberRepository; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; - private final WechatProperties wechatProperties; private EsSyncUtils.EntitySyncer memberSyncer; @@ -67,10 +81,10 @@ public class MemberServiceImpl implements MemberService { return memberRepository.findById(memberId) .flatMap(member -> { if (updateDto.getNickname() != null) { - member.setNickname(updateDto.getNickname()); + member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); } if (updateDto.getGender() != null) { - member.setGender(updateDto.getGender()); + member.setGender(updateDto.getGender().getCode()); } if (updateDto.getBirthday() != null) { member.setBirthday(updateDto.getBirthday()); @@ -79,7 +93,7 @@ public class MemberServiceImpl implements MemberService { member.setAvatar(updateDto.getAvatar()); } if (updateDto.getAddress() != null) { - member.setAddress(updateDto.getAddress()); + member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress())); } return memberRepository.save(member); @@ -99,11 +113,14 @@ public class MemberServiceImpl implements MemberService { String phone = member.getPhone(); String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null; + GenderEnum genderEnum = GenderEnum.fromCode(member.getGender()); + return MemberInfoVO.builder() .id(member.getId()) .nickname(member.getNickname()) .phone(maskedPhone) - .gender(member.getGender()) + .gender(genderEnum) + .genderDesc(genderEnum.getDesc()) .birthday(member.getBirthday()) .avatar(member.getAvatar()) .hasPhone(phone != null) @@ -117,9 +134,7 @@ public class MemberServiceImpl implements MemberService { String encryptedPhone; try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - encryptedPhone = AesUtil.encrypt(phone, secretKey, iv); + encryptedPhone = AesUtil.encrypt(phone); log.info("手机号加密成功"); } catch (Exception e) { log.error("手机号加密失败", e); @@ -147,7 +162,7 @@ public class MemberServiceImpl implements MemberService { public Flux searchMember(SearchMemberDto searchMemberDto) { log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", searchMemberDto.getSearchValue(), - searchMemberDto.getFilter(), + searchMemberDto.getGender(), searchMemberDto.getPageNum(), searchMemberDto.getPageSize()); @@ -155,22 +170,23 @@ public class MemberServiceImpl implements MemberService { if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ log.debug("搜索值为手机号格式,进行加密处理"); - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - searchValue = AesUtil.encrypt(searchValue,secretKey,iv); + searchValue = AesUtil.encrypt(searchValue); } Pageable pageable = PageRequest.of( searchMemberDto.getPageNum() - 1, - searchMemberDto.getPageSize(), - Sort.by(Sort.Direction.DESC, "update_at") + searchMemberDto.getPageSize() ); - return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender( + if (searchValue == null) { + log.warn("搜索值为空,返回空结果"); + return Flux.empty(); + } + + return memberESRepository.findByMemberNoOrPhoneOrNicknameContaining( searchValue, searchValue, searchValue, - searchMemberDto.getFilter() , pageable ); } @@ -187,6 +203,86 @@ public class MemberServiceImpl implements MemberService { return memberRepository.findAllBy(pageable); } + @Override + public Mono getMemberDetail(Long memberId) { + log.info("查询会员详情, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .zipWith( + memberRepository.findCardRecordsWithCardInfoByMemberId(memberId) + .collectList(), + (baseInfo, cardList) -> { + MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class); + + GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender()); + memberDetailVO.setGenderDesc(genderEnum.getDesc()); + + List enrichedCards = cardList.stream() + .peek(vo -> { + if (vo.getMemberCardType() != null) { + try { + MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType()); + vo.setMemberCardTypeDesc(cardType.getDesc()); + } catch (IllegalArgumentException e) { + vo.setMemberCardTypeDesc(vo.getMemberCardType()); + } + } + if (vo.getMemberCardStatus() != null) { + vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架"); + } + }) + .collect(Collectors.toList()); + memberDetailVO.setMemberCards(enrichedCards); + + long activeCount = enrichedCards.stream() + .filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) + .count(); + memberDetailVO.setActiveCardCount((int) activeCount); + memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount); + + return memberDetailVO; + } + ); + } + + + @Override + public Mono adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) { + log.info("前台管理端编辑会员信息, memberId: {}", memberId); + + return memberRepository.findById(memberId) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })) + .flatMap(member -> { + log.error("有用户"); + if (updateDto.getNickname() != null) { + member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); + } + if (updateDto.getGender() != null) { + member.setGender(updateDto.getGender().getCode()); + } + if (updateDto.getBirthday() != null) { + member.setBirthday(updateDto.getBirthday()); + } + if (updateDto.getAvatar() != null) { + member.setAvatar(updateDto.getAvatar()); + } + if (updateDto.getAddress() != null) { + member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress())); + } + + return memberRepository.save(member); + }) + .doOnSuccess(memberSyncer::sync) + .map(savedMember -> true) + .onErrorResume(e -> { + log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e); + return Mono.just(false); + }); + } + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) .flatMap(member -> { @@ -205,4 +301,4 @@ public class MemberServiceImpl implements MemberService { throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java index 7097f3e..f492e78 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.entity.RefundApplication; import cn.novalon.gym.manage.member.enums.RefundStatus; import cn.novalon.gym.manage.member.repository.RefundApplicationRepository; @@ -38,7 +39,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { .then(Mono.defer(() -> { RefundApplication application = RefundApplication.builder() .recordId(recordId) - .reason(reason) + .reason(HtmlEscapeUtil.escape(reason)) .status(RefundStatus.PENDING) .applyTime(LocalDateTime.now()) .build(); @@ -58,7 +59,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); } - return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark) + return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark)) .thenReturn(application) .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId)); @@ -74,7 +75,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); } - return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark) + return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark)) .thenReturn(application) .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId)); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 5e7f299..9d567d0 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ConflictException; import cn.novalon.gym.manage.common.exception.ErrorCode; import cn.novalon.gym.manage.common.exception.NotFoundException; import cn.novalon.gym.manage.common.exception.SystemException; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.WechatLoginDto; import cn.novalon.gym.manage.member.entity.Member; @@ -42,7 +43,6 @@ public class WechatAuthServiceImpl implements WechatAuthService { private final WechatApiService wechatApiService; private final IMemberRepository memberRepository; - private final WechatProperties wechatProperties; private final WechatPhoneUtil wechatPhoneUtil; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; @@ -199,10 +199,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { private String encryptPhone(String phoneNumber) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - - String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv); + String encryptedPhone = AesUtil.encrypt(phoneNumber); log.debug("手机号加密成功"); return encryptedPhone; @@ -214,10 +211,8 @@ public class WechatAuthServiceImpl implements WechatAuthService { public String decryptPhone(String encryptedPhone) { try { - String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); - String iv = wechatProperties.getPhoneEncryption().getIv(); - String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv); + String phoneNumber = AesUtil.decrypt(encryptedPhone); log.debug("手机号解密成功"); return phoneNumber; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index b5b863a..5143d9b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.entity.Member; import cn.novalon.gym.manage.member.es.entity.MemberES; @@ -74,11 +75,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setOfficialOpenId(openId); if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) { - existingMember.setNickname(userInfo.getNickname()); + existingMember.setNickname(HtmlEscapeUtil.escape(userInfo.getNickname())); } if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) { - existingMember.setAvatar(userInfo.getHeadimgurl()); + existingMember.setAvatar(HtmlEscapeUtil.escape(userInfo.getHeadimgurl())); } return memberRepository.save(existingMember) diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java index f54a003..f5fd4af 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/AesUtil.java @@ -1,12 +1,24 @@ package cn.novalon.gym.manage.member.util; +import cn.novalon.gym.manage.member.config.WechatProperties; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.IndexResponse; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.checkerframework.checker.units.qual.K; +import org.elasticsearch.client.RestClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.HashMap; +import java.util.Map; /** * AES加密工具类 @@ -16,24 +28,35 @@ import java.util.Base64; */ @Slf4j +@Component public class AesUtil { private static final String ALGORITHM = "AES"; private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; + private static String KEY; + private static String IV; + + @Autowired + public void setWechatProperties(WechatProperties props) { + KEY = props.getPhoneEncryption().getSecretKey(); // 从配置类读取 + IV = props.getPhoneEncryption().getIv(); + if(KEY == null || IV == null) throw new RuntimeException("请配置AES密钥和偏移量"); + } + + /** * AES 解密 * * @param encryptedData 加密数据,Base64编码 - * @param key AES密钥,Base64编码(32字节) - * @param iv 初始化向量IV,Base64编码(16字节) * @return 解密后的字符串 */ - public static String decrypt(String encryptedData, String key, String iv) { + public static String decrypt(String encryptedData) { + try { byte[] dataByte = Base64.getDecoder().decode(encryptedData); - byte[] keyByte = Base64.getDecoder().decode(key); - byte[] ivByte = Base64.getDecoder().decode(iv); + byte[] keyByte = Base64.getDecoder().decode(KEY); + byte[] ivByte = Base64.getDecoder().decode(IV); Cipher cipher = Cipher.getInstance(TRANSFORMATION); SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); @@ -52,15 +75,13 @@ public class AesUtil { * AES 加密 * * @param data 原始数据 - * @param key AES密钥,Base64编码(32字节) - * @param iv 初始化向量IV,Base64编码(16字节) * @return Base64编码的加密数据 */ - public static String encrypt(String data, String key, String iv) { + public static String encrypt(String data) { try { byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); - byte[] keyByte = Base64.getDecoder().decode(key); - byte[] ivByte = Base64.getDecoder().decode(iv); + byte[] keyByte = Base64.getDecoder().decode(KEY); + byte[] ivByte = Base64.getDecoder().decode(IV); Cipher cipher = Cipher.getInstance(TRANSFORMATION); SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java index e4f3c4e..a9c6c55 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/WechatPhoneUtil.java @@ -56,7 +56,7 @@ public class WechatPhoneUtil { * @param phone 手机号 * @return 脱敏后的手机号,如:138****8000 */ - private String maskPhone(String phone) { + public static String maskPhone(String phone) { if (phone == null || phone.length() < 7) { return "***"; } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java new file mode 100644 index 0000000..785f890 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardInfoVO.java @@ -0,0 +1,92 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 会员卡类型响应 VO + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberCardInfoVO { + + /** + * 主键ID + */ + private Long id; + + /** + * 会员卡ID + */ + private Long memberCardId; + + /** + * 会员卡名称 + */ + private String memberCardName; + + /** + * 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡 + */ + private String memberCardType; + + /** + * 卡类型描述 + */ + private String memberCardTypeDesc; + + /** + * 会员卡价格 + */ + private BigDecimal memberCardPrice; + + /** + * 有效天数(时长卡用) + */ + private Integer memberCardValidityDays; + + /** + * 总次数(次卡用) + */ + private Integer memberCardTotalTimes; + + /** + * 面额(储值卡用) + */ + private BigDecimal memberCardAmount; + + /** + * 状态:0-下架, 1-上架 + */ + private Integer memberCardStatus; + + /** + * 状态描述 + */ + private String memberCardStatusDesc; + + /** + * 扩展配置(JSON格式) + */ + private String extraConfig; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java index db4aed9..966590d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberCardRecordVO.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; /** * 会员卡记录响应 VO - * + * * @author 付嘉 * @date 2026-05-27 */ @@ -68,4 +68,4 @@ public class MemberCardRecordVO { * 创建时间 */ private LocalDateTime createdAt; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java new file mode 100644 index 0000000..51ea67c --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberDetailVO.java @@ -0,0 +1,96 @@ +package cn.novalon.gym.manage.member.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +/** + * 会员详情 VO(管理端使用) + * + * @author 付嘉 + * @date 2026-05-27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberDetailVO { + + // ==================== 会员基础信息 ==================== + + /** + * 会员ID + */ + private Long id; + + /** + * 会员编号 + */ + private String memberNo; + + /** + * 昵称 + */ + private String nickname; + + /** + * 手机号(脱敏显示) + */ + private String phone; + + /** + * 性别描述 + */ + private String genderDesc; + + /** + * 生日 + */ + private Date birthday; + + /** + * 地址 + */ + private String address; + + /** + * 头像URL + */ + private String avatar; + + /** + * 是否关注服务号 + */ + private Boolean subscribed; + + /** + * 最后登录时间 + */ + private LocalDateTime lastLoginAt; + + /** + * 注册时间 + */ + private LocalDateTime createdAt; + + // ==================== 会员卡信息 ==================== + + /** + * 会员持有的卡列表 + */ + private List memberCards; + + /** + * 有效会员卡数量 + */ + private Integer activeCardCount; + + /** + * 过期/用完会员卡数量 + */ + private Integer inactiveCardCount; +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java index d8b60e4..f3e763d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/vo/MemberInfoVO.java @@ -1,15 +1,17 @@ package cn.novalon.gym.manage.member.vo; +import cn.novalon.gym.manage.member.enums.GenderEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.util.Date; /** * 会员信息 VO - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -30,10 +32,13 @@ public class MemberInfoVO { private String phone; // 性别 - private Integer gender; + private GenderEnum gender; + + // 性别描述 + private String genderDesc; // 生日 - private Date birthday; + private LocalDate birthday; // 头像 private String avatar; @@ -43,4 +48,4 @@ public class MemberInfoVO { // 是否已关注公众号 private Boolean isSubscribed; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/resources/db/schema.sql b/gym-manage-api/gym-member/src/main/resources/db/schema.sql index 2eea8da..b76f2df 100644 --- a/gym-manage-api/gym-member/src/main/resources/db/schema.sql +++ b/gym-manage-api/gym-member/src/main/resources/db/schema.sql @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS member_user ( nickname VARCHAR(100), -- 昵称 phone VARCHAR(255), -- 手机号(AES加密存储) gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女 - birthday TIMESTAMP, -- 生日 + birthday DATE, -- 生日 address VARCHAR(500), -- 地址 avatar VARCHAR(500), -- 头像URL subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号 diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index 0278f53..a87aaef 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -10,6 +10,8 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.web.server.WebFilter; @@ -17,9 +19,13 @@ import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) -@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", +@EnableR2dbcRepositories(basePackages = { + "cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository" , - "cn.novalon.gym.manage.gymmembercard.dao"}) + "cn.novalon.gym.manage.gymmembercard.dao", + "cn.novalon.gym.manage.member.repository" +}) +@EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository") public class ManageApplication { private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java new file mode 100644 index 0000000..63fbafb --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/HtmlEscapeUtil.java @@ -0,0 +1,117 @@ +package cn.novalon.gym.manage.common.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * HTML 转义工具类 + * 防止 XSS 注入攻击 + * + * @author 付嘉 + * @date 2026-05-29 + */ +public class HtmlEscapeUtil { + + private static final Map ESCAPE_MAP = new HashMap<>(); + private static final Map UNESCAPE_MAP = new HashMap<>(); + private static final Pattern HTML_PATTERN = Pattern.compile("<[^>]*>"); + + static { + // HTML 特殊字符转义映射 + ESCAPE_MAP.put('&', "&"); + ESCAPE_MAP.put('<', "<"); + ESCAPE_MAP.put('>', ">"); + ESCAPE_MAP.put('"', """); + ESCAPE_MAP.put('\'', "'"); + + // 反向映射 + UNESCAPE_MAP.put("&", '&'); + UNESCAPE_MAP.put("<", '<'); + UNESCAPE_MAP.put(">", '>'); + UNESCAPE_MAP.put(""", '"'); + UNESCAPE_MAP.put("'", '\''); + } + + /** + * 转义 HTML 特殊字符 + * + * @param input 原始字符串 + * @return 转义后的字符串 + */ + public static String escape(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + StringBuilder result = new StringBuilder(); + for (char c : input.toCharArray()) { + String escaped = ESCAPE_MAP.get(c); + if (escaped != null) { + result.append(escaped); + } else { + result.append(c); + } + } + return result.toString(); + } + + /** + * 反转义 HTML 特殊字符 + * + * @param input 转义后的字符串 + * @return 原始字符串 + */ + public static String unescape(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + String result = input; + for (Map.Entry entry : UNESCAPE_MAP.entrySet()) { + result = result.replace(entry.getKey(), String.valueOf(entry.getValue())); + } + return result; + } + + /** + * 移除所有 HTML 标签 + * + * @param input 原始字符串 + * @return 移除标签后的字符串 + */ + public static String stripHtmlTags(String input) { + if (input == null || input.isEmpty()) { + return input; + } + return HTML_PATTERN.matcher(input).replaceAll(""); + } + + /** + * 安全转义(转义 + 移除标签) + * + * @param input 原始字符串 + * @return 安全字符串 + */ + public static String sanitize(String input) { + if (input == null || input.isEmpty()) { + return input; + } + // 先移除 HTML 标签,再转义特殊字符 + String noTags = stripHtmlTags(input); + return escape(noTags); + } + + /** + * 判断是否包含 HTML 标签 + * + * @param input 原始字符串 + * @return true-包含, false-不包含 + */ + public static boolean containsHtmlTags(String input) { + if (input == null || input.isEmpty()) { + return false; + } + return HTML_PATTERN.matcher(input).find(); + } +} \ No newline at end of file -- 2.52.0 From 174e33053e1a0c5e0afd269462c524358ccdddda Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Fri, 29 May 2026 22:27:04 +0800 Subject: [PATCH 16/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0redis=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=8A=8A=E6=95=8F=E6=84=9F=E4=BF=A1=E6=81=AF=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gym/manage/member/config/RedisConfig.java | 38 +++++ .../manage/member/dto/SearchMemberDto.java | 3 - .../manage/member/handler/MemberHandler.java | 83 ++--------- .../member/handler/WechatAuthHandler.java | 23 +-- .../handler/WechatOfficialEventHandler.java | 3 - .../impl/MemberCardRecordServiceImpl.java | 51 ++++++- .../service/impl/MemberCardServiceImpl.java | 36 ++++- .../service/impl/MemberServiceImpl.java | 137 ++++++++++++------ .../impl/RefundApplicationServiceImpl.java | 60 ++++++-- .../service/impl/WechatApiServiceImpl.java | 70 +++++---- .../service/impl/WechatAuthServiceImpl.java | 36 ++++- .../impl/WechatOfficialServiceImpl.java | 96 ++++++++---- .../gym/manage/member/util/RedisUtil.java | 72 +++++++++ .../src/main/resources/member-config.yml | 24 +-- gym-manage-api/manage-app/pom.xml | 4 + 15 files changed, 508 insertions(+), 228 deletions(-) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java new file mode 100644 index 0000000..7321879 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java @@ -0,0 +1,38 @@ +package cn.novalon.gym.manage.member.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis 配置类(响应式版本) + * + * @author 付嘉 + * @date 2026-05-29 + */ +@Configuration +public class RedisConfig { + + /** + * 配置 ReactiveRedisTemplate + */ + @Bean + public ReactiveRedisTemplate reactiveRedisTemplate( + ReactiveRedisConnectionFactory connectionFactory) { + + // 配置序列化上下文 + RedisSerializationContext serializationContext = + RedisSerializationContext.newSerializationContext() + .key(StringRedisSerializer.UTF_8) + .value(new GenericJackson2JsonRedisSerializer()) + .hashKey(StringRedisSerializer.UTF_8) + .hashValue(new GenericJackson2JsonRedisSerializer()) + .build(); + + return new ReactiveRedisTemplate<>(connectionFactory, serializationContext); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java index 8b66ee0..38ac180 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java @@ -12,9 +12,6 @@ public class SearchMemberDto { // 搜索字段 - 包括 会员号、昵称、手机号 private String searchValue; - // 性别排序 - private Integer gender; - // 页码 private Integer pageNum = 1; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 6d1206b..baa75f7 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -11,6 +11,8 @@ import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.sys.util.AuthUtil; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; @@ -30,6 +32,7 @@ import reactor.core.publisher.Mono; @Slf4j @Component @RequiredArgsConstructor +@Tag(name = "会员管理", description = "会员信息管理、微信绑定、服务号关注等") public class MemberHandler { private final MemberService memberService; @@ -37,12 +40,7 @@ public class MemberHandler { private final WechatOfficialService wechatOfficialService; private final AuthUtil authUtil; - /** - * 获取会员信息 - * - * GET /api/member/info - * header: { "Authorization": "Bearer xxx" } - */ + @Operation(summary = "获取会员信息", description = "根据当前登录用户获取会员基本信息") public Mono getMemberInfo(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -55,19 +53,7 @@ public class MemberHandler { .bodyValue(info)); } - /** - * 更新会员信息 - * - * PUT /api/member/info - * header: { "Authorization": "Bearer xxx" } - * Body: { - * "nickname": "新昵称", - * "gender": 1, - * "birthday": "2000-01-01", - * "avatar": "https://example.com/avatar.jpg", - * "address": "北京市朝阳区" - * } - */ + @Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息") public Mono updateMemberInfo(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -81,11 +67,7 @@ public class MemberHandler { .bodyValue(info)); } - /** - * 绑定手机号(微信小程序) - * header: { "Authorization": "Bearer xxx" } - * POST /api/member/phone/bind?code=PHONE_CODE - */ + @Operation(summary = "绑定手机号", description = "通过微信小程序手机号code绑定会员手机号") public Mono bindPhone(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -102,12 +84,7 @@ public class MemberHandler { .bodyValue(success)); } - /** - * 查询服务号关注状态 - * - * GET /api/member/subscribe/status - * - */ + @Operation(summary = "查询服务号关注状态", description = "查询会员是否关注微信服务号") public Mono checkSubscribeStatus(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -122,14 +99,7 @@ public class MemberHandler { }); } - /** - * 管理员更新手机号 - * - * POST /api/admin/member/123/phone - * header: { "Authorization": "Bearer xxx" } - * Body: { "phone": "13800138000" } - * - */ + @Operation(summary = "管理员更新手机号", description = "后台管理员为会员更新手机号") public Mono adminUpdatePhone(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); @@ -161,13 +131,7 @@ public class MemberHandler { }); } - /** - * 前台查看会员信息 - * - * GET /api/admin/member/{id} - * header: { "Authorization": "xxx" } - * - */ + @Operation(summary = "管理员查看会员详情", description = "后台管理员查看指定会员的详细信息") public Mono adminGetMemberInfo(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); @@ -195,13 +159,7 @@ public class MemberHandler { }); } - /** - * 前台编辑会员信息 - * - * PUT /api/admin/member/{id} - * header: { "Authorization": "xxx" } - * Body:{"字段","值"} - */ + @Operation(summary = "管理员编辑会员信息", description = "后台管理员编辑会员信息") public Mono adminUpdateMemberInfo(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); @@ -220,25 +178,19 @@ public class MemberHandler { .bodyValue(detail)); } - /** - * 前台搜索会员列表 - * - * GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10 - * header: { "Authorization": "Bearer xxx" } - */ + @Operation(summary = "搜索会员列表", description = "后台管理员按关键词搜索会员,支持性别筛选和分页") public Mono searchMembers(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); String keyword = request.queryParam("searchValue").orElse(null); - Integer filter = NumberUtils.toInt(request.queryParam("filter").orElse("-1"), -1); Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); - log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", - adminId, keyword, filter, pageNum, pageSize); + log.info("前台搜索会员列表, adminId: {}, keyword: {},pageNum: {}, pageSize: {}", + adminId, keyword, pageNum, pageSize); - return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize)) + return memberService.searchMember(new SearchMemberDto(keyword, pageNum, pageSize)) .map(member -> { // 解密手机号 if (member.getPhone() != null && !member.getPhone().isEmpty()) { @@ -257,12 +209,7 @@ public class MemberHandler { } - /** - * 前台查看会员列表 - * - * GET /api/admin/members/all?pageNum=1&pageSize=10 - * header: { "Authorization": "Bearer xxx" } - */ + @Operation(summary = "查看会员列表", description = "后台管理员分页查看所有会员列表") public Mono getAllMembers(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java index 13cfd53..290a277 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java @@ -2,6 +2,8 @@ package cn.novalon.gym.manage.member.handler; import cn.novalon.gym.manage.member.dto.WechatLoginDto; import cn.novalon.gym.manage.member.service.WechatAuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -20,20 +22,13 @@ import reactor.core.publisher.Mono; @Slf4j @Component @RequiredArgsConstructor +@Tag(name = "微信认证", description = "微信小程序登录、公众号回调等") public class WechatAuthHandler { private final WechatAuthService wechatAuthService; private final WechatOfficialEventHandler wechatOfficialEventHandler; - /** - * 小程序更新 - * - * POST /api/member/auth/miniapp/login - * Body: {"code": "wx_login_code"} - * - * @param request ServerRequest - * @return Mono 登录响应 - */ + @Operation(summary = "微信小程序登录", description = "通过微信小程序code获取session_key,完成会员登录或注册") public Mono miniappLogin(ServerRequest request) { log.info("收到小程序登录请求"); @@ -50,18 +45,12 @@ public class WechatAuthHandler { }); } - /** - * 公众号回调 - * - * POST /api/member/auth/mp/callback - * Body: subscribeopenid - * - */ + @Operation(summary = "微信公众号回调", description = "处理微信公众号事件(关注、取消关注等)") public Mono mpCallback(ServerRequest request) { return wechatOfficialEventHandler.handleEvent(request); } - // 验证微信公众号签名 + @Operation(summary = "验证微信公众号签名", description = "微信公众号服务器验证,返回echostr") public Mono verifyMpSignature(ServerRequest request) { return wechatOfficialEventHandler.verifySignature(request); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java index d01e989..042cb7e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java @@ -39,9 +39,6 @@ public class WechatOfficialEventHandler { return request.bodyToMono(String.class) .flatMap(xmlBody -> { log.info("收到微信公众号事件 {}", xmlBody); - - // TODO: 将XML解析为WechatOfficialEventDto - // 目前简化处理直接获取openId和event String openId = extractOpenId(xmlBody); String event = extractEvent(xmlBody); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java index ce477f2..0cbbd76 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java @@ -3,6 +3,8 @@ package cn.novalon.gym.manage.member.service.impl; import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.service.IMemberCardRecordService; +import cn.novalon.gym.manage.member.util.RedisUtil; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -16,17 +18,35 @@ import java.time.LocalDateTime; * @author 付嘉 * @date 2026-05-27 */ +@Slf4j @Service public class MemberCardRecordServiceImpl implements IMemberCardRecordService { private final MemberCardRecordRepository memberCardRecordRepository; + private final RedisUtil redisUtil; - public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository) { + private static final String MEMBER_CARD_RECORD_CACHE_PREFIX = "member:card:record:"; + private static final long CACHE_EXPIRE_SECONDS = 300; + + public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository, RedisUtil redisUtil) { this.memberCardRecordRepository = memberCardRecordRepository; + this.redisUtil = redisUtil; } @Override public Mono findById(Long recordId) { - return memberCardRecordRepository.findById(recordId); + String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId; + Object cached = redisUtil.get(cacheKey); + if (cached != null && cached instanceof MemberCardRecord) { + log.debug("从缓存获取会员卡记录, recordId: {}", recordId); + return Mono.just((MemberCardRecord) cached); + } + + return memberCardRecordRepository.findById(recordId) + .doOnSuccess(record -> { + if (record != null) { + redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS); + } + }); } @Override @@ -53,17 +73,32 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { @Override public Mono deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { - return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount); + return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount) + .doOnSuccess(updated -> { + if (updated > 0) { + clearRecordCache(recordId); + } + }); } @Override public Mono renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { - return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime); + return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime) + .doOnSuccess(updated -> { + if (updated > 0) { + clearRecordCache(recordId); + } + }); } @Override public Mono updateStatus(Long recordId, String status) { - return memberCardRecordRepository.updateStatus(recordId, status); + return memberCardRecordRepository.updateStatus(recordId, status) + .doOnSuccess(updated -> { + if (updated > 0) { + clearRecordCache(recordId); + } + }); } @Override @@ -80,4 +115,10 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService { public Flux findExpiredCards() { return memberCardRecordRepository.findExpiredCards(); } + + private void clearRecordCache(Long recordId) { + String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId; + redisUtil.delete(cacheKey); + log.debug("清除会员卡记录缓存, recordId: {}", recordId); + } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java index e7831ea..87e345b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java @@ -15,6 +15,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRepository; import cn.novalon.gym.manage.member.service.IMemberCardService; import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; +import cn.novalon.gym.manage.member.util.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -39,6 +40,10 @@ public class MemberCardServiceImpl implements IMemberCardService { private final DistributedLockService distributedLockService; private final ExpirationReminderService expirationReminderService; private final RefundSagaHandler refundSagaHandler; + private final RedisUtil redisUtil; + + private static final String MEMBER_CARD_CACHE_PREFIX = "member:card:"; + private static final long CACHE_EXPIRE_SECONDS = 300; public MemberCardServiceImpl(MemberCardRepository memberCardRepository, MemberCardRecordRepository recordRepository, @@ -46,7 +51,8 @@ public class MemberCardServiceImpl implements IMemberCardService { MemberCardStateMachine stateMachine, DistributedLockService distributedLockService, ExpirationReminderService expirationReminderService, - RefundSagaHandler refundSagaHandler) { + RefundSagaHandler refundSagaHandler, + RedisUtil redisUtil) { this.memberCardRepository = memberCardRepository; this.recordRepository = recordRepository; this.transactionService = transactionService; @@ -54,11 +60,24 @@ public class MemberCardServiceImpl implements IMemberCardService { this.distributedLockService = distributedLockService; this.expirationReminderService = expirationReminderService; this.refundSagaHandler = refundSagaHandler; + this.redisUtil = redisUtil; } @Override public Mono findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { - return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId); + String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId; + Object cached = redisUtil.get(cacheKey); + if (cached != null && cached instanceof MemberCard) { + log.debug("从缓存获取会员卡信息, memberCardId: {}", memberCardId); + return Mono.just((MemberCard) cached); + } + + return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId) + .doOnSuccess(card -> { + if (card != null) { + redisUtil.setWithExpire(cacheKey, card, CACHE_EXPIRE_SECONDS); + } + }); } @Override @@ -95,7 +114,12 @@ public class MemberCardServiceImpl implements IMemberCardService { @Override public Mono save(MemberCard entity) { - return memberCardRepository.save(entity); + return memberCardRepository.save(entity) + .doOnSuccess(saved -> { + if (saved.getMemberCardId() != null) { + clearCardCache(saved.getMemberCardId()); + } + }); } @Override @@ -329,4 +353,10 @@ public class MemberCardServiceImpl implements IMemberCardService { return transactionService.createTransaction(transaction); } + + private void clearCardCache(Long memberCardId) { + String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId; + redisUtil.delete(cacheKey); + log.debug("清除会员卡缓存, memberCardId: {}", memberCardId); + } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index e63e79d..cecdaed 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -17,6 +17,7 @@ import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.BeanConvertUtil; import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.util.RedisUtil; import cn.novalon.gym.manage.member.vo.MemberCardInfoVO; import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO; @@ -56,9 +57,14 @@ public class MemberServiceImpl implements MemberService { private final IMemberRepository memberRepository; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; + private final RedisUtil redisUtil; private EsSyncUtils.EntitySyncer memberSyncer; + private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:"; + private static final String MEMBER_DETAIL_CACHE_PREFIX = "member:detail:"; + private static final long CACHE_EXPIRE_SECONDS = 300; + @PostConstruct public void init() { this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); @@ -66,12 +72,23 @@ public class MemberServiceImpl implements MemberService { @Override public Mono getMemberInfo(Long memberId) { - return memberRepository.findById(memberId) - .map(this::buildMemberInfoResponse) - .switchIfEmpty(Mono.error(() -> { - log.error("会员不存在: memberId={}", memberId); - throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); - })); + String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; + + return redisUtil.get(cacheKey, MemberInfoVO.class) + .flatMap(cached -> { + if (cached != null) { + log.debug("从缓存获取会员信息, memberId: {}", memberId); + return Mono.just(cached); + } + return memberRepository.findById(memberId) + .map(this::buildMemberInfoResponse) + .flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS) + .then(Mono.just(vo))) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); + }); } @Override @@ -98,7 +115,11 @@ public class MemberServiceImpl implements MemberService { return memberRepository.save(member); }) - .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + memberSyncer.sync(savedMember); + return clearMemberCache(memberId) + .then(Mono.just(savedMember)); + }) .map(savedMember -> { log.info("会员信息更新成功, memberId: {}", savedMember.getId()); return buildMemberInfoResponse(savedMember); @@ -160,15 +181,14 @@ public class MemberServiceImpl implements MemberService { @Override public Flux searchMember(SearchMemberDto searchMemberDto) { - log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", + log.info("搜索会员, searchValue: {}, pageNum: {}, pageSize: {}", searchMemberDto.getSearchValue(), - searchMemberDto.getGender(), searchMemberDto.getPageNum(), searchMemberDto.getPageSize()); String searchValue = searchMemberDto.getSearchValue(); - if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ + if (searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")) { log.debug("搜索值为手机号格式,进行加密处理"); searchValue = AesUtil.encrypt(searchValue); } @@ -207,42 +227,53 @@ public class MemberServiceImpl implements MemberService { public Mono getMemberDetail(Long memberId) { log.info("查询会员详情, memberId: {}", memberId); - return memberRepository.findById(memberId) - .zipWith( - memberRepository.findCardRecordsWithCardInfoByMemberId(memberId) - .collectList(), - (baseInfo, cardList) -> { - MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class); + String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId; - GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender()); - memberDetailVO.setGenderDesc(genderEnum.getDesc()); + return redisUtil.get(cacheKey, MemberDetailVO.class) + .flatMap(cached -> { + if (cached != null) { + log.debug("从缓存获取会员详情, memberId: {}", memberId); + return Mono.just(cached); + } + return memberRepository.findById(memberId) + .zipWith( + memberRepository.findCardRecordsWithCardInfoByMemberId(memberId) + .collectList(), + (baseInfo, cardList) -> { + MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class); - List enrichedCards = cardList.stream() - .peek(vo -> { - if (vo.getMemberCardType() != null) { - try { - MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType()); - vo.setMemberCardTypeDesc(cardType.getDesc()); - } catch (IllegalArgumentException e) { - vo.setMemberCardTypeDesc(vo.getMemberCardType()); - } - } - if (vo.getMemberCardStatus() != null) { - vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架"); - } - }) - .collect(Collectors.toList()); - memberDetailVO.setMemberCards(enrichedCards); + GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender()); + memberDetailVO.setGenderDesc(genderEnum.getDesc()); - long activeCount = enrichedCards.stream() - .filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) - .count(); - memberDetailVO.setActiveCardCount((int) activeCount); - memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount); + List enrichedCards = cardList.stream() + .peek(vo -> { + if (vo.getMemberCardType() != null) { + try { + MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType()); + vo.setMemberCardTypeDesc(cardType.getDesc()); + } catch (IllegalArgumentException e) { + vo.setMemberCardTypeDesc(vo.getMemberCardType()); + } + } + if (vo.getMemberCardStatus() != null) { + vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架"); + } + }) + .collect(Collectors.toList()); + memberDetailVO.setMemberCards(enrichedCards); - return memberDetailVO; - } - ); + long activeCount = enrichedCards.stream() + .filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) + .count(); + memberDetailVO.setActiveCardCount((int) activeCount); + memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount); + + return memberDetailVO; + } + ) + .flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS) + .then(Mono.just(vo))); + }); } @@ -256,7 +287,6 @@ public class MemberServiceImpl implements MemberService { throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })) .flatMap(member -> { - log.error("有用户"); if (updateDto.getNickname() != null) { member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); } @@ -275,8 +305,11 @@ public class MemberServiceImpl implements MemberService { return memberRepository.save(member); }) - .doOnSuccess(memberSyncer::sync) - .map(savedMember -> true) + .flatMap(savedMember -> { + memberSyncer.sync(savedMember); + return clearMemberCache(memberId) + .then(Mono.just(true)); + }) .onErrorResume(e -> { log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e); return Mono.just(false); @@ -290,7 +323,11 @@ public class MemberServiceImpl implements MemberService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + memberSyncer.sync(savedMember); + return clearMemberCache(memberId) + .then(Mono.just(savedMember)); + }) .map(savedMember -> { log.info("手机号录入成功, memberId: {}", savedMember.getId()); return true; @@ -301,4 +338,12 @@ public class MemberServiceImpl implements MemberService { throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } + + private Mono clearMemberCache(Long memberId) { + String infoCacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; + String detailCacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId; + return redisUtil.delete(infoCacheKey) + .then(redisUtil.delete(detailCacheKey)) + .doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId)); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java index f492e78..0e9da72 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java @@ -5,6 +5,7 @@ import cn.novalon.gym.manage.member.entity.RefundApplication; import cn.novalon.gym.manage.member.enums.RefundStatus; import cn.novalon.gym.manage.member.repository.RefundApplicationRepository; import cn.novalon.gym.manage.member.service.IRefundApplicationService; +import cn.novalon.gym.manage.member.util.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -22,9 +23,14 @@ import java.time.LocalDateTime; public class RefundApplicationServiceImpl implements IRefundApplicationService { private final RefundApplicationRepository refundApplicationRepository; + private final RedisUtil redisUtil; - public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository) { + private static final String REFUND_APPLICATION_CACHE_PREFIX = "member:refund:"; + private static final long CACHE_EXPIRE_SECONDS = 300; + + public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository, RedisUtil redisUtil) { this.refundApplicationRepository = refundApplicationRepository; + this.redisUtil = redisUtil; } @Override @@ -45,8 +51,10 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { .build(); return refundApplicationRepository.save(application) - .doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}", - app.getId(), recordId)); + .doOnSuccess(app -> { + log.info("创建退款申请成功: applicationId={}, recordId={}", app.getId(), recordId); + clearRefundCache(recordId); + }); })); } @@ -55,14 +63,19 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return refundApplicationRepository.findById(applicationId) .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在"))) .flatMap(application -> { - if (!"PENDING".equals(application.getStatus())) { + if (application.getStatus() != RefundStatus.PENDING) { return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); } return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark)) - .thenReturn(application) - .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", - applicationId, auditorId)); + .flatMap(updatedRows -> { + if (updatedRows == 0) { + return Mono.error(new RuntimeException("批准退款申请失败")); + } + clearRefundCache(application.getRecordId()); + log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId); + return refundApplicationRepository.findById(applicationId); + }); }); } @@ -71,19 +84,42 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { return refundApplicationRepository.findById(applicationId) .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在"))) .flatMap(application -> { - if (!"PENDING".equals(application.getStatus())) { + if (application.getStatus() != RefundStatus.PENDING) { return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); } return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark)) - .thenReturn(application) - .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", - applicationId, auditorId)); + .flatMap(updatedRows -> { + if (updatedRows == 0) { + return Mono.error(new RuntimeException("拒绝退款申请失败")); + } + clearRefundCache(application.getRecordId()); + log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId); + return refundApplicationRepository.findById(applicationId); + }); }); } @Override public Mono findByRecordId(Long recordId) { - return refundApplicationRepository.findByRecordId(recordId); + String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId; + Object cached = redisUtil.get(cacheKey); + if (cached != null && cached instanceof RefundApplication) { + log.debug("从缓存获取退款申请, recordId: {}", recordId); + return Mono.just((RefundApplication) cached); + } + + return refundApplicationRepository.findByRecordId(recordId) + .doOnSuccess(application -> { + if (application != null) { + redisUtil.setWithExpire(cacheKey, application, CACHE_EXPIRE_SECONDS); + } + }); + } + + private void clearRefundCache(Long recordId) { + String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId; + redisUtil.delete(cacheKey); + log.debug("清除退款申请缓存, recordId: {}", recordId); } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java index f760ae4..aac7753 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ErrorCode; import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.service.WechatApiService; +import cn.novalon.gym.manage.member.util.RedisUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -30,6 +31,10 @@ import java.util.Map; public class WechatApiServiceImpl implements WechatApiService { private final WechatProperties wechatProperties; + private final RedisUtil redisUtil; + + private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:"; + private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; // 比官方过期时间短100秒 private final WebClient webClient = WebClient.builder() .baseUrl("https://api.weixin.qq.com") @@ -147,35 +152,46 @@ public class WechatApiServiceImpl implements WechatApiService { public Mono getAccessToken(String appType) { log.debug("获取access_token, appType: {}", appType); - String appId, appSecret; - if ("miniapp".equals(appType)) { - appId = wechatProperties.getMiniapp().getAppId(); - appSecret = wechatProperties.getMiniapp().getAppSecret(); - } else { - appId = wechatProperties.getMp().getAppId(); - appSecret = wechatProperties.getMp().getAppSecret(); - } + String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType; - return webClient.get() - .uri(uriBuilder -> uriBuilder - .path("/cgi-bin/token") - .queryParam("grant_type", "client_credential") - .queryParam("appid", appId) - .queryParam("secret", appSecret) - .build()) - .retrieve() - .bodyToMono(Map.class) - .map(response -> { - if (response.containsKey("access_token")) { - String accessToken = (String) response.get("access_token"); - Integer expiresIn = (Integer) response.get("expires_in"); - log.info("获取access_token成功, expires_in: {}s", expiresIn); - return accessToken; - } else { - String errmsg = (String) response.get("errmsg"); - log.error("获取access_token失败: {}", errmsg); - throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg); + return redisUtil.get(cacheKey, String.class) + .flatMap(cachedToken -> { + if (cachedToken != null) { + log.debug("从缓存获取access_token, appType: {}", appType); + return Mono.just(cachedToken); } + + String appId, appSecret; + if ("miniapp".equals(appType)) { + appId = wechatProperties.getMiniapp().getAppId(); + appSecret = wechatProperties.getMiniapp().getAppSecret(); + } else { + appId = wechatProperties.getMp().getAppId(); + appSecret = wechatProperties.getMp().getAppSecret(); + } + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/cgi-bin/token") + .queryParam("grant_type", "client_credential") + .queryParam("appid", appId) + .queryParam("secret", appSecret) + .build()) + .retrieve() + .bodyToMono(Map.class) + .flatMap(response -> { + if (response.containsKey("access_token")) { + String accessToken = (String) response.get("access_token"); + Integer expiresIn = (Integer) response.get("expires_in"); + log.info("获取access_token成功, expires_in: {}s", expiresIn); + return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS) + .then(Mono.just(accessToken)); + } else { + String errmsg = (String) response.get("errmsg"); + log.error("获取access_token失败: {}", errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg); + } + }); }); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 9d567d0..a462a14 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -16,6 +16,7 @@ import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.MemberNoGenerator; +import cn.novalon.gym.manage.member.util.RedisUtil; import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.member.vo.WechatLoginVO; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; @@ -47,9 +48,13 @@ public class WechatAuthServiceImpl implements WechatAuthService { private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; private final JwtTokenProvider jwtTokenProvider; + private final RedisUtil redisUtil; private EsSyncUtils.EntitySyncer memberSyncer; + private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:"; + private static final long CACHE_EXPIRE_SECONDS = 300; + @PostConstruct public void init() { this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); @@ -79,7 +84,10 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -89,7 +97,10 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -105,7 +116,10 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -124,7 +138,10 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .flatMap(savedMember -> { WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); return Mono.just(response); @@ -185,7 +202,10 @@ public class WechatAuthServiceImpl implements WechatAuthService { member.setPhone(encryptedPhone); member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .map(savedMember -> { log.info("更新会员手机号成功, memberId: {}", savedMember.getId()); return true; @@ -197,6 +217,12 @@ public class WechatAuthServiceImpl implements WechatAuthService { })); } + private void clearMemberCache(Long memberId) { + String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; + redisUtil.delete(cacheKey); + log.debug("清除会员缓存, memberId: {}", memberId); + } + private String encryptPhone(String phoneNumber) { try { String encryptedPhone = AesUtil.encrypt(phoneNumber); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index 5143d9b..22a2d56 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -8,6 +8,7 @@ import cn.novalon.gym.manage.member.es.repository.MemberESRepository; import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.util.EsSyncUtils; +import cn.novalon.gym.manage.member.util.RedisUtil; import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -41,11 +42,16 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { private final MemberESRepository memberESRepository; private final ObjectMapper objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - private final EsSyncUtils esSyncUtils; + private final RedisUtil redisUtil; private EsSyncUtils.EntitySyncer memberSyncer; + private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:"; + private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:"; + private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; + private static final long CACHE_EXPIRE_SECONDS = 300; + @PostConstruct public void init() { this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); @@ -83,16 +89,22 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { } return memberRepository.save(existingMember) - .doOnSuccess(memberSyncer::sync) - .then(sendWelcomeMessage(openId)); + .flatMap(saved -> { + memberSyncer.sync(saved); + return clearMemberCache(saved.getId()) + .then(sendWelcomeMessage(openId)); + }); } else { log.info("老用户关注服务号: memberId={}", existingMember.getId()); existingMember.setSubscribed(true); existingMember.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(existingMember) - .doOnSuccess(memberSyncer::sync) - .then(sendWelcomeMessage(openId)); + .flatMap(saved -> { + memberSyncer.sync(saved); + return clearMemberCache(saved.getId()) + .then(sendWelcomeMessage(openId)); + }); } }) .switchIfEmpty(Mono.defer(() -> { @@ -105,7 +117,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(existingMember) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .then(sendWelcomeMessage(openId)); }) .switchIfEmpty(Mono.defer(() -> { @@ -123,7 +138,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { existingMember.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(existingMember) - .doOnSuccess(memberSyncer::sync) + .doOnSuccess(saved -> { + memberSyncer.sync(saved); + clearMemberCache(saved.getId()); + }) .then(sendWelcomeMessage(openId)); }) .switchIfEmpty(Mono.defer(() -> { @@ -149,8 +167,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { member.setSubscribed(false); member.setLastLoginAt(LocalDateTime.now()); return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) - .then(); + .flatMap(saved -> { + memberSyncer.sync(saved); + return clearMemberCache(saved.getId()).then(); + }); }) .then() .switchIfEmpty(Mono.defer(() -> { @@ -214,7 +234,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { member.setOfficialOpenId(officialOpenId); } return memberRepository.save(member) - .doOnSuccess(memberSyncer::sync) + .flatMap(saved -> { + memberSyncer.sync(saved); + return clearMemberCache(saved.getId()) + .then(Mono.just(saved)); + }) .map(savedMember -> { log.info("关联成功, memberId: {}", savedMember.getId()); return true; @@ -269,28 +293,38 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { /** * 获取微信AccessToken - * - * TODO: 应该使用缓存,避免频繁请求 */ private Mono getAccessToken() { - String appId = wechatProperties.getMp().getAppId(); - String appSecret = wechatProperties.getMp().getAppSecret(); + String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp"; - String url = "https://api.weixin.qq.com/cgi-bin/token" - + "?grant_type=client_credential" - + "&appid=" + appId - + "&secret=" + appSecret; - - return webClient.get() - .uri(url) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(Map.class) - .map(response -> { - if (response.containsKey("errcode")) { - throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg")); + return redisUtil.get(cacheKey, String.class) + .flatMap(cachedToken -> { + if (cachedToken != null) { + log.debug("从缓存获取服务号access_token"); + return Mono.just(cachedToken); } - return (String) response.get("access_token"); + + String appId = wechatProperties.getMp().getAppId(); + String appSecret = wechatProperties.getMp().getAppSecret(); + + String url = "https://api.weixin.qq.com/cgi-bin/token" + + "?grant_type=client_credential" + + "&appid=" + appId + + "&secret=" + appSecret; + + return webClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(Map.class) + .flatMap(response -> { + if (response.containsKey("errcode")) { + throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg")); + } + String accessToken = (String) response.get("access_token"); + return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS) + .then(Mono.just(accessToken)); + }); }); } @@ -336,4 +370,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { return Mono.empty(); // 即使发送失败也不影响主流程 }); } + + private Mono clearMemberCache(Long memberId) { + String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; + return redisUtil.delete(cacheKey) + .doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId)); + } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java new file mode 100644 index 0000000..ea49f50 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java @@ -0,0 +1,72 @@ +package cn.novalon.gym.manage.member.util; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * Redis 工具类(响应式版本) + * + * @author liwentao + * @date 2026/5/15 + */ +@Component +public class RedisUtil { + + @Autowired + private ReactiveRedisTemplate reactiveRedisTemplate; + + /** + * 设置值 + */ + public Mono set(String key, Object value) { + return reactiveRedisTemplate.opsForValue().set(key, value); + } + + /** + * 设置值并指定过期时间(秒) + */ + public Mono setWithExpire(String key, Object value, long timeoutSeconds) { + return reactiveRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds)); + } + + /** + * 获取值 + */ + @SuppressWarnings("unchecked") + public Mono get(String key, Class clazz) { + return reactiveRedisTemplate.opsForValue().get(key) + .map(obj -> clazz.isInstance(obj) ? (T) obj : null); + } + + /** + * 获取值(返回 Object) + */ + public Mono get(String key) { + return reactiveRedisTemplate.opsForValue().get(key); + } + + /** + * 删除key + */ + public Mono delete(String key) { + return reactiveRedisTemplate.delete(key); + } + + /** + * 判断key是否存在 + */ + public Mono hasKey(String key) { + return reactiveRedisTemplate.hasKey(key); + } + + /** + * 设置过期时间(秒) + */ + public Mono expire(String key, long timeoutSeconds) { + return reactiveRedisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds)); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/resources/member-config.yml b/gym-manage-api/gym-member/src/main/resources/member-config.yml index 8c457f6..a76b243 100644 --- a/gym-manage-api/gym-member/src/main/resources/member-config.yml +++ b/gym-manage-api/gym-member/src/main/resources/member-config.yml @@ -2,21 +2,23 @@ wechat: # Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境) mock-enabled: false + miniapp: - app-id: wx4d480112b426100b - app-secret: 78548f0c0ff66c73d3e8b071897eb1e5 + app-id: ${WECHAT_MINIAPP_APP_ID} + app-secret: ${WECHAT_MINIAPP_SECRET} + mp: - app-id: wx6f138c9aacc8a0e8 - app-secret: 5df2e315e9268e96a43bb2cce1d2270b - token: test_token - aes-key: ${WECHAT_MP_AESKEY:test_aes_key} - # 服务器回调地址(微信服务器推送事件的URL) - callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback + app-id: ${WECHAT_MP_APP_ID} + app-secret: ${WECHAT_MP_SECRET} + token: ${WECHAT_MP_TOKEN} + aes-key: ${WECHAT_MP_AESKEY} + callback-url: ${WECHAT_MP_CALLBACK_URL} + # 手机号加密配置 phone-encryption: - secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY= - iv: LMpG6Ih9mmfEAALOCeIJBw== + secret-key: ${PHONE_ENCRYPTION_SECRET_KEY} + iv: ${PHONE_ENCRYPTION_IV} spring: elasticsearch: - uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔) + uris: http://localhost:9200 \ No newline at end of file diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index fb5f6f4..2644308 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -139,6 +139,10 @@ org.springdoc springdoc-openapi-starter-webflux-ui + + org.springframework.boot + spring-boot-starter-data-redis + -- 2.52.0