mirror of
https://github.com/PlayEdu/PlayEdu
synced 2026-10-25 17:36:35 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4b22dc1a | ||
|
|
3866a9c5dc | ||
|
|
c3422327ce | ||
|
|
920ddbaa34 | ||
|
|
e466bdc738 | ||
|
|
902ac9d05d | ||
|
|
b3c709214f | ||
|
|
60df4f2b74 | ||
|
|
57e10f1b55 | ||
|
|
5c453d1cfc | ||
|
|
616f23aa30 | ||
|
|
e08a38399f | ||
|
|
db2bf9a410 |
No files matched your search
+16
-7
@@ -1,15 +1,24 @@
|
||||
## 2.3
|
||||
|
||||
- 修复:LDAP 登录过滤器注入漏洞
|
||||
- 修复:学员头像上传未校验文件类型
|
||||
- 修复:更换账号可绕过登录限流
|
||||
- 新增:[H5]iOS 夸克浏览器提示更换浏览器学习
|
||||
- 优化:[PC/H5]课程详情报错处理
|
||||
- 优化:[H5]播放页 ping 接口传递 hourId
|
||||
|
||||
## 2.2
|
||||
|
||||
- 修复:IP 地理位置解析改用 `ip2region` 本地离线库,移除对外部接口 `whois.pconline.com.cn` 的依赖,消除 "Unexpected end of file from server" 等因外部接口抖动导致的异常日志与出网调用
|
||||
- 修复:IP 地理位置解析改用 `ip2region` 离线库
|
||||
|
||||
## 2.1
|
||||
|
||||
- 优化:Dockerfile 拆分依赖层,前端先 `COPY package.json/pnpm-lock.yaml` 再 `pnpm i`,Java 先 `COPY pom.xml` 并 `dependency:go-offline` 预热依赖,源码变更不再触发依赖重装,显著缩短增量构建耗时
|
||||
- 优化:新增 `playedu-api/.mvn/settings.xml` 与 `.mvn/maven.config`,默认走阿里云 Maven 镜像并启用 `-T 1C` 并行构建,加速 `mvnw` 依赖下载与编译
|
||||
- 修复:后台批量导入学员、章节/课时/附件排序接口缺少权限注解导致的越权问题
|
||||
- 优化:后台配置项改为由 `application.yml` 直接读取环境变量,移除 Dockerfile 中的命令行参数映射
|
||||
- 新增:数据库连接池可通过环境变量配置 (`DB_POOL_MIN_IDLE` / `DB_POOL_MAX_SIZE` / `DB_POOL_CONN_TIMEOUT` / `DB_POOL_IDLE_TIMEOUT` / `DB_POOL_MAX_LIFETIME`)
|
||||
- 新增:`compose.yml` 中 MySQL 密码改为通过 `PLAYEDU_DB_PASS` 环境变量配置,与应用端 `DB_PASS` 共用同一变量
|
||||
- 新增:数据库连接池支持环境变量配置
|
||||
- 新增:`compose.yml` 的 MySQL 密码支持环境变量配置
|
||||
- 修复:后台导入与排序接口越权问题
|
||||
- 优化:Dockerfile 拆分依赖层以复用构建缓存
|
||||
- 优化:Maven 默认阿里云镜像并启用并行构建
|
||||
- 优化:配置项改由 `application.yml` 读取环境变量
|
||||
|
||||
## 2.0
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## 开发规范
|
||||
|
||||
1. 禁止写代码注释。
|
||||
2. changelog 条目单条内容长度不超过 10 个单词。
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "playedu-admin-interface",
|
||||
"private": false,
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Footer: React.FC<PropInterface> = ({ type }) => {
|
||||
className="ml-5"
|
||||
style={{ color: "#D7D7D7", fontSize: 12, marginTop: -5 }}
|
||||
>
|
||||
Version 2.2
|
||||
Version 2.3
|
||||
</span>
|
||||
</Link>
|
||||
</Layout.Footer>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
|
||||
<artifactId>playedu-api</artifactId>
|
||||
<version>2.2</version>
|
||||
<version>2.3</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
||||
+27
-7
@@ -30,20 +30,40 @@ public class LoginLimitCache {
|
||||
@Autowired private PlayEduConfig playEduConfig;
|
||||
|
||||
public void check(String email) throws ServiceException {
|
||||
String limitKey = cacheKey(email);
|
||||
Long reqCount = rateLimiterService.current(limitKey, 600L);
|
||||
if (reqCount >= 10 && !playEduConfig.getTesting()) {
|
||||
Long exp = MemoryCacheUtil.ttlWithoutPrefix(limitKey);
|
||||
String msg = String.format("您的账号已被锁定,请%s后重试", exp > 60 ? exp / 60 + "分钟" : exp + "秒");
|
||||
throw new ServiceException(msg);
|
||||
}
|
||||
checkKey(cacheKey(email), 10L, "您的账号已被锁定");
|
||||
}
|
||||
|
||||
// 按客户端IP计数,避免旋转username绕过账号维度的限流
|
||||
public void checkIp(String ip) throws ServiceException {
|
||||
checkKey(ipCacheKey(ip), 30L, "登录尝试过于频繁");
|
||||
}
|
||||
|
||||
public void destroy(String email) {
|
||||
MemoryCacheUtil.del(cacheKey(email));
|
||||
}
|
||||
|
||||
// 登录成功仅退还本次计数,不清零,防止攻击者用自有账号登录重置整个IP的失败计数
|
||||
public void releaseIp(String ip) {
|
||||
String limitKey = ipCacheKey(ip);
|
||||
if (MemoryCacheUtil.exists(limitKey)) {
|
||||
MemoryCacheUtil.increment(limitKey, -1L, 600L);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkKey(String limitKey, long limit, String reason) throws ServiceException {
|
||||
Long reqCount = rateLimiterService.current(limitKey, 600L);
|
||||
if (reqCount >= limit && !playEduConfig.getTesting()) {
|
||||
Long exp = MemoryCacheUtil.ttlWithoutPrefix(limitKey);
|
||||
String msg = String.format("%s,请%s后重试", reason, exp > 60 ? exp / 60 + "分钟" : exp + "秒");
|
||||
throw new ServiceException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private String cacheKey(String email) {
|
||||
return "login-limit:" + email;
|
||||
}
|
||||
|
||||
private String ipCacheKey(String ip) {
|
||||
return "login-limit-ip:" + ip;
|
||||
}
|
||||
}
|
||||
+8
@@ -69,6 +69,10 @@ public class LoginController {
|
||||
return JsonResponse.error("请使用LDAP登录");
|
||||
}
|
||||
|
||||
// IP维度限流需在查询用户之前执行,否则不存在的邮箱会绕过计数
|
||||
String ip = IpUtil.getClientIp();
|
||||
loginLimitCache.checkIp(ip);
|
||||
|
||||
String email = req.getEmail();
|
||||
|
||||
User user = userService.find(email);
|
||||
@@ -87,6 +91,7 @@ public class LoginController {
|
||||
}
|
||||
|
||||
loginLimitCache.destroy(email);
|
||||
loginLimitCache.releaseIp(ip);
|
||||
|
||||
return JsonResponse.data(loginBus.tokenByUser(user));
|
||||
}
|
||||
@@ -107,6 +112,8 @@ public class LoginController {
|
||||
}
|
||||
|
||||
// 限流控制
|
||||
String ip = IpUtil.getClientIp();
|
||||
loginLimitCache.checkIp(ip);
|
||||
loginLimitCache.check(username);
|
||||
|
||||
// 锁控制-防止并发登录重复写入数据
|
||||
@@ -125,6 +132,7 @@ public class LoginController {
|
||||
|
||||
// 删除限流控制
|
||||
loginLimitCache.destroy(username);
|
||||
loginLimitCache.releaseIp(ip);
|
||||
|
||||
return JsonResponse.data(data);
|
||||
} catch (ServiceException e) {
|
||||
|
||||
+28
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package xyz.playedu.api.controller.frontend;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.SneakyThrows;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import xyz.playedu.api.request.frontend.ChangePasswordRequest;
|
||||
import xyz.playedu.common.constant.BackendConstant;
|
||||
import xyz.playedu.common.constant.CommonConstant;
|
||||
import xyz.playedu.common.constant.FrontendConstant;
|
||||
import xyz.playedu.common.context.FCtx;
|
||||
@@ -38,6 +40,7 @@ import xyz.playedu.common.types.JsonResponse;
|
||||
import xyz.playedu.common.types.UploadFileInfo;
|
||||
import xyz.playedu.common.types.config.S3Config;
|
||||
import xyz.playedu.common.types.mapper.UserCourseHourRecordCourseCountMapper;
|
||||
import xyz.playedu.common.util.HelperUtil;
|
||||
import xyz.playedu.common.util.StringUtil;
|
||||
import xyz.playedu.course.domain.*;
|
||||
import xyz.playedu.course.service.*;
|
||||
@@ -100,8 +103,33 @@ public class UserController {
|
||||
return JsonResponse.data(data);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@PutMapping("/avatar")
|
||||
public JsonResponse changeAvatar(MultipartFile file) {
|
||||
if (file == null || file.isEmpty() || StringUtil.isEmpty(file.getOriginalFilename())) {
|
||||
throw new ServiceException("请上传文件");
|
||||
}
|
||||
|
||||
// 扩展名
|
||||
String ext = HelperUtil.fileExt(file.getOriginalFilename());
|
||||
// 头像仅支持图片格式
|
||||
if (!BackendConstant.RESOURCE_TYPE_IMAGE.equals(
|
||||
BackendConstant.RESOURCE_EXT_2_TYPE.get(ext))) {
|
||||
return JsonResponse.error("当前格式不支持");
|
||||
}
|
||||
// 头像大小限制
|
||||
if (file.getSize() > FrontendConstant.AVATAR_MAX_SIZE) {
|
||||
return JsonResponse.error("头像大小不能超过5MB");
|
||||
}
|
||||
// 校验文件头(魔数)与扩展名一致
|
||||
byte[] header;
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
header = in.readNBytes(8);
|
||||
}
|
||||
if (!HelperUtil.matchImageHeader(header, ext)) {
|
||||
return JsonResponse.error("当前格式不支持");
|
||||
}
|
||||
|
||||
// 校验存储配置是否完整
|
||||
S3Config s3Config = appConfigService.getS3Config();
|
||||
if (StringUtil.isEmpty(s3Config.getAccessKey())
|
||||
|
||||
+2
@@ -40,4 +40,6 @@ public class FrontendConstant {
|
||||
public static final String USER_UPLOAD_IMAGE_SCENE_AVATAR = "avatar";
|
||||
|
||||
public static final String DIR_AVATAR = "user/avatar/";
|
||||
|
||||
public static final int AVATAR_MAX_SIZE = 5 * 1024 * 1024;
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ package xyz.playedu.common.constant;
|
||||
|
||||
public class SystemConstant {
|
||||
|
||||
public static final String VERSION = "v2.2";
|
||||
public static final String VERSION = "v2.3";
|
||||
|
||||
public static final String ENV_PROD = "prod";
|
||||
|
||||
|
||||
@@ -185,4 +185,40 @@ public class HelperUtil {
|
||||
String[] array = filename.split("\\.");
|
||||
return array[array.length - 1].toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验文件头部(魔数)与图片扩展名是否匹配
|
||||
*
|
||||
* @param header 文件头部字节,建议至少8字节
|
||||
* @param extension 文件扩展名,小写,不含点
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean matchImageHeader(byte[] header, String extension) {
|
||||
if (header == null || extension == null) {
|
||||
return false;
|
||||
}
|
||||
switch (extension) {
|
||||
case "png":
|
||||
return startsWith(header, 0, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A);
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return startsWith(header, 0, 0xFF, 0xD8, 0xFF);
|
||||
case "gif":
|
||||
return startsWith(header, 0, 0x47, 0x49, 0x46, 0x38);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean startsWith(byte[] data, int offset, int... prefix) {
|
||||
if (data.length < offset + prefix.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < prefix.length; i++) {
|
||||
if ((data[offset + i] & 0xFF) != prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,47 @@ public class IpUtil {
|
||||
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可信的客户端IP,用于限流等安全场景
|
||||
*
|
||||
* <p>getIpAddress 直接信任客户端可伪造的请求头,不能用于安全判断。
|
||||
*
|
||||
* <p>这里以TCP连接的remoteAddr为起点,仅当其为内网/回环地址(受信代理)时才从X-Forwarded-For最右侧逐级回溯,
|
||||
* 遇到第一个非受信地址即为客户端IP,客户端在X-Forwarded-For左侧伪造的值不会被采用。
|
||||
*
|
||||
* @return IP地址
|
||||
*/
|
||||
public static String getClientIp() {
|
||||
HttpServletRequest request = RequestUtil.handler();
|
||||
if (request == null) {
|
||||
return "unknown";
|
||||
}
|
||||
String ip = request.getRemoteAddr();
|
||||
String xff = request.getHeader("X-Forwarded-For");
|
||||
if (StringUtil.isNotBlank(xff)) {
|
||||
String[] hops = xff.split(",");
|
||||
for (int i = hops.length - 1; i >= 0 && isTrustedProxy(ip); i--) {
|
||||
String hop = hops[i].trim();
|
||||
if (isUnknown(hop)) {
|
||||
break;
|
||||
}
|
||||
ip = hop;
|
||||
}
|
||||
}
|
||||
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
|
||||
}
|
||||
|
||||
private static boolean isTrustedProxy(String ip) {
|
||||
if (ip == null) {
|
||||
return false;
|
||||
}
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip) || "::1".equals(ip)) {
|
||||
return true;
|
||||
}
|
||||
byte[] address = textToNumericFormatV4(ip);
|
||||
return address != null && (address[0] == 0x7F || internalIp(address));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IP获取所在地址
|
||||
*
|
||||
|
||||
+19
-10
@@ -236,12 +236,15 @@ public class LdapUtil {
|
||||
controls.setReturningObjFlag(true);
|
||||
controls.setCountLimit(1);
|
||||
|
||||
String userFilter = "";
|
||||
// 使用JNDI参数化过滤器,由JNDI按RFC 4515转义用户输入,防止过滤器注入
|
||||
String userFilter;
|
||||
Object[] filterArgs;
|
||||
if (StringUtil.isNotEmpty(mail)) {
|
||||
userFilter =
|
||||
String.format("(|(mail=%s)(email=%s)(userPrincipalName=%s))", mail, mail, mail);
|
||||
} else if (StringUtil.isNotEmpty(uid)) {
|
||||
userFilter = String.format("(|(uid=%s)(sAMAccountName=%s))", uid, uid);
|
||||
userFilter = "(|(mail={0})(email={0})(userPrincipalName={0}))";
|
||||
filterArgs = new Object[] {mail};
|
||||
} else {
|
||||
userFilter = "(|(uid={0})(sAMAccountName={0}))";
|
||||
filterArgs = new Object[] {uid};
|
||||
}
|
||||
|
||||
String filter = String.format("(&%s%s)", userFilter, USER_OBJECT_CLASS);
|
||||
@@ -251,7 +254,7 @@ public class LdapUtil {
|
||||
ldapConfig.getUrl(), ldapConfig.getAdminUser(), ldapConfig.getAdminPass());
|
||||
NamingEnumeration<SearchResult> result = null;
|
||||
try {
|
||||
result = ldapContext.search(ldapConfig.getBaseDN(), filter, controls);
|
||||
result = ldapContext.search(ldapConfig.getBaseDN(), filter, filterArgs, controls);
|
||||
} catch (NamingException e) {
|
||||
log.error("LDAP-通过mail或uid登录失败", e);
|
||||
} finally {
|
||||
@@ -326,12 +329,18 @@ public class LdapUtil {
|
||||
|
||||
if (attributes.get("uSNCreated") != null) {
|
||||
// Window AD域
|
||||
ldapUser.setId((String) attributes.get("uSNCreated").get());
|
||||
ldapUser.setUid((String) attributes.get("sAMAccountName").get());
|
||||
ldapUser.setId(getAttribute(attributes, "uSNCreated"));
|
||||
ldapUser.setUid(getAttribute(attributes, "sAMAccountName"));
|
||||
} else {
|
||||
// OpenLDAP
|
||||
ldapUser.setId((String) attributes.get("entryUUID").get());
|
||||
ldapUser.setUid((String) attributes.get("uid").get());
|
||||
ldapUser.setId(getAttribute(attributes, "entryUUID"));
|
||||
ldapUser.setUid(getAttribute(attributes, "uid"));
|
||||
}
|
||||
|
||||
// 关键属性缺失时按无效用户处理,避免异常信息暴露条目属性差异
|
||||
if (StringUtil.isEmpty(ldapUser.getId()) || StringUtil.isEmpty(ldapUser.getUid())) {
|
||||
log.info("LDAP-用户属性缺失|id={}|uid={}", ldapUser.getId(), ldapUser.getUid());
|
||||
return null;
|
||||
}
|
||||
|
||||
// ou计算
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "playedu-h5-interface",
|
||||
"private": false,
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+2155
-2053
File diff suppressed because it is too large.
Load diff
@@ -39,7 +39,7 @@ const CoursePage = () => {
|
||||
const [chapters, setChapters] = useState<ChapterModel[]>([]);
|
||||
const [hours, setHours] = useState<LocalCourseHour | null>(null);
|
||||
const [learnRecord, setLearnRecord] = useState<UserLearnRecordModel | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [learnHourRecord, setLearnHourRecord] =
|
||||
useState<LocalUserLearnHourRecordModel>({});
|
||||
@@ -61,7 +61,9 @@ const CoursePage = () => {
|
||||
document.title = courseItem.title || "课程详情";
|
||||
setCourse(courseItem);
|
||||
setChapters(res.data.chapters);
|
||||
setHours(res.data.hours);
|
||||
if (!isEmptyObject(res.data.hours)) {
|
||||
setHours(res.data.hours);
|
||||
}
|
||||
if (res.data.learn_record) {
|
||||
setLearnRecord(res.data.learn_record);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,35 @@ import styles from "./video.module.scss";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { course as Course } from "../../api/index";
|
||||
import { Toast, Image } from "antd-mobile";
|
||||
import { Toast, Image, Dialog } from "antd-mobile";
|
||||
import backIcon from "../../assets/images/commen/icon-back-n.png";
|
||||
import { Empty } from "../../components";
|
||||
import { HourCompenent } from "./compenents/videoHour";
|
||||
|
||||
declare const window: any;
|
||||
|
||||
/**
|
||||
* 检测是否为 iOS 设备
|
||||
*/
|
||||
const isIOS = (): boolean => {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测是否为夸克浏览器
|
||||
*/
|
||||
const isQuarkBrowser = (): boolean => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
return ua.includes("quark") || ua.includes("qbrowse");
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测是否为 iOS 夸克浏览器
|
||||
*/
|
||||
const isIOSQuarkBrowser = (): boolean => {
|
||||
return isIOS() && isQuarkBrowser();
|
||||
};
|
||||
|
||||
type LocalUserLearnHourRecordModel = {
|
||||
[key: number]: UserLearnHourRecordModel;
|
||||
};
|
||||
@@ -24,7 +46,6 @@ const CoursePlayPage = () => {
|
||||
const systemConfig = useSelector((state: any) => state.systemConfig.value);
|
||||
const user = useSelector((state: any) => state.loginUser.value.user);
|
||||
const [playUrl, setPlayUrl] = useState("");
|
||||
const [playDuration, setPlayDuration] = useState(0);
|
||||
const [playendedStatus, setPlayendedStatus] = useState(false);
|
||||
const [lastSeeValue, setLastSeeValue] = useState({});
|
||||
const [course, setCourse] = useState<CourseModel | null>(null);
|
||||
@@ -42,16 +63,27 @@ const CoursePlayPage = () => {
|
||||
const playRef = useRef(0);
|
||||
const watchRef = useRef(0);
|
||||
const totalRef = useRef(0);
|
||||
const hourIdRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
// 检测是否为 iOS 夸克浏览器
|
||||
if (isIOSQuarkBrowser()) {
|
||||
Dialog.alert({
|
||||
content:
|
||||
"当前夸克浏览器环境下无法记录学习数据,请前往其他浏览器(如 Safari、Chrome)打开学习",
|
||||
confirmText: "知道了",
|
||||
onConfirm: () => {
|
||||
navigate(-1);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
myRef.current = 0;
|
||||
hourIdRef.current = Number(params.hourId);
|
||||
getCourse();
|
||||
getDetail();
|
||||
}, [params.courseId, params.hourId]);
|
||||
|
||||
useEffect(() => {
|
||||
myRef.current = playDuration;
|
||||
}, [playDuration]);
|
||||
|
||||
useEffect(() => {
|
||||
playRef.current = playingTime;
|
||||
}, [playingTime]);
|
||||
@@ -87,7 +119,7 @@ const CoursePlayPage = () => {
|
||||
}
|
||||
//判断是否是最后的课时
|
||||
const index = totalHours.findIndex(
|
||||
(i: any) => i.id === Number(params.hourId)
|
||||
(i: any) => i.id === Number(params.hourId),
|
||||
);
|
||||
if (index === totalHours.length - 1) {
|
||||
setIsLastpage(true);
|
||||
@@ -134,7 +166,7 @@ const CoursePlayPage = () => {
|
||||
window.player && window.player.destroy();
|
||||
setPlayUrl(res.data.resource_url[rid]);
|
||||
initDPlayer(res.data.resource_url[rid], 0, data);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -200,21 +232,19 @@ const CoursePlayPage = () => {
|
||||
|
||||
const playTimeUpdate = (duration: number, isEnd: boolean) => {
|
||||
if (duration - myRef.current >= 10 || isEnd === true) {
|
||||
setPlayDuration(duration);
|
||||
Course.record(
|
||||
Number(params.courseId),
|
||||
Number(params.hourId),
|
||||
duration
|
||||
).then((res: any) => {});
|
||||
Course.playPing(Number(params.courseId), Number(params.hourId)).then(
|
||||
(res: any) => {}
|
||||
myRef.current = duration;
|
||||
Course.record(Number(params.courseId), hourIdRef.current, duration).then(
|
||||
(res: any) => {},
|
||||
);
|
||||
Course.playPing(Number(params.courseId), hourIdRef.current).then(
|
||||
(res: any) => {},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const goNextVideo = () => {
|
||||
const index = totalHours.findIndex(
|
||||
(i: any) => i.id === Number(params.hourId)
|
||||
(i: any) => i.id === Number(params.hourId),
|
||||
);
|
||||
if (index === totalHours.length - 1) {
|
||||
setIsLastpage(true);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "playedu-pc-interface",
|
||||
"private": false,
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -52,10 +52,10 @@ const CoursePage = () => {
|
||||
const [chapters, setChapters] = useState<ChapterModel[]>([]);
|
||||
const [hours, setHours] = useState<HoursModel>({});
|
||||
const [learnRecord, setLearnRecord] = useState<CourseRecordModel | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [learnHourRecord, setLearnHourRecord] = useState<LearnHourRecordsModel>(
|
||||
{}
|
||||
{},
|
||||
);
|
||||
const [tabKey, setTabKey] = useState(Number(result.get("tab") || 1));
|
||||
const [attachments, setAttachments] = useState<AttachModel[]>([]);
|
||||
@@ -73,7 +73,9 @@ const CoursePage = () => {
|
||||
document.title = res.data.course.title;
|
||||
setCourse(res.data.course);
|
||||
setChapters(res.data.chapters);
|
||||
setHours(res.data.hours);
|
||||
if (JSON.stringify(res.data.hours) !== "{}") {
|
||||
setHours(res.data.hours);
|
||||
}
|
||||
setResourceUrl(res.data.resource_url);
|
||||
if (res.data.learn_record) {
|
||||
setLearnRecord(res.data.learn_record);
|
||||
@@ -114,7 +116,7 @@ const CoursePage = () => {
|
||||
id: number,
|
||||
rid: number,
|
||||
fileName: string,
|
||||
type: string
|
||||
type: string,
|
||||
) => {
|
||||
Course.downloadAttachment(cid, id).then((res: any) => {
|
||||
if (type === "TXT") {
|
||||
@@ -172,10 +174,10 @@ const CoursePage = () => {
|
||||
course.thumb === -1
|
||||
? defaultThumb1
|
||||
: course.thumb === -2
|
||||
? defaultThumb2
|
||||
: course.thumb === -3
|
||||
? defaultThumb3
|
||||
: resourceUrl[course.thumb]
|
||||
? defaultThumb2
|
||||
: course.thumb === -3
|
||||
? defaultThumb3
|
||||
: resourceUrl[course.thumb]
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
@@ -374,7 +376,7 @@ const CoursePage = () => {
|
||||
item.id,
|
||||
item.rid,
|
||||
`${item.title}.${item.ext}`,
|
||||
item.type
|
||||
item.type,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user