ありがとうございます。
ここから 第8段階:Service層設計 + Controller設計 に進みます。
今回は、これまで決定した設計方針に従い、
-
Entity変更はJPA
-
検索・表示はMyBatis
-
Serviceが業務ルールを管理
-
Controllerは画面制御のみ
という責務分離で設計します。
また、教材管理では特に重要な以下の業務フローを実装対象にします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | 教師
教材作成
↓
下書き保存
↓
審査申請
↓
管理者承認
↓
公開
受講者
公開教材閲覧
|
1. Service層構成
パッケージ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | service
├ content
│ ├ ContentCommandService.java
│ └ ContentQueryService.java
│
├ course
│ ├ CourseCommandService.java
│ └ CourseQueryService.java
│
├ approval
│ └ ApprovalService.java
│
├ learning
│ └ LearningService.java
│
└ security
└ LoginUserService.java
|
2. Command / Query分離
今回はCQRSの考え方を少し取り入れます。
Command
変更系
利用:
Query
参照系
利用:
3. ContentCommandService
教材作成・更新担当です。
責務:
-
教材作成
-
Version作成
-
下書き保存
-
審査申請
ContentCommandService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | package com.example.lms.service.content;
import com.example.lms.entity.content.Content;
import com.example.lms.entity.content.ContentVersion;
import com.example.lms.entity.user.User;
import com.example.lms.enums.ContentStatus;
import com.example.lms.repository.ContentRepository;
import com.example.lms.repository.ContentVersionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ContentCommandService {
private final ContentRepository contentRepository;
private final ContentVersionRepository contentVersionRepository;
@Transactional
public Long createTextContent(User teacher,String title,String summary,String body){
Content content = new Content();
content.setAuthor(teacher);
contentRepository.save(content);
ContentVersion version = new ContentVersion();
version.setContent(content);
version.setVersionNo(1);
version.setTitle(title);
version.setSummary(summary);
version.setStatus(ContentStatus.DRAFT);
contentVersionRepository.save(version);
content.setCurrentVersion(version);
return content.getId();
}
}
|
4. 教材申請処理
教師が「申請」ボタンを押した場合。
状態変更のみ行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | @Transactional
public void requestReview(Long contentId){
Content content =
contentRepository.findById(contentId)
.orElseThrow();
ContentVersion version =
content.getCurrentVersion();
version.setStatus(
ContentStatus.REVIEWING
);
}
|
5. ApprovalService
管理者承認処理です。
責務:
ApprovalService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | @Service
@RequiredArgsConstructor
public class ApprovalService {
private final ContentVersionRepository versionRepository;
@Transactional
public void approve(Long versionId){
ContentVersion version =
versionRepository.findById(versionId)
.orElseThrow();
version.setStatus(
ContentStatus.PUBLISHED
);
version.setPublishedAt(
LocalDateTime.now()
);
}
@Transactional
public void reject(Long versionId,String comment){
ContentVersion version =
versionRepository.findById(versionId)
.orElseThrow();
version.setStatus(
ContentStatus.REJECTED
);
}
}
|
6. ContentQueryService
表示用です。
MyBatisを利用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | @Service
@RequiredArgsConstructor
public class ContentQueryService {
private final ContentMapper contentMapper;
public List<ContentListDto> search(
ContentSearchCondition condition){
return contentMapper.search(condition);
}
public ContentDetailDto findDetail(Long id){
return contentMapper.findDetail(id);
}
}
|
7. CourseCommandService
コース構築担当です。
責務:
-
コース作成
-
Section追加
-
Lesson追加
-
教材配置
教材配置処理
重要なのは、
Contentをコピーしない
ことです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | @Service
@RequiredArgsConstructor
public class CourseCommandService {
private final LessonContentRepository lessonContentRepository;
private final LessonRepository lessonRepository;
private final ContentRepository contentRepository;
@Transactional
public void addContent(
Long lessonId,
Long contentId){
Lesson lesson =
lessonRepository.findById(lessonId)
.orElseThrow();
Content content =
contentRepository.findById(contentId)
.orElseThrow();
LessonContent lc =
new LessonContent();
lc.setLesson(lesson);
lc.setContent(content);
lc.setRequiredFlag(true);
lessonContentRepository.save(lc);
}
}
|
8. Controller設計
Controllerは薄くします。
教師用Controller
URL:
ContentController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | @Controller
@RequestMapping("/teacher/content")
@RequiredArgsConstructor
public class ContentController {
private final ContentCommandService commandService;
private final ContentQueryService queryService;
@GetMapping
public String list(
Model model,
ContentSearchCondition condition){
model.addAttribute(
"contents",
queryService.search(condition)
);
return "teacher/content/list";
}
@PostMapping("/{id}/review")
public String review(
@PathVariable Long id){
commandService.requestReview(id);
return "redirect:/teacher/content";
}
}
|
9. 管理者Controller
URL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | @Controller
@RequestMapping("/admin/approval")
@RequiredArgsConstructor
public class ApprovalController {
private final ApprovalService approvalService;
@PostMapping("/{id}/approve")
public String approve(
@PathVariable Long id){
approvalService.approve(id);
return "redirect:/admin/approval";
}
@PostMapping("/{id}/reject")
public String reject(
@PathVariable Long id){
approvalService.reject(
id,
"修正してください"
);
return "redirect:/admin/approval";
}
}
|
10. 受講者Controller
URL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | @Controller
@RequestMapping("/student/course")
@RequiredArgsConstructor
public class StudentCourseController {
private final CourseQueryService queryService;
@GetMapping("/{id}")
public String detail(
@PathVariable Long id,
Model model){
model.addAttribute(
"course",
queryService.findTree(id)
);
return "student/course/detail";
}
}
|
11. Spring Security連携
教師の場合:
| @PreAuthorize(
"hasRole('TEACHER')")
|
例:
| @PreAuthorize("hasRole('TEACHER')")
@PostMapping("/{id}/review")
public String review(...)
|
管理者:
| @PreAuthorize(
"hasRole('ADMIN')")
|
12. 認可ルール
| 操作 |
ADMIN |
TEACHER |
MENTOR |
STUDENT |
| 教材作成 |
○ |
○ |
× |
× |
| 教材編集 |
○ |
○(自身のみ) |
× |
× |
| 教材承認 |
○ |
× |
× |
× |
| 教材閲覧 |
○ |
○ |
△ |
○ |
| 質問対応 |
○ |
× |
○ |
× |
13. 教材編集権限チェック
教師は自分の教材だけ変更可能にします。
Serviceで確認します。
1
2
3
4
5
6
7
8
9
10
11
12
13 | private void checkOwner(
Content content,
User loginUser){
if(!content.getAuthor()
.getId()
.equals(loginUser.getId())){
throw new AccessDeniedException(
"編集権限がありません"
);
}
}
|
14. 現時点の完成フロー
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | 教師ログイン
↓
教材作成画面
↓
ContentCommandService
↓
Content
ContentVersion
↓
DRAFT
申請
↓
REVIEWING
管理者
↓
ApprovalService
↓
PUBLISHED
受講者
↓
CourseMapper
↓
公開教材表示
|
第8段階終了時点
実装可能な業務フローが完成しました。
次の 第9段階 では、画面設計とThymeleaf実装に進みます。
作成内容:
-
教師画面
-
教材一覧
-
教材登録
-
Markdown入力
-
動画登録
-
クイズ作成
-
管理者画面
-
受講者画面
-
コース一覧
-
セクションツリー
-
レッスン表示
-
動画再生
-
Markdown HTML表示
-
Bootstrap 5デザイン
まで設計します。