后端

  • 9月初,接到单位领导的任务,要求实现一个在线教育平台,主要展示岗位教学视频,以及相关的作业指导书,并建议要配套一个后台系统方便文件的上传和管理。
  • 作为后端开发,决定使用前后端分离,前端使用vue-admin-element,后端使用Springboot+MyBatis,数据库使用Mysql,容器使用tomcat,以及部署vue项目需要的nginx,jdk使用1.8
  • 由于项目开发只有我一个人,开发过程中遇到了不少问题,但所幸都一一解决了,趁着摸鱼的空档,决定将遇到的问题和解决方法记录下来,方便之后查阅。

1. SpringBoot实现文件上传

1.1 配置类 UploadConfig

  • 限制上传文件大小,以及总的请求文件大小。
/**
 * @author :RukiHuang
 * @description:文件上传配置类
 *                 配置MaxFileSize等属性
 * @date :2022/9/2 9:45
 */
@Configuration
public class UploadConfig {

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        //单个数据大小
        factory.setMaxFileSize(DataSize.ofMegabytes(200));
        //总上传文件的大小
        factory.setMaxRequestSize(DataSize.ofGigabytes(10));
        return factory.createMultipartConfig();
    }
}
  • 也可以通过在 application.properties配置文件中指定
#文件上传
# 文件大小设置已在UploadConfig中配置,也可在配置文件中配置
#单个文件大小
spring.servlet.multipart.max-file-size=200MB
#总文件大小(允许存储文件的文件夹大小)
spring.servlet.multipart.max-request-size=10240MB

1.2 配置文件 application.properties

  • 指定文件上传目标路径以及允许的文件类型
#文件上传的目标路径
file.upload.path=G:\\temp\\
#文件上传允许的类型
file.upload.allowType[0]=application/pdf
file.upload.allowType[1]=video/mp4

1.3 配置文件读取 UploadProperties

/**
 * @author :RukiHuang
 * @description:文件上传
 *                  上传路径
 *                  文件格式
 * @date :2022/9/2 10:05
 */
@Component
@ConfigurationProperties(prefix = "file.upload")
public class UploadProperties {
    private String path;
    private List<String> allowTypeList;

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public List<String> getAllowType() {
        return allowTypeList;
    }

    public void setAllowType(List<String> allowTypeList) {
        this.allowTypeList = allowTypeList;
    }
}

1.4 工具类

1.4.1 唯一ID生成器 IDUtils

  • 生成唯一id
/**
 * @author :RukiHuang
 * @description:唯一ID生成器
 * @date :2022/9/2 10:07
 */
public class IDUtils {
    public static String generateUniqueId() {
        return UUID.randomUUID().toString() + System.currentTimeMillis();
    }
}

1.4.2 文件名替换工具 UploadUtils

  • 替换原始文件名,避免文件名重复
/**
 * @author :RukiHuang
 * @description:文件名替换工具 避免文件名重复
 * @date :2022/9/2 10:09
 */
public class UploadUtils {
    public static String generateFileName(String oldName) {
        String suffix = oldName.substring(oldName.lastIndexOf("."));
        return IDUtils.generateUniqueId() + suffix;
    }
}

1.5 web层 CoursewareController

/**
 * @author :RukiHuang
 * @description:课件的Controller
 * @date :2022/9/1 13:37
 */
@CrossOrigin(origins = "*",maxAge = 3600)
@RequestMapping("/forum")
@RestController
public class CoursewareController {

    private static Logger logger = LoggerFactory.getLogger(DocController.class);

    @Autowired
    CoursewareService coursewareService;

    @Autowired
    UploadService uploadService;

    @RequestMapping("/coursewareUpload/uploadCourseware")
    public ResponseResult uploadVideo(
            @RequestParam("file") MultipartFile file
    ) {
        String filename = null;
        try {
            filename = uploadService.uploadCourseware(file);
            return ResponseResult.ok(filename, "课件上传成功");
        } catch (IOException e) {
            logger.error(e.getMessage());
            return ResponseResult.failed(e.getMessage(),"课件上传失败");
        }

    }

    @RequestMapping(value = "/coursewareUpload/submitCoursewareInfo", method = RequestMethod.POST)
    public ResponseResult submitCoursewareInfo(
            @RequestParam(name = "serverFileName")String serverFileName) {
        try {
            coursewareService.addCoursewareInfo(serverFileName);
            return ResponseResult.ok("提交成功");
        } catch (Exception e) {
            logger.error(e.getMessage());
            return ResponseResult.failed(e.getMessage(), "提交失败");
        }

    }

}

1.6 service层 UploadService CoursewareService

  • UploadService
/**
 * @author :RukiHuang
 * @description:上传service
 * @date :2022/9/2 10:16
 */
@Service
public class UploadServiceImpl implements UploadService {

    @Autowired
    UploadProperties uploadProperties;
    
    @Override
    public String uploadCourseware(MultipartFile file) throws IOException {
        System.out.println(file.getContentType());
        if(!uploadProperties.getAllowType().get(0).equals(file.getContentType())) {
            throw new IOException("课件上传类型错误");
        }
        String fileName = UploadUtils.generateFileName(file.getOriginalFilename());
        File newFile = new File(uploadProperties.getPath()+"\\courseware\\" + fileName);//当前是在windows目录,部署到linux路径要修改为/courseware
        file.transferTo(newFile);
        System.out.println(newFile.getPath());
        return fileName;
    }
}
  • CoursewareService
/**
 * @author :RukiHuang
 * @description:课件service
 * @date :2022/9/1 13:42
 */
@Service
public class CoursewareServiceImpl implements CoursewareService {

    @Autowired
    CoursewareDao coursewareDao;


    @Override
    public void addCoursewareInfo(String serverFileName) throws Exception {
        String[] nameArray = serverFileName.split(" / ");
        String coursewareName = nameArray[0];
        String serverStorageName = nameArray[1];
        String storagePath = "courseware/" + serverStorageName;

        CoursewareInfoDto coursewareInfoDto = new CoursewareInfoDto(coursewareName, storagePath);
        coursewareDao.addCoursewareInfo(coursewareInfoDto);
    }


}

1.7 PostMan测试文件上传

  • 第一步,头文件置空

  • 第一步,请求体中选择上传文件

前端

2. Vue实现文件上传

2.1 单文件上传

2.1.1 业务逻辑

  • 先对文件进行校验,判断文件大小以及类型是否符合要求。
  • 将文件上传至服务器。
  • 服务器返回新文件名和存储路径
  • 将文件名称和服务器名称提交至服务器,并将存储路径传入至数据库。
  • 组件使用的是vue-admin-template自带的iView,iView官方文档

2.1.2 代码实现

<template>
    <div class="home-container">
        <div class="upload-content" style="padding: 20px">
            <Upload
                :before-upload="beforeUpload"
                type="drag"
                action="http://localhost:8080/eduonline/forum/coursewareUpload/submitCoursewareInfo">
                <div style="padding: 20px 0">
                    <Icon type="ios-cloud-upload" size="52" style="color: #3399ff"></Icon>
                    <p>点击或拖拽文件至此<br>文档限制pdf格式,视频限制mp4格式(大小不超过200MB)</p>
                </div>
            </Upload>
            <div style="height: 10px">
                <div v-if="file !== null">
                    上传文件: {{ file.name }}
                    <span style="padding-left: 10px">
                            <Button type="primary" @click="upload" :loading="loadingStatus" ghost size="small">
                                {{ loadingStatus ? '上传中' : '点击上传' }}
                            </Button>
                        </span>
                </div>
            </div>
        </div>
        <div class="searchParam" style="padding: 20px">
            <Input v-model="serverFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px;" disabled/>
            <span style="padding-left: 20px">
                    <Button type="primary" @click="submit">提交</Button>
                </span>
        </div>
    </div>
</template>

<script>
import axios from 'axios'

export default {
    name: 'coursewareUpload',
    data() {
        return {
            file: null,
            filename: '',
            loadingStatus: false,
            serverFileName: '',
        }
    },
    methods: {
        beforeUpload(file) {
            let name = file.name
            let suffix = name.substring(name.lastIndexOf('.'))
            let isNameLegal = true
            let isLs2M = true
            let isRight = true
            isNameLegal = suffix === '.pdf'
            isLs2M = file.size / 1024 / 1024 < 200
            if (!isLs2M || !isNameLegal) {
                this.loadingStatus = false
                this.$Message.error('上传的文件大小不能超过200MB 且 格式为pdf!')
                isRight = false
            }
            this.filename = file.name
            this.file = file
            // 一定要return false 不然会直接上传
            return false
        },
        upload() {
            let formData = new FormData()
            formData.append('file', this.file)
            this.loadingStatus = true
            axios({
                method: 'post',
                url: 'forum/coursewareUpload/uploadCourseware',
                headers: {
                    'content-type': 'multipart/form-data',
                },
                data: formData,
            }).then(res => {

                this.loadingStatus = false
                this.serverFileName = this.filename + ' / ' + res.data.data
            }, err => {
                this.$Message.error('后台服务出问题,请联系技术人员')
            })

        },
        submit() {
            let formData = new FormData()
            formData.append('position', this.position)
            formData.append('type', this.type)
            formData.append('serverFileName', this.serverFileName)
            axios({
                method: 'post',
                url: 'forum/coursewareUpload/submitCoursewareInfo',
                headers: {
                    'content-type': 'application/json',
                },
                data: formData,
            }).then(res => {
                this.$Message.success('提交成功')
                this.position = ''
                this.type = ''
                this.serverFileName = ''
                this.file = null
            }, err => {
                this.$Message.error('提交失败,请联系技术人员')
            })
        },
    },
}
</script>

<style scoped>

</style>

2.2 一个页面多处地方需要文件上传

2.2.1 业务逻辑

  • iview无法实现单个页面多处地方文件上传,file会被替换
  • 使用原生的上传组件进行上传。

2.2.2 代码实现

<template>
    <div class="btn-box">
        <h3>作业指导书:</h3>
        <input class="file-input" type="file" @change="getDocFile($event)" />
        <Button type="primary" @click="uploadDoc" :loading="loadingStatus1">{{ loadingStatus1 ? '上传中' : '上传文件' }}</Button>
        <Input v-model="docServerFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px; padding-left: 30px" disabled/>
        <h3>SOC文档:</h3>
        <input class="file-input" type="file" @change="getSOCFile($event)" />
        <Button type="primary" @click="uploadSOC" :loading="loadingStatus4">{{ loadingStatus4 ? '上传中' : '上传文件' }}</Button>
        <Input v-model="socServerFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px; padding-left: 30px" disabled/>
        <h3>教学视频:</h3>
        <input class="file-input" type="file" @change="getVidFile($event)" />
        <Button type="primary" @click="uploadVideo" :loading="loadingStatus5">{{ loadingStatus5 ? '上传中' : '上传文件' }}</Button>
        <Input v-model="vidServerFileName" placeholder="文件名称 / 服务器文件名称" style="width: 300px; padding-left: 30px" disabled/>
        <h3>工段岗位:</h3>
        <Select v-model="position" style="width:200px; margin: 20px" placeholder="请选择岗位" clearable>
            <OptionGroup label="1 片叶">
                <Option v-for="item in unit_pianye_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
            </OptionGroup>
            <OptionGroup label="2 烘丝">
                <Option v-for="item in unit_hongsi_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
            </OptionGroup>
            <OptionGroup label="3 掺配加香">
                <Option v-for="item in unit_canpeijiaxiang_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
            </OptionGroup>
            <OptionGroup label="4 膨胀">
                <Option v-for="item in unit_pengzhang_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
            </OptionGroup>
            <OptionGroup label="5 梗丝">
                <Option v-for="item in unit_gengsi_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
            </OptionGroup>
            <OptionGroup label="6 残烟间">
                <Option v-for="item in unit_canyanjian_posList" :value="item.value" :key="item.value">{{ item.label }}</Option>
            </OptionGroup>
        </Select>
        <h3>岗位职责:</h3>
        <Input v-model="posResponsibility" type="textarea" :rows="4" placeholder="请输入岗位职责" style="padding: 20px"/>
<!--        <divider />-->
        <Button type="primary" @click="submit" class="btn-submit">提交</Button>
    </div>
</template>

<script>
import axios from 'axios'

export default {
    name: 'attendPoint',
    data() {
        return {
            unit_pianye_posList: [],
            unit_hongsi_posList: [],
            unit_canpeijiaxiang_posList: [],
            unit_pengzhang_posList: [],
            unit_gengsi_posList: [],
            unit_canyanjian_posList: [],
            loadingStatus1: false,
            loadingStatus2: false,
            loadingStatus3: false,
            loadingStatus4: false,
            loadingStatus5: false,
            position: '',
            type: '',
            file1: null,
            file2: null,
            file3: null,
            file4: null,
            file5: null,
            filename1: '',
            filename2: '',
            filename3: '',
            filename4: '',
            filename5: '',
            file: null,
            filename: '',
            docServerFileName: '',
            docProcessServerFileName: '',
            docIndexServerFileName: '',
            socServerFileName: '',
            vidServerFileName: '',
            posResponsibility: '',
        }
    },
    methods: {
        getPositionList() {
            let that = this
            axios({
                method: 'get',
                url: 'position/getPositionList',
                headers: {
                    'content-type': 'application/json',
                },
            }).then(res => {
                that.unit_pianye_posList = res.data.data.unit_pianye_posList
                that.unit_hongsi_posList = res.data.data.unit_hongsi_posList
                that.unit_canpeijiaxiang_posList = res.data.data.unit_canpeijiaxiang_posList
                that.unit_pengzhang_posList = res.data.data.unit_pengzhang_posList
                that.unit_gengsi_posList = res.data.data.unit_gengsi_posList
                that.unit_canyanjian_posList = res.data.data.unit_canyanjian_posList
            }, err => {
                this.$Message.error('后台服务出问题,请联系技术人员')
            })
        },
        // 选取文件
        getDocFile(event) {
            this.file1 = event.target.files[0]
            this.filename1 = this.file1.name
        },
        // 选取文件
        getProcessFile(event) {
            this.file2 = event.target.files[0]
            this.filename2 = this.file2.name
        },
        // 选取文件
        getIndexFile(event) {
            this.file3 = event.target.files[0]
            this.filename3 = this.file3.name
        },
        // 选取文件
        getSOCFile(event) {
            this.file4 = event.target.files[0]
            this.filename4 = this.file4.name
        },
        // 选取文件
        getVidFile(event) {
            this.file5 = event.target.files[0]
            this.filename5 = this.file5.name
        },
        // 上传文件 doc 作业指导书
        uploadDoc() {
            let uncheckedFile = this.file1
            if (uncheckedFile == null) {
                this.$Message.error('未选择文件!请先选择文件!')
                return
            }
            let isRight = this.beforeUploadDoc(uncheckedFile)
            if (!isRight) {
                this.$Message.error('请重新上传文件')
                return
            }
            let checkedFile = uncheckedFile
            let formData = new FormData()
            formData.append('file', checkedFile)
            this.loadingStatus1 = true
            axios({
                method: 'post',
                url: 'positionLearning/docUpload/uploadDoc',
                headers: {
                    'content-type': 'multipart/form-data',
                },
                data: formData,
            }).then(res => {
                this.loadingStatus1 = false
                this.docServerFileName = this.filename + ' / ' + res.data.data
            }, err => {
                this.$Message.error('后台服务出问题,请联系技术人员')
            })
        },
        uploadSOC() {
            let uncheckedFile = this.file4
            if (uncheckedFile == null) {
                this.$Message.error('未选择文件!请先选择文件!')
                return
            }
            let isRight = this.beforeUploadDoc(uncheckedFile)
            if (!isRight) {
                this.$Message.error('请重新上传文件')
                return
            }
            let checkedFile = uncheckedFile
            let formData = new FormData()
            formData.append('file', checkedFile)
            this.loadingStatus4 = true
            axios({
                method: 'post',
                url: 'positionLearning/docUpload/uploadDoc',
                headers: {
                    'content-type': 'multipart/form-data',
                },
                data: formData,
            }).then(res => {
                this.loadingStatus4 = false
                this.socServerFileName = this.filename + ' / ' + res.data.data
            }, err => {
                this.$Message.error('后台服务出问题,请联系技术人员')
            })
        },
        uploadVideo() {
            let uncheckedFile = this.file5
            if (uncheckedFile == null) {
                this.$Message.error('未选择文件!请先选择文件!')
                return
            }
            let isRight = this.beforeUploadVid(uncheckedFile)
            if (!isRight) {
                this.$Message.error('请重新上传文件')
                return
            }
            let formData = new FormData()
            formData.append('file', this.file)
            this.loadingStatus5 = true
            axios({
                method: 'post',
                url: 'positionLearning/videoUpload/uploadVideo',
                headers: {
                    'content-type': 'multipart/form-data',
                },
                data: formData,
            }).then(res => {
                this.loadingStatus5 = false
                this.vidServerFileName = this.filename + ' / ' + res.data.data
            }, err => {
                this.$Message.error('后台服务出问题,请联系技术人员')
            })
        },
        beforeUploadDoc(file) {
            let name = file.name
            let suffix = name.substring(name.lastIndexOf('.'))
            let isNameLegal = true
            let isLs2M = true
            let isRight = true
            isNameLegal = suffix === '.pdf'
            isLs2M = file.size / 1024 / 1024 < 200
            if (!isLs2M || !isNameLegal) {
                this.loadingStatus = false
                this.$Message.error('上传的文件大小不能超过200MB 且 格式为pdf!')
                isRight = false
            }
            this.filename = file.name
            this.file = file
            // 一定要return false 不然会直接上传
            return isRight
        },
        beforeUploadVid(file) {
            let name = file.name
            let suffix = name.substring(name.lastIndexOf('.'))
            let isNameLegal = true
            let isLs2M = true
            let isRight = true
            isNameLegal = suffix === '.mp4'
            isLs2M = file.size / 1024 / 1024 < 200
            if (!isLs2M || !isNameLegal) {
                this.loadingStatus = false
                this.$Message.error('上传的文件大小不能超过200MB 且 格式为mp4!')
                isRight = false
            }
            this.filename = file.name
            this.file = file
            // 一定要return false 不然会直接上传
            return isRight
        },
        submit() {
            let formData = new FormData()
            formData.append('position', this.position)
            formData.append('docServerFileName', this.docServerFileName)
            formData.append('socServerFileName', this.socServerFileName)
            formData.append('vidServerFileName', this.vidServerFileName)
            formData.append('posResponsibility', this.posResponsibility)
            axios({
                method: 'post',
                url: 'page/submitPageInfo',
                headers: {
                    'content-type': 'application/json',
                },
                data: formData,
            }).then(res => {
                // console.log('成功了')
                this.$Message.success('提交成功')
                this.position = ''
                this.docServerFileName = ''
                this.socServerFileName = ''
                this.vidServerFileName = ''
                this.posResponsibility = ''
                this.file1 = null
                this.file2 = null
                this.file3 = null
                this.filename1 = ''
                this.filename2 = ''
                this.filename3 = ''
            }, err => {
                console.log(err)
                this.$Message.error('提交失败,请联系技术人员')
            })
        },
    },
    created() {
        this.getPositionList()
    },
}
</script>

<style scoped>
.btn-box {
    padding: 20px;
}

input {
    /*max-width: fit-content;*/
    /*display: block;*/
    margin: 20px;
}

.btn-submit {
    vertical-align: middle;
    display: flex;
    align-items: center;
    /*justify-content:center;*/
    margin: 50px auto;
}
</style>
Last modification:September 16th, 2022 at 10:24 am