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