实现会员信息管理模块
This commit is contained in:
@@ -108,8 +108,7 @@
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-miniapp</artifactId>
|
||||
<version>4.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
</dependency> <dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-mp</artifactId>
|
||||
<version>4.6.0</version>
|
||||
@@ -129,6 +128,15 @@
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.25</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
-3
@@ -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 {
|
||||
|
||||
// 小程序配置
|
||||
|
||||
+23
@@ -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;
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package cn.novalon.gym.manage.member.es.repository;
|
||||
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* ES 会员数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface MemberESRepository extends ReactiveElasticsearchRepository<MemberES, String> {
|
||||
|
||||
/**
|
||||
* 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配)
|
||||
*/
|
||||
Flux<MemberES> findByMemberNoOrPhoneOrNicknameContainingAndGender(
|
||||
String memberNo, String phone, String nickname,String gender, Pageable pageable);
|
||||
}
|
||||
+178
-31
@@ -1,13 +1,19 @@
|
||||
package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto;
|
||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||
import cn.novalon.gym.manage.member.service.MemberService;
|
||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
@@ -16,7 +22,7 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 会员信息处理器
|
||||
*
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-01
|
||||
*/
|
||||
@@ -29,22 +35,24 @@ public class MemberHandler {
|
||||
private final MemberService memberService;
|
||||
private final WechatAuthService wechatAuthService;
|
||||
private final WechatOfficialService wechatOfficialService;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final WechatProperties wechatProperties;
|
||||
|
||||
/**
|
||||
* 获取会员信息
|
||||
*
|
||||
*
|
||||
* GET /api/member/info
|
||||
* Header: X-Member-Id: 123
|
||||
*/
|
||||
public Mono<ServerResponse> getMemberInfo(ServerRequest request) {
|
||||
|
||||
|
||||
String memberIdStr = request.headers().firstHeader("X-Member-Id");
|
||||
long memberId = NumberUtils.toLong(memberIdStr,0L);
|
||||
|
||||
if (memberId <= 0) throw new IllegalArgumentException("获取会员信息失败: memberId 无效");
|
||||
|
||||
|
||||
log.info("获取会员信息, memberId: {}", memberId);
|
||||
|
||||
|
||||
return memberService.getMemberInfo(memberId)
|
||||
.flatMap(info -> {
|
||||
return ServerResponse.ok()
|
||||
@@ -55,7 +63,7 @@ public class MemberHandler {
|
||||
|
||||
/**
|
||||
* 更新会员信息
|
||||
*
|
||||
*
|
||||
* PUT /api/member/info
|
||||
* Header: X-Member-Id: 123
|
||||
* Body: {
|
||||
@@ -67,14 +75,14 @@ public class MemberHandler {
|
||||
* }
|
||||
*/
|
||||
public Mono<ServerResponse> updateMemberInfo(ServerRequest request) {
|
||||
|
||||
|
||||
String memberIdStr = request.headers().firstHeader("X-Member-Id");
|
||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||
|
||||
if (memberId <= 0) throw new IllegalArgumentException("更新会员信息失败: memberId 无效");
|
||||
|
||||
|
||||
log.info("更新会员信息, memberId: {}", memberId);
|
||||
|
||||
|
||||
return request.bodyToMono(UpdateMemberInfoDto.class)
|
||||
.flatMap(updateDto -> memberService.updateMemberInfo(memberId, updateDto))
|
||||
.flatMap(info -> ServerResponse.ok()
|
||||
@@ -84,23 +92,23 @@ public class MemberHandler {
|
||||
|
||||
/**
|
||||
* 绑定手机号(微信小程序)
|
||||
*
|
||||
*
|
||||
* POST /api/member/phone/bind?code=PHONE_CODE
|
||||
* Header: X-Member-Id: 123
|
||||
*/
|
||||
public Mono<ServerResponse> bindPhone(ServerRequest request) {
|
||||
|
||||
|
||||
String memberIdStr = request.headers().firstHeader("X-Member-Id");
|
||||
Long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||
|
||||
|
||||
if (memberId <= 0) throw new IllegalArgumentException("绑定手机号失败: memberId 无效");
|
||||
|
||||
|
||||
String phoneCode = request.queryParam("phoneCode").orElse("");
|
||||
|
||||
|
||||
if (phoneCode == null || phoneCode.trim().isEmpty()) throw new IllegalArgumentException("手机号code不能为空");
|
||||
|
||||
|
||||
log.info("收到绑定手机号请求, memberId: {}, phoneCode: {}", memberId, phoneCode);
|
||||
|
||||
|
||||
return wechatAuthService.bindPhone(memberId, phoneCode)
|
||||
.flatMap(success -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -109,20 +117,20 @@ public class MemberHandler {
|
||||
|
||||
/**
|
||||
* 查询服务号关注状态
|
||||
*
|
||||
*
|
||||
* GET /api/member/subscribe/status
|
||||
* Header: X-Member-Id: 123
|
||||
*
|
||||
*
|
||||
*/
|
||||
public Mono<ServerResponse> checkSubscribeStatus(ServerRequest request) {
|
||||
|
||||
|
||||
String memberIdStr = request.headers().firstHeader("X-Member-Id");
|
||||
long memberId = NumberUtils.toLong(memberIdStr,0L);
|
||||
|
||||
if (memberId <= 0) throw new IllegalArgumentException("查询服务号关注状态失败: memberId 无效");
|
||||
|
||||
|
||||
log.info("查询服务号关注状态, memberId: {}", memberId);
|
||||
|
||||
|
||||
return wechatOfficialService.checkSubscribeStatus(memberId)
|
||||
.flatMap(subscribed -> {
|
||||
return ServerResponse.ok()
|
||||
@@ -133,30 +141,30 @@ public class MemberHandler {
|
||||
|
||||
/**
|
||||
* 管理员更新手机号
|
||||
*
|
||||
* POST /api/admin/members/123/phone
|
||||
*
|
||||
* POST /api/admin/member/123/phone
|
||||
* Body: { "phone": "13800138000" }
|
||||
*
|
||||
*
|
||||
*/
|
||||
public Mono<ServerResponse> adminUpdatePhone(ServerRequest request) {
|
||||
|
||||
|
||||
String memberIdStr = request.pathVariable("id");
|
||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||
|
||||
|
||||
if (memberId <= 0) throw new IllegalArgumentException("更新手机号失败: memberId 无效");
|
||||
|
||||
|
||||
log.info("收到更新手机号请求, memberId: {}", memberId);
|
||||
|
||||
|
||||
return request.bodyToMono(AdminUpdatePhoneDto.class)
|
||||
.flatMap(body -> {
|
||||
String phone = body.getPhone();
|
||||
|
||||
|
||||
if (phone == null || phone.isEmpty()) return Mono.error(new IllegalArgumentException("手机号不能为空"));
|
||||
|
||||
|
||||
if (!phone.matches("^1[3-9]\\d{9}$")) return Mono.error(new IllegalArgumentException("手机号格式不正确"));
|
||||
|
||||
log.info("开始更新手机号, memberId: {}, phone: {}", memberId, phone);
|
||||
|
||||
|
||||
return memberService.adminUpdatePhone(memberId, phone);
|
||||
})
|
||||
.flatMap(success -> {
|
||||
@@ -167,4 +175,143 @@ public class MemberHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台查看会员信息
|
||||
*
|
||||
* GET /api/admin/member/{id}
|
||||
* header: { "Authorization": "xxx" }
|
||||
*
|
||||
*/
|
||||
public Mono<ServerResponse> adminGetMemberInfo(ServerRequest request) {
|
||||
|
||||
String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authorization == null || !authorization.startsWith("Bearer ")) throw new IllegalArgumentException("无权访问");
|
||||
authorization = authorization.substring(7);
|
||||
// 验证token并获取memberId
|
||||
if (!jwtTokenProvider.validateToken(authorization)) throw new IllegalArgumentException("Authorization 无效");
|
||||
|
||||
String memberIdStr = request.pathVariable("id");
|
||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||
if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误");
|
||||
|
||||
Long adminId = jwtTokenProvider.getUserIdFromToken(authorization);
|
||||
|
||||
// TODO 多表查询:会员信息、团课信息、会员卡信息
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue("成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台编辑会员信息
|
||||
*
|
||||
* PUT /api/admin/member/{id}
|
||||
* header: { "Authorization": "xxx" }
|
||||
* Body:{"字段","值"}
|
||||
*/
|
||||
public Mono<ServerResponse> adminUpdateMemberInfo(ServerRequest request) {
|
||||
|
||||
String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authorization == null || !authorization.startsWith("Bearer ")) throw new IllegalArgumentException("无权访问");
|
||||
authorization = authorization.substring(7);
|
||||
// 验证token并获取memberId
|
||||
if (!jwtTokenProvider.validateToken(authorization)) throw new IllegalArgumentException("Authorization 无效");
|
||||
|
||||
String memberIdStr = request.pathVariable("id");
|
||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||
if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误");
|
||||
|
||||
Long adminId = jwtTokenProvider.getUserIdFromToken(authorization);
|
||||
|
||||
// TODO 多表查询:会员信息、团课信息、会员卡信息
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue("成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台搜索会员列表
|
||||
*
|
||||
* GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
*/
|
||||
public Mono<ServerResponse> searchMembers(ServerRequest request) {
|
||||
|
||||
String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
||||
return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("无权访问");
|
||||
}
|
||||
authorization = authorization.substring(7);
|
||||
if (!jwtTokenProvider.validateToken(authorization)) {
|
||||
return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("Authorization 无效");
|
||||
}
|
||||
|
||||
String keyword = request.queryParam("searchValue").orElse(null);
|
||||
String filter = request.queryParam("filter").orElse(null);
|
||||
int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
||||
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||
|
||||
return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize))
|
||||
.map(member -> {
|
||||
// 解密手机号
|
||||
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
||||
try {
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv);
|
||||
member.setPhone(decryptedPhone);
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||
member.setPhone(null);
|
||||
}
|
||||
}
|
||||
return member;
|
||||
})
|
||||
.collectList()
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 前台查看会员列表
|
||||
*
|
||||
* GET /api/admin/members/all?pageNum=1&pageSize=10
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
*/
|
||||
public Mono<ServerResponse> getAllMembers(ServerRequest request) {
|
||||
|
||||
String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
||||
return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("无权访问");
|
||||
}
|
||||
authorization = authorization.substring(7);
|
||||
if (!jwtTokenProvider.validateToken(authorization)) {
|
||||
return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue("Authorization 无效");
|
||||
}
|
||||
|
||||
int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
||||
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||
|
||||
return memberService.findAll(pageNum, pageSize)
|
||||
.map(member -> {
|
||||
// 解密手机号
|
||||
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
||||
try {
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
String decryptedPhone = AesUtil.decrypt(member.getPhone(), secretKey, iv);
|
||||
member.setPhone(decryptedPhone);
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||
member.setPhone(null);
|
||||
}
|
||||
}
|
||||
return member;
|
||||
})
|
||||
.collectList()
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 微信֤
|
||||
* 微信认证
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-01
|
||||
@@ -51,7 +51,7 @@ public class WechatAuthHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新ص
|
||||
* 公众号回调
|
||||
*
|
||||
* POST /api/member/auth/mp/callback
|
||||
* Body: <xml><Event>subscribe</Event><FromUserName>openid</FromUserName></xml>
|
||||
@@ -61,7 +61,7 @@ public class WechatAuthHandler {
|
||||
return wechatOfficialEventHandler.handleEvent(request);
|
||||
}
|
||||
|
||||
// ֤微信ŷǩ
|
||||
// 验证微信公众号签名
|
||||
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
|
||||
return wechatOfficialEventHandler.verifySignature(request);
|
||||
}
|
||||
|
||||
+47
-47
@@ -15,7 +15,7 @@ import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 微信ŷ更新
|
||||
* 微信公众号事件处理器
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-01
|
||||
@@ -30,30 +30,30 @@ public class WechatOfficialEventHandler {
|
||||
private final WechatProperties wechatProperties;
|
||||
|
||||
/**
|
||||
* 微信ŷ͵更新
|
||||
* 处理微信公众号事件
|
||||
*
|
||||
* ʽXML
|
||||
* Ӧʽsuccess 头像地址
|
||||
* 请求格式:XML
|
||||
* 响应格式:success 或 回复消息内容
|
||||
*/
|
||||
public Mono<ServerResponse> handleEvent(ServerRequest request) {
|
||||
return request.bodyToMono(String.class)
|
||||
.flatMap(xmlBody -> {
|
||||
log.info("收到微信ŷ {}", xmlBody);
|
||||
log.info("收到微信公众号事件 {}", xmlBody);
|
||||
|
||||
// TODO: XML为WechatOfficialEventDto
|
||||
// Ŀǰֱ获取openidevent
|
||||
// TODO: 将XML解析为WechatOfficialEventDto
|
||||
// 目前简化处理直接获取openId和event
|
||||
|
||||
String openId = extractOpenId(xmlBody);
|
||||
String event = extractEvent(xmlBody);
|
||||
|
||||
if (openId == null || event == null) {
|
||||
log.error("微信更新");
|
||||
log.error("无法解析微信公众号事件");
|
||||
return ServerResponse.badRequest().bodyValue("error");
|
||||
}
|
||||
|
||||
log.info(" openId={}, event={}", openId, event);
|
||||
log.info("处理事件 openId={}, event={}", openId, event);
|
||||
|
||||
// 更新ʹ
|
||||
// 根据事件类型处理
|
||||
if ("subscribe".equals(event)) {
|
||||
return wechatOfficialService.handleSubscribeEvent(openId)
|
||||
.then(ServerResponse.ok()
|
||||
@@ -65,24 +65,24 @@ public class WechatOfficialEventHandler {
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue("success"));
|
||||
} else {
|
||||
log.warn("δ֪更新: {}", event);
|
||||
log.warn("未知事件类型: {}", event);
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue("success");
|
||||
}
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("微信更新失败", e);
|
||||
log.error("处理微信公众号事件失败", e);
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue("success"); // ʹ失败Ҳsuccess微信
|
||||
.bodyValue("success"); // 即使处理失败也返回success避免微信重试
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ֤微信ŷǩ
|
||||
* 验证微信公众号签名
|
||||
*
|
||||
* GET֤头像地址
|
||||
* GET请求用于验证服务器地址
|
||||
*/
|
||||
public Mono<ServerResponse> verifySignature(ServerRequest request) {
|
||||
String signature = request.queryParam("signature").orElse("");
|
||||
@@ -90,27 +90,27 @@ public class WechatOfficialEventHandler {
|
||||
String nonce = request.queryParam("nonce").orElse("");
|
||||
String echostr = request.queryParam("echostr").orElse("");
|
||||
|
||||
log.info("========== 微信ǩ֤==========");
|
||||
log.info("收到IJ:");
|
||||
log.info("========== 微信公众号签名验证 ==========");
|
||||
log.info("收到的参数:");
|
||||
log.info(" signature: {}", signature);
|
||||
log.info(" timestamp: {}", timestamp);
|
||||
log.info(" nonce: {}", nonce);
|
||||
log.info(" echostr: {}", echostr);
|
||||
|
||||
// 获取õToken
|
||||
// 获取配置的Token
|
||||
String token = wechatProperties.getMp().getToken();
|
||||
log.info("õToken: {}", token);
|
||||
log.info("配置的Token: {}", token);
|
||||
|
||||
// ֤ǩ
|
||||
// 验证签名
|
||||
if (checkSignature(signature, timestamp, nonce, token)) {
|
||||
log.info("ǩ֤成功echostr: {}", echostr);
|
||||
log.info("========== 微信ǩ֤ ==========");
|
||||
log.info("签名验证成功,返回echostr: {}", echostr);
|
||||
log.info("========== 微信公众号签名验证结束 ==========");
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue(echostr);
|
||||
} else {
|
||||
log.warn("ǩ֤失败");
|
||||
log.info("========== 微信ǩ֤ ==========");
|
||||
log.warn("签名验证失败");
|
||||
log.info("========== 微信公众号签名验证结束 ==========");
|
||||
return ServerResponse.badRequest()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue("error");
|
||||
@@ -118,38 +118,38 @@ public class WechatOfficialEventHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* ֤ǩ
|
||||
* 验证签名
|
||||
*
|
||||
* @param signature 微信żǩ
|
||||
* @param timestamp 创建时间
|
||||
* @param nonce
|
||||
* @param signature 微信加密签名
|
||||
* @param timestamp 时间戳
|
||||
* @param nonce 随机数
|
||||
* @param token Token
|
||||
* @return Ƿ֤通过
|
||||
* @return 是否验证通过
|
||||
*/
|
||||
private boolean checkSignature(String signature, String timestamp, String nonce, String token) {
|
||||
// 1. tokentimestampnonceֵ
|
||||
// 1. 将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("<FromUserName>");
|
||||
int end = xml.indexOf("</FromUserName>");
|
||||
if (start != -1 && end != -1) {
|
||||
String value = xml.substring(start + 14, end);
|
||||
// ȥ CDATA
|
||||
// 去除 CDATA 标记
|
||||
return cleanCdata(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* XML 获取更新
|
||||
* 从XML中获取事件类型
|
||||
*/
|
||||
private String extractEvent(String xml) {
|
||||
int start = xml.indexOf("<Event>");
|
||||
int end = xml.indexOf("</Event>");
|
||||
if (start != -1 && end != -1) {
|
||||
String value = xml.substring(start + 7, end);
|
||||
// ȥ CDATA
|
||||
// 去除 CDATA 标记
|
||||
return cleanCdata(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* CDATA
|
||||
* : <![CDATA[subscribe]]> -> subscribe
|
||||
* 清理 CDATA 标记
|
||||
* 例如: <![CDATA[subscribe]]> -> subscribe
|
||||
*/
|
||||
private String cleanCdata(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
// ȥǰհ
|
||||
// 去除前后空白
|
||||
value = value.trim();
|
||||
// CDATA获取м
|
||||
// ʽ: <![CDATA[xxx]]>
|
||||
// 提取 CDATA 中间内容
|
||||
// 格式: <![CDATA[xxx]]>
|
||||
if (value.startsWith("<![CDATA[") && value.endsWith("]]>")) {
|
||||
return value.substring(9, value.length() - 3);
|
||||
}
|
||||
|
||||
+9
-1
@@ -1,8 +1,10 @@
|
||||
package cn.novalon.gym.manage.member.repository;
|
||||
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
@@ -12,7 +14,7 @@ import reactor.core.publisher.Mono;
|
||||
*/
|
||||
|
||||
@Repository
|
||||
public interface MemberRepository extends R2dbcRepository<Member, Long> {
|
||||
public interface IMemberRepository extends R2dbcRepository<Member, Long> {
|
||||
|
||||
// UnionID查询会员
|
||||
Mono<Member> findByUnionId(String unionId);
|
||||
@@ -25,4 +27,10 @@ public interface MemberRepository extends R2dbcRepository<Member, Long> {
|
||||
|
||||
// 手机号查询
|
||||
Mono<Member> findByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 分页查询所有会员
|
||||
* 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有
|
||||
*/
|
||||
Flux<Member> findAllBy(Pageable pageable);
|
||||
}
|
||||
+21
@@ -1,7 +1,11 @@
|
||||
package cn.novalon.gym.manage.member.service;
|
||||
|
||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
@@ -37,4 +41,21 @@ public interface MemberService {
|
||||
* @return 是否成功
|
||||
*/
|
||||
Mono<Boolean> adminUpdatePhone(Long memberId, String phone);
|
||||
|
||||
/**
|
||||
* 管理端查询会员(es查询)
|
||||
*
|
||||
* @param searchMemberDto 会员信息dto
|
||||
* @return 会员信息
|
||||
*/
|
||||
Flux<MemberES> searchMember(SearchMemberDto searchMemberDto);
|
||||
|
||||
/**
|
||||
* 管理端查询所有会员
|
||||
*
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 页大小
|
||||
* @return 所有会员信息
|
||||
*/
|
||||
Flux<Member> findAll(Integer pageNum, Integer pageSize);
|
||||
}
|
||||
|
||||
+65
-4
@@ -1,17 +1,27 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.repository.MemberRepository;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||
import cn.novalon.gym.manage.member.service.MemberService;
|
||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -27,7 +37,17 @@ import java.time.LocalDateTime;
|
||||
@RequiredArgsConstructor
|
||||
public class MemberServiceImpl implements MemberService {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
private final IMemberRepository memberRepository;
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
private final WechatProperties wechatProperties;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
}
|
||||
|
||||
@Value("${wechat.aes.secret-key:}")
|
||||
private String aesSecretKey;
|
||||
@@ -69,6 +89,7 @@ public class MemberServiceImpl implements MemberService {
|
||||
|
||||
return memberRepository.save(member);
|
||||
})
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.map(savedMember -> {
|
||||
log.info("会员信息更新成功, memberId: {}", savedMember.getId());
|
||||
return buildMemberInfoResponse(savedMember);
|
||||
@@ -130,7 +151,46 @@ public class MemberServiceImpl implements MemberService {
|
||||
return updateMemberPhone(memberId, encryptedPhone);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<MemberES> searchMember(SearchMemberDto searchMemberDto) {
|
||||
|
||||
String searchValue = searchMemberDto.getSearchValue();
|
||||
|
||||
// 1. 处理手机号加密
|
||||
if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
searchValue = AesUtil.encrypt(searchValue,secretKey,iv);
|
||||
}
|
||||
|
||||
// 2. 分页参数
|
||||
Pageable pageable = PageRequest.of(
|
||||
searchMemberDto.getPageNum() - 1,
|
||||
searchMemberDto.getPageSize(),
|
||||
Sort.by(Sort.Direction.DESC, "update_at")
|
||||
);
|
||||
|
||||
// 3. 调用 Repository 查询
|
||||
return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender(
|
||||
searchValue,
|
||||
searchValue,
|
||||
searchValue,
|
||||
searchMemberDto.getFilter() ,
|
||||
pageable
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Member> findAll(Integer pageNum, Integer pageSize) {
|
||||
Pageable pageable = PageRequest.of(
|
||||
pageNum - 1,
|
||||
pageSize
|
||||
);
|
||||
|
||||
return memberRepository.findAllBy(pageable);
|
||||
}
|
||||
|
||||
// 更新会员手机号
|
||||
private Mono<Boolean> updateMemberPhone(Long memberId, String encryptedPhone) {
|
||||
return memberRepository.findById(memberId)
|
||||
@@ -139,6 +199,7 @@ public class MemberServiceImpl implements MemberService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.map(savedMember -> {
|
||||
log.info("手机号录入成功, memberId: {}", savedMember.getId());
|
||||
return true;
|
||||
|
||||
+27
-5
@@ -3,16 +3,20 @@ package cn.novalon.gym.manage.member.service.impl;
|
||||
import cn.novalon.gym.manage.common.config.JwtProperties;
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
||||
import cn.novalon.gym.manage.member.vo.WechatLoginVO;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.repository.MemberRepository;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||
import cn.novalon.gym.manage.member.service.WechatApiService;
|
||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
|
||||
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
|
||||
import cn.novalon.gym.manage.member.vo.WechatLoginVO;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -38,10 +42,20 @@ import java.util.Map;
|
||||
public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
|
||||
private final WechatApiService wechatApiService;
|
||||
private final MemberRepository memberRepository;
|
||||
private final IMemberRepository memberRepository;
|
||||
private final JwtProperties jwtProperties;
|
||||
private final WechatProperties wechatProperties;
|
||||
private final WechatPhoneUtil wechatPhoneUtil;
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 小程序登录 - 通过微信 code 完成登录
|
||||
@@ -72,6 +86,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -81,6 +96,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -96,6 +112,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -114,6 +131,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -182,6 +200,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setPhone(encryptedPhone);
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.map(savedMember -> {
|
||||
log.info("更新会员手机号成功, memberId: {}", savedMember.getId());
|
||||
return true;
|
||||
@@ -263,9 +282,9 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
|
||||
// Step 3: 保存 Member
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
log.info("保存 Member 成功, id: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo());
|
||||
|
||||
// Step 4: 如果有 phoneCode,尝试获取手机号
|
||||
if (phoneCode != null && !phoneCode.isEmpty()) {
|
||||
log.info("检测到 phoneCode,尝试获取手机号");
|
||||
@@ -278,7 +297,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
// 为新会员绑定手机号
|
||||
savedMember.setPhone(encryptedPhone);
|
||||
return memberRepository.save(savedMember)
|
||||
.doOnSuccess(m -> log.info("新用户手机号绑定成功"))
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(m -> {
|
||||
log.info("新用户手机号绑定成功");
|
||||
})
|
||||
.thenReturn(buildLoginResponse(savedMember, true, sessionKey));
|
||||
} else {
|
||||
log.warn("未获取到手机号");
|
||||
|
||||
+29
-8
@@ -1,12 +1,16 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.repository.MemberRepository;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -30,12 +34,22 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
private final IMemberRepository memberRepository;
|
||||
private final WechatProperties wechatProperties;
|
||||
private final WebClient webClient;
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关注事件
|
||||
*/
|
||||
@@ -68,6 +82,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
}
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then(sendWelcomeMessage(openId));
|
||||
} else {
|
||||
log.info("老用户关注服务号: memberId={}", existingMember.getId());
|
||||
@@ -75,6 +90,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then(sendWelcomeMessage(openId));
|
||||
}
|
||||
})
|
||||
@@ -88,6 +104,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then(sendWelcomeMessage(openId));
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -105,6 +122,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then(sendWelcomeMessage(openId));
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -129,7 +147,9 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
log.info("找到会员,更新为未关注状态, memberId: {}", member.getId());
|
||||
member.setSubscribed(false);
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
return memberRepository.save(member);
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then();
|
||||
})
|
||||
.then()
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -192,8 +212,8 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
if (officialOpenId != null && !officialOpenId.isEmpty()) {
|
||||
member.setOfficialOpenId(officialOpenId);
|
||||
}
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.map(savedMember -> {
|
||||
log.info("关联成功, memberId: {}", savedMember.getId());
|
||||
return true;
|
||||
@@ -234,10 +254,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
.build();
|
||||
|
||||
log.info("新用户关注服务号,仅保存标识信息(UnionID和OpenID)");
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(savedMember ->
|
||||
log.info("从服务号创建新会员成功, memberId: {}", savedMember.getId()))
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(savedMember -> {
|
||||
log.info("从服务号创建新会员成功, memberId: {}", savedMember.getId());
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package cn.novalon.gym.manage.member.util;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 通用 ES 同步工具类
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* 1. 注入工具类
|
||||
* @Autowired private EsSyncUtils esSyncUtils;
|
||||
*
|
||||
* 2. 同步数据到 ES(不返回结果,适合 doOnSuccess)
|
||||
* esSyncUtils.sync(Member.class, MemberES.class, member, memberESRepository);
|
||||
*
|
||||
* 3. 同步数据到 ES(返回 Mono,适合链式调用)
|
||||
* esSyncUtils.syncToES(Member.class, MemberES.class, member, memberESRepository).subscribe();
|
||||
*
|
||||
* 4. 如果 Repository 是单例,可以先绑定
|
||||
* var syncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
* syncer.sync(member);
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EsSyncUtils {
|
||||
|
||||
/**
|
||||
* 同步实体到 ES(不返回结果,适合 doOnSuccess)
|
||||
*
|
||||
* @param sourceClass 源实体类(如 Member.class)
|
||||
* @param targetClass 目标ES实体类(如 MemberES.class)
|
||||
* @param source 源实体对象
|
||||
* @param repository ES Repository
|
||||
* @param <S> 源实体类型
|
||||
* @param <T> ES实体类型
|
||||
* @param <ID> ID类型
|
||||
*/
|
||||
public <S, T, ID> void sync(Class<S> sourceClass, Class<T> targetClass,
|
||||
S source, ReactiveElasticsearchRepository<T, ID> repository) {
|
||||
if (source == null) {
|
||||
log.warn("同步 ES 失败:源实体为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
T target = BeanUtil.toBean(source, targetClass);
|
||||
repository.save(target).subscribe(
|
||||
success -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()),
|
||||
error -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), error)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}",
|
||||
sourceClass.getSimpleName(), targetClass.getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步实体到 ES(返回 Mono,适合链式调用)
|
||||
*
|
||||
* @param sourceClass 源实体类
|
||||
* @param targetClass 目标ES实体类
|
||||
* @param source 源实体对象
|
||||
* @param repository ES Repository
|
||||
* @return Mono<Void>
|
||||
*/
|
||||
public <S, T, ID> Mono<Void> syncToES(Class<S> sourceClass, Class<T> targetClass,
|
||||
S source, ReactiveElasticsearchRepository<T, ID> repository) {
|
||||
if (source == null) {
|
||||
log.warn("同步 ES 失败:源实体为空");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
T target = BeanUtil.toBean(source, targetClass);
|
||||
return repository.save(target)
|
||||
.doOnSuccess(t -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()))
|
||||
.doOnError(e -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), e))
|
||||
.then();
|
||||
} catch (Exception e) {
|
||||
log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}",
|
||||
sourceClass.getSimpleName(), targetClass.getSimpleName(), e);
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 Repository,返回一个针对特定实体类型的同步器
|
||||
*
|
||||
* @param sourceClass 源实体类
|
||||
* @param targetClass 目标ES实体类
|
||||
* @param repository ES Repository
|
||||
* @return 实体同步器
|
||||
*/
|
||||
public <S, T, ID> EntitySyncer<S, T, ID> bind(Class<S> sourceClass, Class<T> targetClass,
|
||||
ReactiveElasticsearchRepository<T, ID> repository) {
|
||||
return new EntitySyncer<>(sourceClass, targetClass, repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体同步器(绑定特定类型的同步器,避免重复传 Class)
|
||||
*
|
||||
* @param <S> 源实体类型
|
||||
* @param <T> ES实体类型
|
||||
* @param <ID> ID类型
|
||||
*/
|
||||
public static class EntitySyncer<S, T, ID> {
|
||||
private final Class<S> sourceClass;
|
||||
private final Class<T> targetClass;
|
||||
private final ReactiveElasticsearchRepository<T, ID> repository;
|
||||
|
||||
public EntitySyncer(Class<S> sourceClass, Class<T> targetClass,
|
||||
ReactiveElasticsearchRepository<T, ID> repository) {
|
||||
this.sourceClass = sourceClass;
|
||||
this.targetClass = targetClass;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步(不返回结果)
|
||||
*/
|
||||
public void sync(S source) {
|
||||
if (source == null) return;
|
||||
try {
|
||||
T target = BeanUtil.toBean(source, targetClass);
|
||||
repository.save(target).subscribe(
|
||||
success -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()),
|
||||
error -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), error)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}",
|
||||
sourceClass.getSimpleName(), targetClass.getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步(返回 Mono)
|
||||
*/
|
||||
public Mono<Void> syncMono(S source) {
|
||||
if (source == null) return Mono.empty();
|
||||
try {
|
||||
T target = BeanUtil.toBean(source, targetClass);
|
||||
return repository.save(target)
|
||||
.doOnSuccess(t -> log.debug("同步到 ES 成功, 类型: {}", targetClass.getSimpleName()))
|
||||
.doOnError(e -> log.error("同步到 ES 失败, 类型: {}", targetClass.getSimpleName(), e))
|
||||
.then();
|
||||
} catch (Exception e) {
|
||||
log.error("转换 ES 实体失败, 源类型: {}, 目标类型: {}",
|
||||
sourceClass.getSimpleName(), targetClass.getSimpleName(), e);
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -14,5 +14,9 @@ wechat:
|
||||
callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback
|
||||
# 手机号加密配置
|
||||
phone-encryption:
|
||||
secret-key: dGVzdF9zZWNyZXRfa2V5X2Zvcl9waG9uZV9lbmNyeXB0aW9uMTI=
|
||||
iv: dGVzdF9pdl9mb3JfcGhvbmU=
|
||||
secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY=
|
||||
iv: LMpG6Ih9mmfEAALOCeIJBw==
|
||||
|
||||
spring:
|
||||
elasticsearch:
|
||||
uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔)
|
||||
+7
-4
@@ -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);
|
||||
|
||||
+5
-2
@@ -209,8 +209,11 @@ public class SystemRouter {
|
||||
.GET("/api/member/subscribe/status", memberHandler::checkSubscribeStatus)
|
||||
|
||||
// ========== 会员模块路由 - 管理端 ==========
|
||||
.POST("/api/admin/members/{id}/phone", memberHandler::adminUpdatePhone)
|
||||
|
||||
.POST("/api/admin/member/{id}/phone", memberHandler::adminUpdatePhone)
|
||||
.GET("/api/admin/member/{id}", memberHandler::adminGetMemberInfo)
|
||||
.PUT("/api/admin/member/{id}", memberHandler::adminUpdateMemberInfo)
|
||||
.GET("/api/admin/members", memberHandler::searchMembers)
|
||||
.GET("/api/admin/members/all", memberHandler::getAllMembers)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ spring:
|
||||
url: jdbc:postgresql://localhost:55432/manage_system
|
||||
user: novalon
|
||||
password: novalon123
|
||||
enabled: true
|
||||
enabled: false
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
|
||||
@@ -19,7 +19,7 @@ spring:
|
||||
password: 123456
|
||||
driver-class-name: org.postgresql.Driver
|
||||
flyway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
|
||||
@@ -15,7 +15,7 @@ spring:
|
||||
max-life-time: 1h
|
||||
acquire-timeout: 5s
|
||||
flyway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
validate-on-migrate: true
|
||||
|
||||
@@ -24,12 +24,12 @@ spring:
|
||||
max-life-time: 1h
|
||||
acquire-timeout: 5s
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
|
||||
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system}
|
||||
username: ${DB_USERNAME:novalon}
|
||||
password: ${DB_PASSWORD:novalon123}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
flyway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
@@ -40,6 +40,8 @@ spring:
|
||||
password: disabled
|
||||
profiles:
|
||||
active: dev
|
||||
config:
|
||||
import: classpath:member-config.yml
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
-1
@@ -58,7 +58,6 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
||||
private boolean isPublicPath(String path) {
|
||||
return path.startsWith("/api/auth/") ||
|
||||
path.equals("/actuator/health") ||
|
||||
path.startsWith("/actuator/info") ||
|
||||
path.equals("/api/member/auth/miniapp/login") ||
|
||||
path.equals("/api/member/auth/mp/callback") ||
|
||||
path.equals("/api/auth/login") ||
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ public class SecurityConfig {
|
||||
spec.pathMatchers("/api/auth/**").permitAll()
|
||||
.pathMatchers("/api/public/**").permitAll()
|
||||
.pathMatchers("/ws/**").permitAll()
|
||||
.pathMatchers("/actuator/**").permitAll();
|
||||
.pathMatchers("/actuator/**").permitAll()
|
||||
.pathMatchers("/api/member/checkIn").permitAll();
|
||||
|
||||
if (isDevOrTest) {
|
||||
spec.pathMatchers("/swagger-ui.html").permitAll()
|
||||
|
||||
-2
@@ -1,7 +1,6 @@
|
||||
package cn.novalon.gym.manage.sys.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
|
||||
/**
|
||||
* 用户更新请求DTO
|
||||
@@ -24,7 +23,6 @@ public class UserUpdateRequest {
|
||||
@Schema(description = "是否清除角色关联", example = "false")
|
||||
private Boolean clearRole;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<module>manage-audit</module>
|
||||
<module>manage-notify</module>
|
||||
<module>manage-file</module>
|
||||
<module>gym-member</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
Generated
+82
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user