コンテンツにスキップ

ありがとうございます。

ここから 第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状態設計

前回:

1
2
3
4
5
6
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管理設計

例:

現在公開中:

1
2
3
4
5
6
Content

Javaとは
 |
 +-- Version1
     status=PUBLISHED

教師が修正:

1
2
3
4
5
6
7
8
9
Content

Javaとは
 |
 +-- Version1
 |    公開中
 |
 +-- Version2
      下書き

承認後:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
ContentVersion1

↓

上書き

OK:

1
2
3
4
5
6
7
8
9
ContentVersion1

↓

コピー

↓

ContentVersion2

5. ContentEditService

作成します。

1
2
3
4
5
6
7
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. 教材コピー処理

教材タイプ別データもコピーします。

例:

1
2
3
4
5
6
7
8
9
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@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管理だけでも、

1
2
3
4
5
誰が

いつ

何版を作ったか

は管理できます。

ただし、

1
2
3
4
タイトル変更前
タイトル変更後

状態変更履歴

まで必要なら別テーブルを追加します。

次段階で実装します。


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設計

内容:

  1. 操作履歴テーブル設計

  2. 誰が・いつ・何を変更したか記録

  3. Spring AOPによる自動ログ取得

  4. ファイルアップロードサービス

  5. Cloudflare R2連携

  6. 動画ストリーミング配信設計

  7. PDF/ZIPダウンロード制御

へ進みます。