项目之创建静态资源和设置子模块项目、开发简易上传功能(11)

2021-08-23 16:18:32 浏览数 (1)

48. 在父项目中管理子模块项目使用的依赖

在一个项目中,如果某些依赖只是部分子模块项目需要使用的,应该将这些依赖配置在<dependencyManagement>节点中,凡配置在这个节点中的依赖,任何子模块项目中都不会直接拥有,如果某个子模块项目需要使用这些依赖,依然需要使用<dependency>节点来添加!与在子模块项目中直接添加<denpendency>(父级的<dependencyManagement>没有配置某个依赖而子模块项目中直接添加)的区别在于:如果事先使用父级项目的<dependencyManagement>进行了配置,则子模块项目在添加时,不需要指定版本号,直接使用父级项目配置的版号,以便于在父级项目中统一管理依赖的版本

注意:在父级项目中,添加许多依赖都是不需要指定版本号的,但是,如果将这些依赖配置到<dependencyManagement>中用于指导子模块项目所使用的依赖的版本时,必须显式的指定版本号,否则,子模块项目将不明确需要使用的是哪个版本!

则在父级项目中,关于依赖的管理:

代码语言:javascript复制
<properties>
    <!-- Java Version -->
    <java.version>1.8</java.version>
    <!-- Dependency Version -->
    <mysql.version>8.0.20</mysql.version>
    <mybatis.version>2.1.3</mybatis.version>
    <mybatis.plus.version>3.3.2</mybatis.plus.version>
    <druid.version>1.1.23</druid.version>
    <pagehelper.version>1.2.13</pagehelper.version>
    <thymeleaf.springsecurity5.version>3.0.4.RELEASE</thymeleaf.springsecurity5.version>
    <spring.boot.starter.version>2.3.1.RELEASE</spring.boot.starter.version>
    <lombok.version>1.18.12</lombok.version>
</properties>

<!-- 直接添加在dependencies节点的中的依赖是每个子模块项目都直接拥有的 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>${spring.boot.starter.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

<!-- 添加在dependencyManagement中的依赖只是为了管理子模块项目使用依赖时的版本 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${pagehelper.version}</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>${thymeleaf.springsecurity5.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>${spring.boot.starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>${spring.boot.starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>${spring.boot.starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

由于大量的依赖都已经添加在<dependencyManagement>中了,则straw-generatorstraw-portal项目都不会直接拥有这些依赖,则需要在这2个子模块项目中自行添加所需的依赖!

straw-generator项目中(关于代码生成器的相关依赖由于过于特殊,一定只有当前项目需要使用,所以,对版本的管理方式可以不严格要求):

代码语言:javascript复制
<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.3.2</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-extension</artifactId>
        <version>3.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
        <version>2.3.1.RELEASE</version>
    </dependency>
</dependencies>

straw-portal项目中:

代码语言:javascript复制
<dependencies>
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</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-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

50. 创建静态资源子模块项目

创建新的straw-resource子模块项目,用于管理用户上传的文件等静态资源。

创建出来后,在straw-resourcepom.xml中,自行将父级项目由SpringBoot改为straw项目,删除<dependencies><build>节点(因为没有存在的必要,在父项目中已经配置好了)。

straw项目中的<mudules>中添加子模块项目。

straw-resourceapplication.properties中显式的配置端口号,必须与straw-portal的不同:

代码语言:javascript复制
server.port=8081

全部完成后,更新Maven,straw-portalstraw-resource这2个项目是可以同时启动的!

51. 设置straw-resource子模块项目的静态目录

straw-resource项目的application.properties中添加配置:

代码语言:javascript复制
spring.resources.static-locations=file:D:/IdeaProjects/straw-static-resource

straw-resource项目的静态目录就是以上指定的位置,后续straw-portal项目中涉及上传操作时,上传的文件也应该存放到以上位置。

52.设置straw-resource子模块项目的静态目录

straw-portal项目的application.properties中添加配置:

代码语言:javascript复制
# 发布问题时,将图片上传到哪里,需要与straw-resource项目的静态资源目录保持一致
project.question.image-upload-path=D:/IdeaProjects/straw-static-resource

# 发布问题时,上传的图片通过哪个服务器提供访问,配置的端口号需要与straw-resource项目保持一致
project.question.image-host=http://localhost:8081/

# 发布问题时,允许上传的文件的最大大小
project.question.image-max-size=307200

# 发布问题时,允许上传的图片文件的类型
project.question.image-content-types=image/jpeg, image/png, image/bmp

并且,在straw-portal中调整默认限制的文件大小:

代码语言:javascript复制
@Bean
public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setMaxFileSize(DataSize.ofMegabytes(500));
    factory.setMaxRequestSize(DataSize.ofMegabytes(500));
    return factory.createMultipartConfig();
}

53. 开发简易上传功能

说明:由于上传功能不可以通过在URL上填写参数直接进行测试,为了更快的进行测试并体验上传的效果,暂且忽略不必要的代码,例如上传文件的相关检查等细节问题,当然,测试时也应该使用正确的文件和数据进行测试。当简单的上传已经完成后,再补全细节部分。

QuestionController中开发服务器端的简易上传处理:

代码语言:javascript复制
@Value("${project.question.image-upload-path}")
private String imageUploadPath;
@Value(("${project.question.image-host}"))
private String imageHost;

@PostMapping("/upload-image")
public R<String> uploadImage(MultipartFile imageFile) {
    File dest = new File(imageUploadPath, "1.jpg");

    try {
        imageFile.transferTo(dest);
    } catch (IOException e) {
        e.printStackTrace();
    }

    String imageUrl = imageHost   "1.jpg"; // http://localhost:8081/1.jpg
    log.debug("image url >>> {}", imageUrl);

    return R.ok(imageUrl);
}

本次需要处理的页面是“发表问题”的question/create.html,在发表问题时,使用的富文本编辑Summernote提供了名为callbacks的回调机制,其中,存在名为onImageUpload的回调属性,该属性值是函数,所以,可以自定义函数配置到这个回调属性中,则后续上传图片时,就会自动触发自定义的函数,通过自定义函数实现图片的上传,并返回上传图片的URL,生成图片插入到Summernote富文本编辑器中即可。

question/create.html中,先将底部关于Summernote的JavaScript代码移到新创建的commons/init_summernote.js中,并调整这段代码:

代码语言:javascript复制
$(document).ready(function () {
    $('#summernote').summernote({
        height: 300,
        tabsize: 2,
        lang: 'zh-CN',
        placeholder: '请输入问题的详细描述...',
        callbacks: {
            onImageUpload: function () {
                alert("准备上传图片!");
            }
        }
    });
});

完成后,重启项目,打开“发布问题”页面,插入图片,选择图片文件就会弹出对话框!

然后,在以上回调中,使用$.ajax()提交异步请求,在处理结果时,创建Image对象,将结果中的图片URL作为Image对象的src属性值,并将整个Image对象(就是一个<src>标签)插入到富文本编辑器中:

代码语言:javascript复制
$(document).ready(function () {
    $('#summernote').summernote({
        height: 300,
        tabsize: 2,
        lang: 'zh-CN',
        placeholder: '请输入问题的详细描述...',
        callbacks: {
            onImageUpload: function (files) {
                // ---------------------------------------
                // 当前函数的参数名称是自定义,它表示用户选择的若干个文件
                // Summernote在调用该函数时,会把用户选择的文件作为函数的参数
                // ---------------------------------------
                if (!files || files.length < 1) {
                    alert("请选择您要上传的文件!");
                    return;
                }
                if (files.length > 1) {
                    alert("一次只允许上传1个文件!");
                    return;
                }

                let formData = new FormData();
                let file = files[0];
                formData.append("imageFile", file);
                console.log("form data >>> "   formData);

                $.ajax({
                    url: '/api/v1/questions/upload-image',
                    type: 'post',
                    data: formData,
                    contentType: false,
                    processData: false,
                    success: function(json) {
                        if (json.state == 2000) {
                            // alert(json.data);
                            let img = new Image(); // <img>
                            img.src = json.data; // <img src="xxx">
                            $('#summernote').summernote('insertNode', img);
                        } else {
                            alert(json.message);
                        }
                    }
                });
            }
        }
    });
});

54. 完善服务器端的上传功能

先创建关于文件上传的异常类型:

代码语言:javascript复制
public class FileUploadException extends RuntimeException {
}
代码语言:javascript复制
public class FileEmptyException extends FileUploadException {
}
代码语言:javascript复制
public class FileSizeException extends FileUploadException {
}
代码语言:javascript复制
public class FileTypeException extends FileUploadException {
}
代码语言:javascript复制
public class FileIOException extends FileUploadException {
}

GlobalExceptionHandler中处理以上异常,完整代码如下(需在R.State中添加常量):

代码语言:javascript复制
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler
    public R handleException(Throwable e) {
        if (e instanceof ParameterValidationException) {
            return R.failure(R.State.ERR_PARAMETER_INVALIDATION, e);
        } else if (e instanceof InviteCodeException){
            return R.failure(R.State.ERR_INVITE_CODE, e);
        } else if (e instanceof ClassDisabledException) {
            return R.failure(R.State.ERR_CLASS_DISABLED, e);
        } else if (e instanceof PhoneDuplicateException) {
            return R.failure(R.State.ERR_PHONE_DUPLICATE, e);
        } else if (e instanceof InsertException) {
            return R.failure(R.State.ERR_INSERT, e);
        } else if (e instanceof FileEmptyException) {
            return R.failure(R.State.ERR_UPLOAD_EMPTY, e);
        } else if (e instanceof FileSizeException) {
            return R.failure(R.State.ERR_UPLOAD_FILE_SIZE, e);
        } else if (e instanceof FileTypeException) {
            return R.failure(R.State.ERR_UPLOAD_FILE_TYPE, e);
        } else if (e instanceof FileIOException) {
            return R.failure(R.State.ERR_UPLOAD_FILE_IO, e);
        } else if (e instanceof AccessDeniedException) {
            return R.failure(R.State.ERR_ACCESS_DENIED, e);
        } else {
            log.debug("Unknown Exception", e);
            return R.failure(R.State.ERR_UNKNOWN, e);
        }
    }

}

在处理上传请求之前,先声明2个全局属性,用于读取配置中的“文件最大大小”和“文件类型”:

代码语言:javascript复制
@Value("${project.question.image-max-size}")
private long imageMaxSize;
@Value(("${project.question.image-content-types}"))
private List<String> imageContentTypes;

在处理上传请求的过程中:

  • 应该创建子级文件夹,避免所有的文件都传到指定的同一个文件夹中,推荐使用“年”和“月”分别创建2级子文件夹,上传的图片应该放在“月”的文件夹中;
  • 可以使用UUID作为文件名;
  • 不需要判断原始扩展名,而是直接从原始文件全名中截取即可;
  • 及时打桩,输出关键信息,例如保存文件的文件夹路径、文件名、完整路径等,便于出错时排查问题。

具体代码:

代码语言:javascript复制
@Value("${project.question.image-upload-path}")
private String imageUploadPath;
@Value(("${project.question.image-host}"))
private String imageHost;
@Value("${project.question.image-max-size}")
private long imageMaxSize;
@Value(("${project.question.image-content-types}"))
private List<String> imageContentTypes;

@PostMapping("/upload-image")
public R<String> uploadImage(MultipartFile imageFile) {
    // 判断上传的文件是否为空
    if (imageFile.isEmpty()) {
        throw new FileEmptyException("上传图片失败!请选择有效的图片文件!");
    }
    // 判断上传的文件大小是否超标
    if (imageFile.getSize() > imageMaxSize) {
        throw new FileSizeException("上传图片失败!不允许使用超过"   (imageMaxSize / 1024)   "KB的图片文件!");
    }
    // 判断上传的文件类型是否超标
    if (!imageContentTypes.contains(imageFile.getContentType())) {
        throw new FileTypeException("上传图片失败!图片类型错误!允许上传的图片类型有:"   imageContentTypes);
    }

    // 确定本次上传时使用的文件夹
    String dir = DateTimeFormatter.ofPattern("yyyy/MM").format(LocalDateTime.now());
    File parent = new File(imageUploadPath, dir);
    if (!parent.exists()) {
        parent.mkdirs();
    }
    log.debug("dir >>> {}", parent);

    // 确定本次上传时使用的文件名
    String filename = UUID.randomUUID().toString();
    String originalFilename = imageFile.getOriginalFilename();
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
    String child = filename   suffix;

    // 创建最终保存时的文件对象
    File dest = new File(parent, child);

    // 执行保存
    try {
        imageFile.transferTo(dest);
    } catch (IOException e) {
        throw new FileIOException("上传图片失败!当前服务器忙,请稍后再次尝试!");
    }

    // 确定网络访问路径
    String imageUrl = imageHost   dir   "/"   child; // http://localhost:8081/1.jpg
    log.debug("image url >>> {}", imageUrl);

    // 返回
    return R.ok(imageUrl);
}

55. 显示老师主页

老师的主页文件是index_teacher.html,原本在static文件夹中,先把它拖拽到templates文件夹中。

SystemController中,修改原有访问/index.html路径的处理方法:

代码语言:javascript复制
@GetMapping("/index.html")
public String index(@AuthenticationPrincipal UserInfo userInfo) {
    if (userInfo.getType() == 0) {
        return "index";
    } else {
        return "index_teacher";
    }
}

需要注意:以上判断用户身份时,会判断用户数据的type属性,此前,在UserServiceImpl.login()方法中已经向返回的UserInfo中设置了从数据库中读取到的type属性,则以上代码可以正常获取type值!

56. 老师主页显示问题列表-持久层

(a) 规范需要执行的SQL语句

老师主页显示的问题列表应该显示出老师自己发表的问题,和学生指定该老师回答的问题。

这样的列表数据可以使用此前的QuestionVO来表示每一个问题的数据,列表则使用List<QuestionVO>来表示。

需要执行的SQL语句大致是:

代码语言:javascript复制
select question.*
from question
left join user_question
on question.id=user_question.question_id
where question.user_id=? or user_question.user_id=? and is_delete=0
order by status, modified_time desc;
(b) 在接口中添加抽象方法
代码语言:javascript复制
List<QuestionVO> findTeacherQuestions(Integer teacherId);
© 配置SQL映射
代码语言:javascript复制
(d) 单元测试
代码语言:javascript复制

57. 老师主页显示问题列表-业务层

(a)
(b) 接口与抽象方法

原本存在抽象方法:

代码语言:javascript复制
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer page);

改为:

代码语言:javascript复制
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer type, Integer page);
© 实现业务方法

在原本存在的getQuestionsByUserId()方法的参数列表中添加参数,与以上抽象方法保持一致,然后,在实现过程中:

代码语言:javascript复制
// 设置分页参数
PageHelper.startPage(page, pageSize);
// 调用持久层方法查询问题列表,该列表中的数据只有标签的id,并不包括标签数据
List<QuestionVO> questions;
if (type == 0) {
    questions = questionMapper.findListByUserId(userId);
} else {
    questions = questionMapper.findTeacherQuestions(userId);
}
// 后续代码不变
(d) 单元测试

由于修改了业务方法的声明,当前控制器层的调用会因为参数不匹配而报错,将无法进行单元测试,所以,先处理完控制器层再测试。

58. 老师主页显示问题列表-控制器层

在原来的获取学生问题列表的方法中,调用业务方法时多添加type值即可,该值来自UserInfo参数:

代码语言:javascript复制
@GetMapping("/my")
public R<PageInfo<QuestionVO>> getMyQuestions(Integer page,
       @AuthenticationPrincipal UserInfo userInfo) {
    if (page == null || page < 1) {
        page = 1;
    }
    PageInfo<QuestionVO> questions = questionService.getQuestionsByUserId(userInfo.getId(), userInfo.getType(), page);
    return R.ok(questions);
}

完成后,应该分别测试学生账号登录后显示列表和老师账号登录后显示列表。

59. 老师主页显示问题列表-前端页面

引用question/create.html中的处理即可!也就是说:在question/create.html中将列表区域设置为th:fragment,然后在index_teacher.html中通过th:replace直接引用即可!

0 人点赞