Compare commits

..

6 Commits

Author SHA1 Message Date
xxx
82c53ed87f 优化权限结构 2023-11-13 11:29:47 +08:00
xxx
acb8b79edd fixed: mysql8的链接 2023-11-13 10:57:52 +08:00
xxx
21c016af62 minio的sdk更换为s3 2023-11-13 10:54:56 +08:00
xxx
f0f316c504 更新依赖 2023-11-13 10:13:45 +08:00
xxx
ee1f009966 线上课增加admin_id字段 2023-11-13 10:08:52 +08:00
xxx
179a7d5f62 readme 2023-11-10 11:59:20 +08:00
26 changed files with 448 additions and 464 deletions

View File

@ -1,42 +1,41 @@
<p align="center">
<img src="https://meedu.cloud.oss.meedu.vip/playedu/%E5%A4%B4%E5%9B%BE.jpg"/>
</p>
<p align="center">
<a href="https://playedu.xyz">官网</a> | <a href="https://playedu.xyz/docs/docs/intro/">文档</a> | <a href="https://playedu.xyz/docs/docs/function">功能列表</a> | <a href="https://playedu.xyz/docs/docs/install/quick">快速上手</a>
</p>
### 系统介绍
## 系统介绍
PlayEdu 是由白书科技团队经营多年线上教育系统打造出的一款全新的企业培训方案,致力于为更多企业机构搭建私有化内部培训平台。PlayEdu 基于 Java + MySQL 开发,采用前后端分离模式,前台采用 React18 为核心框架,后台采用 SpringBoot3 为核心框架。
**PlayEdu** 是由白书科技团队经营多年线上教育系统打造出的一款全新的企业培训解决方案,致力于为更多企业机构搭建私有化内部培训平台。**PlayEdu** 基于 Java + MySQL 开发;采用前后端分离模式;前台采用 React18 为核心框架,后台采用 SpringBoot3 为核心框架。提供部门管理、学员管理、在线视频学习、学员进度追踪、视频私有化存储等培训主要功能。**与此同时,我们在开源版本的基础上还提供了功能更加丰富的企业版本。企业版本在开源功能的基础上提供了包括视频云端存储、视频加密、音频学习、文档(PDF|WORD|PPT)在线学习、在线考试、学习计划培训等功能。企业版本更多信息请点击下方的企业版链接查看。**
### 系统演示
## 常用链接
| - | 站点 | 账号 | 密码 |
| ------------ | ------------------------------------------------------ | ------------------- | -------- |
| 学员端口 | [https://demo.playedu.xyz](https://demo.playedu.xyz) | `1@playedu.xyz` | `123123` |
| 后台管理端口 | [https://admin.playedu.xyz](https://admin.playedu.xyz) | `admin@playedu.xyz` | `123123` |
| 站点 | 链接 |
| ---------- | ---------------------------------------------------------------------------- |
| 官网 | [http://www.playedu.xyz](http://www.playedu.xyz) |
| **企业版** | [https://www.playedu.xyz/commercial](https://www.playedu.xyz/commercial) |
| 部署文档 | [https://www.playedu.xyz/book](https://www.playedu.xyz/book) |
| 系统演示 | [https://www.playedu.xyz/demo](https://www.playedu.xyz/demo) |
| 问答社区 | [https://www.playedu.xyz/qa?scene=new](https://www.playedu.xyz/qa?scene=new) |
### 依赖项目
## 依赖前端项目
- [PC 界面程序](https://github.com/PlayEdu/frontend)
- [后台界面程序](https://github.com/PlayEdu/backend)
- [PC 界面程序](https://github.com/PlayEdu/frontend)
- [H5 界面程序](https://github.com/PlayEdu/h5)
### 官方交流群
<p><img src="https://meedu.cloud.oss.meedu.vip/playedu/PlayEduk%E5%AE%A2%E6%9C%8D-zhu.png" width="200" /></p>
### 界面预览
## 界面预览
![学员端口界面预览](https://meedu.cloud.oss.meedu.vip/playedu/%E5%89%8D%E5%8F%B0%E9%A1%B5%E9%9D%A2.jpg)
![管理后台界面预览](https://meedu.cloud.oss.meedu.vip/playedu/%E5%90%8E%E5%8F%B0%E9%A1%B5%E9%9D%A2.jpg)
### 使用协议
## 使用协议
● 要求
- 保留页脚处版权信息。
- 保留源代码中的协议。
- 如果修改了代码,则必须在文件中进行说明。
● 允许
- 私用、商用、修改。

View File

@ -15,6 +15,8 @@
*/
package xyz.playedu.api.controller;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.RedisConnectionFailureException;
@ -40,7 +42,7 @@ public class ExceptionController {
@ExceptionHandler(Exception.class)
public JsonResponse exceptionHandler(Exception e) {
log.error(e.getMessage());
log.error("{}-{}", e, e.getMessage());
return JsonResponse.error("系统错误", 500);
}
@ -95,4 +97,10 @@ public class ExceptionController {
public JsonResponse serviceExceptionHandler(LimitException e) {
return JsonResponse.error("请稍后再试", 429);
}
@ExceptionHandler(AmazonS3Exception.class)
public JsonResponse serviceExceptionHandler(AmazonS3Exception e) {
log.error("s3错误={}", e.getMessage());
return JsonResponse.error(e.getMessage(), 500);
}
}

View File

@ -15,6 +15,7 @@
*/
package xyz.playedu.api.controller.backend;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
@ -28,9 +29,11 @@ import xyz.playedu.api.event.CourseDestroyEvent;
import xyz.playedu.api.request.backend.CourseRequest;
import xyz.playedu.common.annotation.BackendPermission;
import xyz.playedu.common.annotation.Log;
import xyz.playedu.common.bus.BackendBus;
import xyz.playedu.common.constant.BPermissionConstant;
import xyz.playedu.common.constant.BusinessTypeConstant;
import xyz.playedu.common.context.BCtx;
import xyz.playedu.common.domain.AdminUser;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.service.*;
import xyz.playedu.common.types.JsonResponse;
@ -71,8 +74,12 @@ public class CourseController {
@Autowired private DepartmentService departmentService;
@Autowired private AdminUserService adminUserService;
@Autowired private ApplicationContext ctx;
@Autowired private BackendBus backendBus;
@BackendPermission(slug = BPermissionConstant.COURSE)
@GetMapping("/index")
@Log(title = "线上课-列表", businessType = BusinessTypeConstant.GET)
@ -95,6 +102,10 @@ public class CourseController {
filter.setDepIds(depIds);
filter.setIsRequired(isRequired);
if (!backendBus.isSuperAdmin()) {
filter.setAdminId(BCtx.getId());
}
PaginationResult<Course> result = courseService.paginate(page, size, filter);
HashMap<String, Object> data = new HashMap<>();
@ -107,6 +118,17 @@ public class CourseController {
data.put("categories", categoryService.id2name());
data.put("departments", departmentService.id2name());
// 操作人
data.put("admin_users", new HashMap<>());
if (!result.getData().isEmpty()) {
Map<Integer, String> adminUsers =
adminUserService
.chunks(result.getData().stream().map(Course::getAdminId).toList())
.stream()
.collect(Collectors.toMap(AdminUser::getId, AdminUser::getName));
data.put("admin_users", adminUsers);
}
return JsonResponse.data(data);
}
@ -134,7 +156,8 @@ public class CourseController {
req.getIsRequired(),
req.getIsShow(),
req.getCategoryIds(),
req.getDepIds());
req.getDepIds(),
BCtx.getId());
Date now = new Date();
int classHourCount = 0;
@ -240,6 +263,10 @@ public class CourseController {
@Log(title = "线上课-编辑", businessType = BusinessTypeConstant.GET)
public JsonResponse edit(@PathVariable(name = "id") Integer id) throws NotFoundException {
Course course = courseService.findOrFail(id);
if (!backendBus.isSuperAdmin() && !course.getAdminId().equals(BCtx.getId())) {
return JsonResponse.error("无权限操作");
}
List<Integer> depIds = courseService.getDepIdsByCourseId(course.getId());
List<Integer> categoryIds = courseService.getCategoryIdsByCourseId(course.getId());
List<CourseChapter> chapters = chapterService.getChaptersByCourseId(course.getId());
@ -280,6 +307,10 @@ public class CourseController {
@PathVariable(name = "id") Integer id, @RequestBody @Validated CourseRequest req)
throws NotFoundException {
Course course = courseService.findOrFail(id);
if (!backendBus.isSuperAdmin() && !course.getAdminId().equals(BCtx.getId())) {
return JsonResponse.error("无权限操作");
}
courseService.updateWithCategoryIdsAndDepIds(
course,
req.getTitle(),
@ -290,15 +321,24 @@ public class CourseController {
req.getPublishedAt(),
req.getCategoryIds(),
req.getDepIds());
return JsonResponse.success();
}
@BackendPermission(slug = BPermissionConstant.COURSE)
@DeleteMapping("/{id}")
@Log(title = "线上课-删除", businessType = BusinessTypeConstant.DELETE)
@SneakyThrows
public JsonResponse destroy(@PathVariable(name = "id") Integer id) {
Course course = courseService.findOrFail(id);
if (!backendBus.isSuperAdmin() && !course.getAdminId().equals(BCtx.getId())) {
return JsonResponse.error("无权限操作");
}
courseService.removeById(id);
ctx.publishEvent(new CourseDestroyEvent(this, BCtx.getId(), id));
return JsonResponse.success();
}
}

View File

@ -34,10 +34,11 @@ import xyz.playedu.common.domain.AdminUser;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.AdminUserService;
import xyz.playedu.common.service.MinioService;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.common.types.paginate.ResourcePaginateFilter;
import xyz.playedu.common.util.S3Util;
import xyz.playedu.resource.domain.Resource;
import xyz.playedu.resource.domain.ResourceVideo;
import xyz.playedu.resource.service.ResourceService;
@ -56,7 +57,7 @@ public class ResourceController {
@Autowired private ResourceVideoService resourceVideoService;
@Autowired private MinioService minioService;
@Autowired private AppConfigService appConfigService;
@Autowired private BackendBus backendBus;
@ -134,7 +135,8 @@ public class ResourceController {
}
// 删除文件
minioService.removeByPath(resource.getPath());
S3Util s3Util = new S3Util(appConfigService.getS3Config());
s3Util.removeByPath(resource.getPath());
// 如果是视频资源文件则删除对应的时长关联记录
if (BackendConstant.RESOURCE_TYPE_VIDEO.equals(resource.getType())) {
resourceVideoService.removeByRid(resource.getId());
@ -157,6 +159,8 @@ public class ResourceController {
return JsonResponse.success();
}
S3Util s3Util = new S3Util(appConfigService.getS3Config());
for (Resource resourceItem : resources) {
// 权限校验
if (!backendBus.isSuperAdmin()) {
@ -166,7 +170,7 @@ public class ResourceController {
}
// 删除资源源文件
minioService.removeByPath(resourceItem.getPath());
s3Util.removeByPath(resourceItem.getPath());
// 如果是视频资源的话还需要删除视频的关联资源: 封面截图
if (BackendConstant.RESOURCE_TYPE_VIDEO.equals(resourceItem.getType())) {
resourceVideoService.removeByRid(resourceItem.getId());

View File

@ -31,9 +31,10 @@ import xyz.playedu.common.constant.BackendConstant;
import xyz.playedu.common.constant.BusinessTypeConstant;
import xyz.playedu.common.context.BCtx;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.MinioService;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.util.HelperUtil;
import xyz.playedu.common.util.S3Util;
import xyz.playedu.resource.domain.Resource;
import xyz.playedu.resource.service.ResourceService;
import xyz.playedu.resource.service.UploadService;
@ -44,10 +45,11 @@ import java.util.HashMap;
@Slf4j
@RequestMapping("/backend/v1/upload")
public class UploadController {
@Autowired private MinioService minioService;
@Autowired private UploadService uploadService;
@Autowired private AppConfigService appConfigService;
@Autowired private ResourceService resourceService;
@BackendPermission(slug = BPermissionConstant.UPLOAD)
@ -74,9 +76,11 @@ public class UploadController {
return JsonResponse.error("该格式文件不支持上传");
}
S3Util s3Util = new S3Util(appConfigService.getS3Config());
String filename = HelperUtil.randomString(32) + "." + extension; // 文件名
String path = BackendConstant.RESOURCE_TYPE_2_DIR.get(type) + filename; // 存储路径
String uploadId = minioService.uploadId(path);
String uploadId = s3Util.uploadId(path);
HashMap<String, String> data = new HashMap<>();
data.put("resource_type", type);
@ -94,7 +98,9 @@ public class UploadController {
Integer partNumber = MapUtils.getInteger(params, "part_number");
String filename = MapUtils.getString(params, "filename");
String url = minioService.chunkPreSignUrl(filename, partNumber + "", uploadId);
S3Util s3Util = new S3Util(appConfigService.getS3Config());
String url = s3Util.generatePartUploadPreSignUrl(filename, partNumber + "", uploadId);
HashMap<String, String> data = new HashMap<>();
data.put("url", url);
@ -115,7 +121,8 @@ public class UploadController {
String originalFilename = req.getOriginalFilename().replaceAll("(?i)." + extension, "");
// 合并资源文件
String url = minioService.merge(req.getFilename(), req.getUploadId());
S3Util s3Util = new S3Util(appConfigService.getS3Config());
String url = s3Util.merge(req.getFilename(), req.getUploadId());
// 资源素材保存
Resource videoResource =
@ -162,7 +169,9 @@ public class UploadController {
return JsonResponse.error("uploadId必填");
}
String url = minioService.merge(filename, uploadId);
S3Util s3Util = new S3Util(appConfigService.getS3Config());
String url = s3Util.merge(filename, uploadId);
HashMap<String, Object> data = new HashMap<>();
data.put("url", url);

View File

@ -13,7 +13,7 @@ spring:
max-file-size: 10MB
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: "jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false"
url: "jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
username: ""
password: ""
type: com.zaxxer.hikari.HikariDataSource
@ -63,6 +63,7 @@ sa-token:
is-share: false
jwt-secret-key: "playeduxyz"
token-prefix: "Bearer"
is-log: false
playedu:
core:

View File

@ -1,57 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.config;
import io.minio.CreateMultipartUploadResponse;
import io.minio.ListPartsResponse;
import io.minio.MinioAsyncClient;
import io.minio.messages.Part;
import lombok.SneakyThrows;
import java.util.List;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/6 16:12
*/
public class PlayEduMinioClientConfig extends MinioAsyncClient {
public PlayEduMinioClientConfig(MinioAsyncClient client) {
super(client);
}
@SneakyThrows
public String uploadId(String bucket, String filename) {
CreateMultipartUploadResponse response =
super.createMultipartUpload(bucket, null, filename, null, null);
return response.result().uploadId();
}
@SneakyThrows
public void merge(String bucketName, String objectName, String uploadId) {
ListPartsResponse listPartsResponse =
super.listParts(bucketName, null, objectName, 10000, 0, uploadId, null, null);
List<Part> partList = listPartsResponse.result().partList();
Part[] parts = new Part[10000];
int partNumber = 1;
for (Part part : partList) {
parts[partNumber - 1] = new Part(partNumber, part.etag());
partNumber++;
}
super.completeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
}
}

View File

@ -40,6 +40,7 @@ public class BPermissionConstant {
public static final String USER_LEARN_DESTROY = "user-learn-destroy";
public static final String COURSE = "course";
public static final String COURSE_CUD = "course-cud";
public static final String COURSE_USER = "course-user";
public static final String COURSE_USER_DESTROY = "course-user-destroy";

View File

@ -29,7 +29,7 @@ import java.util.List;
*/
public interface AdminPermissionService extends IService<AdminPermission> {
HashMap<String, Boolean> allSlugs();
HashMap<String, Integer> allSlugs();
List<AdminPermission> listOrderBySortAsc();

View File

@ -19,7 +19,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import xyz.playedu.common.domain.AppConfig;
import xyz.playedu.common.types.LdapConfig;
import xyz.playedu.common.types.config.MinioConfig;
import xyz.playedu.common.types.config.S3Config;
import java.util.HashMap;
import java.util.List;
@ -35,7 +35,7 @@ public interface AppConfigService extends IService<AppConfig> {
Map<String, String> keyValues();
MinioConfig getMinioConfig();
S3Config getS3Config();
boolean enabledLdapLogin();

View File

@ -1,40 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service;
import org.springframework.web.multipart.MultipartFile;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/7 13:29
*/
public interface MinioService {
String url(String path);
String saveFile(MultipartFile file, String savePath, String contentType);
String saveBytes(byte[] file, String savePath, String contentType);
String uploadId(String path);
String chunkPreSignUrl(String filename, String partNumber, String uploadId);
String merge(String filename, String uploadId);
void removeByPath(String path);
}

View File

@ -37,11 +37,11 @@ public class AdminPermissionServiceImpl extends ServiceImpl<AdminPermissionMappe
implements AdminPermissionService {
@Override
public HashMap<String, Boolean> allSlugs() {
public HashMap<String, Integer> allSlugs() {
List<AdminPermission> data = list();
HashMap<String, Boolean> map = new HashMap<>();
HashMap<String, Integer> map = new HashMap<>();
for (AdminPermission permission : data) {
map.put(permission.getSlug(), true);
map.put(permission.getSlug(), permission.getId());
}
return map;
}

View File

@ -17,6 +17,8 @@ package xyz.playedu.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import xyz.playedu.common.constant.ConfigConstant;
@ -25,7 +27,8 @@ import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.mapper.AppConfigMapper;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.types.LdapConfig;
import xyz.playedu.common.types.config.MinioConfig;
import xyz.playedu.common.types.config.S3Config;
import xyz.playedu.common.util.StringUtil;
import java.util.ArrayList;
import java.util.HashMap;
@ -33,12 +36,8 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author tengteng
* @description 针对表app_config的数据库操作Service实现
* @createDate 2023-03-09 11:13:33
*/
@Service
@Log4j2
public class AppConfigServiceImpl extends ServiceImpl<AppConfigMapper, AppConfig>
implements AppConfigService {
@ -95,15 +94,30 @@ public class AppConfigServiceImpl extends ServiceImpl<AppConfigMapper, AppConfig
}
@Override
public MinioConfig getMinioConfig() {
MinioConfig minioConfig = new MinioConfig();
public S3Config getS3Config() {
S3Config s3Config = new S3Config();
Map<String, String> config = keyValues();
minioConfig.setAccessKey(config.get(ConfigConstant.MINIO_ACCESS_KEY));
minioConfig.setSecretKey(config.get(ConfigConstant.MINIO_SECRET_KEY));
minioConfig.setBucket(config.get(ConfigConstant.MINIO_BUCKET));
minioConfig.setEndpoint(config.get(ConfigConstant.MINIO_ENDPOINT));
minioConfig.setDomain(config.get(ConfigConstant.MINIO_DOMAIN));
return minioConfig;
s3Config.setAccessKey(config.get(ConfigConstant.MINIO_ACCESS_KEY));
s3Config.setSecretKey(config.get(ConfigConstant.MINIO_SECRET_KEY));
s3Config.setBucket(config.get(ConfigConstant.MINIO_BUCKET));
s3Config.setEndpoint(config.get(ConfigConstant.MINIO_ENDPOINT));
s3Config.setRegion(null);
s3Config.setService("minio");
String domain = config.get(ConfigConstant.MINIO_DOMAIN);
if (s3Config.getService().equals("minio") && StringUtil.isNotEmpty(domain)) {
// 移除 / 后缀
if (StringUtil.endsWith(domain, "/")) {
domain = domain.substring(0, domain.length() - 1);
}
// 判断是否携带了bucket
if (!StringUtil.endsWith(domain, s3Config.getBucket())) {
domain += "/" + s3Config.getBucket();
}
s3Config.setDomain(domain);
}
return s3Config;
}
@Override

View File

@ -1,156 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.service.impl;
import io.minio.*;
import io.minio.http.Method;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import xyz.playedu.common.config.PlayEduMinioClientConfig;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.service.MinioService;
import xyz.playedu.common.types.config.MinioConfig;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@Service
public class MinioServiceImpl implements MinioService {
@Autowired private AppConfigService appConfigService;
@SneakyThrows
private MinioConfig getMinioConfig() {
MinioConfig c = appConfigService.getMinioConfig();
if (c.getAccessKey().isBlank()
|| c.getSecretKey().isBlank()
|| c.getBucket().isBlank()
|| c.getDomain().isBlank()
|| c.getEndpoint().isBlank()) {
throw new ServiceException("MinIO服务未配置");
}
return c;
}
private String bucket() {
return getMinioConfig().getBucket();
}
public MinioClient getMinioClient() {
MinioConfig c = getMinioConfig();
return MinioClient.builder()
.endpoint(c.getEndpoint())
.credentials(c.getAccessKey(), c.getSecretKey())
.build();
}
public PlayEduMinioClientConfig getPlayEduMinioClient() {
MinioConfig c = getMinioConfig();
MinioAsyncClient client =
PlayEduMinioClientConfig.builder()
.endpoint(c.getEndpoint())
.credentials(c.getAccessKey(), c.getSecretKey())
.build();
return new PlayEduMinioClientConfig(client);
}
@Override
public String url(String path) {
MinioConfig c = getMinioConfig();
return c.getDomain()
+ (c.getDomain().endsWith("/") ? "" : "/")
+ c.getBucket()
+ "/"
+ path;
}
@Override
@SneakyThrows
public String saveFile(MultipartFile file, String savePath, String contentType) {
PutObjectArgs objectArgs =
PutObjectArgs.builder().bucket(bucket()).object(savePath).stream(
file.getInputStream(), file.getSize(), -1)
.contentType(contentType)
.build();
getMinioClient().putObject(objectArgs);
return url(savePath);
}
@Override
public String uploadId(String path) {
return getPlayEduMinioClient().uploadId(bucket(), path);
}
@Override
@SneakyThrows
public String chunkPreSignUrl(String filename, String partNumber, String uploadId) {
Map<String, String> extraQueryParams = new HashMap<>();
extraQueryParams.put("partNumber", partNumber);
extraQueryParams.put("uploadId", uploadId);
return getMinioClient()
.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucket())
.object(filename)
.method(Method.PUT)
.expiry(60 * 60 * 24)
.extraQueryParams(extraQueryParams)
.build());
}
@Override
public String merge(String filename, String uploadId) {
getPlayEduMinioClient().merge(bucket(), filename, uploadId);
return url(filename);
}
@Override
@SneakyThrows
public void removeByPath(String path) {
getMinioClient()
.removeObject(RemoveObjectArgs.builder().bucket(bucket()).object(path).build());
}
@Override
@SneakyThrows
public String saveBytes(byte[] file, String savePath, String contentType) {
InputStream inputStream = new ByteArrayInputStream(file);
PutObjectArgs objectArgs =
PutObjectArgs.builder().bucket(bucket()).object(savePath).stream(
inputStream, file.length, -1)
.contentType(contentType)
.build();
getMinioClient().putObject(objectArgs);
return url(savePath);
}
}

View File

@ -18,10 +18,12 @@ package xyz.playedu.common.types.config;
import lombok.Data;
@Data
public class MinioConfig {
public class S3Config {
private String accessKey;
private String secretKey;
private String bucket;
private String endpoint;
private String domain;
private String region;
private String service;
}

View File

@ -42,4 +42,6 @@ public class CoursePaginateFiler {
private Integer pageStart;
private Integer pageSize;
private Integer adminId;
}

View File

@ -0,0 +1,175 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.common.util;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.types.config.S3Config;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Slf4j
public class S3Util {
private S3Config defaultConfig;
public S3Config getS3Config() {
return defaultConfig;
}
public S3Util(S3Config s3Config) {
defaultConfig = s3Config;
}
public S3Util setConfig(S3Config config) {
defaultConfig = config;
return this;
}
public boolean configIsEmpty() {
return defaultConfig == null
|| StringUtil.isEmpty(defaultConfig.getDomain())
|| StringUtil.isEmpty(defaultConfig.getEndpoint())
|| StringUtil.isEmpty(defaultConfig.getAccessKey())
|| StringUtil.isEmpty(defaultConfig.getSecretKey());
}
@SneakyThrows
private AmazonS3 getClient() {
if (defaultConfig == null) {
throw new ServiceException("存储服务未配置");
}
AWSCredentials credentials =
new BasicAWSCredentials(defaultConfig.getAccessKey(), defaultConfig.getSecretKey());
AwsClientBuilder.EndpointConfiguration endpointConfiguration =
new AwsClientBuilder.EndpointConfiguration(
defaultConfig.getEndpoint(), defaultConfig.getRegion());
return AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withEndpointConfiguration(endpointConfiguration)
.build();
}
@SneakyThrows
public String saveFile(MultipartFile file, String savePath, String contentType) {
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);
objectMetadata.setContentLength(file.getInputStream().available());
getClient()
.putObject(
defaultConfig.getBucket(), savePath, file.getInputStream(), objectMetadata);
return generateEndpointPreSignUrl(savePath);
}
@SneakyThrows
public String saveBytes(byte[] file, String savePath, String contentType) {
InputStream inputStream = new ByteArrayInputStream(file);
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);
objectMetadata.setContentLength(inputStream.available());
getClient().putObject(defaultConfig.getBucket(), savePath, inputStream, objectMetadata);
return generateEndpointPreSignUrl(savePath);
}
public String uploadId(String path) {
InitiateMultipartUploadRequest request =
new InitiateMultipartUploadRequest(defaultConfig.getBucket(), path);
InitiateMultipartUploadResult result = getClient().initiateMultipartUpload(request);
return result.getUploadId();
}
public String generatePartUploadPreSignUrl(
String filename, String partNumber, String uploadId) {
GeneratePresignedUrlRequest request =
new GeneratePresignedUrlRequest(
defaultConfig.getBucket(), filename, HttpMethod.PUT);
request.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)); // 一个小时有效期
request.addRequestParameter("partNumber", partNumber); // 分块索引
request.addRequestParameter("uploadId", uploadId); // uploadId
return getClient().generatePresignedUrl(request).toString();
}
@SneakyThrows
public String merge(String filename, String uploadId) {
AmazonS3 client = getClient();
ListPartsRequest listPartsRequest =
new ListPartsRequest(defaultConfig.getBucket(), filename, uploadId);
PartListing parts = client.listParts(listPartsRequest);
if (parts.getParts().isEmpty()) {
throw new ServiceException("没有已上传的分片文件");
}
List<PartETag> eTags = new ArrayList<>();
parts.getParts()
.forEach(
item -> {
eTags.add(new PartETag(item.getPartNumber(), item.getETag()));
});
CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest();
request.setBucketName(defaultConfig.getBucket());
request.setKey(filename);
request.setUploadId(uploadId);
request.setPartETags(eTags);
client.completeMultipartUpload(request);
return generateEndpointPreSignUrl(filename);
}
public void removeByPath(String path) {
DeleteObjectRequest request = new DeleteObjectRequest(defaultConfig.getBucket(), path);
getClient().deleteObject(request);
}
public boolean exists(String path) {
return getClient().doesObjectExist(defaultConfig.getBucket(), path);
}
@SneakyThrows
public String getContent(String path) {
S3Object s3Object = getClient().getObject(defaultConfig.getBucket(), path);
return new String(s3Object.getObjectContent().readAllBytes(), StandardCharsets.UTF_8);
}
public String generateEndpointPreSignUrl(String path) {
if (defaultConfig.getService().equals("minio")) {
return defaultConfig.getDomain() + "/" + path;
}
return "";
}
}

View File

@ -69,6 +69,9 @@ public class Course implements Serializable {
@JsonProperty("created_at")
private Date createdAt;
@JsonProperty("admin_id")
private Integer adminId;
@JsonIgnore private Date updatedAt;
@JsonIgnore private Date deletedAt;

View File

@ -42,7 +42,8 @@ public interface CourseService extends IService<Course> {
Integer isRequired,
Integer isShow,
Integer[] categoryIds,
Integer[] depIds);
Integer[] depIds,
Integer adminId);
void updateWithCategoryIdsAndDepIds(
Course course,

View File

@ -68,7 +68,8 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> impleme
Integer isRequired,
Integer isShow,
Integer[] categoryIds,
Integer[] depIds) {
Integer[] depIds,
Integer adminId) {
// 创建课程
Course course = new Course();
course.setTitle(title);
@ -79,6 +80,7 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> impleme
course.setPublishedAt(new Date());
course.setCreatedAt(new Date());
course.setUpdatedAt(new Date());
course.setAdminId(adminId);
save(course);
// 关联分类
relateCategories(course, categoryIds);

View File

@ -75,6 +75,9 @@
<if test="title != null and title != ''">
AND `courses`.`title` LIKE concat('%',#{title},'%')
</if>
<if test="adminId != null">
AND `courses`.`admin_id` = #{adminId}
</if>
<if test="isRequired != null">
AND `courses`.`is_required` = #{isRequired}
</if>
@ -164,6 +167,9 @@
<if test="title != null and title != ''">
AND `courses`.`title` LIKE concat('%',#{title},'%')
</if>
<if test="adminId != null">
AND `courses`.`admin_id` = #{adminId}
</if>
<if test="isRequired != null">
AND `courses`.`is_required` = #{isRequired}
</if>

View File

@ -26,29 +26,25 @@ import xyz.playedu.common.constant.BackendConstant;
import xyz.playedu.common.constant.FrontendConstant;
import xyz.playedu.common.domain.UserUploadImageLog;
import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.service.MinioService;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.service.UserUploadImageLogService;
import xyz.playedu.common.types.UploadFileInfo;
import xyz.playedu.common.util.Base64Util;
import xyz.playedu.common.util.HelperUtil;
import xyz.playedu.common.util.S3Util;
import xyz.playedu.resource.domain.Resource;
import xyz.playedu.resource.service.ResourceService;
import xyz.playedu.resource.service.UploadService;
import java.util.Date;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/8 14:02
*/
@Service
@Slf4j
public class UploadServiceImpl implements UploadService {
@Autowired private ResourceService resourceService;
@Autowired private MinioService minioService;
@Autowired private AppConfigService appConfigService;
@Autowired private UserUploadImageLogService userUploadImageLogService;
@ -79,13 +75,14 @@ public class UploadServiceImpl implements UploadService {
// 自定义新的存储文件名
fileInfo.setSaveName(HelperUtil.randomString(32) + "." + fileInfo.getExtension());
// 生成保存的相对路径
if (dir == null || dir.length() == 0) {
if (dir == null || dir.isEmpty()) {
dir = BackendConstant.RESOURCE_TYPE_2_DIR.get(fileInfo.getResourceType());
}
fileInfo.setSavePath(dir + fileInfo.getSaveName());
// 保存文件并生成访问url
S3Util s3Util = new S3Util(appConfigService.getS3Config());
String url =
minioService.saveFile(
s3Util.saveFile(
file,
fileInfo.getSavePath(),
BackendConstant.RESOURCE_EXT_2_CONTENT_TYPE.get(fileInfo.getExtension()));
@ -134,8 +131,9 @@ public class UploadServiceImpl implements UploadService {
String savePath = BackendConstant.RESOURCE_TYPE_2_DIR.get(type) + filename;
// 保存文件
S3Util s3Util = new S3Util(appConfigService.getS3Config());
String url =
minioService.saveBytes(
s3Util.saveBytes(
binary, savePath, BackendConstant.RESOURCE_EXT_2_CONTENT_TYPE.get(ext));
// 上传记录
return resourceService.create(

View File

@ -37,76 +37,19 @@ public class AdminPermissionCheck implements CommandLineRunner {
BPermissionConstant.TYPE_ACTION,
new HashMap<>() {
{
// 管理员
put(
"管理员",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("列表");
setSlug(
BPermissionConstant
.ADMIN_USER_INDEX);
}
},
new AdminPermission() {
{
setSort(10);
setName("新增|编辑|删除");
setSlug(BPermissionConstant.ADMIN_USER_CUD);
}
},
});
// 管理员角色
put(
"管理员角色",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("新增|编辑|删除");
setSlug(BPermissionConstant.ADMIN_ROLE);
}
},
});
// 管理员日志
put(
"管理员日志",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("列表");
setSlug(BPermissionConstant.ADMIN_LOG);
}
},
});
// 部门
put(
"部门",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("新增|编辑|删除");
setSlug(BPermissionConstant.DEPARTMENT_CUD);
}
},
new AdminPermission() {
{
setSort(10);
setName("学员学习");
setSlug(
BPermissionConstant
.DEPARTMENT_USER_LEARN);
}
},
});
// 资源分类
// 分类管理
put(
"分类管理",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("列表");
setSlug(
BPermissionConstant
.RESOURCE_CATEGORY_MENU);
}
},
new AdminPermission() {
{
setSort(0);
@ -116,24 +59,15 @@ public class AdminPermissionCheck implements CommandLineRunner {
.RESOURCE_CATEGORY);
}
},
new AdminPermission() {
{
setSort(10);
setName("左侧菜单");
setSlug(
BPermissionConstant
.RESOURCE_CATEGORY_MENU);
}
},
});
// 资源分类
// 资源管理
put(
"资源管理",
new AdminPermission[] {
new AdminPermission() {
{
setSort(10);
setName("左侧菜单");
setSort(30);
setName("列表");
setSlug(BPermissionConstant.RESOURCE_MENU);
}
},
@ -173,20 +107,41 @@ public class AdminPermissionCheck implements CommandLineRunner {
new AdminPermission() {
{
setSort(40);
setName("学习");
setName("学习进度-查看");
setSlug(BPermissionConstant.USER_LEARN);
}
},
new AdminPermission() {
{
setSort(50);
setName("学习-删除");
setName("学习进度-记录删除");
setSlug(
BPermissionConstant
.USER_LEARN_DESTROY);
}
},
});
// 部门
put(
"部门",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("新增|编辑|删除");
setSlug(BPermissionConstant.DEPARTMENT_CUD);
}
},
new AdminPermission() {
{
setSort(10);
setName("查看部门学员学习进度");
setSlug(
BPermissionConstant
.DEPARTMENT_USER_LEARN);
}
},
});
// 线上课
put(
"线上课",
@ -194,10 +149,17 @@ public class AdminPermissionCheck implements CommandLineRunner {
new AdminPermission() {
{
setSort(0);
setName("新增|编辑|删除");
setName("列表");
setSlug(BPermissionConstant.COURSE);
}
},
new AdminPermission() {
{
setSort(5);
setName("新增|编辑|删除");
setSlug(BPermissionConstant.COURSE_CUD);
}
},
new AdminPermission() {
{
setSort(10);
@ -215,23 +177,9 @@ public class AdminPermissionCheck implements CommandLineRunner {
}
},
});
// 其它
put(
"其它",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setName("修改登录密码");
setSlug(
BPermissionConstant
.PASSWORD_CHANGE);
}
},
});
// 系统配置
put(
"系统配置",
"系统",
new AdminPermission[] {
new AdminPermission() {
{
@ -240,14 +188,48 @@ public class AdminPermissionCheck implements CommandLineRunner {
setSlug(BPermissionConstant.SYSTEM_CONFIG);
}
},
});
// 其它
put(
"其它权限",
new AdminPermission[] {
new AdminPermission() {
{
setSort(0);
setSort(10);
setName("管理员日志");
setSlug(BPermissionConstant.ADMIN_LOG);
}
},
new AdminPermission() {
{
setSort(15);
setName("管理员角色");
setSlug(BPermissionConstant.ADMIN_ROLE);
}
},
new AdminPermission() {
{
setSort(20);
setName("管理员-列表");
setSlug(
BPermissionConstant
.ADMIN_USER_INDEX);
}
},
new AdminPermission() {
{
setSort(25);
setName("管理员-新增|编辑|删除");
setSlug(BPermissionConstant.ADMIN_USER_CUD);
}
},
new AdminPermission() {
{
setSort(30);
setName("修改登录密码");
setSlug(
BPermissionConstant
.PASSWORD_CHANGE);
}
},
new AdminPermission() {
{
setSort(35);
setName("文件上传");
setSlug(BPermissionConstant.UPLOAD);
}
@ -312,7 +294,7 @@ public class AdminPermissionCheck implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
HashMap<String, Boolean> slugs = permissionService.allSlugs();
HashMap<String, Integer> slugs = permissionService.allSlugs();
List<AdminPermission> list = new ArrayList<>();
Date now = new Date();
@ -320,25 +302,28 @@ public class AdminPermissionCheck implements CommandLineRunner {
(typeValue, group) -> {
group.forEach(
(groupNameValue, item) -> {
for (int i = 0; i < item.length; i++) {
AdminPermission permissionItem = item[i];
if (slugs.get(permissionItem.getSlug()) != null) {
continue;
}
// 不存在
list.add(
for (AdminPermission permissionItem : item) {
AdminPermission newPermissionItem =
new AdminPermission() {
{
setType(typeValue);
setGroupName(groupNameValue);
setSort(permissionItem.getSort());
setName(permissionItem.getName());
setSlug(permissionItem.getSlug());
setCreatedAt(now);
}
});
};
Integer existsId = slugs.get(permissionItem.getSlug());
if (existsId != null && existsId > 0) {
newPermissionItem.setId(existsId);
permissionService.updateById(newPermissionItem);
continue;
}
// 不存在
newPermissionItem.setCreatedAt(now);
newPermissionItem.setSlug(permissionItem.getSlug());
list.add(newPermissionItem);
}
});
});

View File

@ -701,6 +701,18 @@ public class MigrationCheck implements CommandLineRunner {
""");
}
});
add(
new HashMap<>() {
{
put("table", "");
put("name", "20231113_10_00_add_admin_id_for_courses");
put(
"sql",
"""
ALTER TABLE `courses` add `admin_id` int(10) NOT NULL DEFAULT 0 COMMENT '管理员id';
""");
}
});
}
};

37
pom.xml
View File

@ -5,9 +5,10 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.1</version>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>xyz.playedu</groupId>
<artifactId>playedu</artifactId>
<version>1.2</version>
@ -96,29 +97,10 @@
<version>4.4</version>
</dependency>
<!--JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-gson</artifactId>
<version>0.11.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.2</version>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.572</version>
</dependency>
<dependency>
@ -155,12 +137,6 @@
<artifactId>sa-token-jwt</artifactId>
<version>1.34.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.4</version>
</dependency>
</dependencies>
<build>
@ -186,9 +162,8 @@
<style>AOSP</style>
<reflowLongStrings>true</reflowLongStrings>
</googleJavaFormat>
<licenseHeader>
<file>license-header.txt</file>
<file>header.txt</file>
</licenseHeader>
</java>
</configuration>