ありがとうございます。
ここから 第13段階:監査ログ + 添付ファイル管理 + Cloudflare R2設計 に進みます。
今回の段階では、教材管理システムを本番運用するうえで重要な以下を設計します。
第13段階 全体構成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | 操作
教師
↓
Controller
↓
Service
↓
AOP
↓
AuditLog
ファイル
Content
↓
Attachment
↓
StorageService
↓
Cloudflare R2
|
1. 監査ログ設計
目的
例えば以下を確認できるようにします。
| 2026/08/04 10:30
山田先生
教材「Java入門」
Version2を作成
|
また、
| 管理者
教材「Java入門」
Version2を公開
|
も記録します。
2. AuditLogテーブル
DDL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | CREATE TABLE audit_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
action_type VARCHAR(50) NOT NULL,
target_type VARCHAR(50) NOT NULL,
target_id BIGINT,
description TEXT,
created_at DATETIME NOT NULL,
CONSTRAINT fk_audit_user
FOREIGN KEY(user_id)
REFERENCES users(id)
);
|
3. action_type
Enum管理します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | public enum AuditAction {
CREATE,
UPDATE,
DELETE,
SUBMIT,
APPROVE,
PUBLISH,
LOGIN
}
|
4. Entity
AuditLog.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 | @Entity
@Table(name="audit_logs")
@Getter
@Setter
public class AuditLog {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
private User user;
@Enumerated(EnumType.STRING)
private AuditAction actionType;
private String targetType;
private Long targetId;
@Column(columnDefinition="TEXT")
private String description;
private LocalDateTime createdAt;
}
|
5. Repository
| @Repository
public interface AuditLogRepository
extends JpaRepository<AuditLog,Long>{
}
|
6. AuditService
監査ログ登録専用サービスを作ります。
| service
└ audit
└ AuditService
|
AuditService.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 | @Service
@RequiredArgsConstructor
public class AuditService {
private final AuditLogRepository repository;
@Transactional
public void save(
User user,
AuditAction action,
String targetType,
Long targetId,
String description){
AuditLog log =
new AuditLog();
log.setUser(user);
log.setActionType(action);
log.setTargetType(
targetType
);
log.setTargetId(
targetId
);
log.setDescription(
description
);
log.setCreatedAt(
LocalDateTime.now()
);
repository.save(log);
}
}
|
7. Spring AOPによる自動記録
Serviceへ個別に、
を書く方法もあります。
しかし、増えると管理が難しくなります。
そこでAOPを利用します。
8. Auditアノテーション作成
| @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
AuditAction action();
String target();
}
|
利用例:
| @Audit(
action=AuditAction.PUBLISH,
target="CONTENT"
)
public void publish(Long id){
}
|
9. AuditAspect
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 | @Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {
private final AuditService auditService;
private final LoginUser loginUser;
@AfterReturning("@annotation(audit)")
public void after(
JoinPoint joinPoint,
Audit audit){
User user =
loginUser.get();
Object[] args =
joinPoint.getArgs();
Long id =
(Long)args[0];
auditService.save(
user,
audit.action(),
audit.target(),
id,
joinPoint
.getSignature()
.getName()
);
}
}
|
10. 利用例
教材公開:
| @Audit(
action=AuditAction.PUBLISH,
target="CONTENT"
)
@Transactional
public void publish(
Long versionId){
}
|
自動的に:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | audit_logs
USER
山田
ACTION
PUBLISH
TARGET
CONTENT
ID
100
TIME
2026-08-04
|
が登録されます。
11. 添付ファイル共通化
以前提案した教材タイプ別管理:
とは別に、
すべてのファイルを共通管理します。
12. Attachment設計
用途:
すべて対応します。
13. テーブル
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | CREATE TABLE attachments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
original_name VARCHAR(255),
stored_name VARCHAR(255),
storage_type VARCHAR(20),
storage_path VARCHAR(500),
mime_type VARCHAR(100),
file_size BIGINT,
uploaded_by BIGINT,
created_at DATETIME
);
|
14. Entity
Attachment.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 | @Entity
@Table(name="attachments")
@Getter
@Setter
public class Attachment {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
private String originalName;
private String storedName;
private String storageType;
private String storagePath;
private String mimeType;
private Long fileSize;
@ManyToOne(fetch=FetchType.LAZY)
private User uploadedBy;
private LocalDateTime createdAt;
}
|
15. Contentとの関連
多対多にします。
理由:
同じPDFを複数教材で利用可能にするためです。
中間テーブル
1
2
3
4
5
6
7
8
9
10
11
12 | CREATE TABLE content_attachments (
content_id BIGINT,
attachment_id BIGINT,
PRIMARY KEY(
content_id,
attachment_id
)
);
|
構造:
| Content
+
Attachment
↓
ContentAttachment
|
16. StorageService設計
保存先を抽象化します。
| service
└ storage
├ StorageService
├ LocalStorageService
└ R2StorageService
|
StorageService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | public interface StorageService {
String upload(
MultipartFile file
);
Resource download(
String path
);
void delete(
String path
);
}
|
17. Cloudflare R2構成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | Browser
↓
Spring Boot
↓
StorageService
↓
Cloudflare R2
↓
Object Storage
|
18. R2保存パス設計
推奨:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | lms/
├ videos/
│ └ content001/
│ └ master.m3u8
├ documents/
│ └ content001/
│ └ sample.pdf
└ images/
└ content001/
|
19. 動画設計
MP4直配信ではなく将来的にはHLS推奨です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | 動画アップロード
↓
FFmpeg変換
↓
HLS生成
↓
R2保存
master.m3u8
segment001.ts
segment002.ts
|
20. VideoContent変更案
現在:
↓
変更推奨:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | video_type
youtube
vimeo
r2
external_id
youtube動画ID
storage_key
videos/content001/master.m3u8
|
Entity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | @Entity
public class VideoContent {
private String videoType;
private String externalId;
private String storageKey;
}
|
21. ファイルアップロード処理
Controller:
| @PostMapping("/upload")
public String upload(
MultipartFile file){
String path =
storageService.upload(file);
return path;
}
|
Service:
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 | @Transactional
public Long saveFile(
MultipartFile file){
String path =
storageService.upload(file);
Attachment attachment =
new Attachment();
attachment.setStoragePath(
path
);
repository.save(
attachment
);
return attachment.getId();
}
|
22. セキュリティ制御
重要です。
ファイルURLを直接公開しません。
NG:
| https://r2.xxx.com/sample.pdf
|
OK:
Controller:
| @GetMapping(
"/download/{id}"
)
public ResponseEntity<Resource> download(
@PathVariable Long id){
return storageService.download(id);
}
|
23. 現在の完成構造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | Content
|
+-----------+-----------+
Version Attachment
| |
Video/Text/Quiz/File R2 Storage
操作
User
|
AuditLog
|
第13段階終了時点
追加された機能:
✅ 操作履歴管理
✅ Spring AOP監査ログ
✅ 添付ファイル共通化
✅ R2対応可能なStorage設計
✅ 動画ストリーミング拡張設計
✅ ファイルアクセス制御
次の 第14段階 では、受講機能(LMS部分)を設計します。
内容:
-
受講登録
-
コース受講状態管理
-
レッスン進捗管理
-
動画視聴履歴
-
クイズ回答履歴
-
修了判定
-
学習ダッシュボード
-
メンター向け進捗確認(将来拡張)
へ進みます。