48. 在父項(xiàng)目中管理子模塊項(xiàng)目使用的依賴
在一個(gè)項(xiàng)目中,如果某些依賴只是部分子模塊項(xiàng)目需要使用的,應(yīng)該將這些依賴配置在<dependencyManagement>
節(jié)點(diǎn)中,凡配置在這個(gè)節(jié)點(diǎn)中的依賴,任何子模塊項(xiàng)目中都不會(huì)直接擁有,如果某個(gè)子模塊項(xiàng)目需要使用這些依賴,依然需要使用<dependency>
節(jié)點(diǎn)來添加!與在子模塊項(xiàng)目中直接添加<denpendency>
(父級(jí)的<dependencyManagement>
沒有配置某個(gè)依賴而子模塊項(xiàng)目中直接添加)的區(qū)別在于:如果事先使用父級(jí)項(xiàng)目的<dependencyManagement>
進(jìn)行了配置,則子模塊項(xiàng)目在添加時(shí),不需要指定版本號(hào),直接使用父級(jí)項(xiàng)目配置的版號(hào),以便于在父級(jí)項(xiàng)目中統(tǒng)一管理依賴的版本 !
注意:在父級(jí)項(xiàng)目中,添加許多依賴都是不需要指定版本號(hào)的,但是,如果將這些依賴配置到<dependencyManagement>
中用于指導(dǎo)子模塊項(xiàng)目所使用的依賴的版本時(shí),必須顯式的指定版本號(hào),否則,子模塊項(xiàng)目將不明確需要使用的是哪個(gè)版本!
則在父級(jí)項(xiàng)目中,關(guān)于依賴的管理:
< 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節(jié)點(diǎn)的中的依賴是每個(gè)子模塊項(xiàng)目都直接擁有的 -->
< 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中的依賴只是為了管理子模塊項(xiàng)目使用依賴時(shí)的版本 -->
< 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>
由于大量的依賴都已經(jīng)添加在<dependencyManagement>
中了,則straw-generator
和straw-portal
項(xiàng)目都不會(huì)直接擁有這些依賴,則需要在這2個(gè)子模塊項(xiàng)目中自行添加所需的依賴!
在straw-generator
項(xiàng)目中(關(guān)于代碼生成器的相關(guān)依賴由于過于特殊,一定只有當(dāng)前項(xiàng)目需要使用,所以,對(duì)版本的管理方式可以不嚴(yán)格要求):
< 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
項(xiàng)目中:
< 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. 創(chuàng)建靜態(tài)資源子模塊項(xiàng)目
創(chuàng)建新的straw-resource
子模塊項(xiàng)目,用于管理用戶上傳的文件等靜態(tài)資源。
創(chuàng)建出來后,在straw-resource
的pom.xml
中,自行將父級(jí)項(xiàng)目由SpringBoot
改為straw
項(xiàng)目,刪除<dependencies>
和<build>
節(jié)點(diǎn)(因?yàn)闆]有存在的必要,在父項(xiàng)目中已經(jīng)配置好了),。
在straw
項(xiàng)目中的<mudules>
中添加子模塊項(xiàng)目,。
在straw-resource
的application.properties
中顯式的配置端口號(hào),必須與straw-portal
的不同:
server.port=8081
全部完成后,更新Maven,straw-portal
和straw-resource
這2個(gè)項(xiàng)目是可以同時(shí)啟動(dòng)的!
51. 設(shè)置straw-resource子模塊項(xiàng)目的靜態(tài)目錄
在straw-resource
項(xiàng)目的application.properties
中添加配置:
spring.resources.static-locations=file:D:/IdeaProjects/straw-static-resource
則straw-resource
項(xiàng)目的靜態(tài)目錄就是以上指定的位置,后續(xù)straw-portal
項(xiàng)目中涉及上傳操作時(shí),上傳的文件也應(yīng)該存放到以上位置。
52.設(shè)置straw-resource子模塊項(xiàng)目的靜態(tài)目錄
在straw-portal
項(xiàng)目的application.properties
中添加配置:
# 發(fā)布問題時(shí),將圖片上傳到哪里,需要與straw-resource項(xiàng)目的靜態(tài)資源目錄保持一致
project.question.image-upload-path=D:/IdeaProjects/straw-static-resource
# 發(fā)布問題時(shí),上傳的圖片通過哪個(gè)服務(wù)器提供訪問,配置的端口號(hào)需要與straw-resource項(xiàng)目保持一致
project.question.image-host=http://localhost:8081/
# 發(fā)布問題時(shí),允許上傳的文件的最大大小
project.question.image-max-size=307200
# 發(fā)布問題時(shí),允許上傳的圖片文件的類型
project.question.image-content-types=image/jpeg, image/png, image/bmp
并且,在straw-portal
中調(diào)整默認(rèn)限制的文件大小:
@Bean
public MultipartConfigElement multipartConfigElement ( ) {
MultipartConfigFactory factory = new MultipartConfigFactory ( ) ;
factory. setMaxFileSize ( DataSize. ofMegabytes ( 500 ) ) ;
factory. setMaxRequestSize ( DataSize. ofMegabytes ( 500 ) ) ;
return factory. createMultipartConfig ( ) ;
}
53. 開發(fā)簡(jiǎn)易上傳功能
說明:由于上傳功能不可以通過在URL上填寫參數(shù)直接進(jìn)行測(cè)試,為了更快的進(jìn)行測(cè)試并體驗(yàn)上傳的效果,暫且忽略不必要的代碼,例如上傳文件的相關(guān)檢查等細(xì)節(jié)問題,當(dāng)然,測(cè)試時(shí)也應(yīng)該使用正確的文件和數(shù)據(jù)進(jìn)行測(cè)試,。當(dāng)簡(jiǎn)單的上傳已經(jīng)完成后,再補(bǔ)全細(xì)節(jié)部分,。
在QuestionController
中開發(fā)服務(wù)器端的簡(jiǎn)易上傳處理:
@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) ;
}
本次需要處理的頁面是“發(fā)表問題”的question/create.html ,在發(fā)表問題時(shí),使用的富文本編輯Summernote提供了名為callbacks
的回調(diào)機(jī)制,其中,存在名為onImageUpload
的回調(diào)屬性,該屬性值是函數(shù),所以,可以自定義函數(shù)配置到這個(gè)回調(diào)屬性中,則后續(xù)上傳圖片時(shí),就會(huì)自動(dòng)觸發(fā)自定義的函數(shù),通過自定義函數(shù)實(shí)現(xiàn)圖片的上傳,并返回上傳圖片的URL,生成圖片插入到Summernote富文本編輯器中即可。
在question/create.html 中,先將底部關(guān)于Summernote的JavaScript代碼移到新創(chuàng)建的commons/init_summernote.js 中,并調(diào)整這段代碼:
$ ( document) . ready ( function ( ) {
$ ( '#summernote' ) . summernote ( {
height: 300 ,
tabsize: 2 ,
lang: 'zh-CN' ,
placeholder: '請(qǐng)輸入問題的詳細(xì)描述...' ,
callbacks: {
onImageUpload: function ( ) {
alert ( "準(zhǔn)備上傳圖片!" ) ;
}
}
} ) ;
} ) ;
完成后,重啟項(xiàng)目,打開“發(fā)布問題”頁面,插入圖片,選擇圖片文件就會(huì)彈出對(duì)話框!
然后,在以上回調(diào)中,使用$.ajax()
提交異步請(qǐng)求,在處理結(jié)果時(shí),創(chuàng)建Image
對(duì)象,將結(jié)果中的圖片URL作為Image
對(duì)象的src
屬性值,并將整個(gè)Image
對(duì)象(就是一個(gè)<src>
標(biāo)簽)插入到富文本編輯器中:
$ ( document) . ready ( function ( ) {
$ ( '#summernote' ) . summernote ( {
height: 300 ,
tabsize: 2 ,
lang: 'zh-CN' ,
placeholder: '請(qǐng)輸入問題的詳細(xì)描述...' ,
callbacks: {
onImageUpload: function ( files) {
// ---------------------------------------
// 當(dāng)前函數(shù)的參數(shù)名稱是自定義,它表示用戶選擇的若干個(gè)文件
// Summernote在調(diào)用該函數(shù)時(shí),會(huì)把用戶選擇的文件作為函數(shù)的參數(shù)
// ---------------------------------------
if ( ! files || files. length < 1 ) {
alert ( "請(qǐng)選擇您要上傳的文件!" ) ;
return ;
}
if ( files. length > 1 ) {
alert ( "一次只允許上傳1個(gè)文件!" ) ;
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. 完善服務(wù)器端的上傳功能
先創(chuàng)建關(guān)于文件上傳的異常類型:
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) ;
}
}
}
在處理上傳請(qǐng)求之前,先聲明2個(gè)全局屬性,用于讀取配置中的“文件最大大小”和“文件類型”:
@Value ( "${project.question.image-max-size}" )
private long imageMaxSize;
@Value ( ( "${project.question.image-content-types}" ) )
private List< String> imageContentTypes;
在處理上傳請(qǐng)求的過程中:
應(yīng)該創(chuàng)建子級(jí)文件夾,避免所有的文件都傳到指定的同一個(gè)文件夾中,推薦使用“年”和“月”分別創(chuàng)建2級(jí)子文件夾,上傳的圖片應(yīng)該放在“月”的文件夾中; 可以使用UUID作為文件名; 不需要判斷原始擴(kuò)展名,而是直接從原始文件全名中截取即可; 及時(shí)打樁,輸出關(guān)鍵信息,例如保存文件的文件夾路徑,、文件名,、完整路徑等,便于出錯(cuò)時(shí)排查問題。
具體代碼:
@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 ( "上傳圖片失敗!請(qǐng)選擇有效的圖片文件!" ) ;
}
// 判斷上傳的文件大小是否超標(biāo)
if ( imageFile. getSize ( ) > imageMaxSize) {
throw new FileSizeException ( "上傳圖片失敗!不允許使用超過" + ( imageMaxSize / 1024 ) + "KB的圖片文件!" ) ;
}
// 判斷上傳的文件類型是否超標(biāo)
if ( ! imageContentTypes. contains ( imageFile. getContentType ( ) ) ) {
throw new FileTypeException ( "上傳圖片失敗!圖片類型錯(cuò)誤!允許上傳的圖片類型有:" + imageContentTypes) ;
}
// 確定本次上傳時(shí)使用的文件夾
String dir = DateTimeFormatter. ofPattern ( "yyyy/MM" ) . format ( LocalDateTime. now ( ) ) ;
File parent = new File ( imageUploadPath, dir) ;
if ( ! parent. exists ( ) ) {
parent. mkdirs ( ) ;
}
log. debug ( "dir >>> {}" , parent) ;
// 確定本次上傳時(shí)使用的文件名
String filename = UUID. randomUUID ( ) . toString ( ) ;
String originalFilename = imageFile. getOriginalFilename ( ) ;
String suffix = originalFilename. substring ( originalFilename. lastIndexOf ( "." ) ) ;
String child = filename + suffix;
// 創(chuàng)建最終保存時(shí)的文件對(duì)象
File dest = new File ( parent, child) ;
// 執(zhí)行保存
try {
imageFile. transferTo ( dest) ;
} catch ( IOException e) {
throw new FileIOException ( "上傳圖片失敗!當(dāng)前服務(wù)器忙,請(qǐng)稍后再次嘗試!" ) ;
}
// 確定網(wǎng)絡(luò)訪問路徑
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
路徑的處理方法:
@GetMapping ( "/index.html" )
public String index ( @AuthenticationPrincipal UserInfo userInfo) {
if ( userInfo. getType ( ) == 0 ) {
return "index" ;
} else {
return "index_teacher" ;
}
}
需要注意:以上判斷用戶身份時(shí),會(huì)判斷用戶數(shù)據(jù)的type
屬性,此前,在UserServiceImpl.login()
方法中已經(jīng)向返回的UserInfo
中設(shè)置了從數(shù)據(jù)庫(kù)中讀取到的type
屬性,則以上代碼可以正常獲取type
值!
56. 老師主頁顯示問題列表-持久層
(a) 規(guī)范需要執(zhí)行的SQL語句
老師主頁顯示的問題列表應(yīng)該顯示出老師自己發(fā)表的問題,和學(xué)生指定該老師回答的問題,。
這樣的列表數(shù)據(jù)可以使用此前的QuestionVO
來表示每一個(gè)問題的數(shù)據(jù),列表則使用List<QuestionVO>
來表示。
需要執(zhí)行的SQL語句大致是:
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) 在接口中添加抽象方法
List< QuestionVO> findTeacherQuestions ( Integer teacherId) ;
? 配置SQL映射
(d) 單元測(cè)試
57. 老師主頁顯示問題列表-業(yè)務(wù)層
(a)
(b) 接口與抽象方法
原本存在抽象方法:
PageInfo< QuestionVO> getQuestionsByUserId ( Integer userId, Integer page) ;
改為:
PageInfo< QuestionVO> getQuestionsByUserId ( Integer userId, Integer type, Integer page) ;
? 實(shí)現(xiàn)業(yè)務(wù)方法
在原本存在的getQuestionsByUserId()
方法的參數(shù)列表中添加參數(shù),與以上抽象方法保持一致,然后,在實(shí)現(xiàn)過程中:
// 設(shè)置分頁參數(shù)
PageHelper. startPage ( page, pageSize) ;
// 調(diào)用持久層方法查詢問題列表,該列表中的數(shù)據(jù)只有標(biāo)簽的id,并不包括標(biāo)簽數(shù)據(jù)
List< QuestionVO> questions;
if ( type == 0 ) {
questions = questionMapper. findListByUserId ( userId) ;
} else {
questions = questionMapper. findTeacherQuestions ( userId) ;
}
// 后續(xù)代碼不變
(d) 單元測(cè)試
由于修改了業(yè)務(wù)方法的聲明,當(dāng)前控制器層的調(diào)用會(huì)因?yàn)閰?shù)不匹配而報(bào)錯(cuò),將無法進(jìn)行單元測(cè)試,所以,先處理完控制器層再測(cè)試,。
58. 老師主頁顯示問題列表-控制器層
在原來的獲取學(xué)生問題列表的方法中,調(diào)用業(yè)務(wù)方法時(shí)多添加type
值即可,該值來自UserInfo
參數(shù):
@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) ;
}
完成后,應(yīng)該分別測(cè)試學(xué)生賬號(hào)登錄后顯示列表和老師賬號(hào)登錄后顯示列表,。
59. 老師主頁顯示問題列表-前端頁面
引用question/create.html
中的處理即可!也就是說:在question/create.html
中將列表區(qū)域設(shè)置為th:fragment
,然后在index_teacher.html
中通過th:replace
直接引用即可!