diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/controller/api/MemberStatisticsController.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/controller/api/MemberStatisticsController.java new file mode 100644 index 00000000..9a20c31d --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/controller/api/MemberStatisticsController.java @@ -0,0 +1,36 @@ +package com.hzs.member.statis.controller.api; + +import com.hzs.common.core.constant.CacheConstants; +import com.hzs.common.core.enums.ETop30Type; +import com.hzs.common.core.service.RedisService; +import com.hzs.common.core.web.domain.AjaxResult; +import com.hzs.common.domain.member.ext.CuMemberHonorWallExt; +import com.hzs.common.security.utils.SecurityUtils; +import com.hzs.member.statis.param.DirectStatisticsTop30QueryParam; +import com.hzs.member.statis.service.IMemberStatisticsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 统计 + */ +@Slf4j +@RequestMapping("/api/member-statistics") +@RestController +public class MemberStatisticsController { + + @Resource + private IMemberStatisticsService memberStatisticsService; + @PostMapping("/topPeople") + public AjaxResult topPeople(@RequestBody DirectStatisticsTop30QueryParam param) { + return AjaxResult.success(memberStatisticsService.getDirectStatisticsNumberOfPeople(param)); + } + + @PostMapping("/topAmount") + public AjaxResult topAmount(@RequestBody DirectStatisticsTop30QueryParam param) { + return AjaxResult.success(memberStatisticsService.getDirectStatisticsNumberOfAmount(param)); + } +} diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/controller/manager/BackMemberStatisticsController.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/controller/manager/BackMemberStatisticsController.java new file mode 100644 index 00000000..c34b7ba7 --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/controller/manager/BackMemberStatisticsController.java @@ -0,0 +1,54 @@ +package com.hzs.member.statis.controller.manager; + +import com.hzs.common.core.constant.CacheConstants; +import com.hzs.common.core.enums.ETop30Type; +import com.hzs.common.core.service.RedisService; +import com.hzs.common.core.web.domain.AjaxResult; +import com.hzs.member.statis.param.DirectStatisticsTop30QueryParam; +import com.hzs.member.statis.service.IMemberStatisticsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * 统计 + */ +@Slf4j +@RequestMapping("/manage/member-statistics") +@RestController +public class BackMemberStatisticsController { + + @Resource + private IMemberStatisticsService memberStatisticsService; + + @Resource + private RedisService redisService; + + + @GetMapping("/setShowPeople/{flag}") + public AjaxResult setShowPeople(@PathVariable("flag") String flag) { + redisService.setCacheObject(CacheConstants.TOP_30_ACTIVITY + ETop30Type.PEOPLE.getKey(), flag); + return AjaxResult.success(); + } + + + @GetMapping("/setShowAmount/{flag}") + public AjaxResult setShowAmount(@PathVariable("flag") String flag) { + redisService.setCacheObject(CacheConstants.TOP_30_ACTIVITY + ETop30Type.AMOUNT.getKey(), flag); + return AjaxResult.success(); + } + @PostMapping("/topPeople") + public AjaxResult topPeople(@RequestBody DirectStatisticsTop30QueryParam param) { + param.setFaker(false); + return AjaxResult.success(memberStatisticsService.getDirectStatisticsNumberOfPeople(param)); + } + + @PostMapping("/topAmount") + public AjaxResult topAmount(@RequestBody DirectStatisticsTop30QueryParam param) { + param.setFaker(false); + return AjaxResult.success(memberStatisticsService.getDirectStatisticsNumberOfAmount(param)); + } + + +} diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/mapper/MemberStatisticsMapper.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/mapper/MemberStatisticsMapper.java new file mode 100644 index 00000000..9fdefcfe --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/mapper/MemberStatisticsMapper.java @@ -0,0 +1,16 @@ +package com.hzs.member.statis.mapper; + +import com.hzs.common.domain.member.statis.DirectStatisticsTop30VO; +import com.hzs.member.statis.param.DirectStatisticsTop30QueryParam; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +public interface MemberStatisticsMapper { + + List getDirectStatisticsNumberOfPeople(@Param("param") DirectStatisticsTop30QueryParam param); + + + List getDirectStatisticsNumberOfAmount(@Param("param") DirectStatisticsTop30QueryParam param); +} diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/param/DirectStatisticsTop30QueryParam.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/param/DirectStatisticsTop30QueryParam.java new file mode 100644 index 00000000..4f20d49b --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/param/DirectStatisticsTop30QueryParam.java @@ -0,0 +1,30 @@ +package com.hzs.member.statis.param; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class DirectStatisticsTop30QueryParam implements Serializable { + + /** + * 统计年 + */ + private String year; + + /** + * 统计月 + */ + private String month; + + private Boolean faker; + + + private Date startDate; + + private Date endDate; + + private Long memberId; + +} diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/service/IMemberStatisticsService.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/service/IMemberStatisticsService.java new file mode 100644 index 00000000..62007b14 --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/service/IMemberStatisticsService.java @@ -0,0 +1,14 @@ +package com.hzs.member.statis.service; + +import com.hzs.common.domain.member.statis.DirectStatisticsTop30VO; +import com.hzs.member.statis.param.DirectStatisticsTop30QueryParam; + +import java.util.List; + +public interface IMemberStatisticsService { + + List getDirectStatisticsNumberOfPeople(DirectStatisticsTop30QueryParam param); + + + List getDirectStatisticsNumberOfAmount(DirectStatisticsTop30QueryParam param); +} diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/service/impl/MemberStatisticsServiceImpl.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/service/impl/MemberStatisticsServiceImpl.java new file mode 100644 index 00000000..dbaac459 --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/service/impl/MemberStatisticsServiceImpl.java @@ -0,0 +1,225 @@ +package com.hzs.member.statis.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.hzs.common.core.constant.CacheConstants; +import com.hzs.common.core.enums.ETop30Type; +import com.hzs.common.core.service.RedisService; +import com.hzs.common.core.utils.DataMaskingUtil; +import com.hzs.common.core.utils.DateUtils; +import com.hzs.common.domain.member.statis.DirectStatisticsTop30VO; +import com.hzs.member.statis.mapper.MemberStatisticsMapper; +import com.hzs.member.statis.param.DirectStatisticsTop30QueryParam; +import com.hzs.member.statis.service.IMemberStatisticsService; +import com.hzs.member.statis.util.DataGeneratorUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.*; +import java.util.*; + +@Service +@Slf4j +public class MemberStatisticsServiceImpl implements IMemberStatisticsService { + + @Resource + private MemberStatisticsMapper memberStatisticsMapper; + + @Resource + private RedisService redisService; + @Override + public List getDirectStatisticsNumberOfPeople(DirectStatisticsTop30QueryParam param) { + String show = redisService.getCacheObject(CacheConstants.TOP_30_ACTIVITY + ETop30Type.PEOPLE.getKey()); + if(ObjectUtil.isEmpty(show) || "false".equals(show)){ + return null; + } + if(DateUtils.isYearMonthExceedCurrent(param.getYear() + "/" + param.getMonth())) { + return null; + } + setStartAndEndDate(param); + List list = memberStatisticsMapper.getDirectStatisticsNumberOfPeople(param); + return desensitizationAndFaker(list, param, 1); + } + + @Override + public List getDirectStatisticsNumberOfAmount(DirectStatisticsTop30QueryParam param) { + String show = redisService.getCacheObject(CacheConstants.TOP_30_ACTIVITY + ETop30Type.AMOUNT.getKey()); + if(ObjectUtil.isEmpty(show) || "false".equals(show)){ + return null; + } + if(DateUtils.isYearMonthExceedCurrent(param.getYear() + "/" + param.getMonth())) { + return null; + } + setStartAndEndDate(param); + List list = memberStatisticsMapper.getDirectStatisticsNumberOfAmount(param); + return desensitizationAndFaker(list, param, 2); + } + + /** + * 数据脱敏&格式化数字&假数据 + * @param list + * @param param + * @param type + */ + private List desensitizationAndFaker(List list, DirectStatisticsTop30QueryParam param, Integer type){ + List fakerList = DataGeneratorUtils.generateFakeData( + new ArrayList<>(), 30, 1, DataGeneratorUtils.TimeGranularity.DAY); + List result = list; + if(param.getFaker()){ + result = generateFakeData(list, 30, fakerList, type); + } + for (DirectStatisticsTop30VO directStatisticsTop30VO : result) { + directStatisticsTop30VO.setMemberCode(DataMaskingUtil.mask(directStatisticsTop30VO.getMemberCode(), 2, 2)); + directStatisticsTop30VO.setMemberName(DataMaskingUtil.mask(directStatisticsTop30VO.getMemberName(), 1, 0)); + directStatisticsTop30VO.setNumberOfAmount(directStatisticsTop30VO.getNumberOfAmount().divide(new BigDecimal(10000))); + } + return result; + } + private void setStartAndEndDate(DirectStatisticsTop30QueryParam param) { + // 参数验证 + if (ObjectUtil.isEmpty(param.getYear()) || ObjectUtil.isEmpty(param.getMonth())) { + throw new IllegalArgumentException("年份和月份不能为空"); + } + try { + int year = Integer.parseInt(param.getYear()); + int month = Integer.parseInt(param.getMonth()); + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime firstDay = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime lastDay = yearMonth.atEndOfMonth().atTime(23, 59, 59); + // 获取系统默认时区 + ZoneId zoneId = ZoneId.systemDefault(); + // 转换为Date + param.setStartDate(Date.from(firstDay.atZone(zoneId).toInstant())); + param.setEndDate(Date.from(lastDay.atZone(zoneId).toInstant())); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("年份或月份格式不正确", e); + } catch (Exception e) { + throw new RuntimeException("获取时间范围失败", e); + } + } + /** + * 生成假数据并填充到列表 + * + * @param list 原始数据列表 + * @param totalCount 总数 + * @param fakerList 假数据列表 + * @param type 类型:1为设置numberOfPeople, 2为设置numberOfAmount + * @return 填充后的列表 + */ + public static List generateFakeData( + List list, + int totalCount, + List fakerList, + int type) { + if (list == null) { + list = new ArrayList<>(); + } + + if (fakerList == null || fakerList.isEmpty()) { + throw new IllegalArgumentException("假数据列表不能为空"); + } + + if (totalCount < list.size()) { + throw new IllegalArgumentException("总数不能小于原始列表大小"); + } + + // 计算需要生成的假数据数量 + int needGenerateCount = totalCount - list.size(); + + // 检查假数据列表是否有足够的数据 + if (needGenerateCount > fakerList.size()) { + throw new IllegalArgumentException("假数据列表数据不足"); + } + + // 从假数据列表中移除并获取需要的假数据 + List selectedFakerData = new ArrayList<>(); + for (int i = 0; i < needGenerateCount; i++) { + // 随机选择一个假数据 + int randomIndex = new Random().nextInt(fakerList.size()); + DirectStatisticsTop30VO fakerData = fakerList.remove(randomIndex); + selectedFakerData.add(fakerData); + } + + // 根据类型设置数据 + if (type == 1) { + setNumberOfPeople(selectedFakerData, list); + } else if (type == 2) { + setNumberOfAmount(selectedFakerData, list); + } else { + throw new IllegalArgumentException("类型参数无效,只能是1或2"); + } + + // 将生成的假数据添加到原始列表 + list.addAll(selectedFakerData); + + // 按照对应类型降序排序 + if (type == 1) { + list.sort(Comparator.comparing(DirectStatisticsTop30VO::getNumberOfPeople).reversed()); + } else { + list.sort(Comparator.comparing(DirectStatisticsTop30VO::getNumberOfAmount).reversed()); + } + + return list; + } + + /** + * 设置numberOfPeople + */ + private static void setNumberOfPeople(List selectedFakerData, List originalList) { + // 找出原始列表中最小的numberOfPeople + int minPeopleCount = Integer.MAX_VALUE; + for (DirectStatisticsTop30VO data : originalList) { + if (data.getNumberOfPeople() != null && data.getNumberOfPeople() < minPeopleCount) { + minPeopleCount = data.getNumberOfPeople(); + } + } + if (minPeopleCount == Integer.MAX_VALUE) { + minPeopleCount = 10; // 默认值,可以根据需要调整 + } + Random random = new Random(); + for (DirectStatisticsTop30VO fakerData : selectedFakerData) { + if (minPeopleCount == 1) { + fakerData.setNumberOfPeople(1); + } else { + int value = random.nextInt(minPeopleCount - 1) + 1; + fakerData.setNumberOfPeople(value); + } + } + } + + /** + * 设置numberOfAmount + */ + private static void setNumberOfAmount(List selectedFakerData, List originalList) { + // 找出原始列表中最小的numberOfAmount + BigDecimal minAmount = null; + for (DirectStatisticsTop30VO data : originalList) { + if (data.getNumberOfAmount() != null) { + if (minAmount == null || data.getNumberOfAmount().compareTo(minAmount) < 0) { + minAmount = data.getNumberOfAmount(); + } + } + } + if (minAmount == null) { + minAmount = new BigDecimal(188); // 默认值,可以根据需要调整 + } + Random random = new Random(); + for (DirectStatisticsTop30VO fakerData : selectedFakerData) { + if (minAmount.compareTo(new BigDecimal(188)) == 0) { + BigDecimal value = new BigDecimal(188); + fakerData.setNumberOfAmount(value); + } else { + BigDecimal range = minAmount.subtract(BigDecimal.ONE); + BigDecimal randomValue = new BigDecimal(random.nextDouble() * range.doubleValue()).setScale(4, BigDecimal.ROUND_HALF_UP); + if (randomValue.compareTo(BigDecimal.ZERO) <= 0) { + randomValue = BigDecimal.ONE; + } + fakerData.setNumberOfAmount(randomValue); + } + } + } + +} diff --git a/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/util/DataGeneratorUtils.java b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/util/DataGeneratorUtils.java new file mode 100644 index 00000000..9ccafd07 --- /dev/null +++ b/bd-business/bd-business-member/src/main/java/com/hzs/member/statis/util/DataGeneratorUtils.java @@ -0,0 +1,316 @@ +package com.hzs.member.statis.util; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.json.JSONUtil; +import com.hzs.common.core.constant.SysConstants; +import com.hzs.common.domain.member.statis.DirectStatisticsTop30VO; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +public class DataGeneratorUtils { + + // 时间粒度枚举 + public enum TimeGranularity { + DAY, // 按天 + HOUR // 按小时 + } + private static final String prefix = "BD"; + + // 常见姓氏数组 + private static final String[] COMMON_SURNAMES = { + "王", "李", "张", "刘", "陈", "杨", "黄", "赵", "吴", "周", + "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "林", "罗", + "郑", "梁", "谢", "宋", "唐", "许", "韩", "冯", "邓", "曹", + "彭", "曾", "肖", "田", "董", "袁", "潘", "于", "蒋", "蔡" + }; + + // 当前种子,用于生成数据 + private static int currentSeed = getSeed(TimeGranularity.DAY); // 默认按天 + private static TimeGranularity currentGranularity = TimeGranularity.DAY; + // 预先生成的30条基础数据 + private static List PERIODIC_PRE_GENERATED_DATA = generatePeriodicPredefinedData(); + + /** + * 生成假数据并添加到列表中 + * + * @param list 已有数据列表 + * @param totalCount 需要生成的总数据量 + * @param type 类型:1为设置numberOfPeople,2为设置numberOfAmount + * @param granularity 时间粒度:DAY按天,HOUR按小时 + * @return 包含新生成数据的列表 + */ + public static List generateFakeData( + List list, int totalCount, int type, TimeGranularity granularity) { + // 检查时间是否已变化,如果变化则重新生成数据 + checkAndUpdatePeriodicData(granularity); + + // 创建返回列表(复制原列表,避免修改原列表) + List resultList = new ArrayList<>(list); + + // 如果总数小于等于已有列表大小,直接返回原列表 + if (totalCount <= list.size()) { + return resultList; + } + + // 需要生成的新数据数量 + int needGenerateCount = totalCount - list.size(); + + // 从预生成数据中获取所需数量的数据 + List newData = getPeriodicPreGeneratedDataByCount(needGenerateCount, type); + + // 添加新数据到结果列表 + resultList.addAll(newData); + + // 按照对应字段降序排序 + if (type == 1) { + resultList.sort((a, b) -> b.getNumberOfPeople().compareTo(a.getNumberOfPeople())); + } else if (type == 2) { + resultList.sort((a, b) -> b.getNumberOfAmount().compareTo(a.getNumberOfAmount())); + } + + return resultList; + } + + /** + * (兼容旧版本)按天生成数据 + */ + public static List generateFakeData( + List list, int totalCount, int type) { + return generateFakeData(list, totalCount, type, TimeGranularity.DAY); + } + + /** + * 检查时间是否已变化,如果变化则更新数据 + */ + private static void checkAndUpdatePeriodicData(TimeGranularity granularity) { + int newSeed = getSeed(granularity); + if (newSeed != currentSeed || granularity != currentGranularity) { + currentSeed = newSeed; + currentGranularity = granularity; + PERIODIC_PRE_GENERATED_DATA = generatePeriodicPredefinedData(); + } + } + + /** + * 根据时间粒度获取种子 + * @param granularity 时间粒度 + * @return 对应的种子 + */ + private static int getSeed(TimeGranularity granularity) { + if (granularity == TimeGranularity.HOUR) { + // 按小时:格式为YYYYMMDDHH + LocalDateTime now = LocalDateTime.now(); + return Integer.parseInt(now.format(DateTimeFormatter.ofPattern("yyyyMMddHH"))); + } else { + // 按天:格式为YYYYMMDD + LocalDate today = LocalDate.now(); + return Integer.parseInt(today.format(DateTimeFormatter.ofPattern("yyyyMMdd"))); + } + } + + /** + * 生成当前周期的30条预定义数据 + */ + private static List generatePeriodicPredefinedData() { + List dataList = new ArrayList<>(30); + + // 使用当前周期的种子创建Random对象 + Random periodRandom = new Random(currentSeed); + + // 生成30条基础数据 + for (int i = 0; i < 30; i++) { + DirectStatisticsTop30VO vo = new DirectStatisticsTop30VO(); + + // 设置基础信息,包含时间粒度标识 + long baseId = 4000000L + currentSeed % 1000000L; + vo.setMemberId(baseId + i); + + // 根据时间粒度生成不同的用户编号前缀 +// String prefix = currentGranularity == TimeGranularity.HOUR ? "HOUR" : "DAY"; +// String prefix = SysConstants.HAI_FUN_PREFIX; + vo.setMemberCode(generateMemberCode(currentSeed, 10)); + + // 生成随机中文姓名 + vo.setMemberName(generateRandomChineseName(i, periodRandom)); + + // 使用周期随机数生成数据值 + vo.setNumberOfPeople(1 + periodRandom.nextInt(100)); // 1-100之间的随机值 + + // 生成金额值 + BigDecimal amount = new BigDecimal(100 + periodRandom.nextInt(10000)) + .add(new BigDecimal(periodRandom.nextDouble())) + .setScale(4, BigDecimal.ROUND_HALF_UP); + vo.setNumberOfAmount(amount); + + dataList.add(vo); + } + + return dataList; + } + public static String generateMemberCode(long currentSeed, int i) { + int prefixLength = prefix.length(); + int numPartLength = 10 - prefixLength; + long seedPart = generateRandomSeedPart(numPartLength); + String memberCode = prefix + seedPart; + return memberCode.length() > 10 ? memberCode.substring(0, 10) : memberCode; + } + /** + * 生成指定位数的随机seedPart + * + * @param digits 位数 + * @return 随机数 + */ + private static long generateRandomSeedPart(int digits) { + if (digits <= 0) { + return 0; + } + + // 计算最小值和最大值 + int min = (int) Math.pow(10, digits - 1); + int max = (int) Math.pow(10, digits) - 1; + + // 如果digits为0,返回0 + if (min == 0 && max == 0) { + return 0; + } + + return RandomUtil.randomInt(min, max); + } + /** + * 根据需要的数量从预生成数据中获取数据 + */ + private static List getPeriodicPreGeneratedDataByCount(int count, int type) { + List result = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + // 从预生成数据中循环获取 + DirectStatisticsTop30VO originalVo = PERIODIC_PRE_GENERATED_DATA.get(i % 30); + + // 创建副本以避免修改预生成的数据 + DirectStatisticsTop30VO newVo = new DirectStatisticsTop30VO(); + newVo.setMemberId(originalVo.getMemberId()); + newVo.setMemberName(originalVo.getMemberName()); + newVo.setMemberCode(originalVo.getMemberCode()); + + // 根据类型设置相应的值 + if (type == 1) { + // 类型1:设置numberOfPeople + newVo.setNumberOfPeople(originalVo.getNumberOfPeople()); + newVo.setNumberOfAmount(BigDecimal.ZERO); + } else if (type == 2) { + // 类型2:设置numberOfAmount + newVo.setNumberOfPeople(0); + newVo.setNumberOfAmount(originalVo.getNumberOfAmount()); + } + + result.add(newVo); + } + + return result; + } + + /** + * 生成随机中文姓名 + */ + private static String generateRandomChineseName(int index, Random random) { + // 随机选择姓氏 + String surname = COMMON_SURNAMES[random.nextInt(COMMON_SURNAMES.length)]; + + // 随机决定是二字姓名还是三字姓名 + boolean isTwoCharacters = random.nextBoolean(); + + if (isTwoCharacters) { + // 二字姓名:姓氏 + 1个随机汉字 + return surname + generateRandomChineseChar(random); + } else { + // 三字姓名:姓氏 + 2个随机汉字 + return surname + generateRandomChineseChar(random) + generateRandomChineseChar(random); + } + } + + /** + * 生成随机汉字 + */ + private static char generateRandomChineseChar(Random random) { + // 中文汉字Unicode范围大致在0x4E00到0x9FA5之间 + int start = 0x4E00; + int end = 0x9FA5; + return (char) (start + random.nextInt(end - start + 1)); + } + + /** + * 获取当前周期预生成的30条完整数据 + * @param granularity 时间粒度 + * @return 预生成的数据列表 + */ + public static List getPeriodicPreGeneratedData(TimeGranularity granularity) { + // 检查时间是否已变化 + checkAndUpdatePeriodicData(granularity); + + // 返回预生成数据的副本,避免外部修改 + List copyList = new ArrayList<>(PERIODIC_PRE_GENERATED_DATA.size()); + for (DirectStatisticsTop30VO vo : PERIODIC_PRE_GENERATED_DATA) { + DirectStatisticsTop30VO copy = new DirectStatisticsTop30VO(); + copy.setMemberId(vo.getMemberId()); + copy.setMemberName(vo.getMemberName()); + copy.setMemberCode(vo.getMemberCode()); + copy.setNumberOfPeople(vo.getNumberOfPeople()); + copy.setNumberOfAmount(vo.getNumberOfAmount()); + copyList.add(copy); + } + return copyList; + } + + /** + * (用于测试)根据指定日期和时间粒度获取数据 + */ + public static List getPreGeneratedDataByDate( + int year, int month, int day, int hour, TimeGranularity granularity) { + // 保存当前状态 + int originalSeed = currentSeed; + TimeGranularity originalGranularity = currentGranularity; + List originalData = PERIODIC_PRE_GENERATED_DATA; + + try { + // 设置指定日期的种子 + Calendar calendar = new GregorianCalendar(year, month - 1, day, hour, 0, 0); + Date date = calendar.getTime(); + + if (granularity == TimeGranularity.HOUR) { + LocalDateTime localDateTime = date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); + currentSeed = Integer.parseInt(localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHH"))); + } else { + LocalDate localDate = date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + currentSeed = Integer.parseInt(localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))); + } + + currentGranularity = granularity; + + // 重新生成指定日期的数据 + PERIODIC_PRE_GENERATED_DATA = generatePeriodicPredefinedData(); + + // 返回生成的数据 + return getPeriodicPreGeneratedData(granularity); + } finally { + // 恢复原始状态 + currentSeed = originalSeed; + currentGranularity = originalGranularity; + PERIODIC_PRE_GENERATED_DATA = originalData; + } + } + + public static void main(String[] args) { + // 按天生成数据 + List dailyData = DataGeneratorUtils.generateFakeData( + new ArrayList<>(), 30, 1, DataGeneratorUtils.TimeGranularity.DAY); + System.out.println(JSONUtil.toJsonPrettyStr(dailyData)); + // 按小时生成数据 +// List hourlyData = DataGeneratorUtils.generateFakeData( +// new ArrayList<>(), 50, 2, DataGeneratorUtils.TimeGranularity.HOUR); +// System.out.println(JSONUtil.toJsonPrettyStr(hourlyData)); + } +} diff --git a/bd-business/bd-business-member/src/main/resources/mapper/member/statis/MemberStatisticsMapper.xml b/bd-business/bd-business-member/src/main/resources/mapper/member/statis/MemberStatisticsMapper.xml new file mode 100644 index 00000000..c20f185f --- /dev/null +++ b/bd-business/bd-business-member/src/main/resources/mapper/member/statis/MemberStatisticsMapper.xml @@ -0,0 +1,53 @@ + + + + + + + diff --git a/bd-common/bd-common-core/src/main/java/com/hzs/common/core/constant/CacheConstants.java b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/constant/CacheConstants.java index 32fafeeb..9aeed60e 100644 --- a/bd-common/bd-common-core/src/main/java/com/hzs/common/core/constant/CacheConstants.java +++ b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/constant/CacheConstants.java @@ -330,4 +330,5 @@ public class CacheConstants { */ public final static String AUTO_LOGIN = CACHE_PREFIX + "auth:login:"; + public final static String TOP_30_ACTIVITY = CACHE_PREFIX + "top:30:activity:"; } diff --git a/bd-common/bd-common-core/src/main/java/com/hzs/common/core/enums/ETop30Type.java b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/enums/ETop30Type.java new file mode 100644 index 00000000..deea8c67 --- /dev/null +++ b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/enums/ETop30Type.java @@ -0,0 +1,84 @@ +package com.hzs.common.core.enums; + +import com.hzs.common.core.constant.EnumsPrefixConstants; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 订单类型枚举类 + */ +@AllArgsConstructor +@Getter +public enum ETop30Type { + + /** + * 注册订单 + */ + PEOPLE(1, "直推人数", "people"), + + /** + * 升级订单 + */ + AMOUNT(2, "直推金额", "amount") + + ; + + /** + * 实际值 + */ + private final int value; + /** + * 显示标签 + */ + private final String label; + /** + * 显示标签 + */ + private final String key; + + /** + * 根据值,返回枚举 + * + * @param value + * @return + */ + public static ETop30Type getEnumByValue(int value) { + for (ETop30Type enums : ETop30Type.values()) { + if (enums.getValue() == value) { + return enums; + } + } + return null; + } + + /** + * 根据值,返回名称 + * + * @param value + * @return + */ + public static String getLabelByValue(Integer value) { + if (null == value) { + return ""; + } + for (ETop30Type enums : ETop30Type.values()) { + if (enums.getValue() == value) { + return enums.getLabel(); + } + } + return ""; + } + + + public static List getOrderTypeSpecial() { + List resultList = new ArrayList<>(); + resultList.add(ETop30Type.PEOPLE); + resultList.add(ETop30Type.AMOUNT); + return resultList; + } + + +} diff --git a/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DataMaskingUtil.java b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DataMaskingUtil.java new file mode 100644 index 00000000..33dfb682 --- /dev/null +++ b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DataMaskingUtil.java @@ -0,0 +1,98 @@ +package com.hzs.common.core.utils; + +/** + * 数据脱敏工具类 + * 提供字符串脱敏功能,可指定保留前几位和后几位字符 + */ +public class DataMaskingUtil { + + // 默认脱敏字符 + private static final char DEFAULT_MASK_CHAR = '*'; + + /** + * 对字符串进行脱敏处理 + * @param input 原始字符串 + * @param keepPrefix 保留的前缀位数 + * @param keepSuffix 保留的后缀位数 + * @return 脱敏后的字符串 + */ + public static String mask(String input, int keepPrefix, int keepSuffix) { + // 参数验证 + if (input == null) { + return null; + } + + // 处理边界情况 + if (keepPrefix < 0 || keepSuffix < 0) { + throw new IllegalArgumentException("保留位数不能为负数"); + } + + int length = input.length(); + + // 如果字符串长度小于等于需要保留的位数之和,直接返回原字符串 + if (length <= keepPrefix + keepSuffix) { + return input; + } + + // 构建StringBuilder用于高效拼接字符串 + StringBuilder result = new StringBuilder(length); + + // 添加前缀保留字符 + result.append(input, 0, keepPrefix); + + // 添加脱敏字符 + int maskCount = length - keepPrefix - keepSuffix; + for (int i = 0; i < maskCount; i++) { + result.append(DEFAULT_MASK_CHAR); + } + + // 添加后缀保留字符 + result.append(input, length - keepSuffix, length); + + return result.toString(); + } + + /** + * 对字符串进行脱敏处理,可自定义脱敏字符 + * @param input 原始字符串 + * @param keepPrefix 保留的前缀位数 + * @param keepSuffix 保留的后缀位数 + * @param maskChar 自定义脱敏字符 + * @return 脱敏后的字符串 + */ + public static String mask(String input, int keepPrefix, int keepSuffix, char maskChar) { + // 参数验证 + if (input == null) { + return null; + } + + // 处理边界情况 + if (keepPrefix < 0 || keepSuffix < 0) { + throw new IllegalArgumentException("保留位数不能为负数"); + } + + int length = input.length(); + + // 如果字符串长度小于等于需要保留的位数之和,直接返回原字符串 + if (length <= keepPrefix + keepSuffix) { + return input; + } + + // 构建StringBuilder用于高效拼接字符串 + StringBuilder result = new StringBuilder(length); + + // 添加前缀保留字符 + result.append(input, 0, keepPrefix); + + // 添加脱敏字符 + int maskCount = length - keepPrefix - keepSuffix; + for (int i = 0; i < maskCount; i++) { + result.append(maskChar); + } + + // 添加后缀保留字符 + result.append(input, length - keepSuffix, length); + + return result.toString(); + } +} diff --git a/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DateUtils.java b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DateUtils.java index c33ff760..e6d3dbbc 100644 --- a/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DateUtils.java +++ b/bd-common/bd-common-core/src/main/java/com/hzs/common/core/utils/DateUtils.java @@ -6,6 +6,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.*; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; @@ -895,5 +896,58 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils { DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); return LocalDate.parse(date, fmt); } + /** + * 判断输入的年/月字符串是否超过当前月份 + * + * @param yearMonthStr 年/月字符串,格式为"yyyy/MM",如"2023/12" + * @return 如果输入的年月超过当前月份则返回true,否则返回false + * @throws IllegalArgumentException 如果输入的字符串格式不正确 + */ + public static boolean isYearMonthExceedCurrent(String yearMonthStr) { + // 参数校验 + if (yearMonthStr == null || yearMonthStr.trim().isEmpty()) { + throw new IllegalArgumentException("年/月字符串不能为空"); + } + try { + // 解析输入的年/月字符串 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM"); + YearMonth inputYearMonth = YearMonth.parse(yearMonthStr, formatter); + + // 获取当前年月 + YearMonth currentYearMonth = YearMonth.now(); + + // 比较输入年月和当前年月 + // 如果输入年月大于当前年月,则返回true(表示超过当前月份) + return inputYearMonth.isAfter(currentYearMonth); + + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("年/月字符串格式不正确,应为'yyyy/MM'格式,如'2023/12'", e); + } + } + + /** + * 重载方法,支持自定义日期格式 + * + * @param yearMonthStr 年/月字符串 + * @param pattern 日期格式,如"yyyy-MM"或"yyyyMM"等 + * @return 如果输入的年月超过当前月份则返回true,否则返回false + * @throws IllegalArgumentException 如果输入参数不合法 + */ + public static boolean isYearMonthExceedCurrent(String yearMonthStr, String pattern) { + if (pattern == null || pattern.trim().isEmpty()) { + throw new IllegalArgumentException("日期格式不能为空"); + } + + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + YearMonth inputYearMonth = YearMonth.parse(yearMonthStr, formatter); + YearMonth currentYearMonth = YearMonth.now(); + + return inputYearMonth.isAfter(currentYearMonth); + + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("年/月字符串格式不正确,无法使用模式'" + pattern + "'解析", e); + } + } } diff --git a/bd-common/bd-common-domain/src/main/java/com/hzs/common/domain/member/statis/DirectStatisticsTop30VO.java b/bd-common/bd-common-domain/src/main/java/com/hzs/common/domain/member/statis/DirectStatisticsTop30VO.java new file mode 100644 index 00000000..10c55e0d --- /dev/null +++ b/bd-common/bd-common-domain/src/main/java/com/hzs/common/domain/member/statis/DirectStatisticsTop30VO.java @@ -0,0 +1,42 @@ +package com.hzs.common.domain.member.statis; + +import com.hzs.common.core.annotation.BigDecimalFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DirectStatisticsTop30VO implements Serializable { + /** + * 用户id + */ + private Long memberId; + + /** + * 用户名称 + */ + private String memberName; + + /** + * 用户编号 + */ + private String memberCode; + + /** + * 直推数量 + */ + private Integer numberOfPeople; + + /** + * 直推金额 + */ + @BigDecimalFormat(value = "#0.0000") + private BigDecimal numberOfAmount; +}