完成部分功能的后台管理 #47
+11
-9
@@ -155,8 +155,8 @@ public class MemberHandler {
|
||||
String decryptedPhone = AesUtil.decrypt(detail.getPhone());
|
||||
detail.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", detail.getId(), e);
|
||||
detail.setPhone(null);
|
||||
log.warn("手机号解密失败(可能为明文存储), memberId: {}", detail.getId());
|
||||
detail.setPhone(WechatPhoneUtil.maskPhone(detail.getPhone()));
|
||||
}
|
||||
}
|
||||
return ServerResponse.ok()
|
||||
@@ -204,8 +204,8 @@ public class MemberHandler {
|
||||
String decryptedPhone = AesUtil.decrypt(member.getPhone());
|
||||
member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||
member.setPhone(null);
|
||||
log.warn("手机号解密失败(可能为明文存储), memberId: {}", member.getId());
|
||||
member.setPhone(WechatPhoneUtil.maskPhone(member.getPhone()));
|
||||
}
|
||||
}
|
||||
return member;
|
||||
@@ -222,11 +222,13 @@ public class MemberHandler {
|
||||
|
||||
int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1);
|
||||
int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10);
|
||||
String sortField = request.queryParam("sortField").orElse(null);
|
||||
String sortOrder = request.queryParam("sortOrder").orElse(null);
|
||||
|
||||
log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize);
|
||||
// TODO: 补充签到记录
|
||||
log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}, sortField: {}, sortOrder: {}",
|
||||
adminId, pageNum, pageSize, sortField, sortOrder);
|
||||
|
||||
return memberService.findAll(pageNum, pageSize)
|
||||
return memberService.findAll(pageNum, pageSize, sortField, sortOrder)
|
||||
.map(member -> {
|
||||
// 解密手机号
|
||||
if (member.getPhone() != null && !member.getPhone().isEmpty()) {
|
||||
@@ -234,8 +236,8 @@ public class MemberHandler {
|
||||
String decryptedPhone = AesUtil.decrypt(member.getPhone());
|
||||
member.setPhone(WechatPhoneUtil.maskPhone(decryptedPhone));
|
||||
} catch (Exception e) {
|
||||
log.error("手机号解密失败, memberId: {}", member.getId(), e);
|
||||
member.setPhone(null);
|
||||
log.warn("手机号解密失败(可能为明文存储), memberId: {}", member.getId());
|
||||
member.setPhone(WechatPhoneUtil.maskPhone(member.getPhone()));
|
||||
}
|
||||
}
|
||||
return member;
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ public interface MemberService {
|
||||
* @param pageSize 页大小
|
||||
* @return 所有会员信息
|
||||
*/
|
||||
Flux<Member> findAll(Integer pageNum, Integer pageSize);
|
||||
Flux<Member> findAll(Integer pageNum, Integer pageSize, String sortField, String sortOrder);
|
||||
|
||||
/**
|
||||
* 前台管理端获取会员详情(含会员卡信息)
|
||||
|
||||
+31
-11
@@ -26,6 +26,7 @@ import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -230,13 +231,31 @@ public class MemberServiceImpl implements MemberService {
|
||||
);
|
||||
}
|
||||
|
||||
/** 合法的排序字段白名单,key=前端传值, value=数据库字段名 */
|
||||
private static final java.util.Map<String, String> ALLOWED_SORT_FIELDS = java.util.Map.of(
|
||||
"createdAt", "created_at",
|
||||
"memberNo", "member_no",
|
||||
"gender", "gender",
|
||||
"id", "id"
|
||||
);
|
||||
|
||||
@Override
|
||||
public Flux<Member> findAll(Integer pageNum, Integer pageSize) {
|
||||
log.info("查询所有会员列表, pageNum: {}, pageSize: {}", pageNum, pageSize);
|
||||
public Flux<Member> findAll(Integer pageNum, Integer pageSize, String sortField, String sortOrder) {
|
||||
log.info("查询所有会员列表, pageNum: {}, pageSize: {}, sortField: {}, sortOrder: {}",
|
||||
pageNum, pageSize, sortField, sortOrder);
|
||||
|
||||
// 排序方向:desc(默认) / asc
|
||||
Sort.Direction direction = "asc".equalsIgnoreCase(sortOrder)
|
||||
? Sort.Direction.ASC : Sort.Direction.DESC;
|
||||
|
||||
// 排序字段白名单校验,不在白名单中则使用默认排序
|
||||
String dbField = (sortField != null && ALLOWED_SORT_FIELDS.containsKey(sortField))
|
||||
? ALLOWED_SORT_FIELDS.get(sortField) : "created_at";
|
||||
|
||||
Pageable pageable = PageRequest.of(
|
||||
pageNum - 1,
|
||||
pageSize
|
||||
pageSize,
|
||||
Sort.by(direction, dbField)
|
||||
);
|
||||
|
||||
return memberRepository.findAllBy(pageable);
|
||||
@@ -249,12 +268,13 @@ public class MemberServiceImpl implements MemberService {
|
||||
String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
|
||||
|
||||
return redisUtil.get(cacheKey, MemberDetailVO.class)
|
||||
.flatMap(cached -> {
|
||||
if (cached != null) {
|
||||
log.debug("从缓存获取会员详情, memberId: {}", memberId);
|
||||
return Mono.just(cached);
|
||||
}
|
||||
return memberRepository.findById(memberId)
|
||||
.filter(cached -> cached != null)
|
||||
.switchIfEmpty(Mono.defer(() ->
|
||||
memberRepository.findById(memberId)
|
||||
.switchIfEmpty(Mono.error(() -> {
|
||||
log.error("会员不存在: memberId={}", memberId);
|
||||
throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在");
|
||||
}))
|
||||
.zipWith(
|
||||
memberRepository.findCardRecordsWithCardInfoByMemberId(memberId)
|
||||
.collectList(),
|
||||
@@ -291,8 +311,8 @@ public class MemberServiceImpl implements MemberService {
|
||||
}
|
||||
)
|
||||
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
|
||||
.then(Mono.just(vo)));
|
||||
});
|
||||
.then(Mono.just(vo)))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+30
-2
@@ -1,5 +1,9 @@
|
||||
package cn.novalon.gym.manage.common.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
@@ -8,6 +12,9 @@ import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSeriali
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Redis 配置类(响应式版本)
|
||||
*
|
||||
@@ -17,6 +24,24 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 创建支持 Java 8 时间类型与类型信息的 ObjectMapper,用于 Redis JSON 序列化
|
||||
*/
|
||||
private ObjectMapper createRedisObjectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
javaTimeModule.addSerializer(LocalDateTime.class,
|
||||
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
mapper.registerModule(javaTimeModule);
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
// 启用默认类型信息,确保反序列化时能还原为原始 Java 类型
|
||||
mapper.activateDefaultTyping(
|
||||
mapper.getPolymorphicTypeValidator(),
|
||||
ObjectMapper.DefaultTyping.NON_FINAL
|
||||
);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 ReactiveRedisTemplate
|
||||
*/
|
||||
@@ -24,13 +49,16 @@ public class RedisConfig {
|
||||
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(
|
||||
ReactiveRedisConnectionFactory connectionFactory) {
|
||||
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer =
|
||||
new GenericJackson2JsonRedisSerializer(createRedisObjectMapper());
|
||||
|
||||
// 配置序列化上下文
|
||||
RedisSerializationContext<String, Object> serializationContext =
|
||||
RedisSerializationContext.<String, Object>newSerializationContext()
|
||||
.key(StringRedisSerializer.UTF_8)
|
||||
.value(new GenericJackson2JsonRedisSerializer())
|
||||
.value(jsonSerializer)
|
||||
.hashKey(StringRedisSerializer.UTF_8)
|
||||
.hashValue(new GenericJackson2JsonRedisSerializer())
|
||||
.hashValue(jsonSerializer)
|
||||
.build();
|
||||
|
||||
return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
|
||||
|
||||
+11
-2
@@ -1,5 +1,6 @@
|
||||
package cn.novalon.gym.manage.common.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -13,6 +14,7 @@ import java.time.Duration;
|
||||
* @author liwentao
|
||||
* @date 2026/5/15
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RedisUtil {
|
||||
|
||||
@@ -34,12 +36,19 @@ public class RedisUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值
|
||||
* 获取值(带类型转换)
|
||||
* filter+cast 替代 map 避免 NPE,onErrorResume 兼容旧缓存格式反序列化失败
|
||||
*/
|
||||
@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);
|
||||
.filter(clazz::isInstance)
|
||||
.cast(clazz)
|
||||
.onErrorResume(e -> {
|
||||
// 旧缓存数据格式不兼容时静默跳过,后续逻辑会 fallback 到 DB 查询
|
||||
log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage());
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
-- ============================================
|
||||
-- V29: 新增会员管理菜单
|
||||
-- 描述: 为会员管理功能创建菜单项
|
||||
-- ============================================
|
||||
|
||||
-- 会员管理一级菜单(目录)
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(500, '会员管理', 0, 5, 'M', NULL, NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 会员管理子菜单(页面)
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(501, '会员管理', 500, 1, 'C', 'business:member:list', 'member/member/index', 1, NOW(), NOW());
|
||||
|
||||
-- 会员管理按钮权限
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
|
||||
(5011, '会员查询', 501, 1, 'F', 'business:member:query', NULL, 1, NOW(), NOW()),
|
||||
(5012, '会员新增', 501, 2, 'F', 'business:member:add', NULL, 1, NOW(), NOW()),
|
||||
(5013, '会员修改', 501, 3, 'F', 'business:member:edit', NULL, 1, NOW(), NOW()),
|
||||
(5014, '会员删除', 501, 4, 'F', 'business:member:remove', NULL, 1, NOW(), NOW()),
|
||||
(5015, '会员详情', 501, 5, 'F', 'business:member:detail', NULL, 1, NOW(), NOW());
|
||||
|
||||
-- 重置菜单序列
|
||||
SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu));
|
||||
Reference in New Issue
Block a user