Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174e33053e | ||
|
|
29b73c1f67 |
+38
@@ -0,0 +1,38 @@
|
||||
package cn.novalon.gym.manage.member.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置类(响应式版本)
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-29
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 配置 ReactiveRedisTemplate
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(
|
||||
ReactiveRedisConnectionFactory connectionFactory) {
|
||||
|
||||
// 配置序列化上下文
|
||||
RedisSerializationContext<String, Object> serializationContext =
|
||||
RedisSerializationContext.<String, Object>newSerializationContext()
|
||||
.key(StringRedisSerializer.UTF_8)
|
||||
.value(new GenericJackson2JsonRedisSerializer())
|
||||
.hashKey(StringRedisSerializer.UTF_8)
|
||||
.hashValue(new GenericJackson2JsonRedisSerializer())
|
||||
.build();
|
||||
|
||||
return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
|
||||
}
|
||||
}
|
||||
-3
@@ -12,9 +12,6 @@ public class SearchMemberDto {
|
||||
// 搜索字段 - 包括 会员号、昵称、手机号
|
||||
private String searchValue;
|
||||
|
||||
// 排序
|
||||
private String filter;
|
||||
|
||||
// 页码
|
||||
private Integer pageNum = 1;
|
||||
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
package cn.novalon.gym.manage.member.dto;
|
||||
|
||||
import cn.novalon.gym.manage.member.enums.GenderEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -17,10 +19,10 @@ public class UpdateMemberInfoDto {
|
||||
private String nickname;
|
||||
|
||||
// 性别
|
||||
private Integer gender;
|
||||
private GenderEnum gender;
|
||||
|
||||
// 生日
|
||||
private Date birthday;
|
||||
private LocalDate birthday;
|
||||
|
||||
// 头像
|
||||
private String avatar;
|
||||
|
||||
+3
@@ -33,6 +33,9 @@ public abstract class BaseEntity implements Persistable<Long> {
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
// 判断当前实体是否是新建的
|
||||
@Override
|
||||
public boolean isNew() {
|
||||
|
||||
+3
-6
@@ -1,13 +1,10 @@
|
||||
package cn.novalon.gym.manage.member.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
@@ -44,7 +41,7 @@ public class Member extends BaseEntity {
|
||||
|
||||
//生日
|
||||
@Column("birthday")
|
||||
private Date birthday;
|
||||
private LocalDate birthday;
|
||||
|
||||
//地址
|
||||
@Column("address")
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
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;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package cn.novalon.gym.manage.member.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 性别枚举
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-29
|
||||
*/
|
||||
@Getter
|
||||
public enum GenderEnum {
|
||||
|
||||
UNKNOWN(0, "未知"),
|
||||
MALE(1, "男"),
|
||||
FEMALE(2, "女");
|
||||
|
||||
private final Integer code;
|
||||
private final String desc;
|
||||
|
||||
GenderEnum(Integer code, String desc) {
|
||||
this.code = code;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public static GenderEnum fromCode(Integer code) {
|
||||
if (code == null) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
for (GenderEnum gender : values()) {
|
||||
if (gender.code.equals(code)) {
|
||||
return gender;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
+6
@@ -1,12 +1,18 @@
|
||||
package cn.novalon.gym.manage.member.es.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Document(indexName = "gym_members")
|
||||
public class MemberES {
|
||||
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ 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.annotations.Query;
|
||||
import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -15,6 +16,6 @@ public interface MemberESRepository extends ReactiveElasticsearchRepository<Memb
|
||||
/**
|
||||
* 前台通用搜索:会员号(精确匹配) 或 昵称(模糊匹配) 或 手机号(精确匹配)并且 性别筛选(精确匹配)
|
||||
*/
|
||||
Flux<MemberES> findByMemberNoOrPhoneOrNicknameContainingAndGender(
|
||||
String memberNo, String phone, String nickname,String gender, Pageable pageable);
|
||||
Flux<MemberES> findByMemberNoOrPhoneOrNicknameContaining(
|
||||
String memberNo, String phone, String nickname, Pageable pageable);
|
||||
}
|
||||
+44
-90
@@ -8,8 +8,11 @@ import cn.novalon.gym.manage.member.service.MemberService;
|
||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
|
||||
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
@@ -29,21 +32,15 @@ import reactor.core.publisher.Mono;
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "会员管理", description = "会员信息管理、微信绑定、服务号关注等")
|
||||
public class MemberHandler {
|
||||
|
||||
private final MemberService memberService;
|
||||
private final WechatAuthService wechatAuthService;
|
||||
private final WechatOfficialService wechatOfficialService;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final WechatProperties wechatProperties;
|
||||
private final AuthUtil authUtil;
|
||||
|
||||
/**
|
||||
* 获取会员信息
|
||||
*
|
||||
* GET /api/member/info
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
*/
|
||||
@Operation(summary = "获取会员信息", description = "根据当前登录用户获取会员基本信息")
|
||||
public Mono<ServerResponse> getMemberInfo(ServerRequest request) {
|
||||
|
||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -56,19 +53,7 @@ public class MemberHandler {
|
||||
.bodyValue(info));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会员信息
|
||||
*
|
||||
* PUT /api/member/info
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
* Body: {
|
||||
* "nickname": "新昵称",
|
||||
* "gender": 1,
|
||||
* "birthday": "2000-01-01",
|
||||
* "avatar": "https://example.com/avatar.jpg",
|
||||
* "address": "北京市朝阳区"
|
||||
* }
|
||||
*/
|
||||
@Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息")
|
||||
public Mono<ServerResponse> updateMemberInfo(ServerRequest request) {
|
||||
|
||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -82,11 +67,7 @@ public class MemberHandler {
|
||||
.bodyValue(info));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号(微信小程序)
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
* POST /api/member/phone/bind?code=PHONE_CODE
|
||||
*/
|
||||
@Operation(summary = "绑定手机号", description = "通过微信小程序手机号code绑定会员手机号")
|
||||
public Mono<ServerResponse> bindPhone(ServerRequest request) {
|
||||
|
||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -103,12 +84,7 @@ public class MemberHandler {
|
||||
.bodyValue(success));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询服务号关注状态
|
||||
*
|
||||
* GET /api/member/subscribe/status
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "查询服务号关注状态", description = "查询会员是否关注微信服务号")
|
||||
public Mono<ServerResponse> checkSubscribeStatus(ServerRequest request) {
|
||||
|
||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -123,14 +99,7 @@ public class MemberHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员更新手机号
|
||||
*
|
||||
* POST /api/admin/member/123/phone
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
* Body: { "phone": "13800138000" }
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "管理员更新手机号", description = "后台管理员为会员更新手机号")
|
||||
public Mono<ServerResponse> adminUpdatePhone(ServerRequest request) {
|
||||
|
||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -162,13 +131,7 @@ public class MemberHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台查看会员信息
|
||||
*
|
||||
* GET /api/admin/member/{id}
|
||||
* header: { "Authorization": "xxx" }
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "管理员查看会员详情", description = "后台管理员查看指定会员的详细信息")
|
||||
public Mono<ServerResponse> adminGetMemberInfo(ServerRequest request) {
|
||||
|
||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -179,20 +142,24 @@ public class MemberHandler {
|
||||
|
||||
log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId);
|
||||
|
||||
// TODO 多表查询:会员信息、团课信息、会员卡信息
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue("成功");
|
||||
return memberService.getMemberDetail(memberId)
|
||||
.flatMap(detail -> {
|
||||
if (detail.getPhone() != null && !detail.getPhone().isEmpty()) {
|
||||
try {
|
||||
String decryptedPhone = AesUtil.decrypt(detail.getPhone());
|
||||
detail.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", detail.getId(), e);
|
||||
detail.setPhone(null);
|
||||
}
|
||||
}
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(detail);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台编辑会员信息
|
||||
*
|
||||
* PUT /api/admin/member/{id}
|
||||
* header: { "Authorization": "xxx" }
|
||||
* Body:{"字段","值"}
|
||||
*/
|
||||
@Operation(summary = "管理员编辑会员信息", description = "后台管理员编辑会员信息")
|
||||
public Mono<ServerResponse> adminUpdateMemberInfo(ServerRequest request) {
|
||||
|
||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -201,42 +168,35 @@ public class MemberHandler {
|
||||
long memberId = NumberUtils.toLong(memberIdStr, 0L);
|
||||
if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误");
|
||||
|
||||
// TODO: 补充签到记录
|
||||
log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId);
|
||||
|
||||
// TODO 多表查询:会员信息、团课信息、会员卡信息
|
||||
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue("成功");
|
||||
return request.bodyToMono(UpdateMemberInfoDto.class)
|
||||
.flatMap(updateDto -> memberService.adminUpdateMemberInfo(memberId, updateDto))
|
||||
.flatMap(detail -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(detail));
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台搜索会员列表
|
||||
*
|
||||
* GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
*/
|
||||
@Operation(summary = "搜索会员列表", description = "后台管理员按关键词搜索会员,支持性别筛选和分页")
|
||||
public Mono<ServerResponse> searchMembers(ServerRequest request) {
|
||||
|
||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
||||
|
||||
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);
|
||||
Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
||||
Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||
|
||||
log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}",
|
||||
adminId, keyword, filter, pageNum, pageSize);
|
||||
log.info("前台搜索会员列表, adminId: {}, keyword: {},pageNum: {}, pageSize: {}",
|
||||
adminId, keyword, pageNum, pageSize);
|
||||
|
||||
return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize))
|
||||
return memberService.searchMember(new SearchMemberDto(keyword, 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);
|
||||
String decryptedPhone = AesUtil.decrypt(member.getPhone());
|
||||
member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||
member.setPhone(null);
|
||||
@@ -249,12 +209,7 @@ public class MemberHandler {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 前台查看会员列表
|
||||
*
|
||||
* GET /api/admin/members/all?pageNum=1&pageSize=10
|
||||
* header: { "Authorization": "Bearer xxx" }
|
||||
*/
|
||||
@Operation(summary = "查看会员列表", description = "后台管理员分页查看所有会员列表")
|
||||
public Mono<ServerResponse> getAllMembers(ServerRequest request) {
|
||||
|
||||
Long adminId = authUtil.getMemberIdOrThrow(request);
|
||||
@@ -263,16 +218,15 @@ public class MemberHandler {
|
||||
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||
|
||||
log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize);
|
||||
// TODO: 补充签到记录
|
||||
|
||||
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);
|
||||
String decryptedPhone = AesUtil.decrypt(member.getPhone());
|
||||
member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||
member.setPhone(null);
|
||||
|
||||
+6
-17
@@ -2,6 +2,8 @@ package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -20,20 +22,13 @@ import reactor.core.publisher.Mono;
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "微信认证", description = "微信小程序登录、公众号回调等")
|
||||
public class WechatAuthHandler {
|
||||
|
||||
private final WechatAuthService wechatAuthService;
|
||||
private final WechatOfficialEventHandler wechatOfficialEventHandler;
|
||||
|
||||
/**
|
||||
* 小程序更新
|
||||
*
|
||||
* POST /api/member/auth/miniapp/login
|
||||
* Body: {"code": "wx_login_code"}
|
||||
*
|
||||
* @param request ServerRequest
|
||||
* @return Mono<ServerResponse> 登录响应
|
||||
*/
|
||||
@Operation(summary = "微信小程序登录", description = "通过微信小程序code获取session_key,完成会员登录或注册")
|
||||
public Mono<ServerResponse> miniappLogin(ServerRequest request) {
|
||||
log.info("收到小程序登录请求");
|
||||
|
||||
@@ -50,18 +45,12 @@ public class WechatAuthHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 公众号回调
|
||||
*
|
||||
* POST /api/member/auth/mp/callback
|
||||
* Body: <xml><Event>subscribe</Event><FromUserName>openid</FromUserName></xml>
|
||||
*
|
||||
*/
|
||||
@Operation(summary = "微信公众号回调", description = "处理微信公众号事件(关注、取消关注等)")
|
||||
public Mono<ServerResponse> mpCallback(ServerRequest request) {
|
||||
return wechatOfficialEventHandler.handleEvent(request);
|
||||
}
|
||||
|
||||
// 验证微信公众号签名
|
||||
@Operation(summary = "验证微信公众号签名", description = "微信公众号服务器验证,返回echostr")
|
||||
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
|
||||
return wechatOfficialEventHandler.verifySignature(request);
|
||||
}
|
||||
|
||||
-3
@@ -40,9 +40,6 @@ public class WechatOfficialEventHandler {
|
||||
.flatMap(xmlBody -> {
|
||||
log.info("收到微信公众号事件 {}", xmlBody);
|
||||
|
||||
// TODO: 将XML解析为WechatOfficialEventDto
|
||||
// 目前简化处理直接获取openId和event
|
||||
|
||||
String openId = extractOpenId(xmlBody);
|
||||
String event = extractEvent(xmlBody);
|
||||
|
||||
|
||||
+40
-1
@@ -1,7 +1,9 @@
|
||||
package cn.novalon.gym.manage.member.repository;
|
||||
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -30,7 +32,44 @@ public interface IMemberRepository extends R2dbcRepository<Member, Long> {
|
||||
|
||||
/**
|
||||
* 分页查询所有会员
|
||||
* 方法名 findAllBy 是 Spring Data 的约定,表示按条件查询所有
|
||||
*/
|
||||
Flux<Member> findAllBy(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查询会员的所有卡片
|
||||
*/
|
||||
@Query("SELECT " +
|
||||
" r.id, " +
|
||||
" r.member_card_record_id, " +
|
||||
" r.member_id, " +
|
||||
" r.member_card_id, " +
|
||||
" r.status, " +
|
||||
" r.remaining_times, " +
|
||||
" r.remaining_amount, " +
|
||||
" r.expire_time, " +
|
||||
" r.purchase_time, " +
|
||||
" r.source_order_id, " +
|
||||
" r.created_at, " +
|
||||
" r.updated_at, " +
|
||||
" r.version, " +
|
||||
" r.card_composition, " +
|
||||
" c.id AS card_id, " +
|
||||
" c.member_card_id, " +
|
||||
" c.member_card_name, " +
|
||||
" c.member_card_type, " +
|
||||
" c.member_card_price, " +
|
||||
" c.member_card_validity_days, " +
|
||||
" c.member_card_total_times, " +
|
||||
" c.member_card_amount, " +
|
||||
" c.member_card_status, " +
|
||||
" c.extra_config, " +
|
||||
" c.created_at AS card_created_at, " +
|
||||
" c.updated_at AS card_updated_at " +
|
||||
"FROM member_card_record r " +
|
||||
"LEFT JOIN member_card c ON r.member_card_id = c.id " +
|
||||
"WHERE r.member_id = :memberId " +
|
||||
"AND r.deleted_at IS NULL " +
|
||||
"AND c.deleted_at IS NULL " +
|
||||
"ORDER BY r.created_at DESC")
|
||||
Flux<MemberCardInfoVO> findCardRecordsWithCardInfoByMemberId(Long memberId);
|
||||
}
|
||||
|
||||
+18
@@ -4,6 +4,7 @@ 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.MemberDetailVO;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -58,4 +59,21 @@ public interface MemberService {
|
||||
* @return 所有会员信息
|
||||
*/
|
||||
Flux<Member> findAll(Integer pageNum, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 前台管理端获取会员详情(含会员卡信息)
|
||||
*
|
||||
* @param memberId 会员ID
|
||||
* @return 会员详情
|
||||
*/
|
||||
Mono<MemberDetailVO> getMemberDetail(Long memberId);
|
||||
|
||||
/**
|
||||
* 前台管理端编辑会员信息
|
||||
*
|
||||
* @param memberId 会员ID
|
||||
* @param updateDto 更新信息DTO
|
||||
* @return 更新后的会员详情
|
||||
*/
|
||||
Mono<Boolean> adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto);
|
||||
}
|
||||
|
||||
+46
-5
@@ -3,6 +3,8 @@ package cn.novalon.gym.manage.member.service.impl;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
||||
import cn.novalon.gym.manage.member.util.RedisUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -16,17 +18,35 @@ import java.time.LocalDateTime;
|
||||
* @author 付嘉
|
||||
* @date 2026-05-27
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||
private final MemberCardRecordRepository memberCardRecordRepository;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository) {
|
||||
private static final String MEMBER_CARD_RECORD_CACHE_PREFIX = "member:card:record:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository, RedisUtil redisUtil) {
|
||||
this.memberCardRecordRepository = memberCardRecordRepository;
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MemberCardRecord> findById(Long recordId) {
|
||||
return memberCardRecordRepository.findById(recordId);
|
||||
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
|
||||
Object cached = redisUtil.get(cacheKey);
|
||||
if (cached != null && cached instanceof MemberCardRecord) {
|
||||
log.debug("从缓存获取会员卡记录, recordId: {}", recordId);
|
||||
return Mono.just((MemberCardRecord) cached);
|
||||
}
|
||||
|
||||
return memberCardRecordRepository.findById(recordId)
|
||||
.doOnSuccess(record -> {
|
||||
if (record != null) {
|
||||
redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,17 +73,32 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||
|
||||
@Override
|
||||
public Mono<Integer> deductUsage(Long recordId, Integer deductTimes, Double deductAmount) {
|
||||
return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount);
|
||||
return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount)
|
||||
.doOnSuccess(updated -> {
|
||||
if (updated > 0) {
|
||||
clearRecordCache(recordId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Integer> renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) {
|
||||
return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime);
|
||||
return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime)
|
||||
.doOnSuccess(updated -> {
|
||||
if (updated > 0) {
|
||||
clearRecordCache(recordId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Integer> updateStatus(Long recordId, String status) {
|
||||
return memberCardRecordRepository.updateStatus(recordId, status);
|
||||
return memberCardRecordRepository.updateStatus(recordId, status)
|
||||
.doOnSuccess(updated -> {
|
||||
if (updated > 0) {
|
||||
clearRecordCache(recordId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,4 +115,10 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||
public Flux<MemberCardRecord> findExpiredCards() {
|
||||
return memberCardRecordRepository.findExpiredCards();
|
||||
}
|
||||
|
||||
private void clearRecordCache(Long recordId) {
|
||||
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
|
||||
redisUtil.delete(cacheKey);
|
||||
log.debug("清除会员卡记录缓存, recordId: {}", recordId);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-3
@@ -15,6 +15,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
||||
import cn.novalon.gym.manage.member.util.RedisUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -39,6 +40,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
||||
private final DistributedLockService distributedLockService;
|
||||
private final ExpirationReminderService expirationReminderService;
|
||||
private final RefundSagaHandler refundSagaHandler;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
private static final String MEMBER_CARD_CACHE_PREFIX = "member:card:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
public MemberCardServiceImpl(MemberCardRepository memberCardRepository,
|
||||
MemberCardRecordRepository recordRepository,
|
||||
@@ -46,7 +51,8 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
||||
MemberCardStateMachine stateMachine,
|
||||
DistributedLockService distributedLockService,
|
||||
ExpirationReminderService expirationReminderService,
|
||||
RefundSagaHandler refundSagaHandler) {
|
||||
RefundSagaHandler refundSagaHandler,
|
||||
RedisUtil redisUtil) {
|
||||
this.memberCardRepository = memberCardRepository;
|
||||
this.recordRepository = recordRepository;
|
||||
this.transactionService = transactionService;
|
||||
@@ -54,11 +60,24 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
||||
this.distributedLockService = distributedLockService;
|
||||
this.expirationReminderService = expirationReminderService;
|
||||
this.refundSagaHandler = refundSagaHandler;
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) {
|
||||
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId);
|
||||
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
|
||||
Object cached = redisUtil.get(cacheKey);
|
||||
if (cached != null && cached instanceof MemberCard) {
|
||||
log.debug("从缓存获取会员卡信息, memberCardId: {}", memberCardId);
|
||||
return Mono.just((MemberCard) cached);
|
||||
}
|
||||
|
||||
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId)
|
||||
.doOnSuccess(card -> {
|
||||
if (card != null) {
|
||||
redisUtil.setWithExpire(cacheKey, card, CACHE_EXPIRE_SECONDS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,7 +114,12 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
||||
|
||||
@Override
|
||||
public Mono<MemberCard> save(MemberCard entity) {
|
||||
return memberCardRepository.save(entity);
|
||||
return memberCardRepository.save(entity)
|
||||
.doOnSuccess(saved -> {
|
||||
if (saved.getMemberCardId() != null) {
|
||||
clearCardCache(saved.getMemberCardId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -329,4 +353,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
||||
|
||||
return transactionService.createTransaction(transaction);
|
||||
}
|
||||
|
||||
private void clearCardCache(Long memberCardId) {
|
||||
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
|
||||
redisUtil.delete(cacheKey);
|
||||
log.debug("清除会员卡缓存, memberCardId: {}", memberCardId);
|
||||
}
|
||||
}
|
||||
|
||||
+168
-27
@@ -4,20 +4,32 @@ import cn.novalon.gym.manage.common.exception.ConflictException;
|
||||
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||
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.enums.GenderEnum;
|
||||
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
||||
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.BeanConvertUtil;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
|
||||
import cn.novalon.gym.manage.member.vo.MemberDetailVO;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||
import co.elastic.clients.elasticsearch.core.IndexResponse;
|
||||
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||
import co.elastic.clients.transport.rest_client.RestClientTransport;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.elasticsearch.client.RestClient;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
@@ -26,6 +38,10 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 会员服务实现
|
||||
@@ -41,10 +57,14 @@ public class MemberServiceImpl implements MemberService {
|
||||
private final IMemberRepository memberRepository;
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
private final WechatProperties wechatProperties;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
|
||||
private static final String MEMBER_DETAIL_CACHE_PREFIX = "member:detail:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
@@ -52,12 +72,23 @@ public class MemberServiceImpl implements MemberService {
|
||||
|
||||
@Override
|
||||
public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
|
||||
return memberRepository.findById(memberId)
|
||||
.map(this::buildMemberInfoResponse)
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
log.error("会员不存在: memberId={}", memberId);
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}));
|
||||
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||
|
||||
return redisUtil.get(cacheKey, MemberInfoVO.class)
|
||||
.flatMap(cached -> {
|
||||
if (cached != null) {
|
||||
log.debug("从缓存获取会员信息, memberId: {}", memberId);
|
||||
return Mono.just(cached);
|
||||
}
|
||||
return memberRepository.findById(memberId)
|
||||
.map(this::buildMemberInfoResponse)
|
||||
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
|
||||
.then(Mono.just(vo)))
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
log.error("会员不存在: memberId={}", memberId);
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,10 +98,10 @@ public class MemberServiceImpl implements MemberService {
|
||||
return memberRepository.findById(memberId)
|
||||
.flatMap(member -> {
|
||||
if (updateDto.getNickname() != null) {
|
||||
member.setNickname(updateDto.getNickname());
|
||||
member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname()));
|
||||
}
|
||||
if (updateDto.getGender() != null) {
|
||||
member.setGender(updateDto.getGender());
|
||||
member.setGender(updateDto.getGender().getCode());
|
||||
}
|
||||
if (updateDto.getBirthday() != null) {
|
||||
member.setBirthday(updateDto.getBirthday());
|
||||
@@ -79,12 +110,16 @@ public class MemberServiceImpl implements MemberService {
|
||||
member.setAvatar(updateDto.getAvatar());
|
||||
}
|
||||
if (updateDto.getAddress() != null) {
|
||||
member.setAddress(updateDto.getAddress());
|
||||
member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress()));
|
||||
}
|
||||
|
||||
return memberRepository.save(member);
|
||||
})
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
memberSyncer.sync(savedMember);
|
||||
return clearMemberCache(memberId)
|
||||
.then(Mono.just(savedMember));
|
||||
})
|
||||
.map(savedMember -> {
|
||||
log.info("会员信息更新成功, memberId: {}", savedMember.getId());
|
||||
return buildMemberInfoResponse(savedMember);
|
||||
@@ -99,11 +134,14 @@ public class MemberServiceImpl implements MemberService {
|
||||
String phone = member.getPhone();
|
||||
String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null;
|
||||
|
||||
GenderEnum genderEnum = GenderEnum.fromCode(member.getGender());
|
||||
|
||||
return MemberInfoVO.builder()
|
||||
.id(member.getId())
|
||||
.nickname(member.getNickname())
|
||||
.phone(maskedPhone)
|
||||
.gender(member.getGender())
|
||||
.gender(genderEnum)
|
||||
.genderDesc(genderEnum.getDesc())
|
||||
.birthday(member.getBirthday())
|
||||
.avatar(member.getAvatar())
|
||||
.hasPhone(phone != null)
|
||||
@@ -117,9 +155,7 @@ public class MemberServiceImpl implements MemberService {
|
||||
|
||||
String encryptedPhone;
|
||||
try {
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
encryptedPhone = AesUtil.encrypt(phone, secretKey, iv);
|
||||
encryptedPhone = AesUtil.encrypt(phone);
|
||||
log.info("手机号加密成功");
|
||||
} catch (Exception e) {
|
||||
log.error("手机号加密失败", e);
|
||||
@@ -145,32 +181,32 @@ public class MemberServiceImpl implements MemberService {
|
||||
|
||||
@Override
|
||||
public Flux<MemberES> searchMember(SearchMemberDto searchMemberDto) {
|
||||
log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}",
|
||||
log.info("搜索会员, searchValue: {}, pageNum: {}, pageSize: {}",
|
||||
searchMemberDto.getSearchValue(),
|
||||
searchMemberDto.getFilter(),
|
||||
searchMemberDto.getPageNum(),
|
||||
searchMemberDto.getPageSize());
|
||||
|
||||
String searchValue = searchMemberDto.getSearchValue();
|
||||
|
||||
if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){
|
||||
if (searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")) {
|
||||
log.debug("搜索值为手机号格式,进行加密处理");
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
searchValue = AesUtil.encrypt(searchValue,secretKey,iv);
|
||||
searchValue = AesUtil.encrypt(searchValue);
|
||||
}
|
||||
|
||||
Pageable pageable = PageRequest.of(
|
||||
searchMemberDto.getPageNum() - 1,
|
||||
searchMemberDto.getPageSize(),
|
||||
Sort.by(Sort.Direction.DESC, "update_at")
|
||||
searchMemberDto.getPageSize()
|
||||
);
|
||||
|
||||
return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender(
|
||||
if (searchValue == null) {
|
||||
log.warn("搜索值为空,返回空结果");
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
return memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(
|
||||
searchValue,
|
||||
searchValue,
|
||||
searchValue,
|
||||
searchMemberDto.getFilter() ,
|
||||
pageable
|
||||
);
|
||||
}
|
||||
@@ -187,6 +223,99 @@ public class MemberServiceImpl implements MemberService {
|
||||
return memberRepository.findAllBy(pageable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MemberDetailVO> getMemberDetail(Long memberId) {
|
||||
log.info("查询会员详情, memberId: {}", memberId);
|
||||
|
||||
String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
|
||||
|
||||
return redisUtil.get(cacheKey, MemberDetailVO.class)
|
||||
.flatMap(cached -> {
|
||||
if (cached != null) {
|
||||
log.debug("从缓存获取会员详情, memberId: {}", memberId);
|
||||
return Mono.just(cached);
|
||||
}
|
||||
return memberRepository.findById(memberId)
|
||||
.zipWith(
|
||||
memberRepository.findCardRecordsWithCardInfoByMemberId(memberId)
|
||||
.collectList(),
|
||||
(baseInfo, cardList) -> {
|
||||
MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class);
|
||||
|
||||
GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender());
|
||||
memberDetailVO.setGenderDesc(genderEnum.getDesc());
|
||||
|
||||
List<MemberCardInfoVO> enrichedCards = cardList.stream()
|
||||
.peek(vo -> {
|
||||
if (vo.getMemberCardType() != null) {
|
||||
try {
|
||||
MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType());
|
||||
vo.setMemberCardTypeDesc(cardType.getDesc());
|
||||
} catch (IllegalArgumentException e) {
|
||||
vo.setMemberCardTypeDesc(vo.getMemberCardType());
|
||||
}
|
||||
}
|
||||
if (vo.getMemberCardStatus() != null) {
|
||||
vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架");
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
memberDetailVO.setMemberCards(enrichedCards);
|
||||
|
||||
long activeCount = enrichedCards.stream()
|
||||
.filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1)
|
||||
.count();
|
||||
memberDetailVO.setActiveCardCount((int) activeCount);
|
||||
memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount);
|
||||
|
||||
return memberDetailVO;
|
||||
}
|
||||
)
|
||||
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
|
||||
.then(Mono.just(vo)));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> adminUpdateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) {
|
||||
log.info("前台管理端编辑会员信息, memberId: {}", memberId);
|
||||
|
||||
return memberRepository.findById(memberId)
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
log.error("会员不存在: memberId={}", memberId);
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}))
|
||||
.flatMap(member -> {
|
||||
if (updateDto.getNickname() != null) {
|
||||
member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname()));
|
||||
}
|
||||
if (updateDto.getGender() != null) {
|
||||
member.setGender(updateDto.getGender().getCode());
|
||||
}
|
||||
if (updateDto.getBirthday() != null) {
|
||||
member.setBirthday(updateDto.getBirthday());
|
||||
}
|
||||
if (updateDto.getAvatar() != null) {
|
||||
member.setAvatar(updateDto.getAvatar());
|
||||
}
|
||||
if (updateDto.getAddress() != null) {
|
||||
member.setAddress(HtmlEscapeUtil.escape(updateDto.getAddress()));
|
||||
}
|
||||
|
||||
return memberRepository.save(member);
|
||||
})
|
||||
.flatMap(savedMember -> {
|
||||
memberSyncer.sync(savedMember);
|
||||
return clearMemberCache(memberId)
|
||||
.then(Mono.just(true));
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e);
|
||||
return Mono.just(false);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Boolean> updateMemberPhone(Long memberId, String encryptedPhone) {
|
||||
return memberRepository.findById(memberId)
|
||||
.flatMap(member -> {
|
||||
@@ -194,7 +323,11 @@ public class MemberServiceImpl implements MemberService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(savedMember -> {
|
||||
memberSyncer.sync(savedMember);
|
||||
return clearMemberCache(memberId)
|
||||
.then(Mono.just(savedMember));
|
||||
})
|
||||
.map(savedMember -> {
|
||||
log.info("手机号录入成功, memberId: {}", savedMember.getId());
|
||||
return true;
|
||||
@@ -205,4 +338,12 @@ public class MemberServiceImpl implements MemberService {
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<Long> clearMemberCache(Long memberId) {
|
||||
String infoCacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||
String detailCacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
|
||||
return redisUtil.delete(infoCacheKey)
|
||||
.then(redisUtil.delete(detailCacheKey))
|
||||
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
|
||||
}
|
||||
}
|
||||
+52
-15
@@ -1,9 +1,11 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||
import cn.novalon.gym.manage.member.entity.RefundApplication;
|
||||
import cn.novalon.gym.manage.member.enums.RefundStatus;
|
||||
import cn.novalon.gym.manage.member.repository.RefundApplicationRepository;
|
||||
import cn.novalon.gym.manage.member.service.IRefundApplicationService;
|
||||
import cn.novalon.gym.manage.member.util.RedisUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -21,9 +23,14 @@ import java.time.LocalDateTime;
|
||||
public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
||||
|
||||
private final RefundApplicationRepository refundApplicationRepository;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository) {
|
||||
private static final String REFUND_APPLICATION_CACHE_PREFIX = "member:refund:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository, RedisUtil redisUtil) {
|
||||
this.refundApplicationRepository = refundApplicationRepository;
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,14 +45,16 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
||||
.then(Mono.defer(() -> {
|
||||
RefundApplication application = RefundApplication.builder()
|
||||
.recordId(recordId)
|
||||
.reason(reason)
|
||||
.reason(HtmlEscapeUtil.escape(reason))
|
||||
.status(RefundStatus.PENDING)
|
||||
.applyTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
return refundApplicationRepository.save(application)
|
||||
.doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}",
|
||||
app.getId(), recordId));
|
||||
.doOnSuccess(app -> {
|
||||
log.info("创建退款申请成功: applicationId={}, recordId={}", app.getId(), recordId);
|
||||
clearRefundCache(recordId);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -54,14 +63,19 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
||||
return refundApplicationRepository.findById(applicationId)
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
|
||||
.flatMap(application -> {
|
||||
if (!"PENDING".equals(application.getStatus())) {
|
||||
if (application.getStatus() != RefundStatus.PENDING) {
|
||||
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
|
||||
}
|
||||
|
||||
return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, remark)
|
||||
.thenReturn(application)
|
||||
.doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}",
|
||||
applicationId, auditorId));
|
||||
return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark))
|
||||
.flatMap(updatedRows -> {
|
||||
if (updatedRows == 0) {
|
||||
return Mono.error(new RuntimeException("批准退款申请失败"));
|
||||
}
|
||||
clearRefundCache(application.getRecordId());
|
||||
log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId);
|
||||
return refundApplicationRepository.findById(applicationId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,19 +84,42 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
|
||||
return refundApplicationRepository.findById(applicationId)
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
|
||||
.flatMap(application -> {
|
||||
if (!"PENDING".equals(application.getStatus())) {
|
||||
if (application.getStatus() != RefundStatus.PENDING) {
|
||||
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
|
||||
}
|
||||
|
||||
return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, remark)
|
||||
.thenReturn(application)
|
||||
.doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}",
|
||||
applicationId, auditorId));
|
||||
return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark))
|
||||
.flatMap(updatedRows -> {
|
||||
if (updatedRows == 0) {
|
||||
return Mono.error(new RuntimeException("拒绝退款申请失败"));
|
||||
}
|
||||
clearRefundCache(application.getRecordId());
|
||||
log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId);
|
||||
return refundApplicationRepository.findById(applicationId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RefundApplication> findByRecordId(Long recordId) {
|
||||
return refundApplicationRepository.findByRecordId(recordId);
|
||||
String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId;
|
||||
Object cached = redisUtil.get(cacheKey);
|
||||
if (cached != null && cached instanceof RefundApplication) {
|
||||
log.debug("从缓存获取退款申请, recordId: {}", recordId);
|
||||
return Mono.just((RefundApplication) cached);
|
||||
}
|
||||
|
||||
return refundApplicationRepository.findByRecordId(recordId)
|
||||
.doOnSuccess(application -> {
|
||||
if (application != null) {
|
||||
redisUtil.setWithExpire(cacheKey, application, CACHE_EXPIRE_SECONDS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearRefundCache(Long recordId) {
|
||||
String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId;
|
||||
redisUtil.delete(cacheKey);
|
||||
log.debug("清除退款申请缓存, recordId: {}", recordId);
|
||||
}
|
||||
}
|
||||
|
||||
+43
-27
@@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.service.WechatApiService;
|
||||
import cn.novalon.gym.manage.member.util.RedisUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -30,6 +31,10 @@ import java.util.Map;
|
||||
public class WechatApiServiceImpl implements WechatApiService {
|
||||
|
||||
private final WechatProperties wechatProperties;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
|
||||
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; // 比官方过期时间短100秒
|
||||
|
||||
private final WebClient webClient = WebClient.builder()
|
||||
.baseUrl("https://api.weixin.qq.com")
|
||||
@@ -147,35 +152,46 @@ public class WechatApiServiceImpl implements WechatApiService {
|
||||
public Mono<String> getAccessToken(String appType) {
|
||||
log.debug("获取access_token, appType: {}", appType);
|
||||
|
||||
String appId, appSecret;
|
||||
if ("miniapp".equals(appType)) {
|
||||
appId = wechatProperties.getMiniapp().getAppId();
|
||||
appSecret = wechatProperties.getMiniapp().getAppSecret();
|
||||
} else {
|
||||
appId = wechatProperties.getMp().getAppId();
|
||||
appSecret = wechatProperties.getMp().getAppSecret();
|
||||
}
|
||||
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType;
|
||||
|
||||
return webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/cgi-bin/token")
|
||||
.queryParam("grant_type", "client_credential")
|
||||
.queryParam("appid", appId)
|
||||
.queryParam("secret", appSecret)
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.map(response -> {
|
||||
if (response.containsKey("access_token")) {
|
||||
String accessToken = (String) response.get("access_token");
|
||||
Integer expiresIn = (Integer) response.get("expires_in");
|
||||
log.info("获取access_token成功, expires_in: {}s", expiresIn);
|
||||
return accessToken;
|
||||
} else {
|
||||
String errmsg = (String) response.get("errmsg");
|
||||
log.error("获取access_token失败: {}", errmsg);
|
||||
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
.flatMap(cachedToken -> {
|
||||
if (cachedToken != null) {
|
||||
log.debug("从缓存获取access_token, appType: {}", appType);
|
||||
return Mono.just(cachedToken);
|
||||
}
|
||||
|
||||
String appId, appSecret;
|
||||
if ("miniapp".equals(appType)) {
|
||||
appId = wechatProperties.getMiniapp().getAppId();
|
||||
appSecret = wechatProperties.getMiniapp().getAppSecret();
|
||||
} else {
|
||||
appId = wechatProperties.getMp().getAppId();
|
||||
appSecret = wechatProperties.getMp().getAppSecret();
|
||||
}
|
||||
|
||||
return webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/cgi-bin/token")
|
||||
.queryParam("grant_type", "client_credential")
|
||||
.queryParam("appid", appId)
|
||||
.queryParam("secret", appSecret)
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.flatMap(response -> {
|
||||
if (response.containsKey("access_token")) {
|
||||
String accessToken = (String) response.get("access_token");
|
||||
Integer expiresIn = (Integer) response.get("expires_in");
|
||||
log.info("获取access_token成功, expires_in: {}s", expiresIn);
|
||||
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
|
||||
.then(Mono.just(accessToken));
|
||||
} else {
|
||||
String errmsg = (String) response.get("errmsg");
|
||||
log.error("获取access_token失败: {}", errmsg);
|
||||
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+34
-13
@@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.exception.ConflictException;
|
||||
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||
import cn.novalon.gym.manage.common.exception.SystemException;
|
||||
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.dto.WechatLoginDto;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
@@ -15,6 +16,7 @@ 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.RedisUtil;
|
||||
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
|
||||
import cn.novalon.gym.manage.member.vo.WechatLoginVO;
|
||||
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
|
||||
@@ -42,14 +44,17 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
|
||||
private final WechatApiService wechatApiService;
|
||||
private final IMemberRepository memberRepository;
|
||||
private final WechatProperties wechatProperties;
|
||||
private final WechatPhoneUtil wechatPhoneUtil;
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
@@ -79,7 +84,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -89,7 +97,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -105,7 +116,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -124,7 +138,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.flatMap(savedMember -> {
|
||||
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
|
||||
return Mono.just(response);
|
||||
@@ -185,7 +202,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
member.setPhone(encryptedPhone);
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.map(savedMember -> {
|
||||
log.info("更新会员手机号成功, memberId: {}", savedMember.getId());
|
||||
return true;
|
||||
@@ -197,12 +217,15 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
}));
|
||||
}
|
||||
|
||||
private void clearMemberCache(Long memberId) {
|
||||
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||
redisUtil.delete(cacheKey);
|
||||
log.debug("清除会员缓存, memberId: {}", memberId);
|
||||
}
|
||||
|
||||
private String encryptPhone(String phoneNumber) {
|
||||
try {
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
|
||||
String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv);
|
||||
String encryptedPhone = AesUtil.encrypt(phoneNumber);
|
||||
|
||||
log.debug("手机号加密成功");
|
||||
return encryptedPhone;
|
||||
@@ -214,10 +237,8 @@ public class WechatAuthServiceImpl implements WechatAuthService {
|
||||
|
||||
public String decryptPhone(String encryptedPhone) {
|
||||
try {
|
||||
String secretKey = wechatProperties.getPhoneEncryption().getSecretKey();
|
||||
String iv = wechatProperties.getPhoneEncryption().getIv();
|
||||
|
||||
String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv);
|
||||
String phoneNumber = AesUtil.decrypt(encryptedPhone);
|
||||
|
||||
log.debug("手机号解密成功");
|
||||
return phoneNumber;
|
||||
|
||||
+71
-30
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
@@ -7,6 +8,7 @@ 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.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -40,11 +42,16 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
private final MemberESRepository memberESRepository;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
private final EsSyncUtils esSyncUtils;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
|
||||
|
||||
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
|
||||
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
|
||||
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000;
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
|
||||
@@ -74,24 +81,30 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
existingMember.setOfficialOpenId(openId);
|
||||
|
||||
if (existingMember.getNickname() == null || existingMember.getNickname().isEmpty()) {
|
||||
existingMember.setNickname(userInfo.getNickname());
|
||||
existingMember.setNickname(HtmlEscapeUtil.escape(userInfo.getNickname()));
|
||||
}
|
||||
|
||||
if (existingMember.getAvatar() == null || existingMember.getAvatar().isEmpty()) {
|
||||
existingMember.setAvatar(userInfo.getHeadimgurl());
|
||||
existingMember.setAvatar(HtmlEscapeUtil.escape(userInfo.getHeadimgurl()));
|
||||
}
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then(sendWelcomeMessage(openId));
|
||||
.flatMap(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
return clearMemberCache(saved.getId())
|
||||
.then(sendWelcomeMessage(openId));
|
||||
});
|
||||
} else {
|
||||
log.info("老用户关注服务号: memberId={}", existingMember.getId());
|
||||
existingMember.setSubscribed(true);
|
||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then(sendWelcomeMessage(openId));
|
||||
.flatMap(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
return clearMemberCache(saved.getId())
|
||||
.then(sendWelcomeMessage(openId));
|
||||
});
|
||||
}
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -104,7 +117,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.then(sendWelcomeMessage(openId));
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -122,7 +138,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
existingMember.setLastLoginAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(existingMember)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.doOnSuccess(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
clearMemberCache(saved.getId());
|
||||
})
|
||||
.then(sendWelcomeMessage(openId));
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -148,8 +167,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
member.setSubscribed(false);
|
||||
member.setLastLoginAt(LocalDateTime.now());
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.then();
|
||||
.flatMap(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
return clearMemberCache(saved.getId()).then();
|
||||
});
|
||||
})
|
||||
.then()
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
@@ -213,7 +234,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
member.setOfficialOpenId(officialOpenId);
|
||||
}
|
||||
return memberRepository.save(member)
|
||||
.doOnSuccess(memberSyncer::sync)
|
||||
.flatMap(saved -> {
|
||||
memberSyncer.sync(saved);
|
||||
return clearMemberCache(saved.getId())
|
||||
.then(Mono.just(saved));
|
||||
})
|
||||
.map(savedMember -> {
|
||||
log.info("关联成功, memberId: {}", savedMember.getId());
|
||||
return true;
|
||||
@@ -268,28 +293,38 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
|
||||
/**
|
||||
* 获取微信AccessToken
|
||||
*
|
||||
* TODO: 应该使用缓存,避免频繁请求
|
||||
*/
|
||||
private Mono<String> getAccessToken() {
|
||||
String appId = wechatProperties.getMp().getAppId();
|
||||
String appSecret = wechatProperties.getMp().getAppSecret();
|
||||
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp";
|
||||
|
||||
String url = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
+ "?grant_type=client_credential"
|
||||
+ "&appid=" + appId
|
||||
+ "&secret=" + appSecret;
|
||||
|
||||
return webClient.get()
|
||||
.uri(url)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.map(response -> {
|
||||
if (response.containsKey("errcode")) {
|
||||
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
.flatMap(cachedToken -> {
|
||||
if (cachedToken != null) {
|
||||
log.debug("从缓存获取服务号access_token");
|
||||
return Mono.just(cachedToken);
|
||||
}
|
||||
return (String) response.get("access_token");
|
||||
|
||||
String appId = wechatProperties.getMp().getAppId();
|
||||
String appSecret = wechatProperties.getMp().getAppSecret();
|
||||
|
||||
String url = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
+ "?grant_type=client_credential"
|
||||
+ "&appid=" + appId
|
||||
+ "&secret=" + appSecret;
|
||||
|
||||
return webClient.get()
|
||||
.uri(url)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.flatMap(response -> {
|
||||
if (response.containsKey("errcode")) {
|
||||
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
|
||||
}
|
||||
String accessToken = (String) response.get("access_token");
|
||||
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
|
||||
.then(Mono.just(accessToken));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,4 +370,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
|
||||
return Mono.empty(); // 即使发送失败也不影响主流程
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Long> clearMemberCache(Long memberId) {
|
||||
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
|
||||
return redisUtil.delete(cacheKey)
|
||||
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
|
||||
}
|
||||
}
|
||||
|
||||
+31
-10
@@ -1,12 +1,24 @@
|
||||
package cn.novalon.gym.manage.member.util;
|
||||
|
||||
import cn.novalon.gym.manage.member.config.WechatProperties;
|
||||
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||
import co.elastic.clients.elasticsearch.core.IndexResponse;
|
||||
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||
import co.elastic.clients.transport.rest_client.RestClientTransport;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.checkerframework.checker.units.qual.K;
|
||||
import org.elasticsearch.client.RestClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AES加密工具类
|
||||
@@ -16,24 +28,35 @@ import java.util.Base64;
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AesUtil {
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
|
||||
|
||||
private static String KEY;
|
||||
private static String IV;
|
||||
|
||||
@Autowired
|
||||
public void setWechatProperties(WechatProperties props) {
|
||||
KEY = props.getPhoneEncryption().getSecretKey(); // 从配置类读取
|
||||
IV = props.getPhoneEncryption().getIv();
|
||||
if(KEY == null || IV == null) throw new RuntimeException("请配置AES密钥和偏移量");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AES 解密
|
||||
*
|
||||
* @param encryptedData 加密数据,Base64编码
|
||||
* @param key AES密钥,Base64编码(32字节)
|
||||
* @param iv 初始化向量IV,Base64编码(16字节)
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public static String decrypt(String encryptedData, String key, String iv) {
|
||||
public static String decrypt(String encryptedData) {
|
||||
|
||||
try {
|
||||
byte[] dataByte = Base64.getDecoder().decode(encryptedData);
|
||||
byte[] keyByte = Base64.getDecoder().decode(key);
|
||||
byte[] ivByte = Base64.getDecoder().decode(iv);
|
||||
byte[] keyByte = Base64.getDecoder().decode(KEY);
|
||||
byte[] ivByte = Base64.getDecoder().decode(IV);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM);
|
||||
@@ -52,15 +75,13 @@ public class AesUtil {
|
||||
* AES 加密
|
||||
*
|
||||
* @param data 原始数据
|
||||
* @param key AES密钥,Base64编码(32字节)
|
||||
* @param iv 初始化向量IV,Base64编码(16字节)
|
||||
* @return Base64编码的加密数据
|
||||
*/
|
||||
public static String encrypt(String data, String key, String iv) {
|
||||
public static String encrypt(String data) {
|
||||
try {
|
||||
byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] keyByte = Base64.getDecoder().decode(key);
|
||||
byte[] ivByte = Base64.getDecoder().decode(iv);
|
||||
byte[] keyByte = Base64.getDecoder().decode(KEY);
|
||||
byte[] ivByte = Base64.getDecoder().decode(IV);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(keyByte, ALGORITHM);
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package cn.novalon.gym.manage.member.util;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Redis 工具类(响应式版本)
|
||||
*
|
||||
* @author liwentao
|
||||
* @date 2026/5/15
|
||||
*/
|
||||
@Component
|
||||
public class RedisUtil {
|
||||
|
||||
@Autowired
|
||||
private ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
*/
|
||||
public Mono<Boolean> set(String key, Object value) {
|
||||
return reactiveRedisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值并指定过期时间(秒)
|
||||
*/
|
||||
public Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds) {
|
||||
return reactiveRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Mono<T> get(String key, Class<T> clazz) {
|
||||
return reactiveRedisTemplate.opsForValue().get(key)
|
||||
.map(obj -> clazz.isInstance(obj) ? (T) obj : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值(返回 Object)
|
||||
*/
|
||||
public Mono<Object> get(String key) {
|
||||
return reactiveRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除key
|
||||
*/
|
||||
public Mono<Long> delete(String key) {
|
||||
return reactiveRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断key是否存在
|
||||
*/
|
||||
public Mono<Boolean> hasKey(String key) {
|
||||
return reactiveRedisTemplate.hasKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间(秒)
|
||||
*/
|
||||
public Mono<Boolean> expire(String key, long timeoutSeconds) {
|
||||
return reactiveRedisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class WechatPhoneUtil {
|
||||
* @param phone 手机号
|
||||
* @return 脱敏后的手机号,如:138****8000
|
||||
*/
|
||||
private String maskPhone(String phone) {
|
||||
public static String maskPhone(String phone) {
|
||||
if (phone == null || phone.length() < 7) {
|
||||
return "***";
|
||||
}
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package cn.novalon.gym.manage.member.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 会员卡类型响应 VO
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-27
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MemberCardInfoVO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 会员卡ID
|
||||
*/
|
||||
private Long memberCardId;
|
||||
|
||||
/**
|
||||
* 会员卡名称
|
||||
*/
|
||||
private String memberCardName;
|
||||
|
||||
/**
|
||||
* 会员卡类型:TIME_CARD-时长卡, COUNT_CARD-次卡, STORED_VALUE_CARD-储值卡
|
||||
*/
|
||||
private String memberCardType;
|
||||
|
||||
/**
|
||||
* 卡类型描述
|
||||
*/
|
||||
private String memberCardTypeDesc;
|
||||
|
||||
/**
|
||||
* 会员卡价格
|
||||
*/
|
||||
private BigDecimal memberCardPrice;
|
||||
|
||||
/**
|
||||
* 有效天数(时长卡用)
|
||||
*/
|
||||
private Integer memberCardValidityDays;
|
||||
|
||||
/**
|
||||
* 总次数(次卡用)
|
||||
*/
|
||||
private Integer memberCardTotalTimes;
|
||||
|
||||
/**
|
||||
* 面额(储值卡用)
|
||||
*/
|
||||
private BigDecimal memberCardAmount;
|
||||
|
||||
/**
|
||||
* 状态:0-下架, 1-上架
|
||||
*/
|
||||
private Integer memberCardStatus;
|
||||
|
||||
/**
|
||||
* 状态描述
|
||||
*/
|
||||
private String memberCardStatusDesc;
|
||||
|
||||
/**
|
||||
* 扩展配置(JSON格式)
|
||||
*/
|
||||
private String extraConfig;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package cn.novalon.gym.manage.member.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会员详情 VO(管理端使用)
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-27
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MemberDetailVO {
|
||||
|
||||
// ==================== 会员基础信息 ====================
|
||||
|
||||
/**
|
||||
* 会员ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 会员编号
|
||||
*/
|
||||
private String memberNo;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 手机号(脱敏显示)
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 性别描述
|
||||
*/
|
||||
private String genderDesc;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private Date birthday;
|
||||
|
||||
/**
|
||||
* 地址
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 是否关注服务号
|
||||
*/
|
||||
private Boolean subscribed;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
/**
|
||||
* 注册时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// ==================== 会员卡信息 ====================
|
||||
|
||||
/**
|
||||
* 会员持有的卡列表
|
||||
*/
|
||||
private List<MemberCardInfoVO> memberCards;
|
||||
|
||||
/**
|
||||
* 有效会员卡数量
|
||||
*/
|
||||
private Integer activeCardCount;
|
||||
|
||||
/**
|
||||
* 过期/用完会员卡数量
|
||||
*/
|
||||
private Integer inactiveCardCount;
|
||||
}
|
||||
+7
-2
@@ -1,10 +1,12 @@
|
||||
package cn.novalon.gym.manage.member.vo;
|
||||
|
||||
import cn.novalon.gym.manage.member.enums.GenderEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -30,10 +32,13 @@ public class MemberInfoVO {
|
||||
private String phone;
|
||||
|
||||
// 性别
|
||||
private Integer gender;
|
||||
private GenderEnum gender;
|
||||
|
||||
// 性别描述
|
||||
private String genderDesc;
|
||||
|
||||
// 生日
|
||||
private Date birthday;
|
||||
private LocalDate birthday;
|
||||
|
||||
// 头像
|
||||
private String avatar;
|
||||
|
||||
@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS member_user (
|
||||
nickname VARCHAR(100), -- 昵称
|
||||
phone VARCHAR(255), -- 手机号(AES加密存储)
|
||||
gender INTEGER DEFAULT 0, -- 性别:0-未知,1-男,2-女
|
||||
birthday TIMESTAMP, -- 生日
|
||||
birthday DATE, -- 生日
|
||||
address VARCHAR(500), -- 地址
|
||||
avatar VARCHAR(500), -- 头像URL
|
||||
subscribed BOOLEAN DEFAULT FALSE, -- 是否关注服务号
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
wechat:
|
||||
# Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境)
|
||||
mock-enabled: false
|
||||
|
||||
miniapp:
|
||||
app-id: wx4d480112b426100b
|
||||
app-secret: 78548f0c0ff66c73d3e8b071897eb1e5
|
||||
app-id: ${WECHAT_MINIAPP_APP_ID}
|
||||
app-secret: ${WECHAT_MINIAPP_SECRET}
|
||||
|
||||
mp:
|
||||
app-id: wx6f138c9aacc8a0e8
|
||||
app-secret: 5df2e315e9268e96a43bb2cce1d2270b
|
||||
token: test_token
|
||||
aes-key: ${WECHAT_MP_AESKEY:test_aes_key}
|
||||
# 服务器回调地址(微信服务器推送事件的URL)
|
||||
callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback
|
||||
app-id: ${WECHAT_MP_APP_ID}
|
||||
app-secret: ${WECHAT_MP_SECRET}
|
||||
token: ${WECHAT_MP_TOKEN}
|
||||
aes-key: ${WECHAT_MP_AESKEY}
|
||||
callback-url: ${WECHAT_MP_CALLBACK_URL}
|
||||
|
||||
# 手机号加密配置
|
||||
phone-encryption:
|
||||
secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY=
|
||||
iv: LMpG6Ih9mmfEAALOCeIJBw==
|
||||
secret-key: ${PHONE_ENCRYPTION_SECRET_KEY}
|
||||
iv: ${PHONE_ENCRYPTION_IV}
|
||||
|
||||
spring:
|
||||
elasticsearch:
|
||||
uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔)
|
||||
uris: http://localhost:9200
|
||||
@@ -139,6 +139,10 @@
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
+8
-2
@@ -10,6 +10,8 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta
|
||||
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.EnableElasticsearchRepositories;
|
||||
import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories;
|
||||
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
@@ -17,9 +19,13 @@ import java.util.List;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
|
||||
ReactiveUserDetailsServiceAutoConfiguration.class })
|
||||
@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao",
|
||||
@EnableR2dbcRepositories(basePackages = {
|
||||
"cn.novalon.gym.manage.db.dao",
|
||||
"cn.novalon.gym.manage.sys.audit.repository" ,
|
||||
"cn.novalon.gym.manage.gymmembercard.dao"})
|
||||
"cn.novalon.gym.manage.gymmembercard.dao",
|
||||
"cn.novalon.gym.manage.member.repository"
|
||||
})
|
||||
@EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository")
|
||||
public class ManageApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package cn.novalon.gym.manage.common.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* HTML 转义工具类
|
||||
* 防止 XSS 注入攻击
|
||||
*
|
||||
* @author 付嘉
|
||||
* @date 2026-05-29
|
||||
*/
|
||||
public class HtmlEscapeUtil {
|
||||
|
||||
private static final Map<Character, String> ESCAPE_MAP = new HashMap<>();
|
||||
private static final Map<String, Character> UNESCAPE_MAP = new HashMap<>();
|
||||
private static final Pattern HTML_PATTERN = Pattern.compile("<[^>]*>");
|
||||
|
||||
static {
|
||||
// HTML 特殊字符转义映射
|
||||
ESCAPE_MAP.put('&', "&");
|
||||
ESCAPE_MAP.put('<', "<");
|
||||
ESCAPE_MAP.put('>', ">");
|
||||
ESCAPE_MAP.put('"', """);
|
||||
ESCAPE_MAP.put('\'', "'");
|
||||
|
||||
// 反向映射
|
||||
UNESCAPE_MAP.put("&", '&');
|
||||
UNESCAPE_MAP.put("<", '<');
|
||||
UNESCAPE_MAP.put(">", '>');
|
||||
UNESCAPE_MAP.put(""", '"');
|
||||
UNESCAPE_MAP.put("'", '\'');
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 HTML 特殊字符
|
||||
*
|
||||
* @param input 原始字符串
|
||||
* @return 转义后的字符串
|
||||
*/
|
||||
public static String escape(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (char c : input.toCharArray()) {
|
||||
String escaped = ESCAPE_MAP.get(c);
|
||||
if (escaped != null) {
|
||||
result.append(escaped);
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转义 HTML 特殊字符
|
||||
*
|
||||
* @param input 转义后的字符串
|
||||
* @return 原始字符串
|
||||
*/
|
||||
public static String unescape(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
String result = input;
|
||||
for (Map.Entry<String, Character> entry : UNESCAPE_MAP.entrySet()) {
|
||||
result = result.replace(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有 HTML 标签
|
||||
*
|
||||
* @param input 原始字符串
|
||||
* @return 移除标签后的字符串
|
||||
*/
|
||||
public static String stripHtmlTags(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
return HTML_PATTERN.matcher(input).replaceAll("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全转义(转义 + 移除标签)
|
||||
*
|
||||
* @param input 原始字符串
|
||||
* @return 安全字符串
|
||||
*/
|
||||
public static String sanitize(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
// 先移除 HTML 标签,再转义特殊字符
|
||||
String noTags = stripHtmlTags(input);
|
||||
return escape(noTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否包含 HTML 标签
|
||||
*
|
||||
* @param input 原始字符串
|
||||
* @return true-包含, false-不包含
|
||||
*/
|
||||
public static boolean containsHtmlTags(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return HTML_PATTERN.matcher(input).find();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user