From 174e33053e1a0c5e0afd269462c524358ccdddda Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Fri, 29 May 2026 22:27:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0redis=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=8A=8A=E6=95=8F=E6=84=9F=E4=BF=A1=E6=81=AF=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gym/manage/member/config/RedisConfig.java | 38 +++++ .../manage/member/dto/SearchMemberDto.java | 3 - .../manage/member/handler/MemberHandler.java | 83 ++--------- .../member/handler/WechatAuthHandler.java | 23 +-- .../handler/WechatOfficialEventHandler.java | 3 - .../impl/MemberCardRecordServiceImpl.java | 51 ++++++- .../service/impl/MemberCardServiceImpl.java | 36 ++++- .../service/impl/MemberServiceImpl.java | 137 ++++++++++++------ .../impl/RefundApplicationServiceImpl.java | 60 ++++++-- .../service/impl/WechatApiServiceImpl.java | 70 +++++---- .../service/impl/WechatAuthServiceImpl.java | 36 ++++- .../impl/WechatOfficialServiceImpl.java | 96 ++++++++---- .../gym/manage/member/util/RedisUtil.java | 72 +++++++++ .../src/main/resources/member-config.yml | 24 +-- gym-manage-api/manage-app/pom.xml | 4 + 15 files changed, 508 insertions(+), 228 deletions(-) create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java create mode 100644 gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java new file mode 100644 index 0000000..7321879 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java @@ -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 reactiveRedisTemplate( + ReactiveRedisConnectionFactory connectionFactory) { + + // 配置序列化上下文 + RedisSerializationContext serializationContext = + RedisSerializationContext.newSerializationContext() + .key(StringRedisSerializer.UTF_8) + .value(new GenericJackson2JsonRedisSerializer()) + .hashKey(StringRedisSerializer.UTF_8) + .hashValue(new GenericJackson2JsonRedisSerializer()) + .build(); + + return new ReactiveRedisTemplate<>(connectionFactory, serializationContext); + } +} diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java index 8b66ee0..38ac180 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/dto/SearchMemberDto.java @@ -12,9 +12,6 @@ public class SearchMemberDto { // 搜索字段 - 包括 会员号、昵称、手机号 private String searchValue; - // 性别排序 - private Integer gender; - // 页码 private Integer pageNum = 1; diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 6d1206b..baa75f7 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -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.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; @@ -30,6 +32,7 @@ import reactor.core.publisher.Mono; @Slf4j @Component @RequiredArgsConstructor +@Tag(name = "会员管理", description = "会员信息管理、微信绑定、服务号关注等") public class MemberHandler { private final MemberService memberService; @@ -37,12 +40,7 @@ public class MemberHandler { private final WechatOfficialService wechatOfficialService; private final AuthUtil authUtil; - /** - * 获取会员信息 - * - * GET /api/member/info - * header: { "Authorization": "Bearer xxx" } - */ + @Operation(summary = "获取会员信息", description = "根据当前登录用户获取会员基本信息") public Mono getMemberInfo(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -55,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 updateMemberInfo(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -81,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 bindPhone(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -102,12 +84,7 @@ public class MemberHandler { .bodyValue(success)); } - /** - * 查询服务号关注状态 - * - * GET /api/member/subscribe/status - * - */ + @Operation(summary = "查询服务号关注状态", description = "查询会员是否关注微信服务号") public Mono checkSubscribeStatus(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); @@ -122,14 +99,7 @@ public class MemberHandler { }); } - /** - * 管理员更新手机号 - * - * POST /api/admin/member/123/phone - * header: { "Authorization": "Bearer xxx" } - * Body: { "phone": "13800138000" } - * - */ + @Operation(summary = "管理员更新手机号", description = "后台管理员为会员更新手机号") public Mono adminUpdatePhone(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); @@ -161,13 +131,7 @@ public class MemberHandler { }); } - /** - * 前台查看会员信息 - * - * GET /api/admin/member/{id} - * header: { "Authorization": "xxx" } - * - */ + @Operation(summary = "管理员查看会员详情", description = "后台管理员查看指定会员的详细信息") public Mono adminGetMemberInfo(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); @@ -195,13 +159,7 @@ public class MemberHandler { }); } - /** - * 前台编辑会员信息 - * - * PUT /api/admin/member/{id} - * header: { "Authorization": "xxx" } - * Body:{"字段","值"} - */ + @Operation(summary = "管理员编辑会员信息", description = "后台管理员编辑会员信息") public Mono adminUpdateMemberInfo(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); @@ -220,25 +178,19 @@ public class MemberHandler { .bodyValue(detail)); } - /** - * 前台搜索会员列表 - * - * GET /api/admin/members?searchValue=手机号/姓名/会员号&filter=男/女&pageNum=1&pageSize=10 - * header: { "Authorization": "Bearer xxx" } - */ + @Operation(summary = "搜索会员列表", description = "后台管理员按关键词搜索会员,支持性别筛选和分页") public Mono searchMembers(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); 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 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()) { @@ -257,12 +209,7 @@ public class MemberHandler { } - /** - * 前台查看会员列表 - * - * GET /api/admin/members/all?pageNum=1&pageSize=10 - * header: { "Authorization": "Bearer xxx" } - */ + @Operation(summary = "查看会员列表", description = "后台管理员分页查看所有会员列表") public Mono getAllMembers(ServerRequest request) { Long adminId = authUtil.getMemberIdOrThrow(request); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java index 13cfd53..290a277 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatAuthHandler.java @@ -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 登录响应 - */ + @Operation(summary = "微信小程序登录", description = "通过微信小程序code获取session_key,完成会员登录或注册") public Mono miniappLogin(ServerRequest request) { log.info("收到小程序登录请求"); @@ -50,18 +45,12 @@ public class WechatAuthHandler { }); } - /** - * 公众号回调 - * - * POST /api/member/auth/mp/callback - * Body: subscribeopenid - * - */ + @Operation(summary = "微信公众号回调", description = "处理微信公众号事件(关注、取消关注等)") public Mono mpCallback(ServerRequest request) { return wechatOfficialEventHandler.handleEvent(request); } - // 验证微信公众号签名 + @Operation(summary = "验证微信公众号签名", description = "微信公众号服务器验证,返回echostr") public Mono verifyMpSignature(ServerRequest request) { return wechatOfficialEventHandler.verifySignature(request); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java index d01e989..042cb7e 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/WechatOfficialEventHandler.java @@ -39,9 +39,6 @@ public class WechatOfficialEventHandler { return request.bodyToMono(String.class) .flatMap(xmlBody -> { log.info("收到微信公众号事件 {}", xmlBody); - - // TODO: 将XML解析为WechatOfficialEventDto - // 目前简化处理直接获取openId和event String openId = extractOpenId(xmlBody); String event = extractEvent(xmlBody); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java index ce477f2..0cbbd76 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java @@ -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 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 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 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 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 findExpiredCards() { return memberCardRecordRepository.findExpiredCards(); } + + private void clearRecordCache(Long recordId) { + String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId; + redisUtil.delete(cacheKey); + log.debug("清除会员卡记录缓存, recordId: {}", recordId); + } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java index e7831ea..87e345b 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java @@ -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 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 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); + } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index e63e79d..cecdaed 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -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.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; @@ -56,9 +57,14 @@ public class MemberServiceImpl implements MemberService { private final IMemberRepository memberRepository; private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; + private final RedisUtil redisUtil; private EsSyncUtils.EntitySyncer 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); @@ -66,12 +72,23 @@ public class MemberServiceImpl implements MemberService { @Override public Mono 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 @@ -98,7 +115,11 @@ public class MemberServiceImpl implements MemberService { 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); @@ -160,15 +181,14 @@ public class MemberServiceImpl implements MemberService { @Override public Flux searchMember(SearchMemberDto searchMemberDto) { - log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", + log.info("搜索会员, searchValue: {}, pageNum: {}, pageSize: {}", searchMemberDto.getSearchValue(), - searchMemberDto.getGender(), 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("搜索值为手机号格式,进行加密处理"); searchValue = AesUtil.encrypt(searchValue); } @@ -207,42 +227,53 @@ public class MemberServiceImpl implements MemberService { public Mono getMemberDetail(Long memberId) { log.info("查询会员详情, memberId: {}", memberId); - return memberRepository.findById(memberId) - .zipWith( - memberRepository.findCardRecordsWithCardInfoByMemberId(memberId) - .collectList(), - (baseInfo, cardList) -> { - MemberDetailVO memberDetailVO = BeanConvertUtil.toBean(baseInfo, MemberDetailVO.class); + String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId; - GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender()); - memberDetailVO.setGenderDesc(genderEnum.getDesc()); + 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); - List 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); + GenderEnum genderEnum = GenderEnum.fromCode(baseInfo.getGender()); + memberDetailVO.setGenderDesc(genderEnum.getDesc()); - long activeCount = enrichedCards.stream() - .filter(card -> card.getMemberCardStatus() != null && card.getMemberCardStatus() == 1) - .count(); - memberDetailVO.setActiveCardCount((int) activeCount); - memberDetailVO.setInactiveCardCount(enrichedCards.size() - (int) activeCount); + List 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); - return memberDetailVO; - } - ); + 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))); + }); } @@ -256,7 +287,6 @@ public class MemberServiceImpl implements MemberService { throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })) .flatMap(member -> { - log.error("有用户"); if (updateDto.getNickname() != null) { member.setNickname(HtmlEscapeUtil.escape(updateDto.getNickname())); } @@ -275,8 +305,11 @@ public class MemberServiceImpl implements MemberService { return memberRepository.save(member); }) - .doOnSuccess(memberSyncer::sync) - .map(savedMember -> true) + .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); @@ -290,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; @@ -301,4 +338,12 @@ public class MemberServiceImpl implements MemberService { throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } + + private Mono 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)); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java index f492e78..0e9da72 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java @@ -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.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; @@ -22,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 @@ -45,8 +51,10 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService { .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); + }); })); } @@ -55,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, HtmlEscapeUtil.escape(remark)) - .thenReturn(application) - .doOnSuccess(app -> log.info("批准退款申请成功: applicationId={}, auditorId={}", - applicationId, auditorId)); + .flatMap(updatedRows -> { + if (updatedRows == 0) { + 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) .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, HtmlEscapeUtil.escape(remark)) - .thenReturn(application) - .doOnSuccess(app -> log.info("拒绝退款申请成功: applicationId={}, auditorId={}", - applicationId, auditorId)); + .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 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); } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java index f760ae4..aac7753 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -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 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); + } + }); }); } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 9d567d0..a462a14 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -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.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; @@ -47,9 +48,13 @@ public class WechatAuthServiceImpl implements WechatAuthService { private final MemberESRepository memberESRepository; private final EsSyncUtils esSyncUtils; private final JwtTokenProvider jwtTokenProvider; + private final RedisUtil redisUtil; private EsSyncUtils.EntitySyncer 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,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) { try { String encryptedPhone = AesUtil.encrypt(phoneNumber); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index 5143d9b..22a2d56 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -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.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; @@ -41,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 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); @@ -83,16 +89,22 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { } 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(() -> { @@ -105,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(() -> { @@ -123,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(() -> { @@ -149,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(() -> { @@ -214,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; @@ -269,28 +293,38 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { /** * 获取微信AccessToken - * - * TODO: 应该使用缓存,避免频繁请求 */ private Mono 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)); + }); }); } @@ -336,4 +370,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { return Mono.empty(); // 即使发送失败也不影响主流程 }); } + + private Mono clearMemberCache(Long memberId) { + String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; + return redisUtil.delete(cacheKey) + .doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId)); + } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java new file mode 100644 index 0000000..ea49f50 --- /dev/null +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java @@ -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 reactiveRedisTemplate; + + /** + * 设置值 + */ + public Mono set(String key, Object value) { + return reactiveRedisTemplate.opsForValue().set(key, value); + } + + /** + * 设置值并指定过期时间(秒) + */ + public Mono setWithExpire(String key, Object value, long timeoutSeconds) { + return reactiveRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds)); + } + + /** + * 获取值 + */ + @SuppressWarnings("unchecked") + public Mono get(String key, Class clazz) { + return reactiveRedisTemplate.opsForValue().get(key) + .map(obj -> clazz.isInstance(obj) ? (T) obj : null); + } + + /** + * 获取值(返回 Object) + */ + public Mono get(String key) { + return reactiveRedisTemplate.opsForValue().get(key); + } + + /** + * 删除key + */ + public Mono delete(String key) { + return reactiveRedisTemplate.delete(key); + } + + /** + * 判断key是否存在 + */ + public Mono hasKey(String key) { + return reactiveRedisTemplate.hasKey(key); + } + + /** + * 设置过期时间(秒) + */ + public Mono expire(String key, long timeoutSeconds) { + return reactiveRedisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds)); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/resources/member-config.yml b/gym-manage-api/gym-member/src/main/resources/member-config.yml index 8c457f6..a76b243 100644 --- a/gym-manage-api/gym-member/src/main/resources/member-config.yml +++ b/gym-manage-api/gym-member/src/main/resources/member-config.yml @@ -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 \ No newline at end of file diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index fb5f6f4..2644308 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -139,6 +139,10 @@ org.springdoc springdoc-openapi-starter-webflux-ui + + org.springframework.boot + spring-boot-starter-data-redis +