天天看點

項目之建立靜态資源和設定子子產品項目、開發簡易上傳功能(12)

50. 建立靜态資源子子產品項目

建立新的straw-resource子子產品項目,用于管理使用者上傳的檔案等靜态資源。

建立出來後,在straw-resource的pom.xml中,自行将父級項目由SpringBoot改為straw項目,删除<dependencies>和<build>節點(因為沒有存在的必要,在父項目中已經配置好了)。

在straw項目中的<mudules>中添加子子產品項目。

在straw-resource的application.properties中顯式的配置端口号,必須與straw-portal的不同:

server.port=8081

全部完成後,更新Maven,straw-portal和straw-resource這2個項目是可以同時啟動的!

51. 設定straw-resource子子產品項目的靜态目錄

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

spring.resources.static-locations=file:D:/IdeaProjects/straw-static-resource

1

則straw-resource項目的靜态目錄就是以上指定的位置,後續straw-portal項目中涉及上傳操作時,上傳的檔案也應該存放到以上位置。

52.設定straw-resource子子產品項目的靜态目錄

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

# 釋出問題時,将圖檔上傳到哪裡,需要與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中調整預設限制的檔案大小:
@Bean
public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setMaxFileSize(DataSize.ofMegabytes(500));
    factory.setMaxRequestSize(DataSize.ofMegabytes(500));
    return factory.createMultipartConfig();
}      

53. 開發簡易上傳功能

說明:由于上傳功能不可以通過在URL上填寫參數直接進行測試,為了更快的進行測試并體驗上傳的效果,暫且忽略不必要的代碼,例如上傳檔案的相關檢查等細節問題,當然,測試時也應該使用正确的檔案和資料進行測試。當簡單的上傳已經完成後,再補全細節部分。

在QuestionController中開發伺服器端的簡易上傳處理:

@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中,并調整這段代碼:

$(document).ready(function () {
    $('#summernote').summernote({
        height: 300,
        tabsize: 2,
        lang: 'zh-CN',
        placeholder: '請輸入問題的較長的描述...',
        callbacks: {
            onImageUpload: function () {
                alert("準備上傳圖檔!");
            }
        }
    });
});      

完成後,重新開機項目,打開“釋出問題”頁面,插入圖檔,選擇圖檔檔案就會彈出對話框!

然後,在以上回調中,使用$.ajax()送出異步請求,在處理結果時,建立Image對象,将結果中的圖檔URL作為Image對象的src屬性值,并将整個Image對象(就是一個<src>标簽)插入到富文本編輯器中:

$(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. 完善伺服器端的上傳功能

先建立關于檔案上傳的異常類型:

public class FileUploadException extends RuntimeException {
}
public class FileEmptyException extends FileUploadException {
}
public class FileSizeException extends FileUploadException {
}
public class FileTypeException extends FileUploadException {
}
public class FileIOException extends FileUploadException {
}
在GlobalExceptionHandler中處理以上異常,完整代碼如下(需在R.State中添加常量):
@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個全局屬性,用于讀取配置中的“檔案最大大小”和“檔案類型”:

@Value("${project.question.image-max-size}")
private long imageMaxSize;
@Value(("${project.question.image-content-types}"))
private List<String> imageContentTypes;      

在處理上傳請求的過程中:

應該建立子級檔案夾,避免所有的檔案都傳到指定的同一個檔案夾中,推薦使用“年”和“月”分别建立2級子檔案夾,上傳的圖檔應該放在“月”的檔案夾中;

可以使用UUID作為檔案名;

不需要判斷原始擴充名,而是直接從原始檔案全名中截取即可;

及時打樁,輸出關鍵資訊,例如儲存檔案的檔案夾路徑、檔案名、完整路徑等,便于出錯時排查問題。

具體代碼:

@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);
}