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 1/9] =?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 "" 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 2/9] =?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 3/9] =?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 4/9] =?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: 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 5/9] =?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 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 6/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=20gym-member=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E4=B8=AD=E7=9A=84=E6=97=A0=E7=94=A8=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=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 "" 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 7/9] =?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 + 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 8/9] =?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 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 9/9] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=97=E4=BD=99=20Auth?= =?UTF-8?q?Util=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