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