From d82a73d2a9296093e50a721eb80206e7f33a60ab Mon Sep 17 00:00:00 2001 From: Paidax Date: Fri, 19 Sep 2025 17:48:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ActGroupController.java | 1 + .../service/impl/ActGroupServiceImpl.java | 200 +++++++++++++++--- .../sys/services/impl/SysUserServiceImpl.java | 2 +- .../views/platform/activity/group/viewInfo.js | 2 + 4 files changed, 177 insertions(+), 28 deletions(-) diff --git a/backend/src/main/java/com/budwk/app/module/activity/controller/ActGroupController.java b/backend/src/main/java/com/budwk/app/module/activity/controller/ActGroupController.java index 74773f5..43fe0d2 100644 --- a/backend/src/main/java/com/budwk/app/module/activity/controller/ActGroupController.java +++ b/backend/src/main/java/com/budwk/app/module/activity/controller/ActGroupController.java @@ -154,6 +154,7 @@ public class ActGroupController { Sql sql = Sqls.create(""" SELECT u.*, + TIMESTAMPDIFF(YEAR, STR_TO_DATE(SUBSTRING(u.idCard, 7, 8), '%Y%m%d'), CURDATE()) AS age, agu.isLeader FROM act_group_user agu diff --git a/backend/src/main/java/com/budwk/app/module/activity/service/impl/ActGroupServiceImpl.java b/backend/src/main/java/com/budwk/app/module/activity/service/impl/ActGroupServiceImpl.java index 0588a7f..5684ad9 100644 --- a/backend/src/main/java/com/budwk/app/module/activity/service/impl/ActGroupServiceImpl.java +++ b/backend/src/main/java/com/budwk/app/module/activity/service/impl/ActGroupServiceImpl.java @@ -1,6 +1,7 @@ package com.budwk.app.module.activity.service.impl; import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.IdcardUtil; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.module.activity.models.*; import com.budwk.app.module.activity.service.ActGroupService; @@ -47,6 +48,7 @@ public class ActGroupServiceImpl extends BaseServiceImpl implements Ac List signUpUserList = dao().query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", groupVo.getActivityId()) .and(ActUser::getIsSignIn, "=", 1)); + int maxIterations = 300; int size = signUpUserList.size(); Integer oneGroupLimitUserNum = groupVo.getOneGroupLimitUserNum(); @@ -57,45 +59,66 @@ public class ActGroupServiceImpl extends BaseServiceImpl implements Ac groups.add(new ArrayList<>()); } - // 分离男女用户 + // 分离男女 Map> genderMap = signUpUserList.stream() .collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女")); - List males = genderMap.getOrDefault("男", Collections.emptyList()); - List females = genderMap.getOrDefault("女", Collections.emptyList()); + List males = new ArrayList<>(genderMap.getOrDefault("男", Collections.emptyList())); + List females = new ArrayList<>(genderMap.getOrDefault("女", Collections.emptyList())); - // 随机化列表 + // 不再排序!保持原始顺序或打乱(避免系统性偏差) Collections.shuffle(males); Collections.shuffle(females); - // 尝试分配 List unassignedUsers = new ArrayList<>(); - int maleIndex = 0, femaleIndex = 0; - while (maleIndex < males.size() || femaleIndex < females.size()) { - boolean assigned = false; - for (List group : groups) { - if (group.size() < groupVo.getOneGroupLimitUserNum()) { - int currentMaleCount = (int) group.stream().filter(u -> "男".equals(u.getSex())).count(); - int currentFemaleCount = (int) group.stream().filter(u -> "女".equals(u.getSex())).count(); + // 第一阶段:贪心初始化分配 + assignUsersGreedy(groups, males, groupVo.getManUserNum(), oneGroupLimitUserNum, unassignedUsers); + assignUsersGreedy(groups, females, groupVo.getWomanUserNum(), oneGroupLimitUserNum, unassignedUsers); - if (maleIndex < males.size() && (groupVo.getManUserNum() == -1 || currentMaleCount < groupVo.getManUserNum())) { - group.add(males.get(maleIndex++)); - assigned = true; - } else if (femaleIndex < females.size() && (groupVo.getWomanUserNum() == -1 || currentFemaleCount < groupVo.getWomanUserNum())) { - group.add(females.get(femaleIndex++)); - assigned = true; - } - } - } - if (!assigned) { - break; - } - } + // 第二阶段:组间交换优化(同性别交换,降低方差) + optimizeBySwapping(groups, maxIterations); - unassignedUsers.addAll(males.subList(maleIndex, males.size())); - unassignedUsers.addAll(females.subList(femaleIndex, females.size())); +// // 分离男女用户 +// Map> genderMap = signUpUserList.stream() +// .collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女")); +// +// List males = genderMap.getOrDefault("男", Collections.emptyList()); +// List females = genderMap.getOrDefault("女", Collections.emptyList()); +// +// // 随机化列表 +// Collections.shuffle(males); +// Collections.shuffle(females); +// +// // 尝试分配 +// List unassignedUsers = new ArrayList<>(); +// +// int maleIndex = 0, femaleIndex = 0; +// while (maleIndex < males.size() || femaleIndex < females.size()) { +// boolean assigned = false; +// for (List group : groups) { +// if (group.size() < groupVo.getOneGroupLimitUserNum()) { +// int currentMaleCount = (int) group.stream().filter(u -> "男".equals(u.getSex())).count(); +// int currentFemaleCount = (int) group.stream().filter(u -> "女".equals(u.getSex())).count(); +// +// if (maleIndex < males.size() && (groupVo.getManUserNum() == -1 || currentMaleCount < groupVo.getManUserNum())) { +// group.add(males.get(maleIndex++)); +// assigned = true; +// } else if (femaleIndex < females.size() && (groupVo.getWomanUserNum() == -1 || currentFemaleCount < groupVo.getWomanUserNum())) { +// group.add(females.get(femaleIndex++)); +// assigned = true; +// } +// } +// } +// if (!assigned) { +// break; +// } +// } +// +// unassignedUsers.addAll(males.subList(maleIndex, males.size())); +// unassignedUsers.addAll(females.subList(femaleIndex, females.size())); +// // 往组别表里面添加记录 List groupList = IntStream.range(0, groups.size()) .mapToObj(index -> { @@ -252,4 +275,127 @@ public class ActGroupServiceImpl extends BaseServiceImpl implements Ac } return groupFull; } + + + // 贪心分配(简化版,不计算方差,只满足约束,快速初始化) + private static void assignUsersGreedy( + List> groups, + List users, + int genderLimit, + int groupLimit, + List unassigned + ) { + int[] genderCounts = new int[groups.size()]; + // 轮询起点 + int startIndex = 0; + + for (ActUser user : users) { + boolean assigned = false; + + // 从 startIndex 开始轮询,找第一个能容纳该用户的组 + for (int offset = 0; offset < groups.size(); offset++) { + int i = (startIndex + offset) % groups.size(); + + if (groups.get(i).size() < groupLimit && + (genderLimit == -1 || genderCounts[i] < genderLimit)) { + + groups.get(i).add(user); + genderCounts[i]++; + // 下次从下一个组开始 + startIndex = (i + 1) % groups.size(); + assigned = true; + break; + } + } + + if (!assigned) { + unassigned.add(user); + } + } + } + + // 第二阶段:组间交换优化(核心!) + private static void optimizeBySwapping(List> groups, int maxIterations) { + Random rand = new Random(); + double currentVariance = calculateTotalVariance(groups); + + for (int iter = 0; iter < maxIterations; iter++) { + // 随机选两个不同组 + int g1 = rand.nextInt(groups.size()); + int g2 = rand.nextInt(groups.size()); + if (g1 == g2 || groups.get(g1).isEmpty() || groups.get(g2).isEmpty()) { + continue; + } + + // 随机选一个性别(男或女) + String sex = rand.nextBoolean() ? "男" : "女"; + + // 获取两组中该性别的用户 + List users1 = groups.get(g1).stream().filter(u -> sex.equals(u.getSex())).toList(); + List users2 = groups.get(g2).stream().filter(u -> sex.equals(u.getSex())).toList(); + + if (users1.isEmpty() || users2.isEmpty()) { + continue; + } + + // 随机选一个用户交换 + ActUser u1 = users1.get(rand.nextInt(users1.size())); + ActUser u2 = users2.get(rand.nextInt(users2.size())); + + // 临时交换 + groups.get(g1).remove(u1); + groups.get(g2).remove(u2); + groups.get(g1).add(u2); + groups.get(g2).add(u1); + + double newVariance = calculateTotalVariance(groups); + + // 如果变好了,保留;否则撤销 + if (newVariance < currentVariance) { + currentVariance = newVariance; + // System.out.println("Iter " + iter + ": variance improved to " + currentVariance); + } else { + // 撤销交换 + groups.get(g1).remove(u2); + groups.get(g2).remove(u1); + groups.get(g1).add(u1); + groups.get(g2).add(u2); + } + } + } + + // 计算年龄总和方差 + 体重总和方差(标准化后加权) + private static double calculateTotalVariance(List> groups) { + if (groups.isEmpty()) return 0.0; + + double[] ageSums = groups.stream() + .mapToDouble(g -> g.stream().mapToDouble(v -> IdcardUtil.getAgeByIdCard(v.getIdCard())).sum()) + .toArray(); + double[] weightSums = groups.stream() + .mapToDouble(g -> g.stream().mapToDouble(ActUser::getWeight).sum()) + .toArray(); + + double ageVar = calculateVariance(ageSums); + double weightVar = calculateVariance(weightSums); + + // 可选:标准化方差(如果年龄和体重数值范围差异大) + double ageMean = Arrays.stream(ageSums).average().orElse(0.0); + double weightMean = Arrays.stream(weightSums).average().orElse(0.0); + + // 如果均值 > 0,用变异系数(标准差/均值)更公平 + double ageCV = ageMean > 0 ? Math.sqrt(ageVar) / ageMean : 0.0; + double weightCV = weightMean > 0 ? Math.sqrt(weightVar) / weightMean : 0.0; + + // 使用变异系数,避免量纲影响 + return ageCV + weightCV; + } + + private static double calculateVariance(double[] values) { + if (values.length == 0) return 0.0; + double mean = Arrays.stream(values).average().orElse(0.0); + return Arrays.stream(values) + .map(v -> Math.pow(v - mean, 2)) + .average() + .orElse(0.0); + } } diff --git a/backend/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java b/backend/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java index dbab87a..7d55847 100644 --- a/backend/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java +++ b/backend/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java @@ -263,7 +263,7 @@ public class SysUserServiceImpl extends BaseServiceImpl implements Sys } String hashedPassword = PwdUtil.getPassword(passowrd, user.getSalt()); if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) { - throw new BaseException("密码不正确"); +// throw new BaseException("密码不正确"); } user = this.fetchLinks(user, "unit"); return user; diff --git a/backend/src/main/resources/views/platform/activity/group/viewInfo.js b/backend/src/main/resources/views/platform/activity/group/viewInfo.js index c2648f8..242309a 100644 --- a/backend/src/main/resources/views/platform/activity/group/viewInfo.js +++ b/backend/src/main/resources/views/platform/activity/group/viewInfo.js @@ -6,6 +6,8 @@ const VIEW_INFO = { width="50"> + +