ありがとうございます。
ここから 第12段階:教材編集・審査申請・承認・公開ワークフロー設計 に進みます。
今回は教材管理システムで非常に重要な部分になります。
特に以下の要件を満たします。
-
教師は公開済み教材を直接変更しない
-
編集すると新しいVersionを作成する
-
管理者承認後に公開する
-
過去Versionは履歴として保持する
-
誰がいつ変更したか記録できる構造にする
第12段階の業務フロー
教材更新フロー
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 | 教師
公開中教材を編集
↓
新しいContentVersion作成
↓
DRAFT
↓
審査申請
↓
REVIEWING
↓
管理者確認
↓
APPROVED
↓
公開
↓
PUBLISHED
|
1. ContentVersion状態設計
前回:
| DRAFT
REVIEWING
APPROVED
PUBLISHED
REJECTED
ARCHIVED
|
でしたが、承認フローを明確にするため少し整理します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | public enum ContentStatus {
DRAFT,
REVIEWING,
APPROVED,
PUBLISHED,
REJECTED,
ARCHIVED
}
|
状態遷移:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | DRAFT
|
|申請
↓
REVIEWING
|
|承認
↓
APPROVED
|
|公開
↓
PUBLISHED
REVIEWING
|
|差戻し
↓
REJECTED
|
2. Version管理設計
例:
現在公開中:
| Content
Javaとは
|
+-- Version1
status=PUBLISHED
|
教師が修正:
| Content
Javaとは
|
+-- Version1
| 公開中
|
+-- Version2
下書き
|
承認後:
| Content
Javaとは
|
+-- Version1
| ARCHIVED
|
+-- Version2
PUBLISHED
|
3. ContentVersion Entity追加項目
追加推奨:
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 | @Entity
public class ContentVersion extends BaseEntity {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
private Integer versionNo;
@Enumerated(EnumType.STRING)
private ContentStatus status;
private LocalDateTime submittedAt;
private LocalDateTime approvedAt;
private LocalDateTime publishedAt;
@ManyToOne(fetch=FetchType.LAZY)
private User submittedBy;
@ManyToOne(fetch=FetchType.LAZY)
private User approvedBy;
}
|
4. 教材編集処理
編集時の重要ルール
NG:
OK:
| ContentVersion1
↓
コピー
↓
ContentVersion2
|
5. ContentEditService
作成します。
| service
└ content
├ ContentCommandService
├ ContentEditService
└ ContentPublishService
|
ContentEditService.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 | @Service
@RequiredArgsConstructor
public class ContentEditService {
private final ContentRepository contentRepository;
private final ContentVersionRepository versionRepository;
@Transactional
public Long createNewVersion(
Long contentId,
User teacher){
Content content =
contentRepository.findById(contentId)
.orElseThrow();
checkOwner(
content,
teacher
);
ContentVersion current =
content.getCurrentVersion();
ContentVersion next =
new ContentVersion();
next.setContent(
content
);
next.setVersionNo(
current.getVersionNo()+1
);
next.setTitle(
current.getTitle()
);
next.setSummary(
current.getSummary()
);
next.setStatus(
ContentStatus.DRAFT
);
versionRepository.save(
next
);
return next.getId();
}
}
|
6. 教材コピー処理
教材タイプ別データもコピーします。
例:
| TextContent
Version1
↓
TextContent
Version2
|
Textコピー
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 | private void copyText(
TextContent oldText,
ContentVersion newVersion){
TextContent text =
new TextContent();
text.setVersion(
newVersion
);
text.setBody(
oldText.getBody()
);
text.setFormat(
oldText.getFormat()
);
textRepository.save(text);
}
|
7. 審査申請処理
教師が申請ボタンを押す。
ContentReviewService
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 | @Service
@RequiredArgsConstructor
public class ContentReviewService {
private final ContentVersionRepository repository;
@Transactional
public void submit(
Long versionId,
User teacher){
ContentVersion version =
repository.findById(versionId)
.orElseThrow();
if(
version.getStatus()
!= ContentStatus.DRAFT
){
throw new IllegalStateException(
"申請できません"
);
}
version.setStatus(
ContentStatus.REVIEWING
);
version.setSubmittedAt(
LocalDateTime.now()
);
version.setSubmittedBy(
teacher
);
}
}
|
8. 管理者承認処理
ContentPublishService
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
48 | @Service
@RequiredArgsConstructor
public class ContentPublishService {
private final ContentRepository contentRepository;
private final ContentVersionRepository versionRepository;
@Transactional
public void approve(
Long versionId,
User admin){
ContentVersion version =
versionRepository.findById(versionId)
.orElseThrow();
if(
version.getStatus()
!= ContentStatus.REVIEWING
){
throw new IllegalStateException(
"承認対象ではありません"
);
}
version.setStatus(
ContentStatus.APPROVED
);
version.setApprovedBy(
admin
);
version.setApprovedAt(
LocalDateTime.now()
);
}
}
|
9. 公開処理
承認と公開を分けます。
理由:
に対応できるためです。
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 | @Transactional
public void publish(
Long versionId){
ContentVersion version =
versionRepository.findById(versionId)
.orElseThrow();
Content content =
version.getContent();
ContentVersion old =
content.getCurrentVersion();
if(old != null){
old.setStatus(
ContentStatus.ARCHIVED
);
}
version.setStatus(
ContentStatus.PUBLISHED
);
version.setPublishedAt(
LocalDateTime.now()
);
content.setCurrentVersion(
version
);
}
|
10. 承認画面用DTO
管理者画面ではMyBatis取得します。
ApprovalListDto
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | @Data
public class ApprovalListDto {
private Long versionId;
private String title;
private String authorName;
private String status;
private LocalDateTime submittedAt;
}
|
11. ApprovalMapper
| @Mapper
public interface ApprovalMapper {
List<ApprovalListDto> findWaiting();
ApprovalDetailDto findDetail(
Long versionId
);
}
|
12. 承認一覧SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | <select id="findWaiting"
resultType="com.example.lms.dto.ApprovalListDto">
SELECT
cv.id AS version_id,
cv.title,
u.username AS author_name,
cv.status,
cv.submitted_at
FROM content_versions cv
JOIN content c
ON cv.content_id=c.id
JOIN users u
ON c.author_id=u.id
WHERE cv.status='REVIEWING'
ORDER BY cv.submitted_at
</select>
|
13. Controller
教師申請
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | @PostMapping(
"/{id}/submit"
)
public String submit(
@PathVariable Long id){
reviewService.submit(
id,
loginUser.get()
);
return "redirect:/teacher/content";
}
|
管理者承認
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | @PostMapping(
"/{id}/approve"
)
@PreAuthorize(
"hasRole('ADMIN')")
public String approve(
@PathVariable Long id){
publishService.approve(
id,
loginUser.get()
);
return "redirect:/admin/approval";
}
|
14. 監査ログ設計(次段階準備)
今回のVersion管理だけでも、
は管理できます。
ただし、
まで必要なら別テーブルを追加します。
次段階で実装します。
15. 現在の教材ライフサイクル
完成形:
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 | 教師
|
v
ContentVersion
|
|
+-------------+
| |
DRAFT REVIEWING
|
|
管理者
|
v
APPROVED
|
v
PUBLISHED
|
v
受講者閲覧
|
第12段階終了時点
実装できる機能:
✅ 教材編集
✅ Version管理
✅ 公開済み教材保護
✅ 審査申請
✅ 管理者承認
✅ 公開切替
✅ 過去版保持
次の 第13段階 では、教材システムとして重要な以下を実装します。
第13段階:監査ログ + 添付ファイル管理 + Cloudflare R2設計
内容:
-
操作履歴テーブル設計
-
誰が・いつ・何を変更したか記録
-
Spring AOPによる自動ログ取得
-
ファイルアップロードサービス
-
Cloudflare R2連携
-
動画ストリーミング配信設計
-
PDF/ZIPダウンロード制御
へ進みます。