feat: 合并会员注册功能(会员卡管理 + 微信认证) #7

Merged
fujia merged 17 commits from feature/member-register into dev 2026-05-31 11:46:59 +08:00
15 changed files with 508 additions and 228 deletions
Showing only changes of commit 174e33053e - Show all commits
@@ -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);
}
}
@@ -12,9 +12,6 @@ public class SearchMemberDto {
// 搜索字段 - 包括 会员号、昵称、手机号 // 搜索字段 - 包括 会员号、昵称、手机号
private String searchValue; private String searchValue;
// 性别排序
private Integer gender;
// 页码 // 页码
private Integer pageNum = 1; private Integer pageNum = 1;
@@ -11,6 +11,8 @@ import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
import cn.novalon.gym.manage.sys.util.AuthUtil; import cn.novalon.gym.manage.sys.util.AuthUtil;
import cn.novalon.gym.manage.sys.security.JwtTokenProvider; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
@@ -30,6 +32,7 @@ import reactor.core.publisher.Mono;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "会员管理", description = "会员信息管理、微信绑定、服务号关注等")
public class MemberHandler { public class MemberHandler {
private final MemberService memberService; private final MemberService memberService;
@@ -37,12 +40,7 @@ public class MemberHandler {
private final WechatOfficialService wechatOfficialService; private final WechatOfficialService wechatOfficialService;
private final AuthUtil authUtil; private final AuthUtil authUtil;
/** @Operation(summary = "获取会员信息", description = "根据当前登录用户获取会员基本信息")
* 获取会员信息
*
* GET /api/member/info
* header: { "Authorization": "Bearer xxx" }
*/
public Mono<ServerResponse> getMemberInfo(ServerRequest request) { public Mono<ServerResponse> getMemberInfo(ServerRequest request) {
Long memberId = authUtil.getMemberIdOrThrow(request); Long memberId = authUtil.getMemberIdOrThrow(request);
@@ -55,19 +53,7 @@ public class MemberHandler {
.bodyValue(info)); .bodyValue(info));
} }
/** @Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息")
* 更新会员信息
*
* PUT /api/member/info
* header: { "Authorization": "Bearer xxx" }
* Body: {
* "nickname": "新昵称",
* "gender": 1,
* "birthday": "2000-01-01",
* "avatar": "https://example.com/avatar.jpg",
* "address": "北京市朝阳区"
* }
*/
public Mono<ServerResponse> updateMemberInfo(ServerRequest request) { public Mono<ServerResponse> updateMemberInfo(ServerRequest request) {
Long memberId = authUtil.getMemberIdOrThrow(request); Long memberId = authUtil.getMemberIdOrThrow(request);
@@ -81,11 +67,7 @@ public class MemberHandler {
.bodyValue(info)); .bodyValue(info));
} }
/** @Operation(summary = "绑定手机号", description = "通过微信小程序手机号code绑定会员手机号")
* 绑定手机号(微信小程序)
* header: { "Authorization": "Bearer xxx" }
* POST /api/member/phone/bind?code=PHONE_CODE
*/
public Mono<ServerResponse> bindPhone(ServerRequest request) { public Mono<ServerResponse> bindPhone(ServerRequest request) {
Long memberId = authUtil.getMemberIdOrThrow(request); Long memberId = authUtil.getMemberIdOrThrow(request);
@@ -102,12 +84,7 @@ public class MemberHandler {
.bodyValue(success)); .bodyValue(success));
} }
/** @Operation(summary = "查询服务号关注状态", description = "查询会员是否关注微信服务号")
* 查询服务号关注状态
*
* GET /api/member/subscribe/status
*
*/
public Mono<ServerResponse> checkSubscribeStatus(ServerRequest request) { public Mono<ServerResponse> checkSubscribeStatus(ServerRequest request) {
Long memberId = authUtil.getMemberIdOrThrow(request); Long memberId = authUtil.getMemberIdOrThrow(request);
@@ -122,14 +99,7 @@ public class MemberHandler {
}); });
} }
/** @Operation(summary = "管理员更新手机号", description = "后台管理员为会员更新手机号")
* 管理员更新手机号
*
* POST /api/admin/member/123/phone
* header: { "Authorization": "Bearer xxx" }
* Body: { "phone": "13800138000" }
*
*/
public Mono<ServerResponse> adminUpdatePhone(ServerRequest request) { public Mono<ServerResponse> adminUpdatePhone(ServerRequest request) {
Long adminId = authUtil.getMemberIdOrThrow(request); Long adminId = authUtil.getMemberIdOrThrow(request);
@@ -161,13 +131,7 @@ public class MemberHandler {
}); });
} }
/** @Operation(summary = "管理员查看会员详情", description = "后台管理员查看指定会员的详细信息")
* 前台查看会员信息
*
* GET /api/admin/member/{id}
* header: { "Authorization": "xxx" }
*
*/
public Mono<ServerResponse> adminGetMemberInfo(ServerRequest request) { public Mono<ServerResponse> adminGetMemberInfo(ServerRequest request) {
Long adminId = authUtil.getMemberIdOrThrow(request); Long adminId = authUtil.getMemberIdOrThrow(request);
@@ -195,13 +159,7 @@ public class MemberHandler {
}); });
} }
/** @Operation(summary = "管理员编辑会员信息", description = "后台管理员编辑会员信息")
* 前台编辑会员信息
*
* PUT /api/admin/member/{id}
* header: { "Authorization": "xxx" }
* Body:{"字段","值"}
*/
public Mono<ServerResponse> adminUpdateMemberInfo(ServerRequest request) { public Mono<ServerResponse> adminUpdateMemberInfo(ServerRequest request) {
Long adminId = authUtil.getMemberIdOrThrow(request); Long adminId = authUtil.getMemberIdOrThrow(request);
@@ -220,25 +178,19 @@ public class MemberHandler {
.bodyValue(detail)); .bodyValue(detail));
} }
/** @Operation(summary = "搜索会员列表", description = "后台管理员按关键词搜索会员,支持性别筛选和分页")
* 前台搜索会员列表
*
* GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10
* header: { "Authorization": "Bearer xxx" }
*/
public Mono<ServerResponse> searchMembers(ServerRequest request) { public Mono<ServerResponse> searchMembers(ServerRequest request) {
Long adminId = authUtil.getMemberIdOrThrow(request); Long adminId = authUtil.getMemberIdOrThrow(request);
String keyword = request.queryParam("searchValue").orElse(null); String keyword = request.queryParam("searchValue").orElse(null);
Integer filter = NumberUtils.toInt(request.queryParam("filter").orElse("-1"), -1);
Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); Integer pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); Integer pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", log.info("前台搜索会员列表, adminId: {}, keyword: {},pageNum: {}, pageSize: {}",
adminId, keyword, filter, 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 -> { .map(member -> {
// 解密手机号 // 解密手机号
if (member.getPhone() != null && !member.getPhone().isEmpty()) { if (member.getPhone() != null && !member.getPhone().isEmpty()) {
@@ -257,12 +209,7 @@ public class MemberHandler {
} }
/** @Operation(summary = "查看会员列表", description = "后台管理员分页查看所有会员列表")
* 前台查看会员列表
*
* GET /api/admin/members/all?pageNum=1&pageSize=10
* header: { "Authorization": "Bearer xxx" }
*/
public Mono<ServerResponse> getAllMembers(ServerRequest request) { public Mono<ServerResponse> getAllMembers(ServerRequest request) {
Long adminId = authUtil.getMemberIdOrThrow(request); Long adminId = authUtil.getMemberIdOrThrow(request);
@@ -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.dto.WechatLoginDto;
import cn.novalon.gym.manage.member.service.WechatAuthService; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -20,20 +22,13 @@ import reactor.core.publisher.Mono;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "微信认证", description = "微信小程序登录、公众号回调等")
public class WechatAuthHandler { public class WechatAuthHandler {
private final WechatAuthService wechatAuthService; private final WechatAuthService wechatAuthService;
private final WechatOfficialEventHandler wechatOfficialEventHandler; private final WechatOfficialEventHandler wechatOfficialEventHandler;
/** @Operation(summary = "微信小程序登录", description = "通过微信小程序code获取session_key,完成会员登录或注册")
* 小程序更新
*
* POST /api/member/auth/miniapp/login
* Body: {"code": "wx_login_code"}
*
* @param request ServerRequest
* @return Mono<ServerResponse> 登录响应
*/
public Mono<ServerResponse> miniappLogin(ServerRequest request) { public Mono<ServerResponse> miniappLogin(ServerRequest request) {
log.info("收到小程序登录请求"); log.info("收到小程序登录请求");
@@ -50,18 +45,12 @@ public class WechatAuthHandler {
}); });
} }
/** @Operation(summary = "微信公众号回调", description = "处理微信公众号事件(关注、取消关注等)")
* 公众号回调
*
* POST /api/member/auth/mp/callback
* Body: <xml><Event>subscribe</Event><FromUserName>openid</FromUserName></xml>
*
*/
public Mono<ServerResponse> mpCallback(ServerRequest request) { public Mono<ServerResponse> mpCallback(ServerRequest request) {
return wechatOfficialEventHandler.handleEvent(request); return wechatOfficialEventHandler.handleEvent(request);
} }
// 验证微信公众号签名 @Operation(summary = "验证微信公众号签名", description = "微信公众号服务器验证,返回echostr")
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) { public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
return wechatOfficialEventHandler.verifySignature(request); return wechatOfficialEventHandler.verifySignature(request);
} }
@@ -40,9 +40,6 @@ public class WechatOfficialEventHandler {
.flatMap(xmlBody -> { .flatMap(xmlBody -> {
log.info("收到微信公众号事件 {}", xmlBody); log.info("收到微信公众号事件 {}", xmlBody);
// TODO: 将XML解析为WechatOfficialEventDto
// 目前简化处理直接获取openId和event
String openId = extractOpenId(xmlBody); String openId = extractOpenId(xmlBody);
String event = extractEvent(xmlBody); String event = extractEvent(xmlBody);
@@ -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.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService; 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.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@@ -16,17 +18,35 @@ import java.time.LocalDateTime;
* @author 付嘉 * @author 付嘉
* @date 2026-05-27 * @date 2026-05-27
*/ */
@Slf4j
@Service @Service
public class MemberCardRecordServiceImpl implements IMemberCardRecordService { public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
private final MemberCardRecordRepository memberCardRecordRepository; 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.memberCardRecordRepository = memberCardRecordRepository;
this.redisUtil = redisUtil;
} }
@Override @Override
public Mono<MemberCardRecord> findById(Long recordId) { 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 @Override
@@ -53,17 +73,32 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
@Override @Override
public Mono<Integer> deductUsage(Long recordId, Integer deductTimes, Double deductAmount) { 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 @Override
public Mono<Integer> renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) { 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 @Override
public Mono<Integer> updateStatus(Long recordId, String status) { 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 @Override
@@ -80,4 +115,10 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
public Flux<MemberCardRecord> findExpiredCards() { public Flux<MemberCardRecord> findExpiredCards() {
return memberCardRecordRepository.findExpiredCards(); return memberCardRecordRepository.findExpiredCards();
} }
private void clearRecordCache(Long recordId) {
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
redisUtil.delete(cacheKey);
log.debug("清除会员卡记录缓存, recordId: {}", recordId);
}
} }
@@ -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.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardService; import cn.novalon.gym.manage.member.service.IMemberCardService;
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
import cn.novalon.gym.manage.member.util.RedisUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -39,6 +40,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
private final DistributedLockService distributedLockService; private final DistributedLockService distributedLockService;
private final ExpirationReminderService expirationReminderService; private final ExpirationReminderService expirationReminderService;
private final RefundSagaHandler refundSagaHandler; 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, public MemberCardServiceImpl(MemberCardRepository memberCardRepository,
MemberCardRecordRepository recordRepository, MemberCardRecordRepository recordRepository,
@@ -46,7 +51,8 @@ public class MemberCardServiceImpl implements IMemberCardService {
MemberCardStateMachine stateMachine, MemberCardStateMachine stateMachine,
DistributedLockService distributedLockService, DistributedLockService distributedLockService,
ExpirationReminderService expirationReminderService, ExpirationReminderService expirationReminderService,
RefundSagaHandler refundSagaHandler) { RefundSagaHandler refundSagaHandler,
RedisUtil redisUtil) {
this.memberCardRepository = memberCardRepository; this.memberCardRepository = memberCardRepository;
this.recordRepository = recordRepository; this.recordRepository = recordRepository;
this.transactionService = transactionService; this.transactionService = transactionService;
@@ -54,11 +60,24 @@ public class MemberCardServiceImpl implements IMemberCardService {
this.distributedLockService = distributedLockService; this.distributedLockService = distributedLockService;
this.expirationReminderService = expirationReminderService; this.expirationReminderService = expirationReminderService;
this.refundSagaHandler = refundSagaHandler; this.refundSagaHandler = refundSagaHandler;
this.redisUtil = redisUtil;
} }
@Override @Override
public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { 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 @Override
@@ -95,7 +114,12 @@ public class MemberCardServiceImpl implements IMemberCardService {
@Override @Override
public Mono<MemberCard> save(MemberCard entity) { public Mono<MemberCard> save(MemberCard entity) {
return memberCardRepository.save(entity); return memberCardRepository.save(entity)
.doOnSuccess(saved -> {
if (saved.getMemberCardId() != null) {
clearCardCache(saved.getMemberCardId());
}
});
} }
@Override @Override
@@ -329,4 +353,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
return transactionService.createTransaction(transaction); return transactionService.createTransaction(transaction);
} }
private void clearCardCache(Long memberCardId) {
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
redisUtil.delete(cacheKey);
log.debug("清除会员卡缓存, memberCardId: {}", memberCardId);
}
} }
@@ -17,6 +17,7 @@ import cn.novalon.gym.manage.member.service.MemberService;
import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.BeanConvertUtil; import cn.novalon.gym.manage.member.util.BeanConvertUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils; 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.MemberCardInfoVO;
import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberDetailVO;
import cn.novalon.gym.manage.member.vo.MemberInfoVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO;
@@ -56,9 +57,14 @@ public class MemberServiceImpl implements MemberService {
private final IMemberRepository memberRepository; private final IMemberRepository memberRepository;
private final MemberESRepository memberESRepository; private final MemberESRepository memberESRepository;
private final EsSyncUtils esSyncUtils; private final EsSyncUtils esSyncUtils;
private final RedisUtil redisUtil;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer; 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 @PostConstruct
public void init() { public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
@@ -66,12 +72,23 @@ public class MemberServiceImpl implements MemberService {
@Override @Override
public Mono<MemberInfoVO> getMemberInfo(Long memberId) { public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
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) return memberRepository.findById(memberId)
.map(this::buildMemberInfoResponse) .map(this::buildMemberInfoResponse)
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.then(Mono.just(vo)))
.switchIfEmpty(Mono.error(() -> { .switchIfEmpty(Mono.error(() -> {
log.error("会员不存在: memberId={}", memberId); log.error("会员不存在: memberId={}", memberId);
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
})); }));
});
} }
@Override @Override
@@ -98,7 +115,11 @@ public class MemberServiceImpl implements MemberService {
return memberRepository.save(member); return memberRepository.save(member);
}) })
.doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> {
memberSyncer.sync(savedMember);
return clearMemberCache(memberId)
.then(Mono.just(savedMember));
})
.map(savedMember -> { .map(savedMember -> {
log.info("会员信息更新成功, memberId: {}", savedMember.getId()); log.info("会员信息更新成功, memberId: {}", savedMember.getId());
return buildMemberInfoResponse(savedMember); return buildMemberInfoResponse(savedMember);
@@ -160,9 +181,8 @@ public class MemberServiceImpl implements MemberService {
@Override @Override
public Flux<MemberES> searchMember(SearchMemberDto searchMemberDto) { public Flux<MemberES> searchMember(SearchMemberDto searchMemberDto) {
log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", log.info("搜索会员, searchValue: {}, pageNum: {}, pageSize: {}",
searchMemberDto.getSearchValue(), searchMemberDto.getSearchValue(),
searchMemberDto.getGender(),
searchMemberDto.getPageNum(), searchMemberDto.getPageNum(),
searchMemberDto.getPageSize()); searchMemberDto.getPageSize());
@@ -207,6 +227,14 @@ public class MemberServiceImpl implements MemberService {
public Mono<MemberDetailVO> getMemberDetail(Long memberId) { public Mono<MemberDetailVO> getMemberDetail(Long memberId) {
log.info("查询会员详情, memberId: {}", 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) return memberRepository.findById(memberId)
.zipWith( .zipWith(
memberRepository.findCardRecordsWithCardInfoByMemberId(memberId) memberRepository.findCardRecordsWithCardInfoByMemberId(memberId)
@@ -242,7 +270,10 @@ public class MemberServiceImpl implements MemberService {
return memberDetailVO; return memberDetailVO;
} }
); )
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.then(Mono.just(vo)));
});
} }
@@ -256,7 +287,6 @@ public class MemberServiceImpl implements MemberService {
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
})) }))
.flatMap(member -> { .flatMap(member -> {
log.error("有用户");
if (updateDto.getNickname() != null) { if (updateDto.getNickname() != null) {
member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname()));
} }
@@ -275,8 +305,11 @@ public class MemberServiceImpl implements MemberService {
return memberRepository.save(member); return memberRepository.save(member);
}) })
.doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> {
.map(savedMember -> true) memberSyncer.sync(savedMember);
return clearMemberCache(memberId)
.then(Mono.just(true));
})
.onErrorResume(e -> { .onErrorResume(e -> {
log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e); log.error("编辑会员信息失败, memberId: {}, error: {}", memberId, e.getMessage(), e);
return Mono.just(false); return Mono.just(false);
@@ -290,7 +323,11 @@ public class MemberServiceImpl implements MemberService {
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> {
memberSyncer.sync(savedMember);
return clearMemberCache(memberId)
.then(Mono.just(savedMember));
})
.map(savedMember -> { .map(savedMember -> {
log.info("手机号录入成功, memberId: {}", savedMember.getId()); log.info("手机号录入成功, memberId: {}", savedMember.getId());
return true; return true;
@@ -301,4 +338,12 @@ public class MemberServiceImpl implements MemberService {
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); 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));
}
} }
@@ -5,6 +5,7 @@ import cn.novalon.gym.manage.member.entity.RefundApplication;
import cn.novalon.gym.manage.member.enums.RefundStatus; import cn.novalon.gym.manage.member.enums.RefundStatus;
import cn.novalon.gym.manage.member.repository.RefundApplicationRepository; import cn.novalon.gym.manage.member.repository.RefundApplicationRepository;
import cn.novalon.gym.manage.member.service.IRefundApplicationService; import cn.novalon.gym.manage.member.service.IRefundApplicationService;
import cn.novalon.gym.manage.member.util.RedisUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -22,9 +23,14 @@ import java.time.LocalDateTime;
public class RefundApplicationServiceImpl implements IRefundApplicationService { public class RefundApplicationServiceImpl implements IRefundApplicationService {
private final RefundApplicationRepository refundApplicationRepository; 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.refundApplicationRepository = refundApplicationRepository;
this.redisUtil = redisUtil;
} }
@Override @Override
@@ -45,8 +51,10 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
.build(); .build();
return refundApplicationRepository.save(application) return refundApplicationRepository.save(application)
.doOnSuccess(app -> log.info("创建退款申请成功: applicationId={}, recordId={}", .doOnSuccess(app -> {
app.getId(), recordId)); log.info("创建退款申请成功: applicationId={}, recordId={}", app.getId(), recordId);
clearRefundCache(recordId);
});
})); }));
} }
@@ -55,14 +63,19 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
return refundApplicationRepository.findById(applicationId) return refundApplicationRepository.findById(applicationId)
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在"))) .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
.flatMap(application -> { .flatMap(application -> {
if (!"PENDING".equals(application.getStatus())) { if (application.getStatus() != RefundStatus.PENDING) {
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
} }
return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark)) return refundApplicationRepository.approve(applicationId, "APPROVED", auditorId, HtmlEscapeUtil.escape(remark))
.thenReturn(application) .flatMap(updatedRows -> {
.doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", if (updatedRows == 0) {
applicationId, auditorId)); return Mono.error(new RuntimeException("批准退款申请失败"));
}
clearRefundCache(application.getRecordId());
log.info("批准退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId);
return refundApplicationRepository.findById(applicationId);
});
}); });
} }
@@ -71,19 +84,42 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
return refundApplicationRepository.findById(applicationId) return refundApplicationRepository.findById(applicationId)
.switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在"))) .switchIfEmpty(Mono.error(new RuntimeException("退款申请不存在")))
.flatMap(application -> { .flatMap(application -> {
if (!"PENDING".equals(application.getStatus())) { if (application.getStatus() != RefundStatus.PENDING) {
return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus())); return Mono.error(new RuntimeException("退款申请状态不正确,当前状态: " + application.getStatus()));
} }
return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark)) return refundApplicationRepository.approve(applicationId, "REJECTED", auditorId, HtmlEscapeUtil.escape(remark))
.thenReturn(application) .flatMap(updatedRows -> {
.doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", if (updatedRows == 0) {
applicationId, auditorId)); return Mono.error(new RuntimeException("拒绝退款申请失败"));
}
clearRefundCache(application.getRecordId());
log.info("拒绝退款申请成功: applicationId={}, auditorId={}", applicationId, auditorId);
return refundApplicationRepository.findById(applicationId);
});
}); });
} }
@Override @Override
public Mono<RefundApplication> findByRecordId(Long recordId) { 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);
} }
} }
@@ -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.common.exception.SystemException;
import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.config.WechatProperties;
import cn.novalon.gym.manage.member.service.WechatApiService; import cn.novalon.gym.manage.member.service.WechatApiService;
import cn.novalon.gym.manage.member.util.RedisUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -30,6 +31,10 @@ import java.util.Map;
public class WechatApiServiceImpl implements WechatApiService { public class WechatApiServiceImpl implements WechatApiService {
private final WechatProperties wechatProperties; 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() private final WebClient webClient = WebClient.builder()
.baseUrl("https://api.weixin.qq.com") .baseUrl("https://api.weixin.qq.com")
@@ -147,6 +152,15 @@ public class WechatApiServiceImpl implements WechatApiService {
public Mono<String> getAccessToken(String appType) { public Mono<String> getAccessToken(String appType) {
log.debug("获取access_token, appType: {}", appType); log.debug("获取access_token, appType: {}", appType);
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType;
return redisUtil.get(cacheKey, String.class)
.flatMap(cachedToken -> {
if (cachedToken != null) {
log.debug("从缓存获取access_token, appType: {}", appType);
return Mono.just(cachedToken);
}
String appId, appSecret; String appId, appSecret;
if ("miniapp".equals(appType)) { if ("miniapp".equals(appType)) {
appId = wechatProperties.getMiniapp().getAppId(); appId = wechatProperties.getMiniapp().getAppId();
@@ -165,18 +179,20 @@ public class WechatApiServiceImpl implements WechatApiService {
.build()) .build())
.retrieve() .retrieve()
.bodyToMono(Map.class) .bodyToMono(Map.class)
.map(response -> { .flatMap(response -> {
if (response.containsKey("access_token")) { if (response.containsKey("access_token")) {
String accessToken = (String) response.get("access_token"); String accessToken = (String) response.get("access_token");
Integer expiresIn = (Integer) response.get("expires_in"); Integer expiresIn = (Integer) response.get("expires_in");
log.info("获取access_token成功, expires_in: {}s", expiresIn); log.info("获取access_token成功, expires_in: {}s", expiresIn);
return accessToken; return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
.then(Mono.just(accessToken));
} else { } else {
String errmsg = (String) response.get("errmsg"); String errmsg = (String) response.get("errmsg");
log.error("获取access_token失败: {}", errmsg); log.error("获取access_token失败: {}", errmsg);
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg); throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
} }
}); });
});
} }
@Override @Override
@@ -16,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.AesUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.EsSyncUtils;
import cn.novalon.gym.manage.member.util.MemberNoGenerator; 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.util.WechatPhoneUtil;
import cn.novalon.gym.manage.member.vo.WechatLoginVO; import cn.novalon.gym.manage.member.vo.WechatLoginVO;
import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
@@ -47,9 +48,13 @@ public class WechatAuthServiceImpl implements WechatAuthService {
private final MemberESRepository memberESRepository; private final MemberESRepository memberESRepository;
private final EsSyncUtils esSyncUtils; private final EsSyncUtils esSyncUtils;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final RedisUtil redisUtil;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer; 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 @PostConstruct
public void init() { public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
@@ -79,7 +84,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> { .flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response); return Mono.just(response);
@@ -89,7 +97,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> { .flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response); return Mono.just(response);
@@ -105,7 +116,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> { .flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response); return Mono.just(response);
@@ -124,7 +138,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> { .flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey); WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response); return Mono.just(response);
@@ -185,7 +202,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setPhone(encryptedPhone); member.setPhone(encryptedPhone);
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.map(savedMember -> { .map(savedMember -> {
log.info("更新会员手机号成功, memberId: {}", savedMember.getId()); log.info("更新会员手机号成功, memberId: {}", savedMember.getId());
return true; return true;
@@ -197,6 +217,12 @@ 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) { private String encryptPhone(String phoneNumber) {
try { try {
String encryptedPhone = AesUtil.encrypt(phoneNumber); String encryptedPhone = AesUtil.encrypt(phoneNumber);
@@ -8,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.repository.IMemberRepository;
import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.service.WechatOfficialService;
import cn.novalon.gym.manage.member.util.EsSyncUtils; 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 cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -41,11 +42,16 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
private final MemberESRepository memberESRepository; private final MemberESRepository memberESRepository;
private final ObjectMapper objectMapper = new ObjectMapper() private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final EsSyncUtils esSyncUtils; private final EsSyncUtils esSyncUtils;
private final RedisUtil redisUtil;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer; 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 @PostConstruct
public void init() { public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
@@ -83,16 +89,22 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
} }
return memberRepository.save(existingMember) return memberRepository.save(existingMember)
.doOnSuccess(memberSyncer::sync) .flatMap(saved -> {
memberSyncer.sync(saved);
return clearMemberCache(saved.getId())
.then(sendWelcomeMessage(openId)); .then(sendWelcomeMessage(openId));
});
} else { } else {
log.info("老用户关注服务号: memberId={}", existingMember.getId()); log.info("老用户关注服务号: memberId={}", existingMember.getId());
existingMember.setSubscribed(true); existingMember.setSubscribed(true);
existingMember.setLastLoginAt(LocalDateTime.now()); existingMember.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(existingMember) return memberRepository.save(existingMember)
.doOnSuccess(memberSyncer::sync) .flatMap(saved -> {
memberSyncer.sync(saved);
return clearMemberCache(saved.getId())
.then(sendWelcomeMessage(openId)); .then(sendWelcomeMessage(openId));
});
} }
}) })
.switchIfEmpty(Mono.defer(() -> { .switchIfEmpty(Mono.defer(() -> {
@@ -105,7 +117,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
existingMember.setLastLoginAt(LocalDateTime.now()); existingMember.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(existingMember) return memberRepository.save(existingMember)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.then(sendWelcomeMessage(openId)); .then(sendWelcomeMessage(openId));
}) })
.switchIfEmpty(Mono.defer(() -> { .switchIfEmpty(Mono.defer(() -> {
@@ -123,7 +138,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
existingMember.setLastLoginAt(LocalDateTime.now()); existingMember.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(existingMember) return memberRepository.save(existingMember)
.doOnSuccess(memberSyncer::sync) .doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.then(sendWelcomeMessage(openId)); .then(sendWelcomeMessage(openId));
}) })
.switchIfEmpty(Mono.defer(() -> { .switchIfEmpty(Mono.defer(() -> {
@@ -149,8 +167,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
member.setSubscribed(false); member.setSubscribed(false);
member.setLastLoginAt(LocalDateTime.now()); member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .flatMap(saved -> {
.then(); memberSyncer.sync(saved);
return clearMemberCache(saved.getId()).then();
});
}) })
.then() .then()
.switchIfEmpty(Mono.defer(() -> { .switchIfEmpty(Mono.defer(() -> {
@@ -214,7 +234,11 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
member.setOfficialOpenId(officialOpenId); member.setOfficialOpenId(officialOpenId);
} }
return memberRepository.save(member) return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync) .flatMap(saved -> {
memberSyncer.sync(saved);
return clearMemberCache(saved.getId())
.then(Mono.just(saved));
})
.map(savedMember -> { .map(savedMember -> {
log.info("关联成功, memberId: {}", savedMember.getId()); log.info("关联成功, memberId: {}", savedMember.getId());
return true; return true;
@@ -269,10 +293,17 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
/** /**
* 获取微信AccessToken * 获取微信AccessToken
*
* TODO: 应该使用缓存,避免频繁请求
*/ */
private Mono<String> getAccessToken() { private Mono<String> getAccessToken() {
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp";
return redisUtil.get(cacheKey, String.class)
.flatMap(cachedToken -> {
if (cachedToken != null) {
log.debug("从缓存获取服务号access_token");
return Mono.just(cachedToken);
}
String appId = wechatProperties.getMp().getAppId(); String appId = wechatProperties.getMp().getAppId();
String appSecret = wechatProperties.getMp().getAppSecret(); String appSecret = wechatProperties.getMp().getAppSecret();
@@ -286,11 +317,14 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
.retrieve() .retrieve()
.bodyToMono(Map.class) .bodyToMono(Map.class)
.map(response -> { .flatMap(response -> {
if (response.containsKey("errcode")) { if (response.containsKey("errcode")) {
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg")); throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
} }
return (String) response.get("access_token"); String accessToken = (String) response.get("access_token");
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
.then(Mono.just(accessToken));
});
}); });
} }
@@ -336,4 +370,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
return Mono.empty(); // 即使发送失败也不影响主流程 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));
}
} }
@@ -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));
}
}
@@ -2,21 +2,23 @@
wechat: wechat:
# Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境) # Mock模式:true=使用模拟数据(开发测试),false=调用真实微信API(生产环境)
mock-enabled: false mock-enabled: false
miniapp: miniapp:
app-id: wx4d480112b426100b app-id: ${WECHAT_MINIAPP_APP_ID}
app-secret: 78548f0c0ff66c73d3e8b071897eb1e5 app-secret: ${WECHAT_MINIAPP_SECRET}
mp: mp:
app-id: wx6f138c9aacc8a0e8 app-id: ${WECHAT_MP_APP_ID}
app-secret: 5df2e315e9268e96a43bb2cce1d2270b app-secret: ${WECHAT_MP_SECRET}
token: test_token token: ${WECHAT_MP_TOKEN}
aes-key: ${WECHAT_MP_AESKEY:test_aes_key} aes-key: ${WECHAT_MP_AESKEY}
# 服务器回调地址(微信服务器推送事件的URL callback-url: ${WECHAT_MP_CALLBACK_URL}
callback-url: https://1me240209tk74.vicp.fun/api/member/auth/mp/callback
# 手机号加密配置 # 手机号加密配置
phone-encryption: phone-encryption:
secret-key: nVnA99iBfyK0IE6SkcUYdVAaVrezyn2sLRdLfkIyWnY= secret-key: ${PHONE_ENCRYPTION_SECRET_KEY}
iv: LMpG6Ih9mmfEAALOCeIJBw== iv: ${PHONE_ENCRYPTION_IV}
spring: spring:
elasticsearch: elasticsearch:
uris: http://localhost:9200 # ES 服务器地址(支持多个,逗号分隔) uris: http://localhost:9200
+4
View File
@@ -139,6 +139,10 @@
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId> <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>