Mô tả tổng quan: Dự án ml-course là một vi dịch vụ chịu trách nhiệm quản lý các nghiệp vụ liên quan đến học tập trực tuyến. Phạm vi bao gồm quản lý danh mục, thông tin khóa học, phân chia theo mùa học (season), các bài giảng (episode), hệ thống bình luận, báo cáo vi phạm và tính năng lưu trữ khóa học.
1. Chuẩn bị môi trường và Cấu hình
Trước khi khởi động dự án, cần đảm bảo các dịch vụ cơ sở dữ liệu và trung gian đang hoạt động:
docker start mysql nacos sentinel redis zipkin minio elasticsearch
1.1. Cấu hình dự án (pom.xml)
Tích hợp các thư viện cần thiết cho kiến trúc Microservices: Web, AOP, Driver MySQL, Swagger (Knife4j), Validation, MyBatis-Flex, Redis, Nacos (Discovery & Config), Sentinel, OpenFeign, Minio, Elasticsearch.
<dependencies>
<!-- Common module -->
<dependency>
<groupId>com.joezhou</groupId>
<artifactId>ml-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ORM: MyBatis-Flex -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
</dependency>
<!-- Service Discovery: Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Configuration Center: Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Object Storage: MinIO -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
<!-- Search Engine: Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- Remote Call: OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
1.2. Cấu hình Nacos (bootstrap.yaml)
spring:
application:
name: ml-course
cloud:
nacos:
config:
server-addr: 192.168.40.77:8848
file-extension: yaml
group: ml-group
shared-configs:
- data-id: common-config.yaml
group: ml-group
refresh: true
profiles:
active: dev
1.3. Cấu hình ứng dụng (ml-course-dev.yaml)
server:
port: 24103
spring:
datasource:
url: jdbc:mysql://192.168.40.77:3306/ml_cms?useUnicode=true&characterEncoding=utf-8
elasticsearch:
uris: http://192.168.40.77:9200
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
1.4. Lớp khởi động chính
package com.joezhou;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@MapperScan("com.joezhou.mapper")
@EnableDiscoveryClient
@SpringBootApplication
public class CourseServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CourseServiceApplication.class, args);
}
}
2. Tích hợp Feign Client cho dịch vụ Người dùng
Tạo giao diện để gọi từ xa tới microservice người dùng nhằm lấy thông tin tài khoản.
@FeignClient(value = "ml-user", fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("/internal/user/{uid}")
Result<User> getUserById(@PathVariable("uid") Long uid);
}
@Component
class UserClientFallback implements UserClient {
@Override
public Result<User> getUserById(Long uid) {
log.error("Không thể kết nối đến dịch vụ Người dùng");
return Result.fail();
}
}
3. Phát triển Module Danh mục (Category)
3.1. Định nghĩa DTO và Service
Sử dụng CategoryAddCmd để nhận dữ liệu tạo mới và CategoryModifyCmd để cập nhật.
public interface CategoryManager {
Long create(CategoryAddCmd cmd);
CategoryVO getDetail(Long id);
PageResult<Category> search(CategoryQuery query);
boolean modify(CategoryModifyCmd cmd);
boolean remove(Long id);
boolean batchRemove(List<Long> ids);
}
Logic nghiệp vụ bao gồm kiểm tra trùng lặp tiêu đề và cập nhật thời gian sửa đổi.
@Service
public class CategoryManagerImpl implements CategoryManager {
@Resource
private CategoryMapper categoryMapper;
@Override
public Long create(CategoryAddCmd cmd) {
if (categoryMapper.selectCountByCondition(CategoryTableDef.CATEGORY.TITLE.eq(cmd.getTitle())) > 0) {
throw new AppException("Tiêu đề danh mục đã tồn tại");
}
Category entity = BeanUtil.copyProperties(cmd, Category.class);
entity.setCreated(LocalDateTime.now());
entity.setUpdated(LocalDateTime.now());
categoryMapper.insert(entity);
return entity.getId();
}
// ... các phương thức khác
}
3.2. Tầng điều khiển (API)
@RestController
@RequestMapping("/api/v1/categories")
@Tag(name = "Quản lý Danh mục")
public class CategoryController {
@Resource
private CategoryManager categoryManager;
@PostMapping
@Operation(summary = "Tạo danh mục mới")
public Long create(@Validated @RequestBody CategoryAddCmd cmd) {
return categoryManager.create(cmd);
}
@GetMapping("/{id}")
@Operation(summary = "Chi tiết danh mục")
public CategoryVO detail(@PathVariable Long id) {
return categoryManager.getDetail(id);
}
// ... API khác
}
4. Phát triển Module Khóa học (Course)
4.1. Quan hệ thực thể dữ liệu
Course liên kết nhiều-đến-một với Category, một-đến-nhiều với Season.
@Table("course")
public class Course {
@Id
private Long id;
private String title;
private String author;
private Long fkCategoryId;
@RelationManyToOne(selfField = "fkCategoryId", targetField = "id")
private Category category;
@RelationOneToMany(selfField = "id", targetField = "fkCourseId")
private List<Season> seasonList;
}
4.2. Tìm kiếm toàn văn với Elasticsearch
Định nghĩa document index:
@Document(indexName = "idx_course")
public class CourseDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String author;
// ...
}
Repository tìm kiếm:
public interface CourseSearchRepo extends ElasticsearchRepository<CourseDocument, Long> {
Page<CourseDocument> findByTitleOrAuthor(String keyword, Pageable pageable);
}
4.3. Xử lý tải ảnh bìa và tóm tắt
Logic tải file lên MinIO và cập nhật đường dẫn vào database.
public String uploadCover(MultipartFile file, Long courseId) {
Course course = courseMapper.selectOneById(courseId);
if (course == null) throw new AppException("Khóa học không tồn tại");
String oldFile = course.getCover();
String newFileName = MinioHelper.generateUniqueName(file);
// Cập nhật DB
course.setCover(newFileName);
courseMapper.update(course);
// Xử lý file
if (!"default.jpg".equals(oldFile)) {
MinioHelper.deleteFile(oldFile, "covers/");
}
MinioHelper.uploadFile(file, newFileName, "covers/");
return newFileName;
}
5. Phát triển Module Mùa học (Season)
5.1. Cấu trúc DTO
@Data
public class SeasonAddCmd {
@NotBlank(message = "Tên mùa học không được rỗng")
private String title;
@NotNull(message = "Thuộc khóa học là bắt buộc")
private Long courseId;
private Integer sortIdx;
}
5.2. Xóa cascade (Xóa Season tự động xóa Episode)
@Transactional
public boolean remove(Long seasonId) {
// Lấy danh sách ID các bài học thuộc mùa này
List<Long> episodeIds = episodeMapper.selectIdsBySeasonId(seasonId);
if (CollUtil.isNotEmpty(episodeIds)) {
episodeMapper.deleteBatchByIds(episodeIds);
}
return seasonMapper.deleteById(seasonId) > 0;
}
6. Phát triển Module Bài giảng (Episode)
6.1. Tải video lên
Tương tự như tải ảnh bìa, nhưng thao tác với file video dung lượng lớn.
public String uploadVideo(MultipartFile videoFile, Long episodeId) {
Episode entity = episodeMapper.selectOneById(episodeId);
String oldPath = entity.getVideoPath();
String newName = MinioHelper.generateUniqueName(videoFile);
entity.setVideoPath(newName);
episodeMapper.update(entity);
if (!StrUtil.isEmpty(oldPath)) MinioHelper.deleteFile(oldPath, "videos/");
MinioHelper.uploadFile(videoFile, newName, "videos/");
return newName;
}
6.2. Xuất báo cáo Excel
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EpisodeExportVO {
@ExcelProperty("Tên Khóa học")
private String courseTitle;
@ExcelProperty("Tên Mùa học")
private String seasonTitle;
@ExcelProperty("Tên Bài học")
private String episodeTitle;
// ...
}
@GetMapping("/export/excel")
public void exportExcel(HttpServletResponse response) {
List<EpisodeExportVO> data = episodeManager.getExportData();
EasyExcel.write(response.getOutputStream(), EpisodeExportVO.class)
.sheet("Danh sách bài giảng")
.doWrite(data);
}
7. Hệ thống Bình luận (Comment)
7.1. Thêm bình luận mới
Sử dụng Feign để lấy thông tin người dùng (nickname, avatar) trước khi lưu.
public void addComment(CommentAddCmd cmd) {
Result<User> userRes = userClient.getUserById(cmd.getUserId());
if (userRes == null || userRes.getData() == null) {
throw new AppException("Người dùng không hợp lệ");
}
Comment entity = BeanUtil.copyProperties(cmd, Comment.class);
entity.setUserNickname(userRes.getData().getNickname());
entity.setCreateTime(LocalDateTime.now());
commentMapper.insert(entity);
}
8. Module Báo cáo (Report) và Theo dõi (Follow)
8.1. Xóa dữ liệu liên quan đến User
Khi người dùng bị hủy, cần xóa các báo cáo và lượt theo dõi liên quan.
// Trong ReportService
public void deleteByUser(Long userId) {
UpdateChain.of(reportMapper)
.where(ReportTableDef.REPORT.USER_ID.eq(userId))
.remove();
}
// Trong FollowService
public void deleteByUser(Long userId) {
UpdateChain.of(followMapper)
.where(FollowTableDef.FOLLOW.USER_ID.eq(userId))
.remove();
}