PasswordTest.java
File was deleted backend/pom.xml
@@ -146,6 +146,18 @@ <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!-- Apache POI for Word/Excel document generation --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.5</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.5</version> </dependency> </dependencies> <build> @@ -155,8 +167,16 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> <!-- çæå¯æ§è¡çèå ï¼ä¾¿äºæ¬å°ç´æ¥è¿è¡ --> <skip>false</skip> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <!-- 卿å é¶æ®µå¤å¶ææä¾èµå° target/lib --> @@ -182,21 +202,7 @@ </executions> </plugin> <!-- çæå¯æ§è¡ç¦ JARï¼åå ¥ Main-Class ä¸ Class-Path æå lib/ --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.rongyichuang.RycBackendApplication</mainClass> </manifest> </archive> </configuration> </plugin> <!-- ä½¿ç¨ Spring Boot repackage çæå¯æ§è¡èå ï¼ç§»é¤èªå®ä¹ Jar æ¸ å以é¿å å²çª --> </plugins> </build> </project> backend/src/main/java/com/rongyichuang/activity/dto/ActivityResponse.java
@@ -23,6 +23,8 @@ private Integer state; private LocalDateTime createTime; private LocalDateTime updateTime; // æè¿ä¸æ¬¡è¯å®¡å¯¼åºZIPä¸è½½URL private String reviewExportUrl; // å ³èæ°æ® private RatingSchemeResponse ratingScheme; @@ -57,6 +59,7 @@ this.state = activity.getState(); this.createTime = activity.getCreateTime(); this.updateTime = activity.getUpdateTime(); this.reviewExportUrl = activity.getReviewExportUrl(); // è®¾ç½®ç¶æåç§° this.stateName = getStateNameByValue(activity.getState()); @@ -191,6 +194,14 @@ public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; } public String getReviewExportUrl() { return reviewExportUrl; } public void setReviewExportUrl(String reviewExportUrl) { this.reviewExportUrl = reviewExportUrl; } public RatingSchemeResponse getRatingScheme() { return ratingScheme; backend/src/main/java/com/rongyichuang/activity/entity/Activity.java
@@ -51,6 +51,12 @@ */ @Column(name = "state", nullable = false) private Integer state = 1; /** * è¯å®¡å¯¼åºZIPä¸è½½URLï¼æè¿ä¸æ¬¡å¯¼åºï¼ */ @Column(name = "review_export_url", length = 512) private String reviewExportUrl; // å ³èè¯åæ¨¡æ¿ @ManyToOne(fetch = FetchType.LAZY) @@ -195,6 +201,14 @@ public void setState(Integer state) { this.state = state; } public String getReviewExportUrl() { return reviewExportUrl; } public void setReviewExportUrl(String reviewExportUrl) { this.reviewExportUrl = reviewExportUrl; } // ä¸å¡æ¹æ³ public boolean isMainActivity() { backend/src/main/java/com/rongyichuang/config/SecurityConfig.java
@@ -48,7 +48,8 @@ .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/**", "/actuator/**", "/test/**", "/cleanup/**").permitAll() // 注æï¼åºç¨è®¾ç½®äº context-path=/apiï¼ä¸ºé¿å å¹é æ§ä¹ï¼è¿éåæ¶å¹é å»é¤åå å« context-path çè·¯å¾ .requestMatchers("/auth/**", "/api/auth/**", "/actuator/**", "/test/**", "/cleanup/**").permitAll() .requestMatchers("/api/health/**").permitAll() // å 许å¥åº·æ£æ¥ç«¯ç¹è®¿é® .requestMatchers("/upload/**").permitAll() .requestMatchers("/graphiql/**", "/graphql/**", "/api/graphql/**", "/api/graphiql/**").permitAll() // å 许GraphQLåGraphiQLè®¿é® backend/src/main/java/com/rongyichuang/judge/service/CosService.java
@@ -124,6 +124,39 @@ } /** * ç´æ¥ä¸ä¼ æ¬å°æä»¶å°COS */ public String uploadLocalFile(java.io.File file, String fileName) throws Exception { // çææä»¶è·¯å¾ï¼ææ¥æåç®å½ String dateDir = new java.text.SimpleDateFormat("yyyyMMdd").format(new Date()); String key = dateDir + "/" + fileName; System.out.println("=== COSæ¬å°æä»¶ä¸ä¼ è°è¯ä¿¡æ¯ ==="); System.out.println("æä»¶Key: " + key); System.out.println("æä»¶å¤§å°: " + file.length()); System.out.println("æä»¶è·¯å¾: " + file.getAbsolutePath()); // å建COS客æ·ç«¯ COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); ClientConfig clientConfig = new ClientConfig(new Region(region)); COSClient cosClient = new COSClient(cred, clientConfig); try { // å建ä¸ä¼ è¯·æ± com.qcloud.cos.model.PutObjectRequest putObjectRequest = new com.qcloud.cos.model.PutObjectRequest(bucket, key, file); // æ§è¡ä¸ä¼ com.qcloud.cos.model.PutObjectResult result = cosClient.putObject(putObjectRequest); System.out.println("ä¸ä¼ æåï¼ETag: " + result.getETag()); return key; // è¿åç¸å¯¹è·¯å¾ } finally { cosClient.shutdown(); } } /** * è·åæä»¶è®¿é® URL */ public String getFileUrl(String key) { backend/src/main/java/com/rongyichuang/review/dto/response/ReviewExportJobStatus.java
New file @@ -0,0 +1,44 @@ package com.rongyichuang.review.dto.response; /** * è¯å®¡å¯¼åºä»»å¡ç¶æååº */ public class ReviewExportJobStatus { public enum Status { PENDING, RUNNING, SUCCEEDED, FAILED } private String jobId; private Status status; private String url; private String message; private Integer progress; // 0-100ï¼å¯é public ReviewExportJobStatus() {} public ReviewExportJobStatus(String jobId, Status status, String url, String message, Integer progress) { this.jobId = jobId; this.status = status; this.url = url; this.message = message; this.progress = progress; } public String getJobId() { return jobId; } public void setJobId(String jobId) { this.jobId = jobId; } public Status getStatus() { return status; } public void setStatus(Status status) { this.status = status; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Integer getProgress() { return progress; } public void setProgress(Integer progress) { this.progress = progress; } } backend/src/main/java/com/rongyichuang/review/dto/response/ReviewExportResponse.java
New file @@ -0,0 +1,42 @@ package com.rongyichuang.review.dto.response; /** * è¯å®¡å¯¼åºååºDTO */ public class ReviewExportResponse { private boolean success; private String url; private String message; public ReviewExportResponse() {} public ReviewExportResponse(boolean success, String url, String message) { this.success = success; this.url = url; this.message = message; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java
@@ -4,13 +4,19 @@ import com.rongyichuang.review.dto.response.ReviewProjectPageResponse; import com.rongyichuang.review.dto.response.ReviewProjectResponse; import com.rongyichuang.review.dto.response.ReviewStatisticsResponse; import com.rongyichuang.review.dto.response.ReviewExportResponse; import com.rongyichuang.review.dto.response.ReviewExportJobStatus; import com.rongyichuang.review.service.ReviewService; import com.rongyichuang.review.service.ReviewExportService; import com.rongyichuang.review.service.ReviewExportJobService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.MutationMapping; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.stereotype.Controller; import java.util.List; /** * è¯å®¡ç®¡çGraphQLè§£æå¨ @@ -25,6 +31,12 @@ @Autowired private UserContextUtil userContextUtil; @Autowired private ReviewExportService reviewExportService; @Autowired private ReviewExportJobService reviewExportJobService; /** * æ¥è¯¢ææªè¯å®¡ç项ç®å表 @@ -94,4 +106,40 @@ return reviewService.getReviewStatistics(currentJudgeId); } /** * 导åºè¯å®¡ZIPï¼è¿åä¸è½½é¾æ¥ */ @MutationMapping public ReviewExportResponse exportReviewZip(@Argument Long activityId, @Argument List<Long> stageIds) { log.info("导åºè¯å®¡ZIPï¼activityId: {}, stageIds: {}", activityId, stageIds); // æéæ ¡éªï¼ä» åå·¥ï¼ç®¡çå/䏻忹ï¼å¯æ§è¡å¯¼åº if (!userContextUtil.isCurrentUserEmployee()) { log.warn("导åºè¯å®¡ZIP被æç»ï¼å½åç¨æ·æ åå·¥æéï¼activityId: {}", activityId); return new ReviewExportResponse(false, null, "å½åç¨æ·æ æé导åºè¯å®¡æ°æ®"); } return reviewExportService.exportReviewZip(activityId, stageIds); } /** * 弿¥å¯¼åºï¼å¯å¨è¯å®¡å¯¼åºä»»å¡ï¼è¿åä»»å¡ID */ @MutationMapping public String startReviewExportJob(@Argument Long activityId, @Argument List<Long> stageIds) { log.info("å¯å¨è¯å®¡å¯¼åºä»»å¡ï¼activityId: {}, stageIds: {}", activityId, stageIds); if (!userContextUtil.isCurrentUserEmployee()) { log.warn("å¯å¨è¯å®¡å¯¼åºä»»å¡è¢«æç»ï¼å½åç¨æ·æ åå·¥æéï¼activityId: {}", activityId); return null; } return reviewExportJobService.startJob(activityId, stageIds); } /** * æ¥è¯¢å¯¼åºä»»å¡ç¶æ */ @QueryMapping public ReviewExportJobStatus getReviewExportJobStatus(@Argument String jobId) { return reviewExportJobService.getStatus(jobId); } } backend/src/main/java/com/rongyichuang/review/service/ReviewExportJobService.java
New file @@ -0,0 +1,66 @@ package com.rongyichuang.review.service; import com.rongyichuang.review.dto.response.ReviewExportJobStatus; import com.rongyichuang.review.dto.response.ReviewExportResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.*; /** * 弿¥è¯å®¡å¯¼åºä»»å¡æå¡ */ @Service @Slf4j public class ReviewExportJobService { private final ReviewExportService reviewExportService; private final ExecutorService executor = Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors() / 2)); private final ConcurrentHashMap<String, ReviewExportJobStatus> jobStatusMap = new ConcurrentHashMap<>(); public ReviewExportJobService(ReviewExportService reviewExportService) { this.reviewExportService = reviewExportService; } /** * å¯å¨å¯¼åºä»»å¡ */ public String startJob(Long activityId, List<Long> stageIds) { String jobId = UUID.randomUUID().toString(); ReviewExportJobStatus init = new ReviewExportJobStatus(jobId, ReviewExportJobStatus.Status.PENDING, null, null, 0); jobStatusMap.put(jobId, init); executor.submit(() -> { ReviewExportJobStatus running = new ReviewExportJobStatus(jobId, ReviewExportJobStatus.Status.RUNNING, null, null, 10); jobStatusMap.put(jobId, running); try { ReviewExportResponse res = reviewExportService.exportReviewZip(activityId, stageIds); if (res != null && res.isSuccess()) { ReviewExportJobStatus done = new ReviewExportJobStatus(jobId, ReviewExportJobStatus.Status.SUCCEEDED, res.getUrl(), res.getMessage(), 100); jobStatusMap.put(jobId, done); } else { String msg = res != null ? res.getMessage() : "导åºå¤±è´¥"; ReviewExportJobStatus failed = new ReviewExportJobStatus(jobId, ReviewExportJobStatus.Status.FAILED, null, msg, 100); jobStatusMap.put(jobId, failed); } } catch (Exception e) { log.error("导åºä»»å¡æ§è¡å¤±è´¥, jobId: {}", jobId, e); ReviewExportJobStatus failed = new ReviewExportJobStatus(jobId, ReviewExportJobStatus.Status.FAILED, null, e.getMessage(), 100); jobStatusMap.put(jobId, failed); } }); return jobId; } /** * æ¥è¯¢ä»»å¡ç¶æ */ public ReviewExportJobStatus getStatus(String jobId) { return jobStatusMap.get(jobId); } } backend/src/main/java/com/rongyichuang/review/service/ReviewExportService.java
New file @@ -0,0 +1,432 @@ package com.rongyichuang.review.service; import com.rongyichuang.activity.entity.Activity; import com.rongyichuang.activity.repository.ActivityRepository; import com.rongyichuang.player.repository.ActivityPlayerRepository; import com.rongyichuang.review.dto.response.ReviewExportResponse; import com.rongyichuang.judge.service.CosService; import com.rongyichuang.player.entity.ActivityPlayer; import com.rongyichuang.activity.repository.ActivityPlayerRatingRepository; import com.rongyichuang.activity.repository.ActivityPlayerRatingItemRepository; import com.rongyichuang.rating.repository.RatingItemRepository; import com.rongyichuang.rating.entity.RatingItem; import com.rongyichuang.judge.repository.JudgeRepository; import com.rongyichuang.judge.entity.Judge; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.io.File; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import java.util.Optional; import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGridCol; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder; import java.math.BigInteger; @Service public class ReviewExportService { private static final Logger log = LoggerFactory.getLogger(ReviewExportService.class); private final ActivityRepository activityRepository; private final ActivityPlayerRepository activityPlayerRepository; private final ActivityPlayerRatingRepository activityPlayerRatingRepository; private final ActivityPlayerRatingItemRepository activityPlayerRatingItemRepository; private final RatingItemRepository ratingItemRepository; private final JudgeRepository judgeRepository; private final CosService cosService; public ReviewExportService(ActivityRepository activityRepository, ActivityPlayerRepository activityPlayerRepository, ActivityPlayerRatingRepository activityPlayerRatingRepository, ActivityPlayerRatingItemRepository activityPlayerRatingItemRepository, RatingItemRepository ratingItemRepository, JudgeRepository judgeRepository, CosService cosService) { this.activityRepository = activityRepository; this.activityPlayerRepository = activityPlayerRepository; this.activityPlayerRatingRepository = activityPlayerRatingRepository; this.activityPlayerRatingItemRepository = activityPlayerRatingItemRepository; this.ratingItemRepository = ratingItemRepository; this.judgeRepository = judgeRepository; this.cosService = cosService; } /** * 导åºè¯å®¡ç»æZIPï¼å ä½å®ç°ï¼çæç®åçREADMEæ±æ»å¹¶ä¸ä¼ ï¼è¿åURLï¼ */ public ReviewExportResponse exportReviewZip(Long activityId, List<Long> stageIds) { try { // 1) æ ¡éªæ´»å¨ Optional<Activity> activityOpt = activityRepository.findById(activityId); if (activityOpt.isEmpty()) { return new ReviewExportResponse(false, null, "æ´»å¨ä¸åå¨: " + activityId); } Activity activity = activityOpt.get(); // 2) ç»ç»å¯¼åºæä»¶å // ä» ä¿çåæ¯ãæ°åãè¿å符ï¼ä»¥åææUnicode忝åç¬¦ï¼æ¶µç䏿çï¼ï¼å ¶ä»æ¿æ¢ä¸ºä¸å线 String safeName = activity.getName() != null ? activity.getName().replaceAll("[^\\p{L}\\p{N}-]", "_") : "activity"; String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String fileName = String.format("review-%s-%s.zip", safeName, ts); // 3) æå»ºä¸´æ¶ZIP File zipFile = new File(System.getProperty("java.io.tmpdir"), "ryc-export-" + UUID.randomUUID() + ".zip"); try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { // READMEï¼é¿å 卿¤å¤å ³éZipOutputStreamï¼ ZipEntry readme = new ZipEntry("README.txt"); zos.putNextEntry(readme); StringBuilder readmeSb = new StringBuilder(); readmeSb.append("èæåè¯å®¡å¯¼åº\n"); readmeSb.append("æ´»å¨: ").append(activity.getName()).append(" (ID=").append(activity.getId()).append(")\n"); readmeSb.append("å¯¼åºæ¶é´: ").append(LocalDateTime.now()).append("\n"); if (stageIds != null && !stageIds.isEmpty()) { readmeSb.append("é¶æ®µID: ").append(stageIds).append("\n"); // ç®è¦ç»è®¡æ¯ä¸ªé¶æ®µæ¥å人æ°ï¼å ä½ç»è®¡ï¼ for (Long stageId : stageIds) { int playerCount = activityPlayerRepository.findByStageId(stageId).size(); readmeSb.append("é¶æ®µ ").append(stageId).append(" æ¥å人æ°: ").append(playerCount).append("\n"); } } else { // ä¸»æ´»å¨æ¥å人æ°ï¼ä¸åºåé¶æ®µï¼ int playerCount = activityPlayerRepository.findByActivityId(activityId).size(); readmeSb.append("æ´»å¨æ¥å人æ°: ").append(playerCount).append("\n"); } byte[] readmeBytes = readmeSb.toString().getBytes(StandardCharsets.UTF_8); zos.write(readmeBytes); zos.closeEntry(); // çææ¯ä¸ªéæçDOCXè¯åè¡¨ï¼æå å°ZIPä¸ï¼åºç¡å®ç°ï¼ if (stageIds != null && !stageIds.isEmpty()) { for (Long stageId : stageIds) { Activity stage = activityRepository.findById(stageId).orElse(null); String stageName = stage != null ? stage.getName() : ("é¶æ®µ" + stageId); List<ActivityPlayer> players = activityPlayerRepository.findByStageIdAndStateWithPlayerOrderByCreateTimeDesc(stageId, 1); for (ActivityPlayer ap : players) { addPlayerDocToZip(zos, activity, stageName, ap); } } } else { List<ActivityPlayer> players = activityPlayerRepository.findByActivityIdWithPlayerAndRegion(activityId); for (ActivityPlayer ap : players) { String stageName = ap.getStageId() != null ? activityRepository.findById(ap.getStageId()).map(Activity::getName).orElse("é¶æ®µ" + ap.getStageId()) : "主活å¨"; addPlayerDocToZip(zos, activity, stageName, ap); } } } // 4) ä¸ä¼ å°COS String key = cosService.uploadLocalFile(zipFile, fileName); String url = cosService.getFileUrl(key); log.info("è¯å®¡å¯¼åºä¸ä¼ 宿ï¼COS Key: {}, URL: {}", key, url); // 5) åå ¥ t_activity.review_export_url if (stageIds != null && !stageIds.isEmpty()) { for (Long stageId : stageIds) { Activity stage = activityRepository.findById(stageId).orElse(null); if (stage != null) { stage.setReviewExportUrl(url); activityRepository.save(stage); } } } else { activity.setReviewExportUrl(url); activityRepository.save(activity); } // å é¤ä¸´æ¶æä»¶ try { zipFile.delete(); } catch (Exception ignore) {} return new ReviewExportResponse(true, url, "å¯¼åºæå"); } catch (Exception ex) { log.error("导åºè¯å®¡ZIP失败", ex); return new ReviewExportResponse(false, null, "导åºå¤±è´¥: " + ex.getMessage()); } } private void addPlayerDocToZip(ZipOutputStream zos, Activity activity, String stageName, ActivityPlayer ap) throws Exception { String projectName = ap.getProjectName() != null ? ap.getProjectName() : "æªå½å项ç®"; String playerName = ap.getPlayer() != null && ap.getPlayer().getName() != null ? ap.getPlayer().getName() : ("éæ" + ap.getPlayerId()); // çæDOCXå 容 XWPFDocument doc = new XWPFDocument(); // 设置页é¢ä¸ºA4å¹¶åºç¨åç页边è·ï¼ä¿è¯æå°çå¼ applyPageSettings(doc); // æ é¢ XWPFParagraph title = doc.createParagraph(); title.setAlignment(ParagraphAlignment.CENTER); XWPFRun tr = title.createRun(); tr.setText("è¯å®¡è¯å表"); tr.setBold(true); tr.setFontSize(18); // åºæ¬ä¿¡æ¯ XWPFParagraph p1 = doc.createParagraph(); XWPFRun r1 = p1.createRun(); r1.setText("æ´»å¨ï¼" + safeText(activity.getName()) + " é¶æ®µï¼" + safeText(stageName)); XWPFParagraph p2 = doc.createParagraph(); XWPFRun r2 = p2.createRun(); r2.setText("项ç®ï¼" + safeText(projectName) + " éæï¼" + safeText(playerName)); // æ¥è¯¢ææè¯å§è¯å List<com.rongyichuang.activity.entity.ActivityPlayerRating> ratings = activityPlayerRatingRepository.findByActivityPlayerId(ap.getId()); if (ratings == null || ratings.isEmpty()) { // æ è¯åæ¶ï¼ä»ç¶çæè¯å项模æ¿è¡¨æ ¼ï¼ä¾¿äºæå°æçº¿ä¸è¯å XWPFParagraph pEmpty = doc.createParagraph(); XWPFRun re = pEmpty.createRun(); re.setText("å°æ è¯åï¼ä»¥ä¸ä¸ºè¯å项模æ¿ï¼"); re.setItalic(true); // ä½¿ç¨æ´»å¨çè¯åæ¹æ¡çæç©ºè¡¨æ ¼ï¼è®¾ç½®è¡¨æ ¼å®½åº¦åå宽ï¼é¿å è¡¨æ ¼æ¤åï¼ XWPFTable table = doc.createTable(); applyTableLayout(table, new int[]{6072, 1500, 1500}); XWPFTableRow header = table.getRow(0); ensureCells(header, 3); setCellText(header.getCell(0), "è¯å项"); setCellText(header.getCell(1), "å¾å"); setCellText(header.getCell(2), "满å"); // ä¼å 使ç¨é¶æ®µçè¯åæ¹æ¡ï¼å¦æéææ²¡æé¶æ®µæé¶æ®µä¸åå¨ï¼ååéå°ä¸»æ´»å¨è¯åæ¹æ¡ Long schemeIdToUse = null; if (ap.getStageId() != null) { try { Activity stageEntity = activityRepository.findById(ap.getStageId()).orElse(null); if (stageEntity != null && stageEntity.getRatingSchemeId() != null) { schemeIdToUse = stageEntity.getRatingSchemeId(); } } catch (Exception ignore) {} } if (schemeIdToUse == null) { schemeIdToUse = activity.getRatingSchemeId(); } List<RatingItem> items = schemeIdToUse != null ? ratingItemRepository.findBySchemeIdOrderByOrderNo(schemeIdToUse) : new ArrayList<>(); for (RatingItem item : items) { XWPFTableRow row = table.createRow(); ensureCells(row, 3); setCellText(row.getCell(0), safeText(item.getName())); setCellText(row.getCell(1), "-"); setCellText(row.getCell(2), item.getMaxScore() != null ? String.valueOf(item.getMaxScore()) : "-"); } } else { for (com.rongyichuang.activity.entity.ActivityPlayerRating rating : ratings) { XWPFParagraph judgeTitle = doc.createParagraph(); XWPFRun jr = judgeTitle.createRun(); String judgeName = rating.getJudgeId() != null ? judgeRepository.findById(rating.getJudgeId()).map(Judge::getName).orElse("è¯å§" + rating.getJudgeId()) : "æªç¥è¯å§"; jr.setText("è¯å§ï¼" + safeText(judgeName) + " æ»åï¼" + (rating.getTotalScore() != null ? rating.getTotalScore() : "-") ); jr.setBold(true); // æå»ºè¯åé¡¹è¡¨æ ¼ XWPFTable table = doc.createTable(); applyTableLayout(table, new int[]{6072, 1500, 1500}); XWPFTableRow header = table.getRow(0); ensureCells(header, 3); setCellText(header.getCell(0), "è¯å项"); setCellText(header.getCell(1), "å¾å"); setCellText(header.getCell(2), "满å"); List<RatingItem> items = rating.getRatingSchemeId() != null ? ratingItemRepository.findBySchemeIdOrderByOrderNo(rating.getRatingSchemeId()) : new ArrayList<>(); // æ¥è¯¢è¯å项å¾å List<com.rongyichuang.activity.entity.ActivityPlayerRatingItem> ratingItems = activityPlayerRatingItemRepository.findByActivityPlayerRatingId(rating.getId()); Map<Long, java.math.BigDecimal> scoreMap = new HashMap<>(); for (com.rongyichuang.activity.entity.ActivityPlayerRatingItem ri : ratingItems) { scoreMap.put(ri.getRatingItemId(), ri.getScore()); } for (RatingItem item : items) { XWPFTableRow row = table.createRow(); ensureCells(row, 3); setCellText(row.getCell(0), safeText(item.getName())); java.math.BigDecimal s = scoreMap.get(item.getId()); setCellText(row.getCell(1), s != null ? s.toPlainString() : "-"); setCellText(row.getCell(2), item.getMaxScore() != null ? String.valueOf(item.getMaxScore()) : "-"); } } } // å¨ææ¡£åºé¨æ·»å ä¸å®¶è¯å®¡ç¾ååºåï¼åèæ¨¡æ¿æ ¼å¼ï¼ addSignatureSection(doc); // åå ¥å°ZIP try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { doc.write(baos); String fileBase = sanitizeFileName(stageName) + "/" + sanitizeFileName(projectName + "-" + playerName) + ".docx"; String entryName = "players/" + fileBase; ZipEntry entry = new ZipEntry(entryName); zos.putNextEntry(entry); zos.write(baos.toByteArray()); zos.closeEntry(); } doc.close(); } private String sanitizeFileName(String name) { return name == null ? "unknown" : name.replaceAll("[^\\p{L}\\p{N}-]", "_"); } private String safeText(String text) { return text == null ? "-" : text; } /** * è®¾ç½®è¡¨æ ¼çæ»å®½åº¦åå宽ï¼é¿å åºç°å宽è¿çªå¯¼è´å 容æ¤å¨ä¸èµ· * @param table è¡¨æ ¼ * @param colWidthsTwips æ¯å宽度ï¼åä½ï¼twipsï¼ï¼ç¤ºä¾ï¼{6072, 1500, 1500} */ private void applyTableLayout(XWPFTable table, int[] colWidthsTwips) { // è®¾ç½®è¡¨æ ¼æ»å®½åº¦ï¼é¡µé¢æ å宽度约 9072 twipsï¼ CTTbl ctTbl = table.getCTTbl(); CTTblPr tblPr = ctTbl.getTblPr() == null ? ctTbl.addNewTblPr() : ctTbl.getTblPr(); CTTblWidth tblW = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW(); tblW.setType(STTblWidth.DXA); int total = 0; for (int w : colWidthsTwips) total += w; tblW.setW(BigInteger.valueOf(total)); // 设置åç½æ ¼ï¼åå®½ï¼ CTTblGrid grid = ctTbl.getTblGrid() == null ? ctTbl.addNewTblGrid() : ctTbl.getTblGrid(); // æ¸ çå·²æåå®ä¹ï¼å¦ææï¼ while (grid.sizeOfGridColArray() > 0) { grid.removeGridCol(0); } for (int w : colWidthsTwips) { CTTblGridCol col = grid.addNewGridCol(); col.setW(BigInteger.valueOf(w)); } // è®¾ç½®è¡¨æ ¼è¾¹æ¡ï¼ä¿è¯æå°æææ¸ æ° CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders(); CTBorder border = borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(8)); // 8å «åä¹ä¸ç¹ = 1pt border.setColor("000000"); border = borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(8)); border.setColor("000000"); border = borders.isSetTop() ? borders.getTop() : borders.addNewTop(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(8)); border.setColor("000000"); border = borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(8)); border.setColor("000000"); border = borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(8)); border.setColor("000000"); // ä¿®å¤å³è¾¹æ¡æªè®¾ç½®ç²ç»çé®é¢ï¼ç¡®ä¿åå¨è¾¹æ¡ä¸è´ border = borders.isSetRight() ? borders.getRight() : borders.addNewRight(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(8)); border.setColor("000000"); } /** * ç¡®ä¿ä¸è¡ææå®æ°éçåå æ ¼ */ private void ensureCells(XWPFTableRow row, int cellCount) { int existing = row.getTableCells().size(); for (int i = existing; i < cellCount; i++) { row.addNewTableCell(); } } /** * 设置åå æ ¼ææ¬å¹¶ä¸ºè¯¥åå æ ¼è®¾ç½®å®½åº¦ï¼ä¸ç½æ ¼å¹é ï¼ */ private void setCellText(XWPFTableCell cell, String text) { // ç´æ¥è®¾ç½®ææ¬ try { // æ¸ ç©ºé»è®¤æ®µè½ï¼é¿å éå¤ï¼æäºæ åµä¸åå æ ¼å¯è½æ²¡æé»è®¤æ®µè½ï¼ if (cell.getParagraphs().size() > 0) { cell.removeParagraph(0); } } catch (Exception ignore) {} XWPFParagraph p = cell.addParagraph(); p.setAlignment(ParagraphAlignment.LEFT); XWPFRun run = p.createRun(); run.setFontSize(12); // 使ç¨ä¸æåä½ï¼ä¿è¯ä¸ææ¾ç¤ºæææ´æ¥è¿æ¨¡æ¿ try { run.setFontFamily("å®ä½"); } catch (Exception ignore) {} run.setText(text); // 为åå æ ¼è®¾ç½®å®½åº¦ï¼DXAï¼ä»¥é²å¨æäºWordçæ¬ä¸æªåºç¨è¡¨æ ¼ç½æ ¼å®½åº¦ CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr(); CTTblWidth w = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW(); w.setType(STTblWidth.DXA); // 宽度å¼ç±æå¨åå³å®ï¼è¿éä¸éå¤è®¾ç½®å ·ä½æ°å¼ï¼é¿å ä¸è¡¨æ ¼ç½æ ¼å²çª } /** * æ·»å ä¸å®¶è¯å®¡ç¾ååºåï¼ä¸¤åï¼ç¾åãæ¥æï¼ */ private void addSignatureSection(XWPFDocument doc) { // çåºä¸å®çä¸è¾¹è· XWPFParagraph spacer = doc.createParagraph(); spacer.setSpacingBefore(200); XWPFTable signTable = doc.createTable(1, 2); applyTableLayout(signTable, new int[]{6500, 2572}); XWPFTableRow row = signTable.getRow(0); ensureCells(row, 2); setCellText(row.getCell(0), "ä¸å®¶è¯å®¡ç¾åï¼_______________"); setCellText(row.getCell(1), "ç¾åæ¥æï¼__________"); } /** * 设置页é¢å¤§å°ä¸é¡µè¾¹è·ï¼A4ï¼é»è®¤è¾¹è·çº¦1è±å¯¸ï¼ */ private void applyPageSettings(XWPFDocument doc) { try { // A4 尺寸ï¼å®½ 11907 twipsï¼21cmï¼ï¼é« 16840 twipsï¼29.7cmï¼ var body = doc.getDocument().getBody(); var sectPr = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr(); var pgSz = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz(); pgSz.setW(BigInteger.valueOf(11907)); pgSz.setH(BigInteger.valueOf(16840)); // 页边è·ï¼çº¦ 1 è±å¯¸ï¼1440 twipsï¼ï¼å¯æ ¹æ®éè¦å¾®è° var pgMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar(); pgMar.setLeft(BigInteger.valueOf(1440)); pgMar.setRight(BigInteger.valueOf(1440)); pgMar.setTop(BigInteger.valueOf(1440)); pgMar.setBottom(BigInteger.valueOf(1440)); } catch (Exception ignore) { // å ¼å®¹æ§èèï¼ä¸å½±åææ¡£çæ } } } backend/src/main/resources/graphql/activity.graphqls
@@ -70,6 +70,7 @@ createTime: String updateTime: String reviewExportUrl: String coverImage: MediaResponse backend/src/main/resources/graphql/review.graphqls
@@ -13,6 +13,41 @@ # è·åè¯å®¡ç»è®¡æ°æ® reviewStatistics: ReviewStatisticsResponse! # æ¥è¯¢è¯å®¡å¯¼åºä»»å¡ç¶æ getReviewExportJobStatus(jobId: String!): ReviewExportJobStatus } # æ©å±åæ´ç±»åï¼è¯å®¡å¯¼åº extend type Mutation { # å¯¼åºæå®æ¯èµæé¶æ®µçè¯å®¡ç»æä¸ºZIPå¹¶è¿åä¸è½½é¾æ¥ exportReviewZip(activityId: ID!, stageIds: [ID]): ReviewExportResponse! # å¯å¨å¼æ¥è¯å®¡å¯¼åºä»»å¡ï¼è¿åä»»å¡ID startReviewExportJob(activityId: ID!, stageIds: [ID]): String! } # è¯å®¡å¯¼åºååº type ReviewExportResponse { success: Boolean! url: String message: String } # è¯å®¡å¯¼åºä»»å¡ç¶æ type ReviewExportJobStatus { jobId: String! status: ReviewExportJobStatusEnum! url: String message: String progress: Int } enum ReviewExportJobStatusEnum { PENDING RUNNING SUCCEEDED FAILED } # è¯å®¡é¡¹ç®å页ååºç±»å backend/src/test/java/com/rongyichuang/review/DocxLayoutTest.java
New file @@ -0,0 +1,114 @@ package com.rongyichuang.review; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.junit.jupiter.api.Test; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGridCol; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import java.io.File; import java.io.FileOutputStream; import java.math.BigInteger; /** * å¿«ééªè¯DOCXè¡¨æ ¼å®½åº¦ä¸ç¾ååºåå¸å± */ public class DocxLayoutTest { private void applyTableLayout(XWPFTable table, int[] colWidthsTwips) { CTTbl ctTbl = table.getCTTbl(); CTTblPr tblPr = ctTbl.getTblPr() == null ? ctTbl.addNewTblPr() : ctTbl.getTblPr(); CTTblWidth tblW = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW(); tblW.setType(STTblWidth.DXA); int total = 0; for (int w : colWidthsTwips) total += w; tblW.setW(BigInteger.valueOf(total)); CTTblGrid grid = ctTbl.getTblGrid() == null ? ctTbl.addNewTblGrid() : ctTbl.getTblGrid(); while (grid.sizeOfGridColArray() > 0) { grid.removeGridCol(0); } for (int w : colWidthsTwips) { CTTblGridCol col = grid.addNewGridCol(); col.setW(BigInteger.valueOf(w)); } } private void ensureCells(XWPFTableRow row, int cellCount) { int existing = row.getTableCells().size(); for (int i = existing; i < cellCount; i++) { row.addNewTableCell(); } } private void setCellText(XWPFTableCell cell, String text) { while (cell.getParagraphs().size() > 0) { cell.removeParagraph(0); } XWPFParagraph p = cell.addParagraph(); p.setAlignment(ParagraphAlignment.LEFT); XWPFRun run = p.createRun(); run.setFontSize(12); run.setText(text); CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr(); CTTblWidth w = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW(); w.setType(STTblWidth.DXA); } @Test public void generateSampleDoc() throws Exception { XWPFDocument doc = new XWPFDocument(); XWPFParagraph title = doc.createParagraph(); title.setAlignment(ParagraphAlignment.CENTER); XWPFRun tr = title.createRun(); tr.setText("è¯å®¡è¯å表ï¼å¸å±éªè¯ï¼"); tr.setBold(true); tr.setFontSize(18); XWPFTable table = doc.createTable(); applyTableLayout(table, new int[]{6072, 1500, 1500}); XWPFTableRow header = table.getRow(0); ensureCells(header, 3); setCellText(header.getCell(0), "è¯å项"); setCellText(header.getCell(1), "å¾å"); setCellText(header.getCell(2), "满å"); for (int i = 1; i <= 5; i++) { XWPFTableRow row = table.createRow(); ensureCells(row, 3); setCellText(row.getCell(0), "示ä¾é¡¹ç®" + i); setCellText(row.getCell(1), String.valueOf(10 * i)); setCellText(row.getCell(2), "100"); } // ç¾ååº XWPFParagraph spacer = doc.createParagraph(); spacer.setSpacingBefore(200); XWPFTable signTable = doc.createTable(1, 2); applyTableLayout(signTable, new int[]{6500, 2572}); XWPFTableRow row = signTable.getRow(0); ensureCells(row, 2); setCellText(row.getCell(0), "ä¸å®¶è¯å®¡ç¾åï¼_______________"); setCellText(row.getCell(1), "ç¾åæ¥æï¼__________"); File out = new File("d:/code/new-ryc/tmp/docx-layout-sample.docx"); out.getParentFile().mkdirs(); try (FileOutputStream fos = new FileOutputStream(out)) { doc.write(fos); } doc.close(); } } check-all-education-data.js
File was deleted check-database-activity-times.js
File was deleted check-db-gender-education.js
File was deleted check-phone-db.js
File was deleted check-user-judge.js
File was deleted clear-invalid-tokens.js
File was deleted debug-backend-user-context.js
File was deleted debug-check-phone.js
File was deleted debug-dataset-issue.md
File was deleted debug-employee-check.js
File was deleted debug-employee.js
File was deleted debug-gender-education-data.js
File was deleted debug-jwt-token.js
File was deleted debug-saveUserInfo.js
File was deleted debug-user-info.js
File was deleted doc/12-ÄäÃûÓû§.docxBinary files differ
doc/~$Ñ¡ÆÀ·Ö±í£©2025Äê³ÉÓåµÂü×Ê´´Òµ´óÈüº£Ñ¡.docBinary files differ
doc/£¨º£Ñ¡ÆÀ·Ö±í£©2025Äê³ÉÓåµÂü×Ê´´Òµ´óÈüº£Ñ¡.docBinary files differ
get-valid-token.js
File was deleted simulate-miniprogram-debug.js
File was deleted test-image.svg
File was deleted test_miniprogram_activities_fix.js
File was deleted test_miniprogram_fix.js
File was deleted tmp/docx-layout-sample.docxBinary files differ
tmp/docx-º£Ñ¡-12-new/[Content_Types].xml
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/><Default ContentType="application/xml" Extension="xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/><Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" PartName="/word/document.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" PartName="/word/settings.xml"/></Types> tmp/docx-º£Ñ¡-12-new/_rels/.rels
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="word/document.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"/><Relationship Id="rId2" Target="docProps/app.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"/><Relationship Id="rId3" Target="docProps/core.xml" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"/></Relationships> tmp/docx-º£Ñ¡-12-new/docProps/app.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Apache POI</Application></Properties> tmp/docx-º£Ñ¡-12-new/docProps/core.xml
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:created xsi:type="dcterms:W3CDTF">2025-11-06T05:16:40Z</dcterms:created><dc:creator>Apache POI</dc:creator></cp:coreProperties> tmp/docx-º£Ñ¡-12-new/word/_rels/document.xml.rels
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="settings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"/></Relationships> tmp/docx-º£Ñ¡-12-new/word/document.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:rPr><w:b w:val="on"/><w:sz w:val="36"/></w:rPr><w:t>è¯å®¡è¯å表</w:t></w:r></w:p><w:p><w:r><w:t>æ´»å¨ï¼æºæ §æºè½é¡¹ç®ç» é¶æ®µï¼æµ·é</w:t></w:r></w:p><w:p><w:r><w:t>项ç®ï¼12 éæï¼å¿åç¨æ·</w:t></w:r></w:p><w:p><w:r><w:rPr><w:i w:val="on"/></w:rPr><w:t>å°æ è¯åï¼ä»¥ä¸ä¸ºè¯å项模æ¿ï¼</w:t></w:r></w:p><w:tbl><w:tblPr><w:tblW w:w="9072" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="8" w:color="000000"/><w:left w:val="single" w:sz="8" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:color="000000"/><w:right w:val="single" w:sz="8" w:color="000000"/><w:insideH w:val="single" w:sz="8" w:color="000000"/><w:insideV w:val="single" w:sz="8" w:color="000000"/></w:tblBorders></w:tblPr><w:tblGrid><w:gridCol w:w="6072"/><w:gridCol w:w="1500"/><w:gridCol w:w="1500"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>è¯å项</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>å¾å</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>满å</w:t></w:r></w:p></w:tc></w:tr><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>å¹ç¶å</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>-</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>25</w:t></w:r></w:p></w:tc></w:tr><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>ç¶æ°</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>-</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>25</w:t></w:r></w:p></w:tc></w:tr><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>é å</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>-</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>50</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p><w:pPr><w:spacing w:before="200"/></w:pPr></w:p><w:tbl><w:tblPr><w:tblW w:w="9072" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="8" w:color="000000"/><w:left w:val="single" w:sz="8" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:color="000000"/><w:right w:val="single" w:sz="8" w:color="000000"/><w:insideH w:val="single" w:sz="8" w:color="000000"/><w:insideV w:val="single" w:sz="8" w:color="000000"/></w:tblBorders></w:tblPr><w:tblGrid><w:gridCol w:w="6500"/><w:gridCol w:w="2572"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>ä¸å®¶è¯å®¡ç¾åï¼_______________</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/><w:rFonts w:ascii="å®ä½" w:hAnsi="å®ä½" w:cs="å®ä½" w:eastAsia="å®ä½"/></w:rPr><w:t>ç¾åæ¥æï¼__________</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:sectPr><w:pgSz w:w="11907" w:h="16840"/><w:pgMar w:left="1440" w:right="1440" w:top="1440" w:bottom="1440"/></w:sectPr></w:body></w:document> tmp/docx-º£Ñ¡-12-new/word/settings.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/> tmp/docx-º£Ñ¡-12/[Content_Types].xml
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/><Default ContentType="application/xml" Extension="xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/><Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" PartName="/word/document.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" PartName="/word/settings.xml"/></Types> tmp/docx-º£Ñ¡-12/_rels/.rels
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="word/document.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"/><Relationship Id="rId2" Target="docProps/app.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"/><Relationship Id="rId3" Target="docProps/core.xml" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"/></Relationships> tmp/docx-º£Ñ¡-12/docProps/app.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Apache POI</Application></Properties> tmp/docx-º£Ñ¡-12/docProps/core.xml
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:created xsi:type="dcterms:W3CDTF">2025-11-06T03:40:51Z</dcterms:created><dc:creator>Apache POI</dc:creator></cp:coreProperties> tmp/docx-º£Ñ¡-12/word/_rels/document.xml.rels
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="settings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"/></Relationships> tmp/docx-º£Ñ¡-12/word/document.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:rPr><w:b w:val="on"/><w:sz w:val="36"/></w:rPr><w:t>è¯å®¡è¯å表</w:t></w:r></w:p><w:p><w:r><w:t>æ´»å¨ï¼æºæ §æºè½é¡¹ç®ç» é¶æ®µï¼æµ·é</w:t></w:r></w:p><w:p><w:r><w:t>项ç®ï¼12 éæï¼å¿åç¨æ·</w:t></w:r></w:p><w:p><w:r><w:t>å°æ è¯å</w:t></w:r></w:p></w:body></w:document> tmp/docx-º£Ñ¡-12/word/settings.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/> tmp/review-0/README.txt
New file @@ -0,0 +1,4 @@ èæåè¯å®¡å¯¼åº æ´»å¨: æºæ §æºè½é¡¹ç®ç» (ID=74) å¯¼åºæ¶é´: 2025-11-06T11:56:25.701008300 æ´»å¨æ¥å人æ°: 5 tmp/review-0/players/º£Ñ¡/12-ÄäÃûÓû§.docxBinary files differ
tmp/review-1/README.txt
New file @@ -0,0 +1,5 @@ èæåè¯å®¡å¯¼åº æ´»å¨: æºæ §æºè½é¡¹ç®ç» (ID=74) å¯¼åºæ¶é´: 2025-11-06T11:57:45.558184200 é¶æ®µID: [76] é¶æ®µ 76 æ¥å人æ°: 0 tmp/review-2/README.txt
New file @@ -0,0 +1,5 @@ èæåè¯å®¡å¯¼åº æ´»å¨: æºæ §æºè½é¡¹ç®ç» (ID=74) å¯¼åºæ¶é´: 2025-11-06T11:40:51.537055800 é¶æ®µID: [75] é¶æ®µ 75 æ¥å人æ°: 5 tmp/review-2/players/º£Ñ¡/12-ÄäÃûÓû§.docxBinary files differ
tmp/review-export-2/README.txt
New file @@ -0,0 +1,7 @@ èæåè¯å®¡å¯¼åº æ´»å¨: æºæ §æºè½é¡¹ç®ç» (ID=74) å¯¼åºæ¶é´: 2025-11-06T13:16:40.539277500 é¶æ®µID: [75, 76, 77] é¶æ®µ 75 æ¥å人æ°: 5 é¶æ®µ 76 æ¥å人æ°: 0 é¶æ®µ 77 æ¥å人æ°: 0 tmp/review-export-2/players/º£Ñ¡/12-ÄäÃûÓû§.docxBinary files differ
tmp/review-export/README.txt
New file @@ -0,0 +1,7 @@ èæåè¯å®¡å¯¼åº æ´»å¨: æºæ §æºè½é¡¹ç®ç» (ID=74) å¯¼åºæ¶é´: 2025-11-06T13:03:21.174946400 é¶æ®µID: [75, 76, 77] é¶æ®µ 75 æ¥å人æ°: 5 é¶æ®µ 76 æ¥å人æ°: 0 é¶æ®µ 77 æ¥å人æ°: 0 tmp/review-export/players/º£Ñ¡/12-ÄäÃûÓû§.docxBinary files differ
tmp/review-export/players/º£Ñ¡/docx-xml/[Content_Types].xml
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/><Default ContentType="application/xml" Extension="xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/><Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" PartName="/word/document.xml"/><Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" PartName="/word/settings.xml"/></Types> tmp/review-export/players/º£Ñ¡/docx-xml/_rels/.rels
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="word/document.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"/><Relationship Id="rId2" Target="docProps/app.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"/><Relationship Id="rId3" Target="docProps/core.xml" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"/></Relationships> tmp/review-export/players/º£Ñ¡/docx-xml/docProps/app.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Apache POI</Application></Properties> tmp/review-export/players/º£Ñ¡/docx-xml/docProps/core.xml
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:created xsi:type="dcterms:W3CDTF">2025-11-06T05:03:21Z</dcterms:created><dc:creator>Apache POI</dc:creator></cp:coreProperties> tmp/review-export/players/º£Ñ¡/docx-xml/word/_rels/document.xml.rels
New file @@ -0,0 +1 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="settings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"/></Relationships> tmp/review-export/players/º£Ñ¡/docx-xml/word/document.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:rPr><w:b w:val="on"/><w:sz w:val="36"/></w:rPr><w:t>è¯å®¡è¯å表</w:t></w:r></w:p><w:p><w:r><w:t>æ´»å¨ï¼æºæ §æºè½é¡¹ç®ç» é¶æ®µï¼æµ·é</w:t></w:r></w:p><w:p><w:r><w:t>项ç®ï¼12 éæï¼å¿åç¨æ·</w:t></w:r></w:p><w:p><w:r><w:rPr><w:i w:val="on"/></w:rPr><w:t>å°æ è¯åï¼ä»¥ä¸ä¸ºè¯å项模æ¿ï¼</w:t></w:r></w:p><w:tbl><w:tblPr><w:tblW w:w="9072" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="8" w:color="000000"/><w:left w:val="single" w:sz="8" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:color="000000"/><w:right w:val="single"/><w:insideH w:val="single" w:sz="8" w:color="000000"/><w:insideV w:val="single" w:sz="8" w:color="000000"/></w:tblBorders></w:tblPr><w:tblGrid><w:gridCol w:w="6072"/><w:gridCol w:w="1500"/><w:gridCol w:w="1500"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>è¯å项</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>å¾å</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>满å</w:t></w:r></w:p></w:tc></w:tr><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>å¹ç¶å</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>-</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>25</w:t></w:r></w:p></w:tc></w:tr><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>ç¶æ°</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>-</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>25</w:t></w:r></w:p></w:tc></w:tr><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>é å</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>-</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>50</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:p><w:pPr><w:spacing w:before="200"/></w:pPr></w:p><w:tbl><w:tblPr><w:tblW w:w="9072" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="8" w:color="000000"/><w:left w:val="single" w:sz="8" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:color="000000"/><w:right w:val="single"/><w:insideH w:val="single" w:sz="8" w:color="000000"/><w:insideV w:val="single" w:sz="8" w:color="000000"/></w:tblBorders></w:tblPr><w:tblGrid><w:gridCol w:w="6500"/><w:gridCol w:w="2572"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>ä¸å®¶è¯å®¡ç¾åï¼_______________</w:t></w:r></w:p></w:tc><w:tc><w:tcPr><w:tcW w:type="dxa"/></w:tcPr><w:p><w:pPr><w:jc w:val="left"/></w:pPr><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>ç¾åæ¥æï¼__________</w:t></w:r></w:p></w:tc></w:tr></w:tbl></w:body></w:document> tmp/review-export/players/º£Ñ¡/docx-xml/word/settings.xml
New file @@ -0,0 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/> update-activity-times.js
File was deleted update-activity-times.sql
File was deleted verify-user-judge-mapping.js
File was deleted web/src/api/activity.js
@@ -44,6 +44,7 @@ stateName createTime updateTime reviewExportUrl ratingScheme { id name @@ -61,6 +62,7 @@ state stateName sortOrder reviewExportUrl ratingScheme { id name web/src/api/reviewExport.js
New file @@ -0,0 +1,70 @@ import { graphqlRequest } from '@/config/api' // 导åºè¯å®¡ZIPçGraphQLåæ´ const EXPORT_REVIEW_ZIP_MUTATION = ` mutation ExportReviewZip($activityId: ID!, $stageIds: [ID]) { exportReviewZip(activityId: $activityId, stageIds: $stageIds) { success url message } } ` // 弿¥å¯¼åºï¼å¯å¨è¯å®¡å¯¼åºä»»å¡ const START_REVIEW_EXPORT_JOB_MUTATION = ` mutation StartReviewExportJob($activityId: ID!, $stageIds: [ID]) { startReviewExportJob(activityId: $activityId, stageIds: $stageIds) } ` // æ¥è¯¢è¯å®¡å¯¼åºä»»å¡ç¶æ const GET_REVIEW_EXPORT_JOB_STATUS_QUERY = ` query GetReviewExportJobStatus($jobId: String!) { getReviewExportJobStatus(jobId: $jobId) { jobId status url message progress } } ` /** * 触åè¯å®¡å¯¼åºZIP * @param {number|string} activityId æ´»å¨IDï¼ä¸»æ´»å¨IDï¼ * @param {Array<number>|null} stageIds å¯éçé¶æ®µIDå表ï¼è¥ä¸ºç©ºåå¯¼åºæ´ä¸ªæ´»å¨ * @returns {Promise<{success: boolean, url?: string, message?: string}>} */ export const exportReviewZip = async (activityId, stageIds = null) => { const result = await graphqlRequest(EXPORT_REVIEW_ZIP_MUTATION, { activityId, stageIds }) return result?.data?.exportReviewZip } /** * å¯å¨è¯å®¡å¯¼åºä»»å¡ï¼è¿å jobId * @param {number|string} activityId * @param {Array<number>|null} stageIds * @returns {Promise<string|null>} jobId */ export const startReviewExportJob = async (activityId, stageIds = null) => { const result = await graphqlRequest(START_REVIEW_EXPORT_JOB_MUTATION, { activityId, stageIds }) return result?.data?.startReviewExportJob || null } /** * æ¥è¯¢è¯å®¡å¯¼åºä»»å¡ç¶æ * @param {string} jobId * @returns {Promise<{jobId: string, status: string, url?: string, message?: string, progress?: number} | null>} */ export const getReviewExportJobStatus = async (jobId) => { const result = await graphqlRequest(GET_REVIEW_EXPORT_JOB_STATUS_QUERY, { jobId }) return result?.data?.getReviewExportJobStatus || null } export default { exportReviewZip, startReviewExportJob, getReviewExportJobStatus } web/src/views/ActivityDetail.vue
@@ -5,6 +5,7 @@ <div class="card-header"> <span>æ¯èµè¯¦æ </span> <div> <el-button v-if="canExport" type="primary" :loading="exportingActivity" @click="handleExportActivity">导åºè¯å®¡ZIP</el-button> <el-button @click="handleEdit">ç¼è¾æ¯èµ</el-button> <el-button @click="goBack">è¿å</el-button> </div> @@ -23,6 +24,18 @@ <el-descriptions-item label="æ¯èµå°å">{{ activity.address || '-' }}</el-descriptions-item> <el-descriptions-item label="è¯å模æ¿"> {{ activity.ratingScheme ? activity.ratingScheme.name : '-' }} </el-descriptions-item> <el-descriptions-item label="æè¿è¯å®¡å¯¼åº" :span="2"> <template v-if="activity.reviewExportUrl"> <a :href="activity.reviewExportUrl" target="_blank">ç¹å»ä¸è½½</a> </template> <template v-else> ææ </template> <span v-if="exportJobActivity && exportingActivity" class="export-status"> 导åºä»»å¡è¿è¡ä¸ <span v-if="exportJobActivity.progress !== null && exportJobActivity.progress !== undefined">ï¼{{ exportJobActivity.progress }}%ï¼</span> </span> </el-descriptions-item> <el-descriptions-item label="å建æ¶é´" :span="2">{{ formatDateTime(activity.createTime) }}</el-descriptions-item> </el-descriptions> @@ -54,11 +67,32 @@ <el-tag :type="getStateType(row.state)">{{ row.stateName }}</el-tag> </template> </el-table-column> <el-table-column label="æä½" width="220" fixed="right"> <el-table-column label="æè¿å¯¼åº" width="200"> <template #default="{ row }"> <template v-if="row.reviewExportUrl"> <a :href="row.reviewExportUrl" target="_blank">ä¸è½½ZIP</a> </template> <template v-else> <span style="color: #909399;">ææ </span> </template> <div v-if="exportJobStageMap[row.id] && exportingStageId === row.id" class="export-status"> 导åºä»»å¡è¿è¡ä¸ <span v-if="exportJobStageMap[row.id].progress !== null && exportJobStageMap[row.id].progress !== undefined">ï¼{{ exportJobStageMap[row.id].progress }}%ï¼</span> </div> </template> </el-table-column> <el-table-column label="æä½" width="320" fixed="right"> <template #default="{ row }"> <el-button size="small" @click="viewStageDetail(row)">æ¥ç详æ </el-button> <el-button size="small" type="warning" @click="closeStage(row)" v-if="row.state === 1">å ³é</el-button> <el-button size="small" type="danger" @click="deleteStage(row)">å é¤</el-button> <el-button v-if="canExport" size="small" type="primary" :loading="exportingStageId === row.id" @click="handleExportStage(row)" >导åºé¶æ®µè¯å®¡</el-button> </template> </el-table-column> </el-table> @@ -125,10 +159,12 @@ </template> <script setup> import { ref, onMounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { getActivity } from '@/api/activity' import { exportReviewZip, startReviewExportJob, getReviewExportJobStatus } from '@/api/reviewExport' import { isEmployee } from '@/utils/auth' const router = useRouter() const route = useRoute() @@ -138,6 +174,13 @@ const activity = ref(null) const stageDialogVisible = ref(false) const selectedStage = ref(null) const exportingActivity = ref(false) const exportingStageId = ref(null) // 弿¥å¯¼åºä»»å¡ç¶æ const exportJobActivity = ref(null) // { jobId, status, progress, message } const exportJobStageMap = ref({}) // { [stageId]: { jobId, status, progress, message } } let activityPollTimer = null const stagePollTimers = {} // 计ç®å±æ§ const sortedStages = computed(() => { @@ -248,6 +291,139 @@ onMounted(() => { loadActivity() }) onUnmounted(() => { if (activityPollTimer) { clearInterval(activityPollTimer) activityPollTimer = null } Object.keys(stagePollTimers).forEach((sid) => { if (stagePollTimers[sid]) { clearInterval(stagePollTimers[sid]) delete stagePollTimers[sid] } }) }) // æ¯å¦å¯æ§è¡å¯¼åºï¼ä» åå·¥ï¼ const canExport = computed(() => isEmployee()) // 导åºä¸»æ´»å¨è¯å®¡ZIP const handleExportActivity = async () => { try { await ElMessageBox.confirm('å°å¯¼åºè¯¥æ¯èµä¸ææé¶æ®µçå·²è¯åæ°æ®ä¸ºZIPï¼ç¡®è®¤ç»§ç»ï¼', '确认导åº', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) exportingActivity.value = true // ä¼å 使ç¨å¼æ¥å¯¼åºä»»å¡ const jobId = await startReviewExportJob(route.params.id) if (jobId) { exportJobActivity.value = { jobId, status: 'PENDING', progress: 0 } // 轮询任å¡ç¶æ activityPollTimer = setInterval(async () => { try { const status = await getReviewExportJobStatus(jobId) if (status) { exportJobActivity.value = status if (status.status === 'SUCCEEDED') { clearInterval(activityPollTimer) activityPollTimer = null ElMessage.success('å¯¼åºæå') await loadActivity() exportingActivity.value = false } else if (status.status === 'FAILED') { clearInterval(activityPollTimer) activityPollTimer = null ElMessage.error(status.message || '导åºå¤±è´¥') exportingActivity.value = false } } } catch (e) { console.warn('æ¥è¯¢å¯¼åºä»»å¡ç¶æå¤±è´¥ï¼', e?.message || e) } }, 2000) } else { // åéï¼ä½¿ç¨åæ¥å¯¼åº const res = await exportReviewZip(route.params.id) if (res?.success) { ElMessage.success('å¯¼åºæå') await loadActivity() } else { ElMessage.error(res?.message || '导åºå¤±è´¥') } exportingActivity.value = false } } catch (e) { // ç¨æ·åæ¶æå¼å¸¸ if (e && e.message) { console.warn('å¯¼åºæä½åæ¶æå¤±è´¥ï¼', e.message) } } finally { // 弿¥å¯¼åºæ¶ç±è½®è¯¢åè°è´è´£å ³é loadingï¼æ¤å¤ä» 卿ªå¯å¨ä»»å¡æ¶éç½® if (!activityPollTimer) { exportingActivity.value = false } } } // å¯¼åºæå®é¶æ®µè¯å®¡ZIP const handleExportStage = async (stage) => { try { await ElMessageBox.confirm(`å°å¯¼åºã${stage.name}ãé¶æ®µçå·²è¯åæ°æ®ä¸ºZIPï¼ç¡®è®¤ç»§ç»ï¼`, '确认导åº', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) exportingStageId.value = stage.id const jobId = await startReviewExportJob(route.params.id, [stage.id]) if (jobId) { exportJobStageMap.value[stage.id] = { jobId, status: 'PENDING', progress: 0 } // è½®è¯¢é¶æ®µå¯¼åºç¶æ stagePollTimers[stage.id] = setInterval(async () => { try { const status = await getReviewExportJobStatus(jobId) if (status) { exportJobStageMap.value[stage.id] = status if (status.status === 'SUCCEEDED') { clearInterval(stagePollTimers[stage.id]) delete stagePollTimers[stage.id] ElMessage.success('å¯¼åºæå') await loadActivity() exportingStageId.value = null } else if (status.status === 'FAILED') { clearInterval(stagePollTimers[stage.id]) delete stagePollTimers[stage.id] ElMessage.error(status.message || '导åºå¤±è´¥') exportingStageId.value = null } } } catch (e) { console.warn('æ¥è¯¢é¶æ®µå¯¼åºä»»å¡ç¶æå¤±è´¥ï¼', e?.message || e) } }, 2000) } else { // åéï¼åæ¥å¯¼åº const res = await exportReviewZip(route.params.id, [stage.id]) if (res?.success) { ElMessage.success('å¯¼åºæå') await loadActivity() } else { ElMessage.error(res?.message || '导åºå¤±è´¥') } exportingStageId.value = null } } catch (e) { if (e && e.message) { console.warn('导åºé¶æ®µæä½åæ¶æå¤±è´¥ï¼', e.message) } } finally { if (!stagePollTimers[stage.id]) { exportingStageId.value = null } } } </script> <style scoped> web/src/views/activity-list.vue
@@ -54,9 +54,17 @@ <el-tag :type="getStatusType(row.stateName)">{{ row.stateName }}</el-tag> </template> </el-table-column> <el-table-column label="æä½" width="120" fixed="right" align="center"> <el-table-column label="æä½" width="180" fixed="right" align="center"> <template #default="{ row }"> <div class="table-actions"> <el-button text :icon="View" size="small" @click="handleView(row)" class="action-btn view-btn" title="详æ " /> <el-button text :icon="Edit" @@ -99,7 +107,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { useRouter } from 'vue-router' import { getActivities, updateActivityState } from '@/api/activity' import { Search, Plus, Edit, Delete } from '@element-plus/icons-vue' import { Search, Plus, Edit, Delete, View } from '@element-plus/icons-vue' console.log('=== activity-list.vue ç»ä»¶å¼å§å è½½ ===') @@ -177,6 +185,11 @@ // ç¼è¾æ¯èµ const handleEdit = (row: any) => { router.push(`/activity/edit/${row.id}`) } // æ¥ç详æ const handleView = (row: any) => { router.push(`/activity/${row.id}`) } // å 餿¯èµ @@ -351,6 +364,16 @@ background: rgba(245, 108, 108, 0.1) !important; } .view-btn { color: #67C23A; } .view-btn:hover { color: #5daf34; transform: scale(1.2); background: rgba(103, 194, 58, 0.1) !important; } .pagination { margin-top: 20px; display: flex; web/vite.config.ts
@@ -15,22 +15,11 @@ open: true, proxy: { '/api': { // å°API代ç忬å°å端ï¼ä¾¿äºå®æ´èè°ï¼å端è¿è¡å¨ http://127.0.0.1:8080/apiï¼ target: 'http://127.0.0.1:8080', changeOrigin: true, secure: false, configure: (proxy, options) => { proxy.on('error', (err, req, res) => { console.log('proxy error', err); }); proxy.on('proxyReq', (proxyReq, req, res) => { console.log('Sending Request to the Target:', req.method, req.url); }); proxy.on('proxyRes', (proxyRes, req, res) => { console.log('Received Response from the Target:', proxyRes.statusCode, req.url); }); }, // ä¸éè¦éåè·¯å¾ï¼å 为å端çcontext-pathå°±æ¯/api // rewrite: (path) => path.replace(/^\/api/, '/api') // ä¸éè¦éåè·¯å¾ï¼å 为å端çcontext-pathå°±æ¯ /api } } }, wx/pages/activity/detail.js
@@ -9,6 +9,10 @@ myApplication: null, buttonDisabled: false, buttonText: 'æè¦æ¥å', // 导åºç¸å ³ isEmployee: false, exportingActivity: false, exportingStageId: null, loading: true, error: null }, @@ -88,12 +92,16 @@ } } // è§è²ï¼æ¯å¦åå·¥ï¼ä¸»åæ¹/管çåï¼ const isEmployee = !!(app.globalData?.userInfo?.employee && app.globalData.userInfo.employee.id) this.setData({ activity: res.activity, myApplication: myApplication, loading: false, buttonDisabled: buttonDisabled, buttonText: buttonText buttonText: buttonText, isEmployee: isEmployee }); } else { throw new Error('æªæ¾å°æ¯èµä¿¡æ¯'); @@ -135,5 +143,108 @@ return `${startDate} - ${endDate}`; } return startDate || endDate; }, // å¤å¶é¾æ¥å°åªè´´æ¿ copyLink(e) { const url = e.currentTarget.dataset.url if (!url) { wx.showToast({ title: 'ææ é¾æ¥', icon: 'none' }) return } wx.setClipboardData({ data: url, success: () => { wx.showToast({ title: 'å·²å¤å¶ä¸è½½é¾æ¥', icon: 'success' }) } }) }, // å¨WebViewå æå¼é¾æ¥ï¼ç¨äºé¢è§/ä¸è½½ï¼ openWebView(e) { const url = e.currentTarget.dataset.url if (!url) { wx.showToast({ title: 'ææ é¾æ¥', icon: 'none' }) return } wx.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(url)}&title=${encodeURIComponent('è¯å®¡å¯¼åºZIP')}` }) }, // 触å导åºï¼ä¸»æ´»å¨ï¼ async handleExportActivity() { if (!this.data.isEmployee) { wx.showToast({ title: 'æ æéæ§è¡å¯¼åº', icon: 'none' }) return } if (!this.data.activityId) { wx.showToast({ title: 'æ æçæ´»å¨ID', icon: 'none' }) return } this.setData({ exportingActivity: true }) const mutation = ` mutation ExportReviewZip($activityId: ID!, $stageIds: [ID]) { exportReviewZip(activityId: $activityId, stageIds: $stageIds) { success url message } } ` try { const res = await app.graphqlRequest(mutation, { activityId: this.data.activityId, stageIds: null }) const result = res && res.exportReviewZip if (result && result.success) { wx.showToast({ title: 'å¯¼åºæå', icon: 'success' }) // å·æ°ä»¥æ¾ç¤ºææ°é¾æ¥ await this.loadActivityDetail(this.data.activityId) } else { wx.showToast({ title: result?.message || '导åºå¤±è´¥', icon: 'none' }) } } catch (err) { console.error('导åºå¤±è´¥:', err) wx.showToast({ title: '导åºå¤±è´¥', icon: 'none' }) } finally { this.setData({ exportingActivity: false }) } }, // 触å导åºï¼é¶æ®µï¼ async handleExportStage(e) { if (!this.data.isEmployee) { wx.showToast({ title: 'æ æéæ§è¡å¯¼åº', icon: 'none' }) return } const stageId = e.currentTarget.dataset.stageId if (!stageId) { wx.showToast({ title: 'æ æçé¶æ®µID', icon: 'none' }) return } this.setData({ exportingStageId: stageId }) const mutation = ` mutation ExportReviewZip($activityId: ID!, $stageIds: [ID]) { exportReviewZip(activityId: $activityId, stageIds: $stageIds) { success url message } } ` try { const res = await app.graphqlRequest(mutation, { activityId: this.data.activityId, stageIds: [stageId] }) const result = res && res.exportReviewZip if (result && result.success) { wx.showToast({ title: 'å¯¼åºæå', icon: 'success' }) await this.loadActivityDetail(this.data.activityId) } else { wx.showToast({ title: result?.message || '导åºå¤±è´¥', icon: 'none' }) } } catch (err) { console.error('导åºé¶æ®µå¤±è´¥:', err) wx.showToast({ title: '导åºå¤±è´¥', icon: 'none' }) } finally { this.setData({ exportingStageId: null }) } } }); wx/pages/activity/detail.wxml
@@ -34,6 +34,7 @@ <text class="info-label">æ¯èµå¼å§æ¶é´</text> <text class="info-value">{{filters.formatDateTime(activity.matchTime)}}</text> </view> <!-- æè¿è¯å®¡å¯¼åºä¿¡æ¯ä¸å±ç¤ºä¸è½½å°åï¼æ´ä½ç§»é¤ --> </view> </view> @@ -48,6 +49,10 @@ <view class="timeline-content"> <view class="stage-name">{{item.name}}</view> <view class="stage-date">{{filters.formatDate(item.matchTime)}}</view> <view class="stage-export"> <!-- ä¸å±ç¤ºä¸è½½å°åï¼ä» ä¿çå¯¼åºæé®ï¼åå·¥å¯è§ï¼ --> <button wx:if="{{isEmployee}}" class="mini-btn primary" size="mini" bindtap="handleExportStage" data-stage-id="{{item.id}}" loading="{{exportingStageId === item.id}}">导åºé¶æ®µè¯å®¡</button> </view> </view> </view> </view> @@ -65,6 +70,7 @@ <!-- åºé¨æä½æ --> <view class="footer-actions"> <button class="register-btn" bindtap="handleRegister" disabled="{{buttonDisabled}}">{{buttonText}}</button> <button wx:if="{{isEmployee}}" class="export-btn" bindtap="handleExportActivity" loading="{{exportingActivity}}">导åºè¯å®¡ZIP</button> </view> </block> </view> wx/pages/registration/registration.js
@@ -1140,6 +1140,11 @@ errors.phone = '请è¾å ¥æ£ç¡®çææºå·'; } // 项ç®åç§°ä¸ºå¿ å¡«é¡¹ if (!formData.projectName || !formData.projectName.trim()) { errors.projectName = '请è¾å ¥é¡¹ç®åç§°'; } // ä¿æåæé»è¾ï¼ä¸å¼ºå¶éä»¶å¿ å¡«ï¼ä¸æ ¡éªæå±åºåå¿ å¡« this.setData({ errors }); @@ -1258,7 +1263,7 @@ }, regionId: formData.regionId, projectName: formData.projectName, description: formData.description // é¡¹ç®æè¿°ä¸ééï¼ä¸æäº¤å°å端 } // ç¬¬ä¸æ¥ï¼å æäº¤æ³¨åæ°æ®å°åå°ï¼è·å¾æ³¨åID @@ -1336,7 +1341,6 @@ }, regionId: submitData.regionId || null, projectName: submitData.projectName || '', description: submitData.description || '', attachmentMediaIds: [] // å ä¸ä¼ éä»¶ }; wx/pages/registration/registration.wxml
@@ -157,12 +157,12 @@ </view> </view> <!-- 项ç®ä¿¡æ¯å¡çï¼æéæ±éèï¼ --> <view wx:if="{{false}}" class="card"> <!-- 项ç®ä¿¡æ¯å¡ç --> <view class="card"> <view class="card-title">项ç®ä¿¡æ¯</view> <!-- 项ç®åç§° --> <view class="form-item {{errors.projectName ? 'error' : ''}}"> <text class="label">项ç®åç§°</text> <text class="label required">项ç®åç§°</text> <view class="input-wrapper"> <input class="input" @@ -174,23 +174,6 @@ /> </view> <text wx:if="{{errors.projectName}}" class="error-text">{{errors.projectName}}</text> </view> <!-- é¡¹ç®æè¿° --> <view class="form-item vertical-layout {{errors.description ? 'error' : ''}}"> <text class="label">é¡¹ç®æè¿°</text> <view class="input-wrapper"> <textarea class="textarea" placeholder-class="placeholder-class" placeholder="è¯·è¯¦ç»æè¿°æ¨ç项ç®å 容ãç®æ åç¹è²" value="{{formData.description}}" data-field="description" bindinput="onInputChange" maxlength="1000" /> </view> <text wx:if="{{errors.description}}" class="error-text">{{errors.description}}</text> </view> </view>