添加redis,并把敏感信息改为环境变量存储

This commit was merged in pull request #7.
This commit is contained in:
future
2026-05-29 22:27:04 +08:00
parent 29b73c1f67
commit 174e33053e
15 changed files with 508 additions and 228 deletions
@@ -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 Integer gender;
// 页码
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.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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> getAllMembers(ServerRequest 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.service.WechatAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@@ -20,20 +22,13 @@ import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
@Tag(name = "微信认证", description = "微信小程序登录、公众号回调等")
public class WechatAuthHandler {
private final WechatAuthService wechatAuthService;
private final WechatOfficialEventHandler wechatOfficialEventHandler;
/**
* 小程序更新
*
* POST /api/member/auth/miniapp/login
* Body: {"code": "wx_login_code"}
*
* @param request ServerRequest
* @return Mono<ServerResponse> 登录响应
*/
@Operation(summary = "微信小程序登录", description = "通过微信小程序code获取session_key,完成会员登录或注册")
public Mono<ServerResponse> miniappLogin(ServerRequest request) {
log.info("收到小程序登录请求");
@@ -50,18 +45,12 @@ public class WechatAuthHandler {
});
}
/**
* 公众号回调
*
* POST /api/member/auth/mp/callback
* Body: <xml><Event>subscribe</Event><FromUserName>openid</FromUserName></xml>
*
*/
@Operation(summary = "微信公众号回调", description = "处理微信公众号事件(关注、取消关注等)")
public Mono<ServerResponse> mpCallback(ServerRequest request) {
return wechatOfficialEventHandler.handleEvent(request);
}
// 验证微信公众号签名
@Operation(summary = "验证微信公众号签名", description = "微信公众号服务器验证,返回echostr")
public Mono<ServerResponse> verifyMpSignature(ServerRequest request) {
return wechatOfficialEventHandler.verifySignature(request);
}
@@ -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);
@@ -3,6 +3,8 @@ package cn.novalon.gym.manage.member.service.impl;
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
import cn.novalon.gym.manage.member.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@@ -16,17 +18,35 @@ import java.time.LocalDateTime;
* @author 付嘉
* @date 2026-05-27
*/
@Slf4j
@Service
public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
private final MemberCardRecordRepository memberCardRecordRepository;
private final RedisUtil redisUtil;
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository) {
private static final String MEMBER_CARD_RECORD_CACHE_PREFIX = "member:card:record:";
private static final long CACHE_EXPIRE_SECONDS = 300;
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository, RedisUtil redisUtil) {
this.memberCardRecordRepository = memberCardRecordRepository;
this.redisUtil = redisUtil;
}
@Override
public Mono<MemberCardRecord> findById(Long recordId) {
return memberCardRecordRepository.findById(recordId);
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
Object cached = redisUtil.get(cacheKey);
if (cached != null && cached instanceof MemberCardRecord) {
log.debug("从缓存获取会员卡记录, recordId: {}", recordId);
return Mono.just((MemberCardRecord) cached);
}
return memberCardRecordRepository.findById(recordId)
.doOnSuccess(record -> {
if (record != null) {
redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS);
}
});
}
@Override
@@ -53,17 +73,32 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
@Override
public Mono<Integer> deductUsage(Long recordId, Integer deductTimes, Double deductAmount) {
return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount);
return memberCardRecordRepository.deductUsage(recordId, deductTimes, deductAmount)
.doOnSuccess(updated -> {
if (updated > 0) {
clearRecordCache(recordId);
}
});
}
@Override
public Mono<Integer> renewCard(Long recordId, Integer addTimes, Double addAmount, LocalDateTime newExpireTime) {
return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime);
return memberCardRecordRepository.renewCard(recordId, addTimes, addAmount, newExpireTime)
.doOnSuccess(updated -> {
if (updated > 0) {
clearRecordCache(recordId);
}
});
}
@Override
public Mono<Integer> updateStatus(Long recordId, String status) {
return memberCardRecordRepository.updateStatus(recordId, status);
return memberCardRecordRepository.updateStatus(recordId, status)
.doOnSuccess(updated -> {
if (updated > 0) {
clearRecordCache(recordId);
}
});
}
@Override
@@ -80,4 +115,10 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
public Flux<MemberCardRecord> findExpiredCards() {
return memberCardRecordRepository.findExpiredCards();
}
private void clearRecordCache(Long recordId) {
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
redisUtil.delete(cacheKey);
log.debug("清除会员卡记录缓存, recordId: {}", recordId);
}
}
@@ -15,6 +15,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardService;
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
import cn.novalon.gym.manage.member.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -39,6 +40,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
private final DistributedLockService distributedLockService;
private final ExpirationReminderService expirationReminderService;
private final RefundSagaHandler refundSagaHandler;
private final RedisUtil redisUtil;
private static final String MEMBER_CARD_CACHE_PREFIX = "member:card:";
private static final long CACHE_EXPIRE_SECONDS = 300;
public MemberCardServiceImpl(MemberCardRepository memberCardRepository,
MemberCardRecordRepository recordRepository,
@@ -46,7 +51,8 @@ public class MemberCardServiceImpl implements IMemberCardService {
MemberCardStateMachine stateMachine,
DistributedLockService distributedLockService,
ExpirationReminderService expirationReminderService,
RefundSagaHandler refundSagaHandler) {
RefundSagaHandler refundSagaHandler,
RedisUtil redisUtil) {
this.memberCardRepository = memberCardRepository;
this.recordRepository = recordRepository;
this.transactionService = transactionService;
@@ -54,11 +60,24 @@ public class MemberCardServiceImpl implements IMemberCardService {
this.distributedLockService = distributedLockService;
this.expirationReminderService = expirationReminderService;
this.refundSagaHandler = refundSagaHandler;
this.redisUtil = redisUtil;
}
@Override
public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) {
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId);
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
Object cached = redisUtil.get(cacheKey);
if (cached != null && cached instanceof MemberCard) {
log.debug("从缓存获取会员卡信息, memberCardId: {}", memberCardId);
return Mono.just((MemberCard) cached);
}
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId)
.doOnSuccess(card -> {
if (card != null) {
redisUtil.setWithExpire(cacheKey, card, CACHE_EXPIRE_SECONDS);
}
});
}
@Override
@@ -95,7 +114,12 @@ public class MemberCardServiceImpl implements IMemberCardService {
@Override
public Mono<MemberCard> save(MemberCard entity) {
return memberCardRepository.save(entity);
return memberCardRepository.save(entity)
.doOnSuccess(saved -> {
if (saved.getMemberCardId() != null) {
clearCardCache(saved.getMemberCardId());
}
});
}
@Override
@@ -329,4 +353,10 @@ public class MemberCardServiceImpl implements IMemberCardService {
return transactionService.createTransaction(transaction);
}
private void clearCardCache(Long memberCardId) {
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
redisUtil.delete(cacheKey);
log.debug("清除会员卡缓存, memberCardId: {}", memberCardId);
}
}
@@ -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<Member, MemberES, String> memberSyncer;
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
private static final String MEMBER_DETAIL_CACHE_PREFIX = "member:detail:";
private static final long CACHE_EXPIRE_SECONDS = 300;
@PostConstruct
public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
@@ -66,12 +72,23 @@ public class MemberServiceImpl implements MemberService {
@Override
public Mono<MemberInfoVO> getMemberInfo(Long memberId) {
return memberRepository.findById(memberId)
.map(this::buildMemberInfoResponse)
.switchIfEmpty(Mono.error(() -> {
log.error("会员不存在: memberId={}", memberId);
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
}));
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
return redisUtil.get(cacheKey, MemberInfoVO.class)
.flatMap(cached -> {
if (cached != null) {
log.debug("从缓存获取会员信息, memberId: {}", memberId);
return Mono.just(cached);
}
return memberRepository.findById(memberId)
.map(this::buildMemberInfoResponse)
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.then(Mono.just(vo)))
.switchIfEmpty(Mono.error(() -> {
log.error("会员不存在: memberId={}", memberId);
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
}));
});
}
@Override
@@ -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<MemberES> 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<MemberDetailVO> 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<MemberCardInfoVO> enrichedCards = cardList.stream()
.peek(vo -> {
if (vo.getMemberCardType() != null) {
try {
MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType());
vo.setMemberCardTypeDesc(cardType.getDesc());
} catch (IllegalArgumentException e) {
vo.setMemberCardTypeDesc(vo.getMemberCardType());
}
}
if (vo.getMemberCardStatus() != null) {
vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架");
}
})
.collect(Collectors.toList());
memberDetailVO.setMemberCards(enrichedCards);
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<MemberCardInfoVO> enrichedCards = cardList.stream()
.peek(vo -> {
if (vo.getMemberCardType() != null) {
try {
MemberCardType cardType = MemberCardType.valueOf(vo.getMemberCardType());
vo.setMemberCardTypeDesc(cardType.getDesc());
} catch (IllegalArgumentException e) {
vo.setMemberCardTypeDesc(vo.getMemberCardType());
}
}
if (vo.getMemberCardStatus() != null) {
vo.setMemberCardStatusDesc(vo.getMemberCardStatus() == 1 ? "上架" : "下架");
}
})
.collect(Collectors.toList());
memberDetailVO.setMemberCards(enrichedCards);
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<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.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<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.member.config.WechatProperties;
import cn.novalon.gym.manage.member.service.WechatApiService;
import cn.novalon.gym.manage.member.util.RedisUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@@ -30,6 +31,10 @@ import java.util.Map;
public class WechatApiServiceImpl implements WechatApiService {
private final WechatProperties wechatProperties;
private final RedisUtil redisUtil;
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; // 比官方过期时间短100秒
private final WebClient webClient = WebClient.builder()
.baseUrl("https://api.weixin.qq.com")
@@ -147,35 +152,46 @@ public class WechatApiServiceImpl implements WechatApiService {
public Mono<String> getAccessToken(String appType) {
log.debug("获取access_token, appType: {}", appType);
String appId, appSecret;
if ("miniapp".equals(appType)) {
appId = wechatProperties.getMiniapp().getAppId();
appSecret = wechatProperties.getMiniapp().getAppSecret();
} else {
appId = wechatProperties.getMp().getAppId();
appSecret = wechatProperties.getMp().getAppSecret();
}
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType;
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/cgi-bin/token")
.queryParam("grant_type", "client_credential")
.queryParam("appid", appId)
.queryParam("secret", appSecret)
.build())
.retrieve()
.bodyToMono(Map.class)
.map(response -> {
if (response.containsKey("access_token")) {
String accessToken = (String) response.get("access_token");
Integer expiresIn = (Integer) response.get("expires_in");
log.info("获取access_token成功, expires_in: {}s", expiresIn);
return accessToken;
} else {
String errmsg = (String) response.get("errmsg");
log.error("获取access_token失败: {}", errmsg);
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
return redisUtil.get(cacheKey, String.class)
.flatMap(cachedToken -> {
if (cachedToken != null) {
log.debug("从缓存获取access_token, appType: {}", appType);
return Mono.just(cachedToken);
}
String appId, appSecret;
if ("miniapp".equals(appType)) {
appId = wechatProperties.getMiniapp().getAppId();
appSecret = wechatProperties.getMiniapp().getAppSecret();
} else {
appId = wechatProperties.getMp().getAppId();
appSecret = wechatProperties.getMp().getAppSecret();
}
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/cgi-bin/token")
.queryParam("grant_type", "client_credential")
.queryParam("appid", appId)
.queryParam("secret", appSecret)
.build())
.retrieve()
.bodyToMono(Map.class)
.flatMap(response -> {
if (response.containsKey("access_token")) {
String accessToken = (String) response.get("access_token");
Integer expiresIn = (Integer) response.get("expires_in");
log.info("获取access_token成功, expires_in: {}s", expiresIn);
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
.then(Mono.just(accessToken));
} else {
String errmsg = (String) response.get("errmsg");
log.error("获取access_token失败: {}", errmsg);
throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg);
}
});
});
}
@@ -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<Member, MemberES, String> memberSyncer;
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
private static final long CACHE_EXPIRE_SECONDS = 300;
@PostConstruct
public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
@@ -79,7 +84,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response);
@@ -89,7 +97,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response);
@@ -105,7 +116,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response);
@@ -124,7 +138,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.flatMap(savedMember -> {
WechatLoginVO response = buildLoginResponse(savedMember, false, sessionKey);
return Mono.just(response);
@@ -185,7 +202,10 @@ public class WechatAuthServiceImpl implements WechatAuthService {
member.setPhone(encryptedPhone);
member.setLastLoginAt(LocalDateTime.now());
return memberRepository.save(member)
.doOnSuccess(memberSyncer::sync)
.doOnSuccess(saved -> {
memberSyncer.sync(saved);
clearMemberCache(saved.getId());
})
.map(savedMember -> {
log.info("更新会员手机号成功, memberId: {}", savedMember.getId());
return true;
@@ -197,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);
@@ -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<Member, MemberES, String> memberSyncer;
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
private static final String MEMBER_INFO_CACHE_PREFIX = "member:info:";
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000;
private static final long CACHE_EXPIRE_SECONDS = 300;
@PostConstruct
public void init() {
this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository);
@@ -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<String> getAccessToken() {
String appId = wechatProperties.getMp().getAppId();
String appSecret = wechatProperties.getMp().getAppSecret();
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp";
String url = "https://api.weixin.qq.com/cgi-bin/token"
+ "?grant_type=client_credential"
+ "&appid=" + appId
+ "&secret=" + appSecret;
return webClient.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Map.class)
.map(response -> {
if (response.containsKey("errcode")) {
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
return redisUtil.get(cacheKey, String.class)
.flatMap(cachedToken -> {
if (cachedToken != null) {
log.debug("从缓存获取服务号access_token");
return Mono.just(cachedToken);
}
return (String) response.get("access_token");
String appId = wechatProperties.getMp().getAppId();
String appSecret = wechatProperties.getMp().getAppSecret();
String url = "https://api.weixin.qq.com/cgi-bin/token"
+ "?grant_type=client_credential"
+ "&appid=" + appId
+ "&secret=" + appSecret;
return webClient.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Map.class)
.flatMap(response -> {
if (response.containsKey("errcode")) {
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
}
String accessToken = (String) response.get("access_token");
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
.then(Mono.just(accessToken));
});
});
}
@@ -336,4 +370,10 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
return Mono.empty(); // 即使发送失败也不影响主流程
});
}
private Mono<Long> clearMemberCache(Long memberId) {
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
return redisUtil.delete(cacheKey)
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
}
}
@@ -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:
# 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
+4
View File
@@ -139,6 +139,10 @@
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>