实现会员信息管理模块

This commit is contained in:
future
2026-05-26 23:50:50 +08:00
parent 3d284c8d3a
commit 85ed6f9196
27 changed files with 774 additions and 134 deletions
+10 -2
View File
@@ -108,8 +108,7 @@
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
</dependency> <dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.6.0</version>
@@ -129,6 +128,15 @@
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
<build>
@@ -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 {
// 小程序配置
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<MemberES, String> {
/**
* 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配)
*/
Flux<MemberES> findByMemberNoOrPhoneOrNicknameContainingAndGender(
String memberNo, String phone, String nickname,String gender, Pageable pageable);
}
@@ -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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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));
}
}
@@ -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: <xml><Event>subscribe</Event><FromUserName>openid</FromUserName></xml>
@@ -61,7 +61,7 @@ public class WechatAuthHandler {
return wechatOfficialEventHandler.handleEvent(request);
}
// ֤微信ŷǩ
// 验证微信公众号签名
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
return wechatOfficialEventHandler.verifySignature(request);
}
@@ -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<ServerResponse> 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<ServerResponse> 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. tokentimestampnonce三个参数进行字典序排序
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("<FromUserName>");
int end = xml.indexOf("</FromUserName>");
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("<Event>");
int end = xml.indexOf("</Event>");
if (start != -1 && end != -1) {
String value = xml.substring(start + 7, end);
// ȥ CDATA
// 去除 CDATA 标记
return cleanCdata(value);
}
return null;
}
/**
* CDATA
* : <![CDATA[subscribe]]> -> subscribe
* 清理 CDATA 标记
* 例如: <![CDATA[subscribe]]> -> subscribe
*/
private String cleanCdata(String value) {
if (value == null) {
return null;
}
// ȥǰհ
// 去除前后空白
value = value.trim();
// CDATA获取м
// ʽ: <![CDATA[xxx]]>
// 提取 CDATA 中间内容
// 格式: <![CDATA[xxx]]>
if (value.startsWith("<![CDATA[") && value.endsWith("]]>")) {
return value.substring(9, value.length() - 3);
}
@@ -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<Member, Long> {
public interface IMemberRepository extends R2dbcRepository<Member, Long> {
// UnionID查询会员
Mono<Member> findByUnionId(String unionId);
@@ -25,4 +27,10 @@ public interface MemberRepository extends R2dbcRepository<Member, Long> {
// 手机号查询
Mono<Member> findByPhone(String phone);
/**
* 分页查询所有会员
* 方法名 findAllBy Spring Data 的约定表示按条件查询所有
*/
Flux<Member> findAllBy(Pageable pageable);
}
@@ -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<Boolean> adminUpdatePhone(Long memberId, String phone);
/**
* 管理端查询会员(es查询)
*
* @param searchMemberDto 会员信息dto
* @return 会员信息
*/
Flux<MemberES> searchMember(SearchMemberDto searchMemberDto);
/**
* 管理端查询所有会员
*
* @param pageNum 页码
* @param pageSize 页大小
* @return 所有会员信息
*/
Flux<Member> findAll(Integer pageNum, Integer pageSize);
}
@@ -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<Member, MemberES, String> 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<MemberES> 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<Member> findAll(Integer pageNum, Integer pageSize) {
Pageable pageable = PageRequest.of(
pageNum - 1,
pageSize
);
return memberRepository.findAllBy(pageable);
}
// 更新会员手机号
private Mono<Boolean> 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;
@@ -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<Member, MemberES, String> 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("未获取到手机号");
@@ -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<Member, MemberES, String> 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();
}
@@ -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 <S> 源实体类型
* @param <T> ES实体类型
* @param <ID> ID类型
*/
public <S, T, ID> void sync(Class<S> sourceClass, Class<T> targetClass,
S source, ReactiveElasticsearchRepository<T, ID> 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<Void>
*/
public <S, T, ID> Mono<Void> syncToES(Class<S> sourceClass, Class<T> targetClass,
S source, ReactiveElasticsearchRepository<T, ID> 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 <S, T, ID> EntitySyncer<S, T, ID> bind(Class<S> sourceClass, Class<T> targetClass,
ReactiveElasticsearchRepository<T, ID> repository) {
return new EntitySyncer<>(sourceClass, targetClass, repository);
}
/**
* 实体同步器(绑定特定类型的同步器,避免重复传 Class)
*
* @param <S> 源实体类型
* @param <T> ES实体类型
* @param <ID> ID类型
*/
public static class EntitySyncer<S, T, ID> {
private final Class<S> sourceClass;
private final Class<T> targetClass;
private final ReactiveElasticsearchRepository<T, ID> repository;
public EntitySyncer(Class<S> sourceClass, Class<T> targetClass,
ReactiveElasticsearchRepository<T, ID> 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<Void> 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();
}
}
}
}
@@ -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 服务器地址(支持多个,逗号分隔)
@@ -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);
@@ -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();
}
}
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -58,7 +58,6 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
private boolean isPublicPath(String path) {
return path.startsWith("/api/auth/") ||
path.equals("/actuator/health") ||
path.startsWith("/actuator/info") ||
path.equals("/api/member/auth/miniapp/login") ||
path.equals("/api/member/auth/mp/callback") ||
path.equals("/api/auth/login") ||
@@ -50,7 +50,8 @@ public class SecurityConfig {
spec.pathMatchers("/api/auth/**").permitAll()
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/ws/**").permitAll()
.pathMatchers("/actuator/**").permitAll();
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/api/member/checkIn").permitAll();
if (isDevOrTest) {
spec.pathMatchers("/swagger-ui.html").permitAll()
@@ -1,7 +1,6 @@
package cn.novalon.gym.manage.sys.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
/**
* 用户更新请求DTO
@@ -24,7 +23,6 @@ public class UserUpdateRequest {
@Schema(description = "是否清除角色关联", example = "false")
private Boolean clearRole;
@Email(message = "邮箱格式不正确")
public String getEmail() {
return email;
}
+1
View File
@@ -42,6 +42,7 @@
<module>manage-audit</module>
<module>manage-notify</module>
<module>manage-file</module>
<module>gym-member</module>
</modules>
<dependencyManagement>
+82 -13
View File
@@ -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