xuzhancheng 6 روز پیش
کامیت
b861868fce

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

+ 98 - 0
pom.xml

@@ -0,0 +1,98 @@
+<?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>3.5.11</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com</groupId>
+    <artifactId>grape-city</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>grape-city</name>
+    <description>grape-city</description>
+    <url/>
+    <licenses>
+        <license/>
+    </licenses>
+    <developers>
+        <developer/>
+    </developers>
+    <scm>
+        <connection/>
+        <developerConnection/>
+        <tag/>
+        <url/>
+    </scm>
+    <properties>
+        <java.version>17</java.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.grapecitysoft.documents</groupId>
+            <artifactId>gcexcel</artifactId>
+            <version>8.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>2.0.57</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+    <!-- 使用 huawei / aliyun 的 Maven 源,提升下载速度 -->
+    <repositories>
+        <repository>
+            <id>huaweicloud</id>
+            <name>huawei</name>
+            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
+        </repository>
+        <repository>
+            <id>aliyunmaven</id>
+            <name>aliyun</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+        </repository>
+
+        <repository>
+            <id>spring-milestones</id>
+            <name>Spring Milestones</name>
+            <url>https://repo.spring.io/milestone</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>spring-snapshots</id>
+            <name>Spring Snapshots</name>
+            <url>https://repo.spring.io/snapshot</url>
+            <releases>
+                <enabled>false</enabled>
+            </releases>
+        </repository>
+    </repositories>
+</project>

+ 13 - 0
src/main/java/com/grapecity/GrapeCityApplication.java

@@ -0,0 +1,13 @@
+package com.grapecity;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class GrapeCityApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(GrapeCityApplication.class, args);
+    }
+
+}

+ 530 - 0
src/main/java/com/grapecity/controller/GrapeCityController.java

@@ -0,0 +1,530 @@
+package com.grapecity.controller;
+
+
+import com.alibaba.fastjson2.*;
+import com.grapecity.documents.excel.*;
+import com.grapecity.documents.excel.drawing.ImageType;
+import com.grapecity.documents.excel.template.DataSource.JsonDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.*;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.List;
+/**
+ * GrapeCity 控制器
+ * 提供 Excel 模板处理和 PDF 生成功能
+ */
+@RestController
+@RequestMapping("/grapecity")
+public class GrapeCityController {
+    private static final Logger logger = LoggerFactory.getLogger(GrapeCityController.class);
+
+
+    /**
+     * 获取 Excel 模板中所有图片路径
+     * 遍历工作表中的所有单元格,收集以 .png 或 .jpg 结尾的路径值
+     *
+     * @return JSON 对象,包含图片路径数组和总数
+     * @throws Exception 处理过程中可能抛出的异常
+     */
+    @PostMapping("filePath")
+    public JSONObject filePath(@RequestBody Map<String, Object> request) throws Exception {
+        if (request == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+
+        logger.debug("收到 filePath 请求:{}", request);
+
+        // 从 JSON 中获取 templateBytes,避免反序列化为 ArrayList
+        byte[] templateBytesArray = convertToByteArray(request.get("templateBytes"), "templateBytes");
+        Map<String, Object> data = extractDataMap(request.get("data"));
+
+        Set<String> imagePaths = new HashSet<>();
+        Workbook workbook = loadWorkbook(templateBytesArray, data);
+
+        for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
+            IWorksheet worksheet = workbook.getWorksheets().get(i);
+            collectImagePaths(worksheet, imagePaths);
+        }
+
+        return new JSONObject().fluentPut("paths", imagePaths);
+    }
+
+    /**
+     * 加载工作簿并设置数据源
+     *
+     * @param templateBytes Excel 模板文件的字节数组
+     * @param data          填充数据
+     * @return 加载后的 Workbook 对象
+     * @throws Exception 加载失败时抛出异常
+     */
+    private Workbook loadWorkbook(byte[] templateBytes, Map<String, Object> data) throws Exception {
+        if (templateBytes == null || templateBytes.length == 0) {
+            throw new IllegalArgumentException("模板字节数组不能为空");
+        }
+
+        WorkbookOptions workbookOptions = new WorkbookOptions();
+        workbookOptions.setPixelBasedColumnWidth(true);
+        XlsxOpenOptions options = new XlsxOpenOptions();
+        options.setImportFlags(EnumSet.of(ImportFlags.Data));
+        options.setDoNotAutoFitAfterOpened(true);
+
+        Workbook workbook = new Workbook(workbookOptions);
+        try (InputStream inputStream = new ByteArrayInputStream(templateBytes)) {
+            workbook.open(inputStream, OpenFileFormat.Sjs);
+        }
+
+        for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
+            IWorksheet worksheet = workbook.getWorksheets().get(i);
+            setupWorksheet(worksheet, data);
+        }
+
+        return workbook;
+    }
+
+    /**
+     * 设置工作表配置和数据源
+     * 包括页面设置(纸张大小、边距等)和数据绑定
+     *
+     * @param worksheet 要设置的工作表
+     * @param data      填充数据
+     */
+    private void setupWorksheet(IWorksheet worksheet, Map<String, Object> data) {
+        worksheet.getPageSetup().setPrintHeadings(false);
+        worksheet.getPageSetup().setPaperSize(PaperSize.A4);
+        worksheet.getPageSetup().setLeftMargin(7);
+        worksheet.getPageSetup().setRightMargin(7);
+        worksheet.getPageSetup().setCenterHorizontally(true);
+        worksheet.setDataSource(new JsonDataSource(JSON.toJSONString(data)));
+
+        if (!worksheet.getName().contains("封面") && !worksheet.getName().contains("注意")) {
+            worksheet.getPageSetup().setIsAutoFirstPageNumber(true);
+        }
+    }
+
+    /**
+     * 收集工作表中的所有图片路径
+     * 遍历所有单元格,查找值为图片路径(.png 或 .jpg)的单元格
+     *
+     * @param worksheet  要遍历的工作表
+     * @param imagePaths 用于存储找到的图片路径的集合
+     */
+    private void collectImagePaths(IWorksheet worksheet, Set<String> imagePaths) {
+        // 收集单元格中的背景图片路径
+        for (int x = 0; x < worksheet.getRowCount(); x++) {
+            for (int y = 0; y < worksheet.getColumnCount(); y++) {
+                IRange range = worksheet.getCells().get(x, y);
+                Object valueObj = range.getValue();
+                if (range.getBindingPath() != null && valueObj != null) {
+                    String value = valueObj.toString();
+                    if (value.endsWith(".png") || value.endsWith(".jpg")) {
+                        if (value.contains(",")) {
+                            String[] paths = value.split(",");
+                            for (String path : paths) {
+                                String trimmedPath = path.trim();
+                                if (!trimmedPath.isEmpty()) {
+                                    imagePaths.add(trimmedPath);
+                                }
+                            }
+                        } else {
+                            imagePaths.add(value);
+                        }
+                    }
+                }
+            }
+        }
+
+        // 收集浮动图片路径
+        collectFloatingImages(worksheet, imagePaths);
+    }
+
+    /**
+     * 收集浮动图片路径
+     * 目前主要处理 Illustration 字段中的浮动图片(可选扩展)
+     *
+     * @param worksheet  工作表
+     * @param imagePaths 图片路径集合
+     */
+    private void collectFloatingImages(IWorksheet worksheet, Set<String> imagePaths) {
+        // 这里可以添加收集浮动图片的逻辑,如果需要的话
+        // 目前 pdf 方法中有处理 Illustration 字段,但 filePath 可能不需要
+    }
+
+    /**
+     * 生成完整的 PDF 文件
+     * 将 Excel 模板填充数据后转换为 PDF,支持背景图片、合并图片和浮动图片
+     *
+     * @return 生成的 PDF 文件的字节数组
+     * @throws Exception 处理过程中可能抛出的异常
+     */
+    @PostMapping("pdf")
+    public byte[] fullPdf(@RequestBody Map<String, Object> request) throws Exception {
+        if (request == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+
+        logger.debug("收到 pdf 请求:{}", request);
+
+        // 转换 templateBytes
+        byte[] templateBytes = convertToByteArray(request.get("templateBytes"), "templateBytes");
+        Map<String, Object> data = extractDataMap(request.get("data"));
+
+        // 转换 fileBytes
+        Map<String, byte[]> fileBytes = convertFileBytesMap(request.get("fileBytes"));
+
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        WorkbookOptions workbookOptions = new WorkbookOptions();
+        workbookOptions.setPixelBasedColumnWidth(true);
+        //Don't autofit row height
+        XlsxOpenOptions options = new XlsxOpenOptions();
+        options.setImportFlags(EnumSet.of(ImportFlags.Data));
+        options.setDoNotAutoFitAfterOpened(true);
+
+        Workbook workbook = null;
+        InputStream inputStream = null;
+        try {
+            workbook = new Workbook(workbookOptions);
+            inputStream = new ByteArrayInputStream(templateBytes);
+            workbook.open(inputStream, OpenFileFormat.Sjs);
+
+            for (int i = 0; i < workbook.getWorksheets().getCount(); i++) {
+                IWorksheet worksheet = workbook.getWorksheets().get(i);
+                worksheet.getPageSetup().setPrintHeadings(false);
+                worksheet.getPageSetup().setPaperSize(PaperSize.A4);
+                worksheet.getPageSetup().setLeftMargin(7); // 左边距
+                worksheet.getPageSetup().setRightMargin(7); // 右边距
+                worksheet.getPageSetup().setCenterHorizontally(true);
+                worksheet.setDataSource(new JsonDataSource(JSON.toJSONString(data)));
+                if (!worksheet.getName().contains("封面") && !worksheet.getName().contains("注意")) {
+                    worksheet.getPageSetup().setIsAutoFirstPageNumber(true);
+                }
+
+                // 填充签名图片
+                processCellBackgroundImages(worksheet, fileBytes);
+
+                // 浮动图片
+                processFloatingImages(worksheet, data, fileBytes);
+            }
+
+            PrintManager printManager = new PrintManager();
+            //Workbook.FontsFolderPath = this.fontsFolderPath;
+            Workbook.FontProvider = new IFontProvider() {
+                @Override
+                public List<String> getFontFilePaths() {
+                    return new ArrayList<>(Arrays.asList(
+                            "fonts/simsun.ttf"
+                    ));
+                }
+
+                @Override
+                public InputStream getFont(String fontFilePath) {
+                    return getClass().getClassLoader().getResourceAsStream(fontFilePath);
+                }
+            };
+            PdfSaveOptions pdfOptions = new PdfSaveOptions();
+            pdfOptions.setIncludeAutoMergedCells(true);
+            List<PageInfo> pages = printManager.paginate(workbook);
+            printManager.savePageInfosToPDF(byteArrayOutputStream, pages, pdfOptions);
+            return byteArrayOutputStream.toByteArray();
+
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    logger.warn("关闭输入流失败", e);
+                }
+            }
+            if (byteArrayOutputStream != null) {
+                try {
+                    byteArrayOutputStream.close();
+                } catch (IOException e) {
+                    logger.warn("关闭输出流失败", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * 处理单元格背景图片
+     */
+    private void processCellBackgroundImages(IWorksheet worksheet, Map<String, byte[]> fileBytes) {
+        for (int x = 0; x < worksheet.getRowCount(); x++) {
+            for (int y = 0; y < worksheet.getColumnCount(); y++) {
+                IRange range = worksheet.getCells().get(x, y);
+                Object valueObj = range.getValue();
+                if (range.getBindingPath() != null && valueObj != null) {
+                    String value = valueObj.toString();
+                    // 签名图片
+                    if (value.endsWith(".png") || value.endsWith(".jpg")) {
+                        // 是非多张图片
+                        if (!value.contains(",")) {
+                            byte[] bytes = fileBytes.get(value);
+                            if (bytes != null) {
+                                range.setValue(null);
+                                IRange mergeArea = range.getMergeArea();
+                                mergeArea.setBackgroundImage(bytes);
+                            } else {
+                                logger.warn("未找到图片数据:{}", value);
+                            }
+                        } else {
+                            String[] split = value.split(",");
+                            byte[][] bytes = new byte[split.length][];
+                            for (int k = 0; k < split.length; k++) {
+                                bytes[k] = fileBytes.get(split[k].trim());
+                            }
+                            range.setValue(null);
+                            IRange mergeArea = range.getMergeArea();
+
+                            // 横向合并图片
+                            byte[] mergedImage = mergeImages(bytes);
+                            mergeArea.setBackgroundImage(mergedImage);
+                        }
+                    }
+                    // 复选框
+                    if (range.getCellType() instanceof com.grapecity.documents.excel.CheckBoxCellType) {
+                        if (!"true".equals(value)) {
+                            range.setValue(null);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 处理浮动图片
+     */
+    private void processFloatingImages(IWorksheet worksheet, Map<String, Object> data, Map<String, byte[]> fileBytes) {
+        if (data.get("Illustration") == null) {
+            return;
+        }
+
+        Object illustrationObj = data.get("Illustration");
+        String illustration = illustrationObj != null ? illustrationObj.toString() : null;
+
+        if (illustration != null && illustration.startsWith("[") && illustration.endsWith("]")) {
+            JSONArray jsonArray = JSON.parseArray(illustration);
+            List<JSONObject> list = new ArrayList<>();
+
+            for (int k = 0; k < jsonArray.size(); k++) {
+                JSONObject jsonObject = jsonArray.getJSONObject(k);
+                if (jsonObject.getString("sheet").equals(worksheet.getName())) {
+                    String url = jsonObject.getString("url");
+                    byte[] bytes = fileBytes.get(url);
+                    if (bytes != null) {
+                        try (InputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
+                            worksheet.getShapes().addPictureInPixel(
+                                    byteArrayInputStream,
+                                    ImageType.JPG,
+                                    jsonObject.getDouble("x"),
+                                    jsonObject.getDouble("y"),
+                                    jsonObject.getDouble("width"),
+                                    jsonObject.getDouble("height")
+                            );
+                        } catch (IOException e) {
+                            logger.warn("添加浮动图片失败:{}", url, e);
+                        }
+                    } else {
+                        logger.warn("未找到浮动图片数据:{}", url);
+                    }
+                } else {
+                    list.add(jsonObject);
+                }
+            }
+            data.put("Illustration", JSON.toJSONString(list));
+        }
+    }
+
+    /**
+     * 合并图片
+     * 将多张图片合并成一个图片,高度压缩成一致
+     *
+     * @param images 图片数组
+     * @return 合并后的图片
+     */
+    public static byte[] mergeImages(byte[][] images) {
+        if (images == null || images.length == 0) {
+            return new byte[0];
+        }
+
+        // 如果只有一张图片且不为空,直接返回
+        if (images.length == 1 && images[0] != null && images[0].length > 0) {
+            return images[0];
+        }
+
+        try {
+            BufferedImage[] bufferedImages = new BufferedImage[images.length];
+            int totalWidth = 0;
+            int maxHeight = 0;
+
+            // 读取所有图片并计算总宽度和最大高度
+            for (int i = 0; i < images.length; i++) {
+                if (images[i] == null || images[i].length == 0) {
+                    logger.warn("跳过空图片索引:{}", i);
+                    continue;
+                }
+                bufferedImages[i] = ImageIO.read(new ByteArrayInputStream(images[i]));
+                if (bufferedImages[i] != null) {
+                    totalWidth += bufferedImages[i].getWidth();
+                    maxHeight = Math.max(maxHeight, bufferedImages[i].getHeight());
+                }
+            }
+
+            if (totalWidth == 0 || maxHeight == 0) {
+                logger.warn("没有有效的图片可以合并");
+                return new byte[0];
+            }
+
+            // 创建新的图片画布
+            BufferedImage mergedImage = new BufferedImage(totalWidth, maxHeight, BufferedImage.TYPE_INT_RGB);
+            Graphics2D g = mergedImage.createGraphics();
+
+            try {
+                // 设置白色背景
+                g.setPaint(Color.WHITE);
+                g.fillRect(0, 0, totalWidth, maxHeight);
+
+                // 绘制所有图片
+                int xOffset = 0;
+                for (BufferedImage img : bufferedImages) {
+                    if (img == null) {
+                        continue;
+                    }
+
+                    int width = img.getWidth();
+                    int height = img.getHeight();
+
+                    // 按比例缩放图片以适应高度
+                    if (height != maxHeight) {
+                        double scale = (double) maxHeight / height;
+                        int scaledWidth = (int) (width * scale);
+
+                        BufferedImage scaledImage = new BufferedImage(scaledWidth, maxHeight, BufferedImage.TYPE_INT_RGB);
+                        Graphics2D g2d = scaledImage.createGraphics();
+                        try {
+                            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+                            g2d.drawImage(img, 0, 0, scaledWidth, maxHeight, null);
+                        } finally {
+                            g2d.dispose();
+                        }
+
+                        g.drawImage(scaledImage, xOffset, 0, null);
+                        xOffset += scaledWidth;
+                    } else {
+                        g.drawImage(img, xOffset, 0, null);
+                        xOffset += width;
+                    }
+                }
+            } finally {
+                g.dispose();
+            }
+
+            // 转换为字节数组
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            try {
+                ImageIO.write(mergedImage, "jpg", baos);
+                return baos.toByteArray();
+            } finally {
+                baos.close();
+            }
+
+        } catch (IOException e) {
+            logger.error("合并图片失败", e);
+            throw new RuntimeException("合并图片失败", e);
+        }
+    }
+
+    /**
+     * 将 Object 转换为 byte[] 数组
+     * 支持 List<Integer>、List<Byte>、byte[] 等多种格式
+     *
+     * @param obj       待转换的对象
+     * @param paramName 参数名称,用于错误提示
+     * @return 转换后的 byte[] 数组
+     * @throws IllegalArgumentException 当对象格式不正确时抛出
+     */
+    @SuppressWarnings("unchecked")
+    private byte[] convertToByteArray(Object obj, String paramName) {
+        if (obj == null) {
+            throw new IllegalArgumentException(paramName + " 不能为空");
+        }
+
+        if (obj instanceof byte[]) {
+            return (byte[]) obj;
+        }
+
+        if (obj instanceof List) {
+            List<?> list = (List<?>) obj;
+            byte[] result = new byte[list.size()];
+            for (int i = 0; i < list.size(); i++) {
+                Object item = list.get(i);
+                if (item instanceof Number) {
+                    result[i] = ((Number) item).byteValue();
+                } else if (item instanceof Integer) {
+                    result[i] = ((Integer) item).byteValue();
+                } else {
+                    throw new IllegalArgumentException(paramName + " 中包含非数字元素");
+                }
+            }
+            return result;
+        }
+
+        throw new IllegalArgumentException(paramName + " 必须是 byte[] 或 List<Number> 类型");
+    }
+
+    /**
+     * 从请求中提取 Data Map
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> extractDataMap(Object dataObj) {
+        if (dataObj == null) {
+            return new HashMap<>();
+        }
+
+        if (dataObj instanceof Map) {
+            return (Map<String, Object>) dataObj;
+        }
+
+        throw new IllegalArgumentException("data 必须是 Map 类型");
+    }
+
+    /**
+     * 转换 fileBytes Map
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, byte[]> convertFileBytesMap(Object fileBytesObj) {
+        if (fileBytesObj == null) {
+            return new HashMap<>();
+        }
+
+        if (!(fileBytesObj instanceof Map)) {
+            throw new IllegalArgumentException("fileBytes 必须是 Map 类型");
+        }
+
+        Map<?, ?> rawMap = (Map<?, ?>) fileBytesObj;
+        Map<String, byte[]> result = new HashMap<>(rawMap.size());
+
+        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+            String key = entry.getKey() != null ? entry.getKey().toString() : null;
+            if (key == null || key.isEmpty()) {
+                logger.warn("跳过空的图片路径 key");
+                continue;
+            }
+
+            byte[] value = convertToByteArray(entry.getValue(), "fileBytes[" + key + "]");
+            result.put(key, value);
+        }
+
+        return result;
+    }
+}

+ 5 - 0
src/main/resources/application.yaml

@@ -0,0 +1,5 @@
+spring:
+  application:
+    name: grape-city
+server:
+  port: 48010

BIN
src/main/resources/fonts/simsun.ttf


+ 13 - 0
src/test/java/com/grapecity/GrapeCityApplicationTests.java

@@ -0,0 +1,13 @@
+package com.grapecity;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GrapeCityApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}