From 8b0305997ebb55902fbea1cdd92a34bbafb9a485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Fri, 9 Jan 2026 13:45:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(schedule):=20=E4=BC=98=E5=8C=96=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E9=80=BB=E8=BE=91,=E5=90=8C=E4=B8=80=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=B0=BD=E9=87=8F=E5=9C=A8=E5=90=8C=E4=B8=80=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=AE=B5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按项目为单位进行分配,而不是按分组 - 计算每个项目的总时长,优先找能容纳整个项目的时间段 - 如果项目总时长不超过时间段容量(480分钟),所有分组都在同一时间段 - 只有当项目总时长超过单个时间段容量时才分散到多个时间段 - 分散时优先选择同一场地的连续时间段 --- .../buckets/.bloomcycle.bin/xl.meta | Bin 743 -> 743 bytes .../buckets/.usage-cache.bin.bkp/xl.meta | Bin 630 -> 632 bytes .../buckets/.usage-cache.bin/xl.meta | Bin 630 -> 632 bytes .../.minio.sys/buckets/.usage.json/xl.meta | Bin 2360 -> 2360 bytes .../.usage-cache.bin.bkp/xl.meta | Bin 627 -> 636 bytes .../000000-assets/.usage-cache.bin/xl.meta | Bin 627 -> 636 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 | 212 ++++++++++-------- 9 files changed, 116 insertions(+), 96 deletions(-) diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 145f5c9b1742623c8e0c199a7e2dca3229392bbb..013bd5846f8107868d0ca2e201b69874953797ce 100644 GIT binary patch delta 445 zcmaFP`kZxw2G7rPb#M7s9${hpb0lS=iLpRLV^s027{>$Cd+w+ROxcn!vAmuQqGsYB z8wCS{l+;8619P(^OM}!z6ARyyj&zb5fo2a|?1(fwFfP1mFrLWf`vj zSTBX(FEczp<0^vt$ncqf6`VU+&hYOyF$6ct@bamA1b3R@+Yn0x_mbf+-Fi583ZIcg zW*LI(WyB)$5yDLdx^zmLkx2Irg!CaJmAStW+_fY*xDetWky_eQxM!u QMn>Dc5Zw2S*JBm|0E#S|c>n+a 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}( 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 8fe8b70035d59c51ce02cff147aebfcee8efe6ba..b38fe2c28626ec732e25f21f524a52f4ed68f795 100644 GIT binary patch delta 358 zcmeyy@`Gi92G7rPbqBxj9AWA2oUm}BiLrqFhe(T%wO)b?|JN1@KVQFPVtJhU(NzJ7 zMJ0~GnN_L96E89xT>@eO>7~9QnYpQ#B|0F6O#EXyIf79wb4hARV){*mWFt#MV+&JL zBXctgBMVFOw8WG&gVYoQLo@TVBy)qLg=Oc`lTI_7yY%AzZpOwXd8IiyN1p0!O5T1& zU%i}hmYr?viACFt7pENRZtRkht5|EY?W@^#4vnq)e_a?v8P>8gT$y};QM&%sDxbvS zlF)*b#FEtGJgf{rkeM*`J!4Cf!Rp}b%mRmRnR-%&<5m7f2LW70+g1V&djtU{kO~wn@a~_yI*m_yOkuT_!^3w$Xb5M|A delta 357 zcmeyt@{MJJ2G5U6|2TG>KEg70qov(M6Jr7M+d+!V$Guv+Q;vVPF218Nu{=)w@T!2s zq7uj8%&OGli5D3TF9ETD^itoD%-qz=5*-jjCjK#1Uy@pqn0`~iD8jMDWtR{10r zmxLCiB$lKe=V4_4g3N@8+YA%?rmhao&MfdqP0Y#6OFy*KIWajSwRu@$adB!%amS+M zj0ua1s~8sL7c(p>D`r@nk_#k?tBPlXOjuNwF`Z#y@}Y(LAcd3G8i0ZBCC1iR51+tloX1iLt=tc*cXT>m7u)rf&WXtxsm*H*z(6;#xHz?>xMNXr#)L)1RhJlBV?7q- z7t1XwD`r@nk_#k?tBPkcFfnm+FtCHnTvV1ZT@J)xU|5)ZXc+@gF_5L-AqO;J5(k5Y cVB--LjV9v->^Tq29&EiV;K&zrO8Mym08k}}HUIzs delta 342 zcmeyt@{MJJ2G5U6e{XSE9bq}nHviN_6Jvq%O_g^wjT~pp-r#lTa?y#y z`8aIXQk8#e6{>dmM{;Ov)&J|lAj&Y8l_6#F0Y>Ti8>@U0i%UWaQW8s2kMpoH06}KL z#BGL&eN$HlXJ;08q$cKM=A|E6>YSLIk=nd0vA8(3q_|^Ia>j&3#Z?T8@{1W3l@&8A zPRRul#Z|?#K_)CJ%b3ovF!|6zeUQRQYYo6a_Yz}ktOrns91{~a2Lrnt7sCP>4h62G dzZ`oSlv56Las;PL;8fnyQ*-`~Wewh{XT^ diff --git a/minio_data/.minio.sys/buckets/.usage.json/xl.meta b/minio_data/.minio.sys/buckets/.usage.json/xl.meta index f009352fa8e0df44c400960f70bd019e3782bf93..31727de97b485651d3ceb3de2cafd10035c08ad0 100644 GIT binary patch delta 161 zcmdlXv_oiu2G7rPb?>6jA7RN}y7=Wp6Jr7YCp~q|n>-jMT-H%DEPg6IvAmuQqGsYB z8-=vgRP$7W)Ktqv1B2vbW3v=v!&Ea1Q^OQP^JFvgG_%POjB0va>dAi?8<*sj=Hwh@ zUlXkKc9SNT)%(i8$2y9el#Bo0GA=n-_Rn&yLgKl^HvO>4YZztuO-!u}E%gkHOifIU N4Gl~--(XB&2LQpcKtlik delta 161 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$5p@p7>k%fu5 Nxq-R)<{OL&>;SjDKXd>9 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 a9611f02fa3887d6702d299928e982e97fa2096c..d6d981696c2f9f1316aa55a38cf343d5bd68780d 100644 GIT binary patch delta 320 zcmey&@`q)D2G7rPb(y}Kj?B_I}%Ug{f?nVWi9q61>c#6LC)X(^^gDVD~@M&^l0$w^7ZX=W)V zsm7*>W{IXrMyZCWlOq`QEH^9USu-{+$t%stIr8$Jg>L41v4Z5}$ybkOggspL+LmEe zhUISGmMCkJXpgEJ92#5o|FSULWo2;Tn!KM;P5RX;pTy#l(1Mi2lGNiotPDVqnK1P| zBk%M_lbA#@w&<4@JnpXtjD7KV!1_S#SDv6a)Cr~Rq<>FCMIqU z26m9{MP(V&WaEc7)|)M7G036JvqjQ*W(*Hr<1F>b3e0ZYvW+Czi*lA6ONT zSXAN|oLQAxJn@U0i%UWaQW8s2kMpoH06}KL z#BGKwxr~#TMB+OZC1*_BqF-8&lb@KfsJMz@QGPMQqOxL!#VNT!qPVJfHjtY!onc|} zA&}Y$KsA>bTVp+d>g1T1xH%Zu5VM6*QGB%@Tr)X5QydY1Psg!>sAm*kb^Gr}G&du_|G zD#LQOZ%dT5Nwi1R4GxX1`hQs%?y@qta82INs3!etl}}=ENoYYzVoB<89##e*$V`~} zo{@L@qe)C68C&#A3v%)kQ#uwUXG~aBTy=@DHP&NMezDx5vSNnCDY-zRxT<(I0}~TB z2Ln4u_oA|l>2e?j1H;1P!ytVVf%+JLdKefEu`@Jq2t*_?M3vO^_}3px*)T(S4&$mX GG8F*Z$AH!V delta 311 zcmeyv@|k6V2G5U6e`~jO9$~qcx;1m6iLn3&?<2J*OsqO#ET3EmVj75dZ}+nW^U?bi4KS%6aUyK7#k&-nV4IcB%52N7#k*9q#2u< znKYA!Lh#(DtN$uTi;b1<;W9p+$Y;85_=*j3tMf8fF+v-IMSqbF|gWLr4;0{|FX BfLH(k 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 9463a1b7a0402634c3436912a2548a6f39700436..e974690570e355055cf38f4d210182b021f06a90 100644 GIT binary patch delta 175 zcmV;g08szG1iu83C=B_|a=4(3!~|LU^v01iGZ3Z*Z<+B&MI4!(v~xQ)>uQmAe+K!^ za=4(3k^DU$F*Gw`V=*>2WHdE7Vl`qoG-fwuWH)1CVq;`EWjQdDSOG3AueHQd0fD4$ zb!=?JtYyI=UZcR9=l10Bb?|RAq}s}~2+W6x*~zivf6u+OfRnQUD;w&nOks0$Rd8fs dbY;g31poj50BK;A?lA>77?b${LIgsnF05oEPG0~3 delta 175 zcmdnbvY%yw2G5U6e-+zJjB>?CPy&p>E857uV-vrl2@9Ob7alY zoW)`Sm%3z|qmyjpubw~pbp2kRghJL8y}#`jPMlI?4C8@`G aSQ&sIGhyO3gVL|@lYcTgu~d9cvj+fTQA&jX diff --git a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta index 983c9825e852ee51dee6ac85fe84b3be567e434a..51778577b260f2130cf3e20536e7052cf704e93d 100644 GIT binary patch delta 190 zcmV;v073u11iu83C=B_|a)VW4#01JOh69l_GZ5DzNX~a}L?Wnr$6-HYMn{o%e+K!^ za)VW4k^D82SOF$nq-AtrXW1ZQF*Gw`V=*>2WHdE7Vl`qoG-fwuWH)1CVq;`EWjQdR zcFt#F%K*;4ZloyzfuwGAY;44=Wx*j{qrjWz_T=$(@NYDv+RC&D%!i5D$+6>q&%L#P sld}OU8|tb|VRLj2WKaN?8-!^xW& qwWV*Y@<}W%2`xxTEJ;1i!^!{znF$lO8I*pFpZt^2iKXImnmqt} { - int aType = a.getProjectType() != null ? a.getProjectType() : 1; - int bType = b.getProjectType() != null ? b.getProjectType() : 1; - if (aType != bType) { - return bType - aType; - } - int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0; - int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0; - return bDuration - aDuration; + // 按项目ID分组,计算每个项目的总时长 + Map> groupsByProject = groups.stream() + .collect(Collectors.groupingBy(ScheduleGroupData::getProjectId)); + + // 计算每个项目的总时长 + Map projectTotalDuration = new HashMap<>(); + groupsByProject.forEach((projectId, projectGroups) -> { + int total = projectGroups.stream().mapToInt(g -> g.getEstimatedDuration() != null ? g.getEstimatedDuration() : 0).sum(); + projectTotalDuration.put(projectId, total); }); - log.info("排序后分组顺序(集体优先): {}", - groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", "))); + // 排序策略: 集体项目优先,然后按项目总时长降序(大项目优先安排) + List sortedProjectIds = new ArrayList<>(groupsByProject.keySet()); + sortedProjectIds.sort((a, b) -> { + List groupsA = groupsByProject.get(a); + List groupsB = groupsByProject.get(b); + int typeA = groupsA.get(0).getProjectType() != null ? groupsA.get(0).getProjectType() : 1; + int typeB = groupsB.get(0).getProjectType() != null ? groupsB.get(0).getProjectType() : 1; + if (typeA != typeB) { + return typeB - typeA; // 集体项目优先 + } + return projectTotalDuration.get(b) - projectTotalDuration.get(a); // 时长长的优先 + }); + + log.info("项目排序(集体优先,时长降序): {}", sortedProjectIds.stream() + .map(id -> groupsByProject.get(id).get(0).getGroupName().split(" ")[0] + "(" + projectTotalDuration.get(id) + "分钟)") + .collect(Collectors.joining(", "))); - // 记录每个项目已分配的场地ID,用于同一项目优先分配到同一场地 - Map projectVenueMap = new HashMap<>(); int assignedCount = 0; + Map projectVenueMap = new HashMap<>(); // 记录项目分配的场地 + Map projectSlotMap = new HashMap<>(); // 记录项目分配的时间段 - for (ScheduleGroupData group : groups) { - Long projectId = group.getProjectId(); - Long preferredVenueId = projectVenueMap.get(projectId); + // 按项目分配(同一项目的所有分组尽量在同一场地同一时间段) + for (Long projectId : sortedProjectIds) { + List projectGroups = groupsByProject.get(projectId); + int projectDuration = projectTotalDuration.get(projectId); + String projectName = projectGroups.get(0).getGroupName().split(" ")[0]; - SlotInfo bestSlot = null; - - // 优先策略: 如果该项目已有分配的场地,优先选择同一场地的其他时间段 - 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(null); + log.info("开始分配项目 '{}': {}个分组, 总时长{}分钟", projectName, projectGroups.size(), projectDuration); + + // 找到能容纳整个项目的最佳槽位(优先选择负载最小的) + SlotInfo bestSlot = slots.stream() + .filter(s -> s.currentLoad + projectDuration <= s.capacity) + .min((a, b) -> { + // 优先选择负载最小的槽位 + return Integer.compare(a.currentLoad, b.currentLoad); + }) + .orElse(null); + + if (bestSlot != null) { + // 找到能容纳整个项目的槽位,所有分组都分配到这个槽位 + log.info("项目 '{}' 整体分配到: 场地={}, 时段={}, 剩余容量={}分钟", + projectName, bestSlot.venueName, bestSlot.timeSlot, bestSlot.capacity - bestSlot.currentLoad); - 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); + for (ScheduleGroupData group : projectGroups) { + group.setAssignedVenueId(bestSlot.venueId); + group.setAssignedVenueName(bestSlot.venueName); + group.setAssignedDate(bestSlot.date); + group.setAssignedTimeSlot(bestSlot.timeSlot); + group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex); + group.setAssignedTimePeriod(bestSlot.period); + + bestSlot.currentLoad += group.getEstimatedDuration(); + assignedCount++; + + log.info(" 分组 '{}' 分配完成, 时长={}分钟, 槽位负载={}分钟", + group.getGroupName(), group.getEstimatedDuration(), bestSlot.currentLoad); } - 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.setAssignedTimePeriod(bestSlot.period); - - bestSlot.currentLoad += group.getEstimatedDuration(); - assignedCount++; - - // 记录该项目分配的场地 - if (!projectVenueMap.containsKey(projectId)) { projectVenueMap.put(projectId, bestSlot.venueId); - log.info("项目 '{}' 首次分配到场地: {}", group.getGroupName(), bestSlot.venueName); + projectSlotMap.put(projectId, bestSlot.timeSlotIndex); + } else { + // 没有单个槽位能容纳整个项目,需要分散到多个槽位 + log.warn("项目 '{}' 总时长{}分钟超过单个时段容量,需要分散分配", projectName, projectDuration); + + // 优先选择同一场地的连续时间段 + final Long[] preferredVenueId = {null}; + + for (ScheduleGroupData group : projectGroups) { + SlotInfo targetSlot = null; + + // 如果已有优先场地,先尝试在同一场地找槽位 + if (preferredVenueId[0] != null) { + targetSlot = slots.stream() + .filter(s -> s.venueId.equals(preferredVenueId[0])) + .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) + .min((a, b) -> Integer.compare(a.timeSlotIndex, b.timeSlotIndex)) // 优先选择较早的时间段 + .orElse(null); + } + + // 如果优先场地没有容量,选择任意有容量的槽位 + if (targetSlot == null) { + targetSlot = slots.stream() + .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) + .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) + .orElse(null); + } + + // 如果都没有,选择负载最小的(允许超载) + if (targetSlot == null) { + targetSlot = slots.stream() + .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) + .orElse(slots.get(0)); + log.warn(" 分组 '{}' 需要超载分配", group.getGroupName()); + } + + group.setAssignedVenueId(targetSlot.venueId); + group.setAssignedVenueName(targetSlot.venueName); + group.setAssignedDate(targetSlot.date); + group.setAssignedTimeSlot(targetSlot.timeSlot); + group.setAssignedTimeSlotIndex(targetSlot.timeSlotIndex); + group.setAssignedTimePeriod(targetSlot.period); + + targetSlot.currentLoad += group.getEstimatedDuration(); + assignedCount++; + + // 记录优先场地 + if (preferredVenueId[0] == null) { + preferredVenueId[0] = targetSlot.venueId; + } + + log.info(" 分组 '{}' 分配到: 场地={}, 时段={}, 时长={}分钟", + group.getGroupName(), targetSlot.venueName, targetSlot.timeSlot, group.getEstimatedDuration()); + } + + projectVenueMap.put(projectId, preferredVenueId[0]); } - - log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)", - group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot, - group.getEstimatedDuration(), bestSlot.currentLoad, - (int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity)); } 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) {