Codex Assistant
2025-11-06 375c18a6d2713ff19b22093eec57315992d8333f
增加评审下载
16个文件已修改
40个文件已添加
24个文件已删除
4184 ■■■■■ 已修改文件
PasswordTest.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/pom.xml 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/activity/dto/ActivityResponse.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/activity/entity/Activity.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/config/SecurityConfig.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/judge/service/CosService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/review/dto/response/ReviewExportJobStatus.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/review/dto/response/ReviewExportResponse.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/review/service/ReviewExportJobService.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/java/com/rongyichuang/review/service/ReviewExportService.java 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/graphql/activity.graphqls 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/main/resources/graphql/review.graphqls 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
backend/src/test/java/com/rongyichuang/review/DocxLayoutTest.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check-all-education-data.js 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check-database-activity-times.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check-db-gender-education.js 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check-phone-db.js 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
check-user-judge.js 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
clear-invalid-tokens.js 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-backend-user-context.js 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-check-phone.js 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-dataset-issue.md 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-employee-check.js 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-employee.js 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-gender-education-data.js 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-jwt-token.js 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-saveUserInfo.js 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
debug-user-info.js 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/12-匿名用户.docx 补丁 | 查看 | 原始文档 | blame | 历史
doc/~$选评分表)2025年成渝德眉资创业大赛海选.doc 补丁 | 查看 | 原始文档 | blame | 历史
doc/(海选评分表)2025年成渝德眉资创业大赛海选.doc 补丁 | 查看 | 原始文档 | blame | 历史
get-valid-token.js 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
simulate-miniprogram-debug.js 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test-image.svg 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_miniprogram_activities_fix.js 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
test_miniprogram_fix.js 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-layout-sample.docx 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/[Content_Types].xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/_rels/.rels 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/docProps/app.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/docProps/core.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/word/_rels/document.xml.rels 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/word/document.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12-new/word/settings.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/[Content_Types].xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/_rels/.rels 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/docProps/app.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/docProps/core.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/word/_rels/document.xml.rels 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/word/document.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/docx-海选-12/word/settings.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-0/README.txt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-0/players/海选/12-匿名用户.docx 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-1/README.txt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-2/README.txt 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-2/players/海选/12-匿名用户.docx 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export-2/README.txt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export-2/players/海选/12-匿名用户.docx 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/README.txt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/12-匿名用户.docx 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/[Content_Types].xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/_rels/.rels 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/docProps/app.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/docProps/core.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/word/_rels/document.xml.rels 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/word/document.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tmp/review-export/players/海选/docx-xml/word/settings.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
update-activity-times.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
update-activity-times.sql 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
verify-user-judge-mapping.js 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/api/activity.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/api/reviewExport.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/ActivityDetail.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/src/views/activity-list.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web/vite.config.ts 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/activity/detail.js 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/activity/detail.wxml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/registration/registration.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
wx/pages/registration/registration.wxml 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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-ÄäÃûÓû§.docx
Binary files differ
doc/~$Ñ¡ÆÀ·Ö±í£©2025Äê³ÉÓåµÂü×Ê´´Òµ´óÈüº£Ñ¡.doc
Binary files differ
doc/£¨º£Ñ¡ÆÀ·Ö±í£©2025Äê³ÉÓåµÂü×Ê´´Òµ´óÈüº£Ñ¡.doc
Binary 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.docx
Binary 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-ÄäÃûÓû§.docx
Binary 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-ÄäÃûÓû§.docx
Binary 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-ÄäÃûÓû§.docx
Binary 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-ÄäÃûÓû§.docx
Binary 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>