From 529ac8113121cec8a4271608e9db293efe185407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Fri, 9 Jan 2026 13:28:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(schedule):=20=E5=90=8C=E4=B8=80=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=BC=98=E5=85=88=E5=88=86=E9=85=8D=E5=88=B0=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E5=9C=BA=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增projectVenueMap记录每个项目首次分配的场地 - 同一项目的后续分组优先分配到相同场地 - 只有当优先场地容量不足时才分配到其他场地 - 新增日志输出项目-场地分配情况 --- .../buckets/.bloomcycle.bin/xl.meta | Bin 743 -> 743 bytes .../buckets/.usage-cache.bin.bkp/xl.meta | Bin 630 -> 630 bytes .../buckets/.usage-cache.bin/xl.meta | Bin 630 -> 630 bytes .../.minio.sys/buckets/.usage.json/xl.meta | Bin 2360 -> 2360 bytes .../.usage-cache.bin.bkp/xl.meta | Bin 636 -> 627 bytes .../000000-assets/.usage-cache.bin/xl.meta | Bin 636 -> 627 bytes .../assets/.usage-cache.bin.bkp/xl.meta | Bin 575 -> 575 bytes .../buckets/assets/.usage-cache.bin/xl.meta | Bin 575 -> 575 bytes .../MartialScheduleArrangeServiceImpl.java | 125 +++++++++++------- 9 files changed, 77 insertions(+), 48 deletions(-) diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 7b70dcc43fc90533ccad9c0d6fc5189a56aee2f2..145f5c9b1742623c8e0c199a7e2dca3229392bbb 100644 GIT binary patch delta 445 zcmaFP`kZxw2G5U6|18z&joTXCMC08crq&ny47UZM?W$!Quz!mfv8C-c* zuZiI2Su45eBDkvznL95cxX%ne#mmCE{YnO3@M zcY=_i#p2TtZZgoN6Z{N2-~2^LcNzArQ$}!)8E!M^gL5Y`8Sb;vM{w;7D^yM)xK)PB O{VEXLZH9-AIsgE6B9k}( delta 445 zcmaFP`kZxw2G4ifJGat&kFZ?+FV`{A#8^N`$bQ8w1lEfP&FCPy&p)obYGA7X4=l2@9ObCfaI znM*M9;aYX`VAdyzF)=5za#a6pyly}L+=+7Kw0Q{K7L}x?9Oq#LsmM&|dZjP<%xqn9WpYlcbAE0?PAX9L4ub$(L3^71gDv$J z5&T8^moye4xcBsbYx%*s9a0AD+czP&VFpG%-3aa^1J-Hb2<|xpl@ew+x0B1jzH%;t z>t>)U%md*j16|tLWRP3<9wEKYz&KPL!Tn`mT-FNbc9|JezcWE_iwx#^u_3tYtUmom L;=VGdn5GB-D`t`# diff --git a/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta index 8c6bf7ae31a40f6960a927d3ed614b3616ec0096..8fe8b70035d59c51ce02cff147aebfcee8efe6ba 100644 GIT binary patch delta 208 zcmV;>05AXc1oi}wC=B@0{s_3s!~~tOIX;mzGY~i1QXvD!N`;GL$M-vP+$fQEe+Kx{ z{s_3sk^D6$q-AtrXW1Y!WHdK1WHT`~FgIj2W-~N4Fkv)hF)%PQFk)q7G%#h8F##qu zp?1z^V#@%|FRo~EQPDi)X#V3%Ap8RJ&Er6m5XAbPq_ zS(CE?C>z+SOks0$Rd8fsbY;g31poj50BK;6wlQIjm6P@XYZZVaWWb3CQ)G|{BeaZs K&)gZCPt<3#I#uuh delta 207 zcmV;=05Jdd1oi}wC=B;K+^#ST!~`Prgej3UGY}%?ehp>4Ni?*+MyIf}{d$pie+KtG z+^#STk^D82SOF$nq-AtrXW1ZOGBPUEoGGj4iWjQ!DIb&gEGdM71H#9kBG+{NN zcFt#F%K*+Mr2s(zfuwGAY;45i;PsuMEOk>8`m2XXoUd=1KAnODiZ;5>q=8*S;7pW! zld}OR8@sAZVRLj$dEF^8L71DcO5D?8pSzKFe+Kx{ z{n`jRk^DR$GGsJ2F=R6_HZV72HfA$4HZWl{Wic=?GcaOhWHc~klUM;REjbmx&H;g> zZgp&I#N$<=KNa^{37)2;FDe$85nz{GG#TSox}_xktsr{3Pg#?*0Vo^Ts!U;XbX9O< vVRU833lJ;a{uUJ?5;>pJg|CrqoQ{%%~x~dzDXOaY<-FN@7Xs taUNC%AjnMUdS#%tcH87{jM<_L3MmIVIhIN6;kdb-r|SG1$sYE#jsOavPbUBX diff --git a/minio_data/.minio.sys/buckets/.usage.json/xl.meta b/minio_data/.minio.sys/buckets/.usage.json/xl.meta index 08266bbbc14044028fd8ef1b5d9eb2fd72d5c790..f009352fa8e0df44c400960f70bd019e3782bf93 100644 GIT binary patch delta 163 zcmdlXv_oiu2G5U6|Ewp)9bvgY{c6%g6JvqOl=+R4{LZC|JWLPfI7Kr|EU#ySsG0c3 zM!~|=GA+r>ILRp4GR4Tm!XP=x#L_6y$RIhzAj#0!z+iF&qnch$Pkt$5&%(gm P!o<+Xz-;pk#sqc%t!F@a diff --git a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta index ecb413fbcde8025a836ef8d0d428c55ebbb5dc07..a9611f02fa3887d6702d299928e982e97fa2096c 100644 GIT binary patch delta 326 zcmeyv@|k6V2G5U6e>WaEc7)|)M7G036JvqjQ*W(*Hr<1F>b3e0ZYvW+Czi*lA6ONT zSXAN|oLQAxJnPu2f64P%g7#k&-nV4IcB%52N z7#k*9q#2usdqIyojLZVm=^xx*X`4IBzy8oNqc>dMUERRz^xhf#B zsKhZivnsWC;zfp&OF%3jz0@}(GdK0JLiGNHdM=+{oEJ-a%Ouwm+XlZO_WNd0^ zVrFb&YG#pQU}#{Gn38B}Xk?ORZeg6Zu#1 z7NjJWq#oyCWdMTAgsxZm*Y!V7ViL*NqF-8&lb@K6-}D#{ksBz;KA2p@BmnB8efY Vq^8He{#eR}8On1QUwM4H4FG#tgy#SN diff --git a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta index 563925b093d2f3e34d3898bea89ba96a96e6d48c..a77b66c6993454c77b67547b665fd871e543c983 100644 GIT binary patch delta 326 zcmeyv@|k6V2G5U6e`~jO9$~qcx;1m6iLn3&?<2J*OsqO#ET3EmVj75dZ}+nW^U?bi4KS%6aSd1FG(#)OuwmMY?NeXVs2rQY;Kui zY?x?~W^8J1o|tNBY-wU_WMW`4*^p7qabelH^rX`a=W0wGH5eP0+9C={3c;Q~2 zhNovl4qjcC)0ezbe)012S?15@y*$<{dwR>~SsWT$_5ZRkOl4(Q!Z~?Aqnh-MRX&Nu zC7}f=i6yDWc~}{MATwd&Hba(N#z{;f@g0kjGbV1)FD=N)PfS@dPCpzKpe;SRSW-a#cWL zQHf)4W>sqO#ET3kmw;G6dZ}+nW^U?bi4KS%6aSb_j$l;FSdvl)XXBqz|g=VF(uK|(8wgs+`>3*VcEI#q|*%NroElh#MrnbuQVs;$jkFtZ)Bf~ z2Tj}_niOzQ_Q%orPnIlBE8^2Bz10~iv3#-=mSZ+~S zF~j1NTp&?gRXm%4iHVzofgPlKQCY@xIS_+^VPW!NkiLmPeGEW73=D_Z85%eQB9a)Q VN@{xi>yM>un4vs}@s-E7+W>f^gmeG^ diff --git a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta index 321100aa9d9614370ea9d51ccb0b6b973fd31591..9463a1b7a0402634c3436912a2548a6f39700436 100644 GIT binary patch delta 175 zcmdnbvY%yw2G5U6e-+zJj);9bvJ*c7ORq6Jr6DgL~geH~Q@F**lZ#_)FLPiRJZd5H%D3 z*vO})r6yY%q^6pirvialQld$6vZZBmYO0};rIESeWl(Es__#Y;WsuUjCL?YXIMgXoN>CYOq5FaP#7Ve)21ZRy>sd=iUGLJLw7OHz;X aurdHaWjwCm*kb^g%hV#7*5{Is4abCl}}=ENoYYzVoB<8 a9##e*$V`~H&7kya{N$gEPAnCl)9e9KoJg$z delta 175 zcmdnbvY%yw2G4if+Zzt)A7K%bW&bhJ#8^PA?uV=9N%w}GmY1cbN4f8rSYFQtQ8V$6 zjeJ^KYOUTIFwku}XL zk0?zR`aeIrc&X?7bqj>DJva4j5S{VVExmh{PhxRNXhBM1N$PPP aRt6x*Oz3*0?_>L8@=r!5mj6x1E&~8LC`#%8 diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java index ebb6012..40c82d3 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java @@ -570,11 +570,11 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang // 创建所有槽位(场地 × 时间段组合) List slots = new ArrayList<>(); - int timeSlotIndex = 0; // 时间段索引,对应前端的 timeSlotIndex - for (TimeSlot timeSlot : timeSlots) { // 先遍历时间段 - for (MartialVenue venue : venues) { // 再遍历场地 + int timeSlotIndex = 0; + for (TimeSlot timeSlot : timeSlots) { + for (MartialVenue venue : venues) { SlotInfo slot = new SlotInfo(); - slot.timeSlotIndex = timeSlotIndex; // 同一时间段的所有场地,共享相同的 timeSlotIndex + slot.timeSlotIndex = timeSlotIndex; slot.venueId = venue.getId(); slot.venueName = venue.getVenueName(); slot.date = timeSlot.getDate(); @@ -584,20 +584,18 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang slot.currentLoad = 0; slots.add(slot); } - timeSlotIndex++; // 每个时间段结束后,索引+1 + timeSlotIndex++; } log.info("总共初始化了 {} 个场地×时间段组合", slots.size()); // 排序策略: 集体项目(projectType=2)优先,同类型按预计时长降序 groups.sort((a, b) -> { - // 1. 集体项目优先于单人项目 int aType = a.getProjectType() != null ? a.getProjectType() : 1; int bType = b.getProjectType() != null ? b.getProjectType() : 1; if (aType != bType) { - return bType - aType; // 2(集体) > 1(单人) + return bType - aType; } - // 2. 同类型按预计时长降序(先安排时间长的) int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0; int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0; return bDuration - aDuration; @@ -606,72 +604,102 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang log.info("排序后分组顺序(集体优先): {}", groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", "))); - // 使用轮询算法进行均匀分配 + // 记录每个项目已分配的场地ID,用于同一项目优先分配到同一场地 + Map projectVenueMap = new HashMap<>(); int assignedCount = 0; - int currentSlotIndex = 0; for (ScheduleGroupData group : groups) { + Long projectId = group.getProjectId(); + Long preferredVenueId = projectVenueMap.get(projectId); + SlotInfo bestSlot = null; - int attempts = 0; - int maxAttempts = slots.size(); - - // 第一轮:从当前槽位开始轮询,寻找第一个容量足够的槽位 - int startIndex = currentSlotIndex; - while (attempts < maxAttempts) { - SlotInfo slot = slots.get(currentSlotIndex); - - // 检查容量是否足够 - if (slot.currentLoad + group.getEstimatedDuration() <= slot.capacity) { - bestSlot = slot; - break; - } - - // 尝试下一个槽位 - currentSlotIndex = (currentSlotIndex + 1) % slots.size(); - attempts++; - } - - // 第二轮:如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载) - if (bestSlot == null) { - log.warn("分组 '{}' 需要{}分钟,超过单个槽位容量{}分钟,将选择负载最小的槽位(允许超载)", - group.getGroupName(), group.getEstimatedDuration(), slots.get(0).capacity); - + + // 优先策略: 如果该项目已有分配的场地,优先选择同一场地的其他时间段 + if (preferredVenueId != null) { bestSlot = slots.stream() + .filter(s -> s.venueId.equals(preferredVenueId)) + .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) - .orElse(slots.get(0)); - - // 设置currentSlotIndex到bestSlot的位置 - for (int i = 0; i < slots.size(); i++) { - if (slots.get(i) == bestSlot) { - currentSlotIndex = i; - break; - } + .orElse(null); + + if (bestSlot != null) { + log.info("项目 '{}' 的分组 '{}' 优先分配到同一场地: {}", + group.getGroupName(), group.getGroupName(), bestSlot.venueName); + } + } + + // 如果优先场地没有足够容量,则按负载均衡策略选择 + if (bestSlot == null) { + Map> slotsByVenue = slots.stream() + .collect(Collectors.groupingBy(s -> s.venueId)); + + bestSlot = slots.stream() + .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) + .min((a, b) -> { + int loadA = slotsByVenue.get(a.venueId).stream().mapToInt(s -> s.currentLoad).sum(); + int loadB = slotsByVenue.get(b.venueId).stream().mapToInt(s -> s.currentLoad).sum(); + if (loadA != loadB) { + return Integer.compare(loadA, loadB); + } + return Integer.compare(a.currentLoad, b.currentLoad); + }) + .orElse(null); + } + + // 如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载) + if (bestSlot == null) { + log.warn("分组 '{}' 需要{}分钟,超过单个槽位容量,将选择负载最小的槽位(允许超载)", + group.getGroupName(), group.getEstimatedDuration()); + + if (preferredVenueId != null) { + bestSlot = slots.stream() + .filter(s -> s.venueId.equals(preferredVenueId)) + .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) + .orElse(null); + } + + if (bestSlot == null) { + bestSlot = slots.stream() + .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) + .orElse(slots.get(0)); } } - // 分配到选定的槽位 group.setAssignedVenueId(bestSlot.venueId); group.setAssignedVenueName(bestSlot.venueName); group.setAssignedDate(bestSlot.date); group.setAssignedTimeSlot(bestSlot.timeSlot); - group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex); // 保存时间段索引 + group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex); group.setAssignedTimePeriod(bestSlot.period); - // 更新槽位负载 bestSlot.currentLoad += group.getEstimatedDuration(); assignedCount++; + + // 记录该项目分配的场地 + if (!projectVenueMap.containsKey(projectId)) { + projectVenueMap.put(projectId, bestSlot.venueId); + log.info("项目 '{}' 首次分配到场地: {}", group.getGroupName(), bestSlot.venueName); + } log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)", group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot, group.getEstimatedDuration(), bestSlot.currentLoad, (int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity)); - - // 移动到下一个槽位(轮询) - currentSlotIndex = (currentSlotIndex + 1) % slots.size(); } log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size()); + // 输出每个项目的场地分配情况 + log.info("=== 项目-场地分配情况 ==="); + Map venueNameMap = venues.stream().collect(Collectors.toMap(MartialVenue::getId, MartialVenue::getVenueName)); + projectVenueMap.forEach((projectId, venueId) -> { + String projectName = groups.stream() + .filter(g -> g.getProjectId().equals(projectId)) + .map(ScheduleGroupData::getGroupName) + .findFirst().orElse("未知项目"); + log.info("项目: {} -> 场地: {}", projectName, venueNameMap.get(venueId)); + }); + // 输出每个槽位的负载情况 log.info("=== 各槽位负载统计 ==="); for (SlotInfo slot : slots) { @@ -683,6 +711,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang } } + // 槽位信息内部类 private static class SlotInfo { int timeSlotIndex; // 时间段索引 (0=第1天上午, 1=第1天下午, 2=第2天上午, ...)