From 496537ceef345ba03df36c964386f7843d3c9a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Thu, 8 Jan 2026 15:42:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=A1=B94:=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=87=BA=E5=9C=BA=E9=A1=BA=E5=BA=8F=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 新增LineupGroupVO和LineupParticipantVO类 - 在MartialMiniController中添加/schedule/status和/schedule/lineup接口 - 注入MartialScheduleStatusMapper和MartialScheduleGroupMapper Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../buckets/.bloomcycle.bin/xl.meta | Bin 743 -> 743 bytes .../buckets/.usage-cache.bin.bkp/xl.meta | Bin 632 -> 628 bytes .../buckets/.usage-cache.bin/xl.meta | Bin 632 -> 628 bytes .../.minio.sys/buckets/.usage.json/xl.meta | Bin 2360 -> 2360 bytes .../.usage-cache.bin.bkp/xl.meta | Bin 636 -> 636 bytes .../000000-assets/.usage-cache.bin/xl.meta | Bin 636 -> 636 bytes .../assets/.usage-cache.bin.bkp/xl.meta | Bin 575 -> 575 bytes .../buckets/assets/.usage-cache.bin/xl.meta | Bin 575 -> 575 bytes .../controller/MartialExportController.java | 72 +-- .../controller/MartialMiniController.java | 150 ++++++ .../MartialMiniController.java.backup | 499 ++++++++++++++++-- .../martial/excel/ScheduleExportExcel2.java | 47 ++ .../martial/pojo/vo/LineupGroupVO.java | 22 + .../martial/pojo/vo/LineupParticipantVO.java | 18 + .../service/IMartialScheduleService.java | 54 +- .../impl/MartialScheduleServiceImpl.java | 35 ++ 16 files changed, 763 insertions(+), 134 deletions(-) create mode 100644 src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java create mode 100644 src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java create mode 100644 src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 05f6351d9a51d3bc11750a43fdd0c8b1b5defaab..a222be542b5e9e37fe9e4af5424c77258e3560b6 100644 GIT binary patch delta 460 zcmaFP`kZxw2G7RXS{q;9Ji_AU?-wx9#8{xR;MpVRP$!jD_xrY$sc$Dwr52niwRdnHrg!rWvJLq$FFUnWmT-7+ac}T9}$zS}rU*m!5Q* z;oQ?@I+cu#OY%x{a*i?v+pVej*X(*t{C(?x&v*J)Yi_yvI5#a>GUrC;s&jgWU#vUg z$^Zr}%acosic<4R7?$LvR+OA|J+i#GB(bO@HKqPI4=YGXW_dt2iqOH(`?F#RD|?b?akhiaBiHT_N51o2yU+SM(+d!ccu2T&Ls%$Qyqpq-{IVN fMeXm^^AX%w?GMqd2<}W_?Jv9J5&Wy#9}D6EE03jX delta 460 zcmaFP`kZxw22a>jJ8!9zM_8N>w9TGqVk|HxIeFz;Y0ovS{@b~eepfY3EU#ySsG0c3 zRDDTmNn-jiDoV{OVOWxvT2XS=-eGxhNn%k+YRYjQR*;I!*c(r@x+PWDC08crq&ny4 z7UZM?W$!QuKoryiRa{NbPPcmt*Kl=?cHt#n1owt^-Ctuk_nL@yKr{=28=&2)qK4r1 zXzy98g5aLep0WKHoO_)`dx=6Gg6p8YE}K^!!LQNYsa%fW?l29L=tFS7X#be+59i)6 f(EiBy6T!{V5x$IY$Bh-*=g-6;WS?mNyYmnL)f1p$ 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 52e3f9af502c7711bae248388087f20a0fd9e2fe..44bb2bf9b24a8ef3f2530263d0c953279b6c4cc2 100644 GIT binary patch delta 341 zcmeyt@`YuB2G7RXT7B=ej7~9QnYpQ#B|0F6O#EY`U}kA-nv!I0m|~fllxmb{Xpxd^ zmS$#dm}X*Xkz$aPG&zD%&+4*7`%A{gC3&SeIY%CvYF;|zZGX5guU1idb>dHUwdhl- za(@yE>y+7luKd-&&!MqZ|E~*!C_^tRL(1d>j8gTkhgbO|7MFw;q$HN49_L|Y0D{c; z_^aBhjIXW^&dx0GNKMSi%u7GC)HyLZBei*1VsUY5NpZ)bcb1oFawt delta 345 zcmeyu@`Gi922a>jJLUa{kFaF>-%p%qVl1%Dc99&1zC-NtyaK(+hkk@lERRz^x+);C zsKhZivnsWC;zfp|OF%3jz0@}(GdK0JLiGOSq3`|TcEK^gAQj$^=O)L$}4O1*r zlP%5D(u|T#jg3>xCr2>qSs5hfrZ6@x$t%stIr3EWlKb&*hqfB})IIn7s*=6i; z`NdL;%8D5lr{n^O;;Q1=42(?NtPJcRGZ&R*OqT*N7#J2NA6mu$R19S4%Y#gq#KB-8 c*my)mqse#yd(H#12U{--IPwJ@Df<}@0KZp+g8%>k diff --git a/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta index c0ca47f545f4ee6ddeb193c8891fdb198411c7bd..5d743d53896522522e96ed55fdb06b65647074f2 100644 GIT binary patch delta 341 zcmeyt@`YuB2G7RXTDfx;A7K&tAbV}1iLrpj$EFjj=Xx;AH~8g#D<#opVtJhU!Bqi? zMJ0~GnN_L96E89xTmoVN>7~9QnYpQ#B|0F6O#EY`U}kA-nv!I0m|~fllxmb{Xpxd^ zmS$#dm}X*Xkz$aPG&zD%&uUM;*ki`VC3&SeIY%CvYF;|zZGX5guU1idb>dHUwdhl- za(@yE>y+7luKd-&&!MqZ|E~*!C_^tRL(1d>j8gTkhgbO|7MFw;q$HN49_L|Y0D{c; z_^aBhjIXW^&dx0GNKMSi%u7GC)HyLZBei*1VsUY5NpZ)bcb12;YR0 delta 345 zcmeyu@`Gi922a>j+pqJwkFYRb&9IwjVl42pKwi~sjnBr$c{_Qxd|c!(u{=)w=&FFk zq7uj8%&OGli5D4;E&;KC^itoD%-qz=5*-jjCjPNeFfcK(uuM%cN=ZshG_f==H%zfi zO|~>oOEXF~H8xH$pB%xcXVs!r>(AJ@B(F3l=g3phOYXQ}^8St4j86lNWM) zg%9lHSu9Te3`yoV%%QPW|E~*!D8pJ-hAWc~FiO?i+pqFTEG`KxNJ%V7J0vjMV0}24J9@SX`W1Qrxj9Ib*`2;;KuGxk_6X z^Tq29&EiV;K&zrr0i!r0IZgVCjbBd diff --git a/minio_data/.minio.sys/buckets/.usage.json/xl.meta b/minio_data/.minio.sys/buckets/.usage.json/xl.meta index 6cf07058ea5d468ef06b9435b77885af037d679e..9c2ab0e9072ff2e91308e3cf8af755520c8a7dd1 100644 GIT binary patch delta 259 zcmdlXv_oiu2G7RXTALf=jOgVwInh9rb4Q*fr+K5p{1EYvRPVEqJ@!(p{1!&nrX5@N@}u!N#eq?bLmN^ z8P3(U^{Oy7F3Bs+$vMitrvIn>`^m;jvxMf)_^?9R+_yyaV}GXMU$3kBff5|auX`r1 zVU*CY2r)3XGBL6;G}bdWHZnCgF*7%hQqob%Ps&P7E-7}-FU>1avNE!q{D#qv-PF|3 z*v!;qvLRC}oXyBAJ$VDuaz@L^E7^>hjEpAtGOuB@*lfij$H-(^3)E(~xtnz@GXPf0 BUG@L~ delta 260 zcmdlXv_oiu22a>jJDB$?ImNS}7Udd+6WN10Lmw64N>1HbyIYuV4TA((=&E2ePnE?a= BTQC3s 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 8e176c6f65d6ebfe071b5d20d155fc9d5d86b5d8..f70f5bea1062192b013817fd69d6270e05a2da20 100644 GIT binary patch delta 233 zcmeyv@`q)D2G7RXn&$(j9$~pEtowbUiLt=1!YwQe+nv@gWJ_^8vX&!uVtG9qM9suM zrs_*lOA^y>Dp;hNSf(Xg8l)yC8>bo?7#bNFCmR_XTPCL(8l@N*nM^ihR5M*zb}l{X zG{d>mYZ5gX8<*sj=HwiCdFwFew$&%Us%%i?wz<)AYT4^MiklgXo;>T-xVujC$-AV< zn;8vdTo14ENh~f2El5c$Nj=WP$^ZnJ@$px+Zf{sViAhbh)oj+fx$vkFXSTNKBk)Vl2@3F8j}NL3hsWc4uBKe7>D`VtG9qM9suM zrjsKW)nb;UmL#U%R4_?3OEEMuGDu4?H8C_zHMg`hGBq_bPBk|+O|?w1uvl1jEqLs#PWJJh?-ppty<9c|NPhxRNXhBM1 zN$PPPRt6x*jE}#nb$i3|Nla?0t$y1Uj+XWxPj<8s)c#t^J#8`m;=Hs-f;@+}b?q7aum#>sJvAmuQqGsYB z8wHb8vlK%kBZIUQQxikeRC7y9BU4i|<5Y8F(^Sh83yaASjC#7uP5KrxHZI94&B;0P z^7be7S??MhzNyyQ-!(MX)U0_ko9BDq2R9wzFwLJYL|07S%xEZMZ@iX-lbUL-($+=!#ZrsPiWwHC(q)OcdMN?`Ue;3h 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 1394540798295de2a9ce46b4a53b5c855ecd072e..7d13aa91edd3cb1cdab3905e83fb803e0bb7d1e5 100644 GIT binary patch delta 176 zcmdnbvY%yw2G7RXT8^eJM_4+}oQs%fVl2R+=#u)P+pS^4qNtv&yFZTw=c1|Z0ckH4xNxoh$dMrRg}!W$I;jTkj0jBP{BS2@fWk7z>o*aY)$Q)zFsX;-!i9Cu-8kpfU%4>p8{;XhrEey0 rX4H~)J-o^%vA86(ASJOR^*9eJ0}y1!$6wWs+%@?Jqce*~;f)FaEq_d> delta 190 zcmdnbvY%yw22a>jTkfsKM_3M5olKr+Vk}^NQ&G~d(OGxS#YwLiQ(`|&EU#ySsG0c3 zbaDivTFjEvlEn0z3dX6%7KzD;sfo#{X~rg|Daol8X-P(=W~s)81_sGyDGSTar6-+c zICm$(u8y&BNnUAA&XF~>R}?eLe8iU?`~2(ddFSpH)2cpO)n{wmW%A#i{@Z%-+T_iQ qTGICRt9%lROF|1$5=&B#^RO}iL1yfYCtABdP5!~?%(C2~^&|koEl list = resultService.exportResults(competitionId, projectId); String fileName = "成绩单_" + DateUtil.today(); String sheetName = projectId != null ? "项目成绩单" : "全部成绩"; ExcelUtil.export(response, fileName, sheetName, list, ResultExportExcel.class); } - /** - * Task 3.2: 导出运动员名单 - */ @GetMapping("/athletes") @Operation(summary = "导出运动员名单", description = "导出指定赛事的运动员名单Excel") - public void exportAthletes( - @RequestParam Long competitionId, - HttpServletResponse response - ) { + public void exportAthletes(@RequestParam Long competitionId, HttpServletResponse response) { List list = athleteService.exportAthletes(competitionId); String fileName = "运动员名单_" + DateUtil.today(); ExcelUtil.export(response, fileName, "运动员名单", list, AthleteExportExcel.class); } - /** - * Task 3.3: 导出赛程表 - */ @GetMapping("/schedule") @Operation(summary = "导出赛程表", description = "导出指定赛事的赛程安排Excel") - public void exportSchedule( - @RequestParam Long competitionId, - HttpServletResponse response - ) { + public void exportSchedule(@RequestParam Long competitionId, HttpServletResponse response) { List list = scheduleService.exportSchedule(competitionId); String fileName = "赛程表_" + DateUtil.today(); ExcelUtil.export(response, fileName, "赛程安排", list, ScheduleExportExcel.class); } - /** - * Task 3.4: 生成单个证书(HTML格式) - */ - @GetMapping("/certificate/{resultId}") - @Operation(summary = "生成证书", description = "生成获奖证书HTML页面,可打印为PDF") - public void generateCertificate( - @PathVariable Long resultId, - HttpServletResponse response - ) throws IOException { - // 1. 获取证书数据 - CertificateVO certificate = resultService.generateCertificateData(resultId); + @GetMapping("/schedule2") + @Operation(summary = "导出赛程表-模板2", description = "按场地导出比赛时间表格式的赛程安排") + public void exportScheduleTemplate2(@RequestParam Long competitionId, @RequestParam(required = false) Long venueId, + @RequestParam(required = false) String venueName, @RequestParam(required = false) String timeSlot, HttpServletResponse response) { + List list = scheduleService.exportScheduleTemplate2(competitionId, venueId); + String fileName = "比赛时间_" + (venueName != null ? venueName : "全部场地") + "_" + DateUtil.today(); + String sheetName = (venueName != null ? venueName : "全部场地") + (timeSlot != null ? "_" + timeSlot : ""); + ExcelUtil.export(response, fileName, sheetName, list, ScheduleExportExcel2.class); + } - // 2. 读取HTML模板 + @GetMapping("/certificate/{resultId}") + @Operation(summary = "生成证书", description = "生成获奖证书HTML页面") + public void generateCertificate(@PathVariable Long resultId, HttpServletResponse response) throws IOException { + CertificateVO certificate = resultService.generateCertificateData(resultId); Path templatePath = Path.of("src/main/resources/templates/certificate/certificate.html"); String template = Files.readString(templatePath, StandardCharsets.UTF_8); - - // 3. 替换模板变量 String html = template .replace("${playerName}", certificate.getPlayerName()) .replace("${competitionName}", certificate.getCompetitionName()) @@ -109,15 +82,10 @@ public class MartialExportController { .replace("${medalClass}", certificate.getMedalClass()) .replace("${organization}", certificate.getOrganization()) .replace("${issueDate}", certificate.getIssueDate()); - - // 4. 返回HTML response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(html); } - /** - * Task 3.4: 批量生成证书数据 - */ @GetMapping("/certificates/batch") @Operation(summary = "批量生成证书数据", description = "批量获取项目获奖选手的证书数据") public R> batchGenerateCertificates(@RequestParam Long projectId) { @@ -125,14 +93,10 @@ public class MartialExportController { return R.data(certificates); } - /** - * Task 3.4: 获取单个证书数据(JSON格式) - */ @GetMapping("/certificate/data/{resultId}") - @Operation(summary = "获取证书数据", description = "获取证书数据(JSON格式),供前端渲染") + @Operation(summary = "获取证书数据", description = "获取证书数据(JSON格式)") public R getCertificateData(@PathVariable Long resultId) { CertificateVO certificate = resultService.generateCertificateData(resultId); return R.data(certificate); } - } diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java index a87bc25..83a7e81 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java @@ -19,6 +19,9 @@ import org.springblade.modules.martial.pojo.vo.MiniAthleteAdminVO; import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO; import org.springblade.modules.martial.pojo.vo.MiniLoginVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; +import org.springblade.modules.martial.pojo.vo.LineupGroupVO; +import org.springblade.modules.martial.pojo.vo.LineupParticipantVO; +import org.springblade.modules.martial.pojo.vo.ScheduleGroupDetailVO; import com.alibaba.fastjson.JSON; import org.springblade.modules.martial.service.*; import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO; @@ -27,6 +30,8 @@ import org.springblade.modules.martial.pojo.entity.MtVenue; import org.springblade.modules.martial.pojo.entity.MartialVenue; import org.springblade.modules.martial.pojo.entity.MartialResult; import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.modules.martial.mapper.MartialScheduleStatusMapper; +import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; @@ -34,6 +39,8 @@ import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.Duration; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -60,6 +67,8 @@ public class MartialMiniController extends BladeController { private final IMartialScoreService scoreService; private final BladeRedis bladeRedis; private final IMartialResultService resultService; + private final MartialScheduleStatusMapper scheduleStatusMapper; + private final MartialScheduleGroupMapper scheduleGroupMapper; // Redis缓存key前缀 private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:"; @@ -817,4 +826,145 @@ public class MartialMiniController extends BladeController { return R.data(list); } + // ========== 出场顺序相关 API ========== + + /** + * 获取编排状态 + */ + @GetMapping("/schedule/status") + @Operation(summary = "获取编排状态", description = "检查赛事编排是否完成") + public R> getScheduleStatus(@RequestParam Long competitionId) { + Map result = new HashMap<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialScheduleStatus::getCompetitionId, competitionId); + wrapper.eq(MartialScheduleStatus::getIsDeleted, 0); + wrapper.last("LIMIT 1"); + MartialScheduleStatus status = scheduleStatusMapper.selectOne(wrapper); + + if (status == null) { + result.put("isCompleted", false); + result.put("scheduleStatus", 0); + result.put("statusText", "未编排"); + return R.data(result); + } + + boolean isCompleted = status.getScheduleStatus() != null && status.getScheduleStatus() == 2; + result.put("isCompleted", isCompleted); + result.put("scheduleStatus", status.getScheduleStatus()); + result.put("statusText", getScheduleStatusText(status.getScheduleStatus())); + result.put("totalGroups", status.getTotalGroups()); + result.put("totalParticipants", status.getTotalParticipants()); + result.put("lockedTime", status.getLockedTime()); + + return R.data(result); + } + + private String getScheduleStatusText(Integer status) { + if (status == null) return "未编排"; + switch (status) { + case 0: return "未编排"; + case 1: return "编排中"; + case 2: return "已锁定"; + default: return "未知"; + } + } + + /** + * 获取出场顺序 + */ + @GetMapping("/schedule/lineup") + @Operation(summary = "获取出场顺序", description = "获取已编排的出场顺序列表") + public R> getLineup( + @RequestParam Long competitionId, + @RequestParam(required = false) Long projectId + ) { + Map result = new HashMap<>(); + + // 使用现有mapper查询编排详情 + List details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId); + + if (details == null || details.isEmpty()) { + result.put("groups", new ArrayList<>()); + return R.data(result); + } + + // 按项目过滤 + if (projectId != null) { + // 需要通过groupName或其他字段判断项目,这里先获取项目名 + MartialProject project = projectService.getById(projectId); + if (project != null) { + String projectName = project.getProjectName(); + details = details.stream() + .filter(d -> d.getGroupName() != null && d.getGroupName().contains(projectName)) + .collect(Collectors.toList()); + } + } + + // 转换为LineupGroupVO格式 + Map groupMap = new HashMap<>(); + for (ScheduleGroupDetailVO detail : details) { + Long groupId = detail.getGroupId(); + LineupGroupVO group = groupMap.get(groupId); + if (group == null) { + group = new LineupGroupVO(); + group.setGroupId(groupId); + group.setGroupName(detail.getGroupName()); + group.setCategory(detail.getCategory()); + group.setVenueName(detail.getVenueName()); + group.setTimeSlot(detail.getTimeSlot()); + group.setTableNo(generateTableNo(detail)); + group.setParticipants(new ArrayList<>()); + groupMap.put(groupId, group); + } + + // 添加参赛者 + if (detail.getParticipantId() != null) { + LineupParticipantVO participant = new LineupParticipantVO(); + participant.setId(detail.getParticipantId()); + participant.setOrder(detail.getPerformanceOrder() != null ? detail.getPerformanceOrder() : group.getParticipants().size() + 1); + participant.setPlayerName(detail.getPlayerName()); + participant.setOrganization(detail.getOrganization()); + participant.setStatus(detail.getScheduleStatus() != null ? detail.getScheduleStatus() : "waiting"); + group.getParticipants().add(participant); + } + } + + result.put("groups", new ArrayList<>(groupMap.values())); + return R.data(result); + } + + /** + * 生成表号: 场地(1位) + 时段(1位) + 序号(2位) + */ + private String generateTableNo(ScheduleGroupDetailVO detail) { + // 场地编号(简单取第一个数字或默认1) + int venueNo = 1; + if (detail.getVenueName() != null) { + String venueName = detail.getVenueName(); + for (char c : venueName.toCharArray()) { + if (Character.isDigit(c)) { + venueNo = Character.getNumericValue(c); + break; + } + } + } + + // 时段:上午=1, 下午=2 + int period = 1; + if (detail.getTimeSlot() != null) { + try { + int hour = Integer.parseInt(detail.getTimeSlot().split(":")[0]); + period = hour < 12 ? 1 : 2; + } catch (Exception e) { + // ignore + } + } + + // 序号:使用displayOrder或默认1 + int orderNo = detail.getDisplayOrder() != null ? detail.getDisplayOrder() : 1; + + return String.format("%d%d%02d", venueNo, period, orderNo); + } + } diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup index fcd1a79..a87bc25 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup @@ -21,12 +21,21 @@ import org.springblade.modules.martial.pojo.vo.MiniLoginVO; import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO; import com.alibaba.fastjson.JSON; import org.springblade.modules.martial.service.*; +import org.springblade.modules.martial.pojo.dto.ChiefJudgeConfirmDTO; +import org.springblade.modules.martial.pojo.dto.GeneralJudgeConfirmDTO; +import org.springblade.modules.martial.pojo.entity.MtVenue; +import org.springblade.modules.martial.pojo.entity.MartialVenue; +import org.springblade.modules.martial.pojo.entity.MartialResult; +import org.springblade.core.redis.cache.BladeRedis; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -45,9 +54,17 @@ public class MartialMiniController extends BladeController { private final IMartialJudgeService judgeService; private final IMartialCompetitionService competitionService; private final IMartialVenueService venueService; + private final IMtVenueService mtVenueService; private final IMartialProjectService projectService; private final IMartialAthleteService athleteService; private final IMartialScoreService scoreService; + private final BladeRedis bladeRedis; + private final IMartialResultService resultService; + + // Redis缓存key前缀 + private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:"; + // 登录缓存过期时间(7天) + private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7); /** * 登录验证 @@ -91,26 +108,55 @@ public class MartialMiniController extends BladeController { invite.setDeviceInfo(dto.getDeviceInfo()); judgeInviteService.updateById(invite); - MartialVenue venue = null; + // 从 martial_venue 表获取场地信息 + MartialVenue martialVenue = null; if (invite.getVenueId() != null) { - venue = venueService.getById(invite.getVenueId()); + martialVenue = venueService.getById(invite.getVenueId()); } - List projects = parseProjects(invite.getProjects()); + // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目 + List projects = new ArrayList<>(); + Integer refereeTypeVal = invite.getRefereeType(); + String roleVal = invite.getRole(); + boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3) + || "general_judge".equals(roleVal) || "general".equals(roleVal); + + if (isGeneralJudge) { + // 总裁判看所有项目 + projects = getAllProjectsByCompetition(competition.getId()); + } else if (Func.isNotEmpty(invite.getProjects())) { + projects = parseProjects(invite.getProjects()); + } else if (invite.getVenueId() != null) { + // 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表 + projects = getProjectsByVenue(invite.getVenueId()); + } + // 如果没有场地,projects保持为空列表 MiniLoginVO vo = new MiniLoginVO(); vo.setToken(token); - vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); + String role = invite.getRole(); + Integer refereeType = invite.getRefereeType(); + if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) { + vo.setUserRole("general"); + } else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) { + vo.setUserRole("admin"); + } else { + vo.setUserRole("pub"); + } vo.setMatchId(competition.getId()); vo.setMatchName(competition.getCompetitionName()); vo.setMatchTime(competition.getCompetitionStartTime() != null ? competition.getCompetitionStartTime().toString() : ""); vo.setJudgeId(judge.getId()); vo.setJudgeName(judge.getName()); - vo.setVenueId(venue != null ? venue.getId() : null); - vo.setVenueName(venue != null ? venue.getVenueName() : null); + vo.setVenueId(martialVenue != null ? martialVenue.getId() : null); + vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null); vo.setProjects(projects); + // 将登录信息缓存到Redis(服务重启后仍然有效) + String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE); + return R.data(vo); } @@ -152,8 +198,136 @@ public class MartialMiniController extends BladeController { } boolean success = scoreService.save(score); + + // 评分保存成功后,计算并更新选手总分 + if (success) { + Long athleteId = parseLong(dto.getAthleteId()); + Long projectId = parseLong(dto.getProjectId()); + Long venueId = parseLong(dto.getVenueId()); + if (athleteId != null && projectId != null) { + updateAthleteTotalScore(athleteId, projectId, venueId); + } + } + return success ? R.success("评分提交成功") : R.fail("评分提交失败"); } + + /** + * 计算并更新选手总分 + * 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值 + * 特殊情况:裁判数量<3时,直接取平均分 + * 只有所有裁判都评分完成后才更新总分 + */ + private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) { + try { + // 1. 查询该场地的裁判员数量 + int requiredJudgeCount = getRequiredJudgeCount(venueId); + + // 2. 获取主裁判ID列表 + List chiefJudgeIds = getChiefJudgeIds(venueId); + + // 3. 查询该选手在该项目的所有评分(排除主裁判的评分) + LambdaQueryWrapper scoreQuery = new LambdaQueryWrapper<>(); + scoreQuery.eq(MartialScore::getAthleteId, athleteId); + scoreQuery.eq(MartialScore::getProjectId, projectId); + scoreQuery.eq(MartialScore::getIsDeleted, 0); + // 排除主裁判的所有评分(包括普通评分和修改记录) + if (!chiefJudgeIds.isEmpty()) { + scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds); + } + List scores = scoreService.list(scoreQuery); + + // 4. 判断是否所有裁判都已评分 + if (scores == null || scores.isEmpty()) { + return; + } + + // 如果配置了裁判数量,检查是否评分完成 + if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) { + // 未完成评分,清空总分 + MartialAthlete athlete = athleteService.getById(athleteId); + if (athlete != null && athlete.getTotalScore() != null) { + athlete.setTotalScore(null); + athleteService.updateById(athlete); + } + return; + } + + // 4. 计算总分(去掉最高最低分取平均) + BigDecimal totalScore = calculateTotalScore(scores); + + // 5. 更新选手总分 + if (totalScore != null) { + MartialAthlete athlete = athleteService.getById(athleteId); + if (athlete != null) { + athlete.setTotalScore(totalScore); + athleteService.updateById(athlete); + } + } + } catch (Exception e) { + // 记录错误但不影响评分提交 + e.printStackTrace(); + } + } + + /** + * 获取项目应评分的裁判数量(裁判员,不包括主裁判) + * 按项目过滤:检查 projects JSON 字段是否包含该项目ID + */ + private int getRequiredJudgeCount(Long venueId) { + if (venueId == null || venueId <= 0) { + return 0; + } + LambdaQueryWrapper judgeQuery = new LambdaQueryWrapper<>(); + judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId); + judgeQuery.eq(MartialJudgeInvite::getRefereeType, 2); // Only count referees (type=2), exclude chief judge (type=1) and general judge (type=3) + List judges = judgeInviteService.list(judgeQuery); + // Use distinct judge_id to count unique judges + return (int) judges.stream() + .map(MartialJudgeInvite::getJudgeId) + .filter(Objects::nonNull) + .distinct() + .count(); + } + + /** + * 计算总分 + * 算法:去掉一个最高分和一个最低分,取剩余分数的平均值 + * 特殊情况:裁判数量<3时,直接取平均分 + */ + private BigDecimal calculateTotalScore(List scores) { + if (scores == null || scores.isEmpty()) { + return null; + } + + // 提取所有分数并排序 + List scoreValues = scores.stream() + .map(MartialScore::getScore) + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toList()); + + int count = scoreValues.size(); + if (count == 0) { + return null; + } + + if (count < 3) { + // 裁判数量<3,直接取平均分 + BigDecimal sum = scoreValues.stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP); + } + + // 去掉最高分和最低分(已排序,去掉第一个和最后一个) + List middleScores = scoreValues.subList(1, count - 1); + + // 计算平均分 + BigDecimal sum = middleScores.stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP); + } /** * 安全地将String转换为Long @@ -171,8 +345,8 @@ public class MartialMiniController extends BladeController { /** * 获取选手列表(支持分页) - * - 普通裁判:获取所有选手,标记是否已评分 - * - 裁判长:获取已有评分的选手列表 + * - 裁判员:获取所有选手,标记是否已评分 + * - 主裁判:获取所有裁判员都评分完成的选手列表 */ @GetMapping("/score/athletes") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @@ -181,6 +355,7 @@ public class MartialMiniController extends BladeController { @RequestParam Integer refereeType, @RequestParam(required = false) Long projectId, @RequestParam(required = false) Long venueId, + @RequestParam(required = false) Long competitionId, @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size ) { @@ -188,6 +363,11 @@ public class MartialMiniController extends BladeController { LambdaQueryWrapper athleteQuery = new LambdaQueryWrapper<>(); athleteQuery.eq(MartialAthlete::getIsDeleted, 0); + // 按比赛ID过滤(重要:确保只显示当前比赛的选手) + if (competitionId != null) { + athleteQuery.eq(MartialAthlete::getCompetitionId, competitionId); + } + if (projectId != null) { athleteQuery.eq(MartialAthlete::getProjectId, projectId); } @@ -196,35 +376,48 @@ public class MartialMiniController extends BladeController { List athletes = athleteService.list(athleteQuery); - // 2. 获取所有评分记录 + // 2. 获取该场地所有主裁判的judge_id列表 + List chiefJudgeIds = getChiefJudgeIds(venueId); + + // 3. 获取所有评分记录(排除主裁判的评分) LambdaQueryWrapper scoreQuery = new LambdaQueryWrapper<>(); scoreQuery.eq(MartialScore::getIsDeleted, 0); + if (projectId != null) { + scoreQuery.eq(MartialScore::getProjectId, projectId); + } + // 添加场地过滤 + if (venueId != null && venueId > 0) { + scoreQuery.eq(MartialScore::getVenueId, venueId); + } + // 排除主裁判的评分 + if (!chiefJudgeIds.isEmpty()) { + scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds); + } List allScores = scoreService.list(scoreQuery); // 按选手ID分组统计评分 java.util.Map> scoresByAthlete = allScores.stream() .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); - // 3. 根据裁判类型处理选手列表 + // 4. 获取该场地的应评裁判数量 + int requiredJudgeCount = getRequiredJudgeCount(venueId); + + // 5. 根据裁判类型处理选手列表 List filteredList; if (refereeType == 1) { - // 裁判长:返回已有评分的选手 + // 主裁判:返回所有选手,前端根据totalScore判断是否显示修改按钮 filteredList = athletes.stream() - .filter(athlete -> { - List scores = scoresByAthlete.get(athlete.getId()); - return scores != null && !scores.isEmpty(); - }) - .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) + .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .collect(java.util.stream.Collectors.toList()); } else { - // 普通裁判:返回所有选手,标记是否已评分 + // 裁判员:返回所有选手,标记是否已评分 filteredList = athletes.stream() - .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) + .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .collect(java.util.stream.Collectors.toList()); } - // 4. 手动分页 + // 6. 手动分页 int total = filteredList.size(); int fromIndex = (current - 1) * size; int toIndex = Math.min(fromIndex + size, total); @@ -236,13 +429,31 @@ public class MartialMiniController extends BladeController { pageRecords = filteredList.subList(fromIndex, toIndex); } - // 5. 构建分页结果 + // 7. 构建分页结果 IPage page = new Page<>(current, size, total); page.setRecords(pageRecords); return R.data(page); } + /** + * 获取场地所有主裁判的judge_id列表 + */ + private List getChiefJudgeIds(Long venueId) { + if (venueId == null) { + return new ArrayList<>(); + } + LambdaQueryWrapper judgeQuery = new LambdaQueryWrapper<>(); + judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId); + judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge"); + List chiefJudges = judgeInviteService.list(judgeQuery); + return chiefJudges.stream() + .map(MartialJudgeInvite::getJudgeId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + /** * 获取评分详情 */ @@ -254,10 +465,10 @@ public class MartialMiniController extends BladeController { } /** - * 修改评分(裁判长) + * 修改评分(主裁判) */ @PutMapping("/score/modify") - @Operation(summary = "修改评分", description = "裁判长修改选手总分") + @Operation(summary = "修改评分", description = "主裁判修改选手总分") public R modifyScore(@RequestBody MiniScoreModifyDTO dto) { boolean success = scoreService.modifyScoreByAdmin(dto); return success ? R.success("修改成功") : R.fail("修改失败"); @@ -268,26 +479,107 @@ public class MartialMiniController extends BladeController { */ @PostMapping("/logout") @Operation(summary = "退出登录", description = "清除登录状态") - public R logout() { + public R logout(@RequestHeader(value = "Authorization", required = false) String token) { + // 从Redis删除登录缓存 + if (token != null && !token.isEmpty()) { + String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + bladeRedis.del(cacheKey); + } return R.success("退出成功"); } /** - * Token验证 + * Token验证(从Redis恢复登录状态) */ @GetMapping("/verify") - @Operation(summary = "Token验证", description = "验证当前token是否有效") - public R verify() { - return R.success("Token有效"); + @Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态") + public R verify(@RequestHeader(value = "Authorization", required = false) String token) { + if (token == null || token.isEmpty()) { + return R.fail("Token不能为空"); + } + + // 从Redis获取登录信息 + String cacheKey = MINI_LOGIN_CACHE_PREFIX + token; + MiniLoginVO loginInfo = bladeRedis.get(cacheKey); + + if (loginInfo != null) { + // 刷新缓存过期时间 + bladeRedis.setEx(cacheKey, loginInfo, LOGIN_CACHE_EXPIRE); + return R.data(loginInfo); + } + + // Redis中没有,尝试从数据库恢复 + LambdaQueryWrapper inviteQuery = new LambdaQueryWrapper<>(); + inviteQuery.eq(MartialJudgeInvite::getAccessToken, token); + inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery); + + if (invite == null) { + return R.fail("Token无效"); + } + + if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) { + return R.fail("Token已过期"); + } + + // 重建登录信息 + MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); + MartialJudge judge = judgeService.getById(invite.getJudgeId()); + MartialVenue martialVenue = invite.getVenueId() != null ? venueService.getById(invite.getVenueId()) : null; + // 获取项目列表:总裁判看所有项目,其他裁判根据场地获取项目 + List projects = new ArrayList<>(); + Integer refereeTypeVal = invite.getRefereeType(); + String roleVal = invite.getRole(); + boolean isGeneralJudge = (refereeTypeVal != null && refereeTypeVal == 3) + || "general_judge".equals(roleVal) || "general".equals(roleVal); + + if (isGeneralJudge) { + // 总裁判看所有项目 + projects = getAllProjectsByCompetition(competition.getId()); + } else if (Func.isNotEmpty(invite.getProjects())) { + projects = parseProjects(invite.getProjects()); + } else if (invite.getVenueId() != null) { + // 未指定项目,根据场地获取项目;如果场地没有项目则返回空列表 + projects = getProjectsByVenue(invite.getVenueId()); + } + // 如果没有场地,projects保持为空列表 + + MiniLoginVO vo = new MiniLoginVO(); + vo.setToken(token); + String role = invite.getRole(); + Integer refereeType = invite.getRefereeType(); + if ("general_judge".equals(role) || "general".equals(role) || (refereeType != null && refereeType == 3)) { + vo.setUserRole("general"); + } else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 1)) { + vo.setUserRole("admin"); + } else { + vo.setUserRole("pub"); + } + vo.setMatchId(competition != null ? competition.getId() : null); + vo.setMatchName(competition != null ? competition.getCompetitionName() : null); + vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ? + competition.getCompetitionStartTime().toString() : ""); + vo.setJudgeId(judge != null ? judge.getId() : null); + vo.setJudgeName(judge != null ? judge.getName() : null); + vo.setVenueId(martialVenue != null ? martialVenue.getId() : null); + vo.setVenueName(martialVenue != null ? martialVenue.getVenueName() : null); + vo.setProjects(projects); + + // 重新缓存到Redis + bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE); + + return R.data(vo); } /** * 转换选手实体为VO + * 新增:只有评分完成时才显示总分 */ private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO( MartialAthlete athlete, List scores, - Long currentJudgeId) { + Long currentJudgeId, + int requiredJudgeCount) { org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); vo.setAthleteId(athlete.getId()); vo.setName(athlete.getPlayerName()); @@ -296,7 +588,9 @@ public class MartialMiniController extends BladeController { vo.setTeam(athlete.getTeamName()); vo.setOrderNum(athlete.getOrderNum()); vo.setCompetitionStatus(athlete.getCompetitionStatus()); - vo.setTotalScore(athlete.getTotalScore()); + + // 设置应评分裁判数量 + vo.setRequiredJudgeCount(requiredJudgeCount); // 设置项目名称 if (athlete.getProjectId() != null) { @@ -307,8 +601,10 @@ public class MartialMiniController extends BladeController { } // 设置评分状态 + int scoredCount = 0; if (scores != null && !scores.isEmpty()) { - vo.setScoredJudgeCount(scores.size()); + scoredCount = scores.size(); + vo.setScoredJudgeCount(scoredCount); // 查找当前裁判的评分 MartialScore myScore = scores.stream() @@ -326,6 +622,23 @@ public class MartialMiniController extends BladeController { vo.setScored(false); vo.setScoredJudgeCount(0); } + + // 判断评分是否完成(所有裁判都已评分) + boolean scoringComplete = false; + if (requiredJudgeCount > 0) { + scoringComplete = scoredCount >= requiredJudgeCount; + } else { + // 如果没有配置裁判数量,只要有评分就算完成 + scoringComplete = scoredCount > 0; + } + vo.setScoringComplete(scoringComplete); + + // 只有评分完成时才显示总分 + if (scoringComplete) { + vo.setTotalScore(athlete.getTotalScore()); + } else { + vo.setTotalScore(null); + } return vo; } @@ -378,4 +691,130 @@ public class MartialMiniController extends BladeController { return projects; } + + /** + * 获取比赛的所有项目 + */ + private List getAllProjectsByCompetition(Long competitionId) { + List projects = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialProject::getCompetitionId, competitionId); + wrapper.eq(MartialProject::getIsDeleted, 0); + + List projectList = projectService.list(wrapper); + + if (Func.isNotEmpty(projectList)) { + projects = projectList.stream().map(project -> { + MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo(); + info.setProjectId(project.getId()); + info.setProjectName(project.getProjectName()); + return info; + }).collect(Collectors.toList()); + } + + return projects; + } + + /** + * 根据场地获取项目列表 + */ + private List getProjectsByVenue(Long venueId) { + List projects = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialProject::getVenueId, venueId); + wrapper.eq(MartialProject::getIsDeleted, 0); + + List projectList = projectService.list(wrapper); + + if (Func.isNotEmpty(projectList)) { + projects = projectList.stream().map(project -> { + MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo(); + info.setProjectId(project.getId()); + info.setProjectName(project.getProjectName()); + return info; + }).collect(Collectors.toList()); + } + + return projects; + } + + + // ========== 三级裁判评分流程 API ========== + + /** + * 主裁判确认/修改分数 + */ + @PostMapping("/chief/confirm") + @Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数") + public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) { + Long resultId = parseLong(dto.getResultId()); + Long chiefJudgeId = parseLong(dto.getChiefJudgeId()); + if (resultId == null || chiefJudgeId == null) { + return R.fail("参数错误"); + } + boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote()); + return success ? R.success("确认成功") : R.fail("确认失败"); + } + + /** + * 总裁确认/修改分数 + */ + @PostMapping("/general/confirm") + @Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数") + public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) { + Long resultId = parseLong(dto.getResultId()); + Long generalJudgeId = parseLong(dto.getGeneralJudgeId()); + if (resultId == null || generalJudgeId == null) { + return R.fail("参数错误"); + } + boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote()); + return success ? R.success("确认成功") : R.fail("确认失败"); + } + + /** + * 获取待主裁判确认的成绩列表 + */ + @GetMapping("/chief/pending") + @Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表") + public R> getPendingChiefConfirmList(@RequestParam Long venueId) { + List list = resultService.getPendingChiefConfirmList(venueId); + return R.data(list); + } + + /** + * 获取待总裁确认的成绩列表 + */ + @GetMapping("/general/pending") + @Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)") + public R> getPendingGeneralConfirmList(@RequestParam Long competitionId) { + List list = resultService.getPendingGeneralConfirmList(competitionId); + return R.data(list); + } + + /** + * 获取所有场地列表(总裁用) + */ + @GetMapping("/general/venues") + @Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表") + public R> getAllVenues(@RequestParam Long competitionId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(MartialVenue::getCompetitionId, competitionId); + wrapper.eq(MartialVenue::getIsDeleted, 0); + wrapper.orderByAsc(MartialVenue::getVenueName); + List venues = venueService.list(wrapper); + return R.data(venues); + } + + /** + * 获取已总裁确认的成绩列表 + */ + @GetMapping("/general/confirmed") + @Operation(summary = "已总裁确认列表", description = "获取已总裁确认的成绩列表") + public R> getConfirmedGeneralList(@RequestParam Long competitionId) { + List list = resultService.getConfirmedGeneralList(competitionId); + return R.data(list); + } + } diff --git a/src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java b/src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java new file mode 100644 index 0000000..17cd8af --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/excel/ScheduleExportExcel2.java @@ -0,0 +1,47 @@ +package org.springblade.modules.martial.excel; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.alibaba.excel.annotation.write.style.ContentRowHeight; +import com.alibaba.excel.annotation.write.style.HeadRowHeight; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Schedule export Excel - Template 2 (Competition Schedule Format) + * Format: Sequence, Project, Participants, Groups, Time, Table Number + */ +@Data +@ColumnWidth(12) +@HeadRowHeight(25) +@ContentRowHeight(20) +public class ScheduleExportExcel2 implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @ExcelProperty("序号") + @ColumnWidth(8) + private Integer sequenceNo; + + @ExcelProperty("项目") + @ColumnWidth(25) + private String projectName; + + @ExcelProperty("人数") + @ColumnWidth(8) + private Integer participantCount; + + @ExcelProperty("组数") + @ColumnWidth(8) + private Integer groupCount; + + @ExcelProperty("时间") + @ColumnWidth(10) + private Integer durationMinutes; + + @ExcelProperty("表号") + @ColumnWidth(10) + private String tableNo; +} diff --git a/src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java new file mode 100644 index 0000000..6b3e2e1 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupGroupVO.java @@ -0,0 +1,22 @@ +package org.springblade.modules.martial.pojo.vo; + +import lombok.Data; +import java.io.Serializable; +import java.util.List; + +/** + * 出场顺序分组VO + */ +@Data +public class LineupGroupVO implements Serializable { + private static final long serialVersionUID = 1L; + + private Long groupId; + private String groupName; + private String projectName; + private String category; + private String venueName; + private String timeSlot; + private String tableNo; + private List participants; +} diff --git a/src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java new file mode 100644 index 0000000..7ae5a98 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/pojo/vo/LineupParticipantVO.java @@ -0,0 +1,18 @@ +package org.springblade.modules.martial.pojo.vo; + +import lombok.Data; +import java.io.Serializable; + +/** + * 出场顺序参赛者VO + */ +@Data +public class LineupParticipantVO implements Serializable { + private static final long serialVersionUID = 1L; + + private Long id; + private Integer order; + private String playerName; + private String organization; + private String status; +} diff --git a/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java b/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java index b739955..363892c 100644 --- a/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java +++ b/src/main/java/org/springblade/modules/martial/service/IMartialScheduleService.java @@ -2,6 +2,7 @@ package org.springblade.modules.martial.service; import com.baomidou.mybatisplus.extension.service.IService; import org.springblade.modules.martial.excel.ScheduleExportExcel; +import org.springblade.modules.martial.excel.ScheduleExportExcel2; import org.springblade.modules.martial.pojo.dto.MoveScheduleGroupDTO; import org.springblade.modules.martial.pojo.dto.SaveScheduleDraftDTO; import org.springblade.modules.martial.pojo.dto.ScheduleResultDTO; @@ -10,74 +11,27 @@ import org.springblade.modules.martial.pojo.entity.MartialSchedule; import java.util.List; /** - * Schedule 服务类 - * - * @author BladeX + * Schedule Service */ public interface IMartialScheduleService extends IService { - /** - * Task 3.3: 导出赛程表 - */ List exportSchedule(Long competitionId); - /** - * 获取赛程编排结果 - * @param competitionId 赛事ID - * @return 赛程编排结果 - */ + List exportScheduleTemplate2(Long competitionId, Long venueId); + ScheduleResultDTO getScheduleResult(Long competitionId); - /** - * 保存编排草稿 - * @param dto 编排草稿数据 - * @return 是否成功 - */ boolean saveDraftSchedule(SaveScheduleDraftDTO dto); - /** - * 完成编排并锁定 - * @param competitionId 赛事ID - * @return 是否成功 - */ boolean saveAndLockSchedule(Long competitionId); - /** - * 移动赛程分组到指定场地和时间段 - * @param dto 移动请求数据 - * @return 是否成功 - */ boolean moveScheduleGroup(MoveScheduleGroupDTO dto); - /** - * 获取调度数据 - * @param competitionId 赛事ID - * @param venueId 场地ID - * @param timeSlotIndex 时间段索引 - * @return 调度数据 - */ org.springblade.modules.martial.pojo.vo.DispatchDataVO getDispatchData(Long competitionId, Long venueId, Integer timeSlotIndex); - /** - * 调整出场顺序 - * @param dto 调整请求数据 - * @return 是否成功 - */ boolean adjustOrder(org.springblade.modules.martial.pojo.dto.AdjustOrderDTO dto); - /** - * 批量保存调度 - * @param dto 保存调度数据 - * @return 是否成功 - */ boolean saveDispatch(org.springblade.modules.martial.pojo.dto.SaveDispatchDTO dto); - /** - * 更新参赛者签到状态 - * @param participantId 参赛者ID - * @param status 状态:未签到/已签到/异常 - * @return 是否成功 - */ boolean updateParticipantCheckInStatus(Long participantId, String status); - } diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java index 4c2f160..03f0337 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java @@ -3,6 +3,7 @@ package org.springblade.modules.martial.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springblade.modules.martial.excel.ScheduleExportExcel; +import org.springblade.modules.martial.excel.ScheduleExportExcel2; import org.springblade.modules.martial.mapper.MartialScheduleDetailMapper; import org.springblade.modules.martial.mapper.MartialScheduleGroupMapper; import org.springblade.modules.martial.mapper.MartialScheduleParticipantMapper; @@ -136,6 +137,40 @@ public class MartialScheduleServiceImpl extends ServiceImpl exportScheduleTemplate2(Long competitionId, Long venueId) { + List exportList = new ArrayList<>(); + List details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId); + if (details.isEmpty()) { + return exportList; + } + if (venueId != null) { + details = details.stream().filter(d -> venueId.equals(d.getVenueId())).collect(Collectors.toList()); + } + Map> groupMap = details.stream() + .collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId)); + List sortedGroupIds = details.stream() + .collect(Collectors.toMap(ScheduleGroupDetailVO::getGroupId, d -> d.getDisplayOrder() != null ? d.getDisplayOrder() : 999, (a, b) -> a)) + .entrySet().stream().sorted(Map.Entry.comparingByValue()).map(Map.Entry::getKey).collect(Collectors.toList()); + int sequenceNo = 1; + int tableNoBase = 1101; + for (Long groupId : sortedGroupIds) { + List groupDetails = groupMap.get(groupId); + if (groupDetails == null || groupDetails.isEmpty()) continue; + ScheduleGroupDetailVO firstDetail = groupDetails.get(0); + long participantCount = groupDetails.stream().filter(d -> d.getParticipantId() != null).count(); + int durationMinutes = (int) (participantCount * 4); + ScheduleExportExcel2 excel = new ScheduleExportExcel2(); + excel.setSequenceNo(sequenceNo++); + excel.setProjectName(firstDetail.getGroupName()); + excel.setParticipantCount((int) participantCount); + excel.setGroupCount(1); + excel.setDurationMinutes(durationMinutes); + excel.setTableNo(String.valueOf(tableNoBase++)); + exportList.add(excel); + } + return exportList; + } /** * 获取赛程编排结果(优化版本:使用单次JOIN查询)