通用技术 记一次测试工具从 V1.0 到 V1.1 的回溯和成果(二)

dun · 2025年08月06日 · 442 次阅读

接上篇,公司里测试需要有一些 AI 相关产出和落地情况,然后测试自己开始做工具(造轮子),经过 多个工作日空闲时间的努力,写出了一个基于 pdf、图片的测试用例生成(按概率随机)工具 V1.0。以下是基于后端业务实现,由于已更新到 V1.1,本片代码基于 V1.1 实现。

1、V1.1 版本工作原理(能用就行原理)


2、从 V1.0 到 V1.1 前后过程

  1. V1.0 版本,图片需要先进行 OCR 识别提取文字,再调用文本模型,期间会出现识别误差,V1.1 改为使用 qwen-vl-plus 模型。 测试进行技术调研(一切皆可 AI),如果使用多模态模型,图片需要公网访问,然后参考了市面上常见(免费送存储空间)的腾讯 COS 对象存储服务,进行文件存储,本地数据库存储文件属性(文件 ID,文件名称、文件公网 url、文件下载 URL、创建时间),只需要在调用模型时查数据库获取图片的公网 URL,即可实现开放的多模态接口。
  2. 文档类型的文件,秉持快速迭代(能不改就不改)原则,沿用了 V1.0 的流程,文件本地服务器进行交互,最终方案被提小建议,文件信息需要入库,方便溯源,觉得挺有道理,V1.1 遂实现文件数据(文件名称、fileId、文件下载 URL、创建时间)入库。
  3. 前端交互优化:

    1. V1.0 进行文件操作时,每次提交 promot 操作都需要先上传文件,再生成 xmind 测试用例。
    2. V1.1 只需上传一次文件,如果需要修改 promot,直接提交 promot,文件不需要再次上传。
    3. V1.0 前端页面生成 xmind 文件后,无法在线预览,必须先下载再本地打开。
    4. V1.1 前端页面新增 xmind 预览组件,生成 xmind 文件后,可以在线预览,返回的数据不满足可以多次生成和预览。
  4. 业务接口:

    1. 文件处理:
    2. 文件上传,入库文件 ID、文件名称,千问的 qwen-long 模型存储 doc、docx 时调这个接口: http://localhost:8028/fileconfig/fileupload
    3. 获取文件信息: http://localhost:8028/fileconfig/fileInfo
    4. 测试点生成:
    5. 图片上传到腾讯 COS:http://localhost:8028/api/uploadtest
    6. 图片生成 xmind 测试点:http://localhost:8028/api/imageUpload
    7. 文档生成 xmind 测试点:http://localhost:8028/api/wordUpload

springBoot 配置文件 application-dev.yml

 server:
  port: 8028

shared:
  resource:
    capacity: 500

spring:
  application:
    name: xlxz
  web:
    resources:
      static-locations: classpath:/templates/
      add-mappings: true
datasource:
    # 添加默认数据源配置
    jdbc-url: jdbc:mysql://XXXX:3306/web_database?useSSL=false&serverTimezone=UTC
    username: XXXX
    password: XXXX
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000

  # Thymeleaf配置
  thymeleaf:
    cache: false
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML

  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

# 日志配置
logging:
  level:
    com:
      community:
        sqlapp: DEBUG
    org:
      springframework:
        jdbc:
          core: DEBUG

# 千问API配置 (请替换为实际的API密钥)
qianwen:
  api:
    url: https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation
    key: (自己申请)
    model: qwen-turbo

qianwenvl:
  vlapi:
    vlurl: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
    qianwenlongurl: https://dashscope.aliyuncs.com/compatible-mode/v1/files
    qwenlongurl: https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation
    vlkey:  (自己申请)
    vlmodel: qwen-vl-plus

# MyBatis配置
mybatis:
  mapper-locations: classpath:mapping/*.xml
  type-aliases-package: com.community.sqlapp.entity

#腾讯COS对象存储配置
tencent:
  cos:
    secretid:   (自己申请)
    secretkey:  (自己申请)
    region:  (自己申请)
    bucketname:  (自己申请)   # 格式:<bucketName>-<appId>
    cosurl: https://<bucketName>-<appId>.cos.region.myqcloud.com

springBoot 依赖 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.microDynamic</groupId>
    <artifactId>microDynamic</artifactId>
    <!--<version>0.0.1-SNAPSHOT</version>-->
    <name>microDynamic</name>
    <description>microDynamic</description>
    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.1.0</version>
        </dependency>

        <!--xmind解析 -->
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version> <!-- 确保版本 >= 2.11.0 -->
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version> <!-- 确保版本 >= 2.11.0 -->
        </dependency>
        <!--xmind解析poi -->
        <!--xpath不加这个依赖会报错-->
        <dependency>
            <groupId>com.github.eljah</groupId>
            <artifactId>xmindjbehaveplugin</artifactId>
            <version>0.8</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.18.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>com.qcloud</groupId>
            <artifactId>cos_api</artifactId>
            <version>5.6.54</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.openai/openai-java -->
        <dependency>
            <groupId>com.openai</groupId>
            <artifactId>openai-java</artifactId>
            <version>2.12.0</version>
        </dependency>
        <!-- JSON解析 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.16.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.16.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>

        <!-- XML处理 -->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!-- ZIP压缩 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-compress</artifactId>
            <version>1.21</version>
        </dependency>

        <dependency>
            <groupId>io.github.bonigarcia</groupId>
            <artifactId>webdrivermanager</artifactId>
            <version>3.0.0</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>

        <!--文件操作依赖包-->
        <dependency>
            <groupId>net.sourceforge.javacsv</groupId>
            <artifactId>javacsv</artifactId>
            <version>2.0</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.5.8</version> <!-- 你也可以使用 4.5.13 或 4.5.14 等新版本 -->
        </dependency>
        <!--   读写Excel文件依赖     -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.3</version>
        </dependency>
        <!-- Spark Core -->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_${scala.binary.version}</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <!--工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.3</version>
        </dependency>

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.3</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jdom</groupId>
            <artifactId>jdom</artifactId>
            <version>1.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.14.3</version> <!-- 请根据实际情况选择合适的版本 -->
        </dependency>
    </dependencies>

    <build>
        <finalName>microDynamic</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <excludes>
                    <exclude>**/*.txt</exclude>
                </excludes>
            </resource>

        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>

            <plugin>
                <artifactId> maven-assembly-plugin </artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>com.testlink.Excel2XMLUI</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

https 连接类 QwenHttpClientUtil.java

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class QwenHttpClientUtil {
    // 从配置文件读取key、apiurl等信息,避免硬编码
    @Value("${qianwenvl.vlapi.vlkey}")
    private String dashscopeApiKey;
    @Value("${qianwenvl.vlapi.vlurl}")
    private String dashscopeApiUrl;
    @Value("${qianwenvl.vlapi.qwenlongurl}")
    private String dashscopeqwenlongurl;
    /**
     * 千问vl大模型
     * @param jsonBody
     * @return
     * @throws IOException
     */
    public String sendPostRequest(String jsonBody) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(dashscopeApiUrl);

        // 设置请求头
        httpPost.setHeader("Authorization", "Bearer " + dashscopeApiKey);
        httpPost.setHeader("Content-Type", "application/json");

        // 设置请求体
        httpPost.setEntity(new StringEntity(jsonBody, "UTF-8"));

        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
            return EntityUtils.toString(response.getEntity());
        } finally {
            httpClient.close();
        }
    }

    /**
     * 千问long大模型
     * @param jsonBody
     * @return
     * @throws IOException
     */
    public String sendGetLongRequest(String jsonBody) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(dashscopeqwenlongurl);

        // 设置请求头
        httpPost.setHeader("Authorization", "Bearer " + dashscopeApiKey);
        httpPost.setHeader("Content-Type", "application/json");

        // 设置请求体
        httpPost.setEntity(new StringEntity(jsonBody, "UTF-8"));

        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
            return EntityUtils.toString(response.getEntity());
        } finally {
            httpClient.close();
        }
    }
}

腾讯 COS 对象存储工具类 TencentCosUtil.java

import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.File;

@Configuration
public class TencentCosUtil {

    @Value("${tencent.cos.secretid}")
    private String secretId;
    @Value("${tencent.cos.secretkey}")
    private String secretKey;
    @Value("${tencent.cos.region}")
    private String region;

    /**
     * 调用静态方法getCosClient()就会获得COSClient实例
     * @return
     */
    @Bean
    public COSClient cosClient() {
        // 1 初始化用户身份信息(secretId, secretKey)。
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
        // 2.1 设置存储桶的地域(上文获得)
        Region regions = new Region(region);
        ClientConfig clientConfig = new ClientConfig(regions);

        // 2.2 使用https协议传输
        clientConfig.setHttpProtocol(HttpProtocol.https);
        // 3 生成 cos 客户端。
        COSClient cosClient = new COSClient(cred, clientConfig);
        // 返回COS客户端
        return cosClient;
    }

    // 静态上传方法
    public static String uploadFile(File file, String cosPath, String bucket, COSClient cosClient) {
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, cosPath, file);
        cosClient.putObject(putObjectRequest);
        // 拼接url(假设私有读,实际可用签名url或自定义域名)
        String url = String.format("https://%s.cos.%s.myqcloud.com/%s", bucket, cosClient.getClientConfig().getRegion().getRegionName(), cosPath);
        return url;
    }
} 

获取 fileId 类

将文件通过 OpenAI 兼容接口上传到阿里云百炼平台,保存至平台安全存储空间后获取文件 ID,GetFileIdMethodClass.java

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 将文件通过OpenAI兼容接口上传到阿里云百炼平台,保存至平台安全存储空间后获取文件ID
 */
@Component
public class GetFileIdMethodClass {
    @Value("${qianwenvl.vlapi.vlkey}")
    private String dashscopeApiKey;
    @Value("${qianwenvl.vlapi.vlurl}")
    private String dashscopeApiUrl;
    @Value("${qianwenvl.vlapi.qianwenlongurl}")
    private String dashscopeApiLongUrl;

    public String getFileId(String tempFilePath) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(dashscopeApiLongUrl);
        // 设置请求头
        httpPost.setHeader("Authorization", "Bearer " + dashscopeApiKey);
        // 构建 form-data 请求体
        File file = new File(tempFilePath);
        FileBody fileBody = new FileBody(file, ContentType.DEFAULT_BINARY);
        StringBody stringBody = new StringBody("file-extract", ContentType.TEXT_PLAIN);
        HttpEntity requestEntity = MultipartEntityBuilder.create()
                .addPart("file", fileBody) // 文件字段,字段名为 "file"
                .addPart("purpose", stringBody) // 字符串字段,字段名为 "stringField"
                .build();

        httpPost.setEntity(requestEntity);

        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
            return EntityUtils.toString(response.getEntity());
        } finally {
            httpClient.close();
        }
    }
}

mapping 文件 DataServerMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.community.sqlapp.mapper.FileInfoMapper">

    <resultMap id="fileInfoResultMap" type="com.community.sqlapp.entity.FileInfoPo">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="fileid" jdbcType="VARCHAR" property="fileId" />
        <result column="filename" jdbcType="VARCHAR" property="fileName" />
        <result column="fileid_info" jdbcType="VARCHAR" property="fileIdInfo" />
    </resultMap>

    <!-- 插入文件信息 -->
    <insert id="insertFileInfo" parameterType="com.community.sqlapp.entity.FileInfoPo" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO web_database.files_info (fileid, filename, fileid_info)
        VALUES (#{fileId}, #{fileName}, #{fileIdInfo})
    </insert>

    <!-- 插入腾讯COS文件信息 -->
    <insert id="insertTencentCosFile" parameterType="com.community.sqlapp.entity.TencentCosFile" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO tencent_cos_file (file_name, file_url)
        VALUES (#{fileName}, #{fileUrl})
    </insert>

    <!-- 根据文件名查询文件信息 -->
    <select id="getFileInfoByFileName" parameterType="String" resultMap="fileInfoResultMap">
        SELECT id, fileid, filename, fileid_info
        FROM web_database.files_info
        WHERE filename = #{fileName}
    </select>

    <!-- 根据文件名查询最新的文件信息(按上传时间降序排序) -->
    <select id="getLatestFileInfoByFileName" parameterType="String" resultMap="fileInfoResultMap">
        SELECT id, fileid, filename, fileid_info
        FROM web_database.files_info
        WHERE filename = #{fileName}
        ORDER BY create_time DESC
            LIMIT 1
    </select>

    <!-- 查询所有文件信息 -->
    <select id="getAllFileInfo" resultMap="fileInfoResultMap">
        SELECT id, fileid, filename, fileid_info
        FROM web_database.files_info
        ORDER BY id DESC
    </select>

</mapper>

mapper 类

FileInfoMapper.java、TencentCosFileMapper.java。

import com.community.sqlapp.entity.FileInfoPo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface FileInfoMapper {

    /**
     * 插入文件信息
     * @param fileInfo
     * @return
     */
    int insertFileInfo(FileInfoPo fileInfo);

    /**
     * 根据文件名查询文件信息
     * @param fileName
     * @return
     */
    FileInfoPo getFileInfoByFileName(String fileName);

    /**
     * 根据文件名查询最新的文件信息(按上传时间降序排序)
     * @param fileName
     * @return
     */
    FileInfoPo getLatestFileInfoByFileName(String fileName);
}
import com.community.sqlapp.entity.TencentCosFile;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;

@Mapper
public interface TencentCosFileMapper {
    @Insert("INSERT INTO tencent_cos_file (file_name, file_url) VALUES (#{fileName}, #{fileUrl})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(TencentCosFile file);
} 

实体类 FileInfoPo.java,TencentCosFile.java

import lombok.Data;

@Data
public class FileInfoPo {
    private Integer id;  // 自增ID
    private String fileId;
    private String fileName;
    private String fileIdInfo;
}
import lombok.Data;

@Data
public class TencentCosFile {
    private Integer id;
    private String fileName;
    private String fileUrl;
} 

service 实现类

文件上传,信息入库实现类(FileInfoService.java),千问大模型,生成测试点实现类(QwenVlToXmindService.java),xmind 解析生成 xmind 文件实现类(TestPointConverService.java)。

import com.community.sqlapp.dpsToXmind.GetFileIdMethodClass;
import com.community.sqlapp.entity.FileInfoPo;
import com.community.sqlapp.mapper.FileInfoMapper;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.beans.factory.annotation.Value;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

/**
 * 文件上传,信息入库实现类
 */
@Service
public class FileInfoService {

    @Autowired
    private FileInfoMapper fileInfoMapper;
    @Autowired
    private GetFileIdMethodClass getFileIdMethodClass;
    @Autowired
    private COSClient cosClient;
    @Value("${tencent.cos.bucketname}")
    private String bucketName;
    @Value("${tencent.cos.cosurl}")
    private String cosUrl;
    /**
     * 上传文件并保存文件信息到数据库
     * @param file
     * @return
     */
    public FileInfoPo uploadFile(MultipartFile file) throws IOException {
        // 生成唯一的文件ID
        String fileId = UUID.randomUUID().toString();

        // 获取原始文件名
        String originalFileName = file.getOriginalFilename();

        // 创建文件信息对象
        FileInfoPo fileInfo = new FileInfoPo();
        try {
            // 创建临时文件
            File tempFile = File.createTempFile("upload_", "_" + originalFileName);
            // 将MultipartFile转换为临时文件
            file.transferTo(tempFile);
            // 使用GetFileIdMethodClass获取千问文件服务器返回的文件信息,传递临时文件的完整绝对路径
            String apiFileInfo = getFileIdMethodClass.getFileId(tempFile.getAbsolutePath());
            if (apiFileInfo != null && !apiFileInfo.trim().isEmpty()) {
                // 解析JSON字符串,提取id字段
                try {
                    JSONObject jsonObject = JSON.parseObject(apiFileInfo);
                    String extractedFileId = jsonObject.getString("id");
                    if (extractedFileId != null && !extractedFileId.trim().isEmpty()) {
                        //从千问文件服务器返回的文件信息中提取fileid
                        fileId = extractedFileId;
                    }
                } catch (Exception e) {
                    System.out.println("JSON解析失败: " + e.getMessage());
                }

                fileInfo.setFileIdInfo(apiFileInfo);
                fileInfo.setFileId(fileId);
            }
            // 删除临时文件
            tempFile.delete();
        } catch (Exception e) {
            // 如果GetFileIdMethodClass失败,继续使用UUID
            System.out.println("API调用失败,使用UUID作为文件ID: " + e.getMessage());
        }
        fileInfo.setFileName(originalFileName);
        // 保存到数据库
        fileInfoMapper.insertFileInfo(fileInfo);
        return fileInfo;
    }

    /**
     * 根据文件名查询文件信息
     * @param fileName
     * @return
     */
    public FileInfoPo getFileInfoByFileName(String fileName) {
        return fileInfoMapper.getFileInfoByFileName(fileName);
    }

    /**
     * 图片上传到腾讯COS,获取公网访问域名
     * @param file
     * @return
     * @throws IOException
     */
    public String uploadCOSFile(MultipartFile file) throws IOException {
        // 生成唯一文件名
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
        String key = "/images/" + UUID.randomUUID() + suffix;

        // 元数据
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(file.getSize());
        metadata.setContentType(file.getContentType());

        try (InputStream inputStream = file.getInputStream()) {
            PutObjectRequest putObjectRequest =
                    new PutObjectRequest(bucketName, key, inputStream, metadata);
            PutObjectResult result = cosClient.putObject(putObjectRequest);
            System.out.println(result.getRequestId());
            //cosClient.shutdown();
            // 返回可访问地址
            return  cosUrl + key;
        }
    }
}
import com.community.sqlapp.dpsToXmind.QwenHttpClientUtil;
import com.community.sqlapp.entity.FileInfoPo;
import com.community.sqlapp.mapper.FileInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;

/**
 * 千问大模型,生成测试点实现类
 */
@Service
public class QwenVlToXmindService {

    @Autowired
    private QwenHttpClientUtil httpClientUtil;
    @Autowired
    private FileInfoMapper fileInfoMapper;

    /**
     * 图片请求qwen-vl-plus大模型
     * @param imageUrl
     * @param userPrompt
     * @return
     * @throws IOException
     */
    public String parseDocument(String imageUrl, String userPrompt) throws IOException {
        String dashscopeModel = "qwen-vl-plus"; // 可从配置读取
        // 智能prompt,要求AI只返回JSON结构
        String smartPrompt = (userPrompt == null || userPrompt.isEmpty() ? "请根据上传的需求文档,自动提取所有功能点并生成详细的测试点,要求结构化、分层级、覆盖全面。" : userPrompt);
        System.out.println("userPrompt: "+userPrompt);
        StringBuilder jsonBody = new StringBuilder();
        jsonBody.append("{");
        jsonBody.append("\"model\": \"").append(dashscopeModel).append("\",");
        jsonBody.append("\"messages\": [");
        jsonBody.append("  {");
        jsonBody.append("    \"role\": \"user\",");
        jsonBody.append("    \"content\": [");
        // 图片内容
        jsonBody.append("      {\"type\": \"image_url\", \"image_url\": {\"url\": \"").append(imageUrl).append("\"}},");
        // 文本内容
        jsonBody.append("      {\"type\": \"text\", \"text\": \"").append(smartPrompt).append("\"}");
        jsonBody.append("    ]");
        jsonBody.append("  }");
        jsonBody.append("]");
        jsonBody.append("}");
        System.out.println("请求体: "+jsonBody.toString());
        return httpClientUtil.sendPostRequest(jsonBody.toString());
    }

    /**
     * word文档使用qwen-long大模型
     * @param fileName 文件名
     * @param userPrompt 用户提示
     * @return
     * @throws IOException
     */
    public String parseLongDocument(String fileName, String userPrompt) throws IOException {
        String dashscopeModel = "qwen-long"; // 可从配置读取
        // 智能prompt,要求AI只返回JSON结构
        String smartPrompt = (userPrompt == null || userPrompt.isEmpty() ? "请根据上传的需求文档,自动提取所有功能点并生成详细的测试点,要求结构化、分层级、覆盖全面。" : userPrompt);

        // 从数据库获取fileid
        FileInfoPo fileInfo = fileInfoMapper.getLatestFileInfoByFileName(fileName);
        if (fileInfo == null) {
            throw new IOException("未找到文件信息:" + fileName);
        }

        String fileId = fileInfo.getFileId();
        System.out.println("从数据库获取的fileId: "+fileId);

        StringBuilder jsonBody = new StringBuilder();
        jsonBody.append("{" +
                "    \"model\": \""+dashscopeModel+"\"," +
                "    \"input\": {" +
                "        \"messages\": [" +
                "            {\"role\": \"system\",\"content\": \"fileid://"+fileId+"\"}," +
                "            {\"role\": \"user\",\"content\": \""+smartPrompt+"\"}" +
                "        ]" +
                "    }," +
                "    \"parameters\": {" +
                "        \"result_format\": \"message\"" +
                "    }" +
                "}");
        System.out.println("请求体: "+jsonBody.toString());
        return httpClientUtil.sendGetLongRequest(jsonBody.toString());
    }
}
import com.google.gson.*;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedHashMap;

/**
 * xmind解析生成xmind文件实现类
 */
@Service
public class TestPointConverService {
    private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
    /**
     * 递归将任意JSON结构转为XMind树结构
     * 支持 {"测试点": [...] } 这种结构,所有分层、子测试点、测试方法都能还原为XMind节点
     */
    public static String jsonToXmindTree(String json) {
        JsonElement rootElem = JsonParser.parseString(json);
        JsonObject rootObj;
        if (rootElem.isJsonObject()) {
            rootObj = rootElem.getAsJsonObject();
        } else {
            throw new IllegalArgumentException("输入必须为JSON对象");
        }
        // 取第一个key作为根节点
        Set<Map.Entry<String, JsonElement>> entries = rootObj.entrySet();
        if (entries.isEmpty()) throw new IllegalArgumentException("JSON对象不能为空");
        Map.Entry<String, JsonElement> first = entries.iterator().next();
        JsonObject xmindRoot = new JsonObject();
        xmindRoot.addProperty("topic", first.getKey());
        JsonArray children = new JsonArray();
        buildXmindChildren(first.getValue(), children);
        if (children.size() > 0) xmindRoot.add("children", children);
        return gson.toJson(xmindRoot);
    }

    // 递归构建children,增强适配多层级结构
    private static void buildXmindChildren(JsonElement elem, JsonArray children) {
        if (elem == null || elem.isJsonNull()) return;
        if (elem.isJsonArray()) {
            for (JsonElement e : elem.getAsJsonArray()) {
                buildXmindChildren(e, children);
            }
        } else if (elem.isJsonObject()) {
            JsonObject obj = elem.getAsJsonObject();
            String topic = null;
            JsonArray nodeChildren = new JsonArray();

            // 优先常规字段
            if (obj.has("一级模块")) {
                topic = obj.get("一级模块").isJsonPrimitive() ? obj.get("一级模块").getAsString() : "一级模块";
                if (obj.has("二级模块")) {
                    buildXmindChildren(obj.get("二级模块"), nodeChildren);
                }
            } else if (obj.has("二级模块")) {
                if (obj.get("二级模块").isJsonArray()) {
                    topic = null;
                    buildXmindChildren(obj.get("二级模块"), nodeChildren);
                } else if (obj.get("二级模块").isJsonPrimitive()) {
                    topic = obj.get("二级模块").getAsString();
                    if (obj.has("测试点")) {
                        buildXmindChildren(obj.get("测试点"), nodeChildren);
                    }
                } else if (obj.get("二级模块").isJsonObject()) {
                    topic = "二级模块";
                    buildXmindChildren(obj.get("二级模块"), nodeChildren);
                }
            } else if (obj.has("测试点")) {
                JsonElement testPointElem = obj.get("测试点");
                if (testPointElem.isJsonArray()) {
                    topic = "测试点";
                    buildXmindChildren(testPointElem, nodeChildren);
                } else if (testPointElem.isJsonPrimitive()) {
                    topic = testPointElem.getAsString();
                    if (obj.has("测试用例名称")) {
                        buildXmindChildren(obj.get("测试用例名称"), nodeChildren);
                    }
                } else if (testPointElem.isJsonObject()) {
                    topic = "测试点";
                    buildXmindChildren(testPointElem, nodeChildren);
                }
            } else if (obj.has("测试用例名称")) {
                topic = obj.get("测试用例名称").isJsonPrimitive() ? obj.get("测试用例名称").getAsString() : "测试用例名称";
            } else if (obj.has("名称")) {
                topic = obj.get("名称").isJsonPrimitive() ? obj.get("名称").getAsString() : "名称";
            } else if (obj.has("topic")) {
                topic = obj.get("topic").isJsonPrimitive() ? obj.get("topic").getAsString() : "topic";
            } else {
                // 新增:如果是普通对象,遍历所有key,每个key作为一个子节点
                for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
                    String key = entry.getKey();
                    JsonElement value = entry.getValue();
                    JsonObject node = new JsonObject();
                    node.addProperty("topic", key);
                    JsonArray childArr = new JsonArray();
                    if (value.isJsonArray() || value.isJsonObject()) {
                        buildXmindChildren(value, childArr);
                        if (childArr.size() > 0) node.add("children", childArr);
                    } else if (value.isJsonPrimitive()) {
                        // 叶子节点直接显示key: value
                        node.addProperty("topic", key + ": " + value.getAsString());
                    }
                    children.add(node);
                }
                return;
            }

            // 只在有topic时生成节点
            if (topic != null) {
                JsonObject node = new JsonObject();
                node.addProperty("topic", topic);
                if (nodeChildren.size() > 0) node.add("children", nodeChildren);
                children.add(node);
            } else if (nodeChildren.size() > 0) {
                for (JsonElement c : nodeChildren) {
                    children.add(c);
                }
            }
        } else if (elem.isJsonPrimitive()) {
            JsonObject node = new JsonObject();
            node.addProperty("topic", elem.getAsString());
            children.add(node);
        }
    }

    /**
     * 将平铺的数组结构自动分层为树状结构,支持一级模块、二级模块、测试点、测试用例名称四级分组
     */
    public static String flatJsonToXmindTree(String json) {
        JsonElement elem = JsonParser.parseString(json);
        if (elem.isJsonArray()) {
            JsonArray arr = elem.getAsJsonArray();
            // 构建树:Map<一级, Map<二级, Map<测试点, List<测试用例名称>>>>
            Map<String, Map<String, Map<String, List<String>>>> tree = new LinkedHashMap<>();
            for (JsonElement e : arr) {
                JsonObject obj = e.getAsJsonObject();
                String lv1 = obj.has("一级模块") ? obj.get("一级模块").getAsString() : "未知一级";
                String lv2 = obj.has("二级模块") ? obj.get("二级模块").getAsString() : "未知二级";
                // 处理测试点为数组或字符串
                List<String> testPoints = new ArrayList<>();
                if (obj.has("测试点")) {
                    JsonElement tpElem = obj.get("测试点");
                    if (tpElem.isJsonArray()) {
                        for (JsonElement tp : tpElem.getAsJsonArray()) {
                            testPoints.add(tp.getAsString());
                        }
                    } else if (tpElem.isJsonPrimitive()) {
                        testPoints.add(tpElem.getAsString());
                    }
                } else {
                    testPoints.add("未知测试点");
                }
                // 处理测试用例名称为数组或字符串
                List<String> caseNames = new ArrayList<>();
                if (obj.has("测试用例名称")) {
                    JsonElement cnElem = obj.get("测试用例名称");
                    if (cnElem.isJsonArray()) {
                        for (JsonElement cn : cnElem.getAsJsonArray()) {
                            caseNames.add(cn.getAsString());
                        }
                    } else if (cnElem.isJsonPrimitive()) {
                        caseNames.add(cnElem.getAsString());
                    }
                } else {
                    caseNames.add("用例");
                }
                // 构建树结构
                for (int i = 0; i < Math.max(testPoints.size(), caseNames.size()); i++) {
                    String lv3 = i < testPoints.size() ? testPoints.get(i) : "未知测试点";
                    String lv4 = i < caseNames.size() ? caseNames.get(i) : "用例";
                    tree.computeIfAbsent(lv1, k -> new LinkedHashMap<>())
                            .computeIfAbsent(lv2, k -> new LinkedHashMap<>())
                            .computeIfAbsent(lv3, k -> new ArrayList<>())
                            .add(lv4);
                }
            }
            // 构建XMind JSON
            JsonObject root = new JsonObject();
            root.addProperty("topic", "一级目录");
            JsonArray lv1Arr = new JsonArray();
            for (Map.Entry<String, Map<String, Map<String, List<String>>>> e1 : tree.entrySet()) {
                JsonObject lv1Node = new JsonObject();
                lv1Node.addProperty("topic", e1.getKey());
                JsonArray lv2Arr = new JsonArray();
                for (Map.Entry<String, Map<String, List<String>>> e2 : e1.getValue().entrySet()) {
                    JsonObject lv2Node = new JsonObject();
                    lv2Node.addProperty("topic", e2.getKey());
                    JsonArray lv3Arr = new JsonArray();
                    for (Map.Entry<String, List<String>> e3 : e2.getValue().entrySet()) {
                        JsonObject lv3Node = new JsonObject();
                        lv3Node.addProperty("topic", e3.getKey());
                        JsonArray lv4Arr = new JsonArray();
                        for (String lv4 : e3.getValue()) {
                            JsonObject lv4Node = new JsonObject();
                            lv4Node.addProperty("topic", lv4);
                            lv4Arr.add(lv4Node);
                        }
                        if (lv4Arr.size() > 0) lv3Node.add("children", lv4Arr);
                        lv3Arr.add(lv3Node);
                    }
                    if (lv3Arr.size() > 0) lv2Node.add("children", lv3Arr);
                    lv2Arr.add(lv2Node);
                }
                if (lv2Arr.size() > 0) lv1Node.add("children", lv2Arr);
                lv1Arr.add(lv1Node);
            }
            if (lv1Arr.size() > 0) root.add("children", lv1Arr);
            return gson.toJson(root);
        } else if (elem.isJsonObject()) {
            // 新增:对象结构直接用递归树方法
            return jsonToXmindTree(json);
        } else {
            throw new IllegalArgumentException("不支持的JSON结构");
        }
    }

    /**
     * 生成xmind8文件(包含content.xml和META-INF/manifest.xml,标准结构)
     */
    public static void generateXMind(String json, String outputPath) throws Exception {
        String contentXml = jsonToXMindContentXml(json);
        // 1. 生成content.xml
        File tempDir = new File("temp_xmind");
        if (!tempDir.exists()) tempDir.mkdirs();
        File contentFile = new File(tempDir, "content.xml");
        try (FileOutputStream fos = new FileOutputStream(contentFile)) {
            fos.write(contentXml.getBytes("UTF-8"));
        }
        // 2. 生成META-INF/manifest.xml
        File metaInfDir = new File(tempDir, "META-INF");
        if (!metaInfDir.exists()) metaInfDir.mkdirs();
        File manifestFile = new File(metaInfDir, "manifest.xml");
        String manifestXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
                "<manifest xmlns=\"urn:xmind:xmap:xmlns:manifest:1.0\">\n" +
                "  <file-entry full-path=\"content.xml\" media-type=\"text/xml\"/>\n" +
                "</manifest>\n";
        try (FileOutputStream fos = new FileOutputStream(manifestFile)) {
            fos.write(manifestXml.getBytes("UTF-8"));
        }
        // 3. 打包为zip(xmind)
        try (FileOutputStream fos = new FileOutputStream(outputPath);
             ZipOutputStream zos = new ZipOutputStream(fos)) {
            // content.xml
            ZipEntry entry1 = new ZipEntry("content.xml");
            zos.putNextEntry(entry1);
            byte[] bytes1 = Files.readAllBytes(contentFile.toPath());
            zos.write(bytes1, 0, bytes1.length);
            zos.closeEntry();
            // META-INF/manifest.xml
            ZipEntry entry2 = new ZipEntry("META-INF/manifest.xml");
            zos.putNextEntry(entry2);
            byte[] bytes2 = Files.readAllBytes(manifestFile.toPath());
            zos.write(bytes2, 0, bytes2.length);
            zos.closeEntry();
        }
        // 清理临时文件
        contentFile.delete();
        manifestFile.delete();
        metaInfDir.delete();
        tempDir.delete();
    }

    // 将json结构转为xmind8 content.xml内容(极简结构,仅topic树)
    private static String jsonToXMindContentXml(String json) {
        JsonObject root = gson.fromJson(json, JsonObject.class);
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n");
        sb.append("<xmap-content xmlns=\"urn:xmind:xmap:xmlns:content:2.0\" version=\"2.0\">\n");
        sb.append("  <sheet id=\"1\" timestamp=\"" + System.currentTimeMillis() + "\">\n");
        sb.append("    <title>").append(root.get("topic").getAsString()).append("</title>\n");
        sb.append(jsonTopicToXml(root, 3));
        sb.append("  </sheet>\n");
        sb.append("</xmap-content>\n");
        return sb.toString();
    }

    private static String jsonTopicToXml(JsonObject node, int indent) {
        StringBuilder sb = new StringBuilder();
        String topic = node.has("topic") ? node.get("topic").getAsString() : "";
        String pad = "  ".repeat(indent);
        sb.append(pad).append("<topic id=\"t" + System.nanoTime() + "\">\n");
        sb.append(pad).append("  <title>").append(topic).append("</title>\n");
        if (node.has("children")) {
            JsonArray children = node.getAsJsonArray("children");
            if (children.size() > 0) {
                sb.append(pad).append("  <children>\n");
                sb.append(pad).append("    <topics type=\"attached\">\n");
                for (int i = 0; i < children.size(); i++) {
                    JsonObject child = children.get(i).getAsJsonObject();
                    sb.append(jsonTopicToXml(child, indent + 3));
                }
                sb.append(pad).append("    </topics>\n");
                sb.append(pad).append("  </children>\n");
            }
        }
        sb.append(pad).append("</topic>\n");
        return sb.toString();
    }
}

文件 To 测试用例 Controller:FileToTestPointController.java

import com.community.sqlapp.entity.TencentCosFile;
import com.community.sqlapp.mapper.TencentCosFileMapper;
import com.community.sqlapp.service.FileInfoService;
import com.community.sqlapp.service.QwenVlToXmindService;
import com.community.sqlapp.service.TestPointConverService;
import com.google.gson.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.xmind.core.CoreException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping("/api")
public class FileToTestPointController {

    @Autowired
    private QwenVlToXmindService qwenVlService;
    @Autowired
    private FileInfoService fileInfoService;
    @Autowired
    private TencentCosFileMapper tencentCosFileMapper;

    /**
     * 图片生成xmind测试点
     * @param file
     * @param promot
     * @return
     * @throws IOException
     * @throws CoreException
     */
    @PostMapping("/imageUpload")
    public Map<String, Object> imageUploadFile(@RequestParam("file") MultipartFile file, @RequestParam(value = "promot", required = false) String promot) throws IOException, CoreException {
        Map<String, Object> response = new HashMap<>();
        try {
            // 2. 上传到腾讯云COS
            String fileUrl = fileInfoService.uploadCOSFile(file);
            System.out.println("fileUrl: "+fileUrl);
            // 3. 存入数据库
            TencentCosFile cosFile = new TencentCosFile();
            cosFile.setFileName(file.getOriginalFilename());
            cosFile.setFileUrl(fileUrl);
            tencentCosFileMapper.insert(cosFile);

            // 4. 用fileUrl调用大模型
            String promotInfo = "请严格以如下JSON格式返回,不要输出多余的解释或Markdown,返回json内容包含一级模块、二级模块、测试点、测试用例名称";
            String userPrompt = (promot != null && !promot.isEmpty() ? promot : "请根据上传的需求文档自动提取所有功能点并生成详细的测试点,要求结构化、分层级、覆盖全面。") + promotInfo;
            //调用 qwen-vl-plus 模型解析文档内容(新版API,文档+文本)
            String aiJson = qwenVlService.parseDocument(fileUrl, userPrompt);
            System.out.println("返回数据: " + aiJson);
            String xmindPath = null;
            try {
                // 1. 提取content字段
                JsonObject resp = JsonParser.parseString(aiJson).getAsJsonObject();
                if (resp.has("output")) {
                    resp = resp.getAsJsonObject("output");
                }
                String content = "";
                if (resp.has("choices")) {
                    JsonArray choices = resp.getAsJsonArray("choices");
                    if (choices.size() > 0) {
                        JsonObject msg = choices.get(0).getAsJsonObject();
                        if (msg.has("message")) {
                            JsonObject message = msg.getAsJsonObject("message");
                            if (message.has("content")) {
                                content = message.get("content").getAsString();
                            }
                        }
                    }
                }
                if (content.isEmpty()) {
                    response.put("success", false);
                    response.put("error", "AI返回内容无content字段,原始返回:" + aiJson);
                    return response;
                }
                // 2. 去除markdown代码块包裹
                content = content.replaceAll("(?s)```json|```", "").trim();
                System.out.println("提取的content内容:\n" + content);
                // 3. 平铺数组转树状结构
                String xmindJson = TestPointConverService.flatJsonToXmindTree(content);
                System.out.println("解析成功!生成的JSON结构:\n" + xmindJson);
                // 4. 生成XMind文件
                String suffix = file.getOriginalFilename();
                int index = suffix.indexOf('.');
                if (index != -1) { // 确保找到了点号
                    suffix = suffix.substring(0, index); // 获取第一个点号之前的部分
                }
                System.out.println("文件名称"+ suffix);
                xmindPath = suffix+".xmind";
                TestPointConverService.generateXMind(xmindJson, xmindPath);
                System.out.println("XMind文件生成成功:" + xmindPath);
                response.put("success", true);
                response.put("message", "XMind 文件已生成");
                response.put("downloadUrl", "/fileconfig/download?file="+xmindPath);
            } catch (Exception e) {
                System.err.println("解析失败:" + e.getMessage());
                e.printStackTrace();
                response.put("success", false);
                response.put("error", "解析失败:" + e.getMessage());
            }
        } catch (Exception e) {
            response.put("success", false);
            response.put("error", e.getMessage());
        }
        return response;
    }

    /**
     * 图片上传到腾讯COS
     * @param file
     * @return
     */
    @PostMapping("/uploadtest")
    public ResponseEntity<String> uploadTest(@RequestParam("file") MultipartFile file) {
        try {
            String url = String.valueOf(fileInfoService.uploadCOSFile(file));
            System.out.println("fileUrl: "+url);
            return ResponseEntity.ok(url);
        } catch (Exception e) {
            return ResponseEntity.status(500).body("上传失败:" + e.getMessage());
        }
    }

    /**
     * 文件生成测试点,根据文件名从数据库获取fileid
     * @param fileName 文件名
     * @param promot 提示词
     * @return
     * @throws IOException
     * @throws CoreException
     */
    @PostMapping("/wordUpload")
    public Map<String, Object> wordUpload(@RequestParam("fileName") String fileName, @RequestParam(value = "promot", required = false) String promot) throws IOException, CoreException {
        Map<String, Object> response = new HashMap<>();
        try {
            String promotInfo="请严格以如下JSON格式返回,不要输出多余的解释或Markdown,返回json内容包含一级模块、二级模块、测试点、测试用例名称";
            String userPrompt = promot != null && !promot.isEmpty() ? promot+promotInfo : "请根据上传的需求文档自动提取所有功能点并生成详细的测试点,要求结构化、分层级、覆盖全面。"+promotInfo;

            // 调用 Qwen-long 模型解析文档内容(新版API,文档+文本)
            String aiJson = qwenVlService.parseLongDocument(fileName, userPrompt);
            System.out.println("返回数据: " + aiJson);
            String xmindPath = null;
            try {
                // 1. 提取content字段
                JsonObject resp = JsonParser.parseString(aiJson).getAsJsonObject();
                if (resp.has("output")) {
                    resp = resp.getAsJsonObject("output");
                }
                String content = "";
                if (resp.has("choices")) {
                    JsonArray choices = resp.getAsJsonArray("choices");
                    if (choices.size() > 0) {
                        JsonObject msg = choices.get(0).getAsJsonObject();
                        if (msg.has("message")) {
                            JsonObject message = msg.getAsJsonObject("message");
                            if (message.has("content")) {
                                content = message.get("content").getAsString();
                            }
                        }
                    }
                }
                if (content.isEmpty()) {
                    response.put("success", false);
                    response.put("error", "AI返回内容无content字段,原始返回:" + aiJson);
                    return response;
                }
                // 2. 去除markdown代码块包裹
                content = content.replaceAll("(?s)```json|```", "").trim();
                System.out.println("提取的content内容:\n" + content);
                // 3. 平铺数组转树状结构
                String xmindJson = TestPointConverService.flatJsonToXmindTree(content);
                System.out.println("解析成功!生成的JSON结构:\n" + xmindJson);
                // 4. 生成XMind文件
                xmindPath = fileName+".xmind";
                TestPointConverService.generateXMind(xmindJson, xmindPath);
                System.out.println("XMind文件生成成功:" + xmindPath);

                response.put("success", true);
                response.put("message", "XMind 文件已生成");
                response.put("downloadUrl", "/fileconfig/download?file="+xmindPath);
            } catch (Exception e) {
                System.err.println("解析失败:" + e.getMessage());
                e.printStackTrace();
                response.put("success", false);
                response.put("error", "解析失败:" + e.getMessage());
            }
        } catch (Exception e) {
            response.put("success", false);
            response.put("error", e.getMessage());
        }
        return response;
    }
}

文件上传 Controller:FileUploadMethodController.java

import com.community.sqlapp.service.FileInfoService;
import com.community.sqlapp.entity.FileInfoPo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/fileconfig")
public class FileUploadMethodController {

    @Autowired
    private FileInfoService fileInfoService;
    /**
     * 显示文件上传页面
     */
    @GetMapping("/upload")
    public String showUploadPage() {
        return "fileupload";
    }

    /**
     * 文件上传,入库文件ID、文件名称,千问的qwen-long模型存储doc、docx时调这个接口
     * @param file
     * @return
     */
    @PostMapping("/fileupload")
    @ResponseBody
    public Map<String, Object> qwenLongFileUpload(@RequestParam("file") MultipartFile file) {
        Map<String, Object> response = new HashMap<>();
        try {
            // 调用文件上传服务
            FileInfoPo fileInfo = fileInfoService.uploadFile(file);

            response.put("success", true);
            response.put("message", "文件上传成功");
            response.put("fileId", fileInfo.getFileId());
            response.put("fileName", fileInfo.getFileName());
            response.put("id", fileInfo.getId());
        } catch (Exception e) {
            response.put("success", false);
            response.put("error", e.getMessage());
        }
        return response;
    }

    /**
     * 获取文件信息
     * @param file
     * @return
     * @throws Exception
     */
    @PostMapping("/fileInfo")
    @ResponseBody
    public Map<String, Object> parseFileContent(@RequestParam("file") MultipartFile file) throws Exception {
        Map<String, Object> response = new HashMap<>();
        try {
            String fileName = file.getOriginalFilename();
            FileInfoPo fileInfo = fileInfoService.getFileInfoByFileName(fileName);

            if (fileInfo != null) {
                response.put("success", true);
                response.put("fileInfo", fileInfo);
            } else {
                response.put("success", false);
                response.put("message", "文件信息未找到");
            }
        } catch (Exception e) {
            response.put("success", false);
            response.put("error", e.getMessage());
        }
        return response;
    }
}

启动类:MicroDynamicApplication.java

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ConfigurationPropertiesScan
@MapperScan("com.community.sqlapp.mapper")
@ComponentScan(basePackages = {"com.community.sqlapp"})
@EnableConfigurationProperties
public class MicroDynamicApplication {

    public static void main(String[] args) {
        SpringApplication.run(MicroDynamicApplication.class, args);
    }
} 

sql 文件

-- 创建files_info表的SQL脚本
-- 数据库web_database

CREATE TABLE IF NOT EXISTS `web_database`.`files_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
  `fileid` varchar(255) NOT NULL COMMENT '文件ID',
  `filename` varchar(500) NOT NULL COMMENT '文件名',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_fileid` (`fileid`),
  KEY `idx_filename` (`filename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件信息表';

CREATE TABLE tencent_cos_file (
        id INT AUTO_INCREMENT PRIMARY KEY,
        file_name VARCHAR(255) NOT NULL,
        file_url VARCHAR(1024) NOT NULL,
        create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

最后

至此 V1.1 版本目前告一段落,完成所有项目文件相关的数据切换到对象存储服务,BUG 和代码健壮性还是很多需要改进的地方,页面还没有完全前后端分离。继续回归业务测试本职工作。下次时间空闲出来了再继续改造。

暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册