feat: add teacher student demo sessions
parent
52ba31731c
commit
0ef5edd1f3
@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS student_demo_session (
|
||||
id varchar(64) NOT NULL,
|
||||
ticket_hash char(64) NOT NULL,
|
||||
teacher_user_id varchar(64) NOT NULL,
|
||||
teacher_name varchar(128) NULL,
|
||||
school_id varchar(64) NOT NULL,
|
||||
teaching_class_id varchar(64) NOT NULL,
|
||||
teaching_class_name varchar(255) NULL,
|
||||
expires_at datetime NOT NULL,
|
||||
used_at datetime NULL,
|
||||
create_time datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_student_demo_session_ticket_hash (ticket_hash),
|
||||
KEY idx_student_demo_session_expires_at (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -0,0 +1,49 @@
|
||||
package com.sztzjy.linkCommerce.controller.stu;
|
||||
|
||||
import com.sztzjy.linkCommerce.annotation.AnonymousAccess;
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.config.security.TokenProvider;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionCreateRequest;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionExchangeRequest;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionLogin;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionTicket;
|
||||
import com.sztzjy.linkCommerce.service.StudentDemoSessionService;
|
||||
import com.sztzjy.linkCommerce.util.ResultEntity;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@Api(tags = "教师学生端讲解会话")
|
||||
@RestController
|
||||
public class StudentDemoSessionController {
|
||||
@Autowired
|
||||
public StudentDemoSessionService studentDemoSessionService;
|
||||
|
||||
@PostMapping("/api/teacher/student-demo-sessions")
|
||||
@ApiOperation("教师创建学生端讲解票据")
|
||||
public ResultEntity<StudentDemoSessionTicket> create(@RequestBody StudentDemoSessionCreateRequest body,
|
||||
HttpServletRequest request) {
|
||||
StudentDemoSessionTicket ticket = studentDemoSessionService.create(currentUser(request),
|
||||
body == null ? null : body.getTeachingClassId());
|
||||
return new ResultEntity<>(HttpStatus.OK, "Demo ticket created", ticket);
|
||||
}
|
||||
|
||||
@PostMapping("/api/student-demo-sessions/exchange")
|
||||
@AnonymousAccess
|
||||
@ApiOperation("学生端讲解票据兑换")
|
||||
public ResultEntity<StudentDemoSessionLogin> exchange(@RequestBody StudentDemoSessionExchangeRequest body) {
|
||||
StudentDemoSessionLogin login = studentDemoSessionService.exchange(body == null ? null : body.getTicket());
|
||||
return new ResultEntity<>(HttpStatus.OK, "Demo session created", login);
|
||||
}
|
||||
|
||||
protected JwtUser currentUser(HttpServletRequest request) {
|
||||
return TokenProvider.getJWTUser(request);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.sztzjy.linkCommerce.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class StudentDemoSession {
|
||||
private String id;
|
||||
private String ticketHash;
|
||||
private String teacherUserId;
|
||||
private String teacherName;
|
||||
private String schoolId;
|
||||
private String teachingClassId;
|
||||
private String teachingClassName;
|
||||
private Date expiresAt;
|
||||
private Date usedAt;
|
||||
private Date createTime;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
public String getTicketHash() { return ticketHash; }
|
||||
public void setTicketHash(String ticketHash) { this.ticketHash = ticketHash; }
|
||||
public String getTeacherUserId() { return teacherUserId; }
|
||||
public void setTeacherUserId(String teacherUserId) { this.teacherUserId = teacherUserId; }
|
||||
public String getTeacherName() { return teacherName; }
|
||||
public void setTeacherName(String teacherName) { this.teacherName = teacherName; }
|
||||
public String getSchoolId() { return schoolId; }
|
||||
public void setSchoolId(String schoolId) { this.schoolId = schoolId; }
|
||||
public String getTeachingClassId() { return teachingClassId; }
|
||||
public void setTeachingClassId(String teachingClassId) { this.teachingClassId = teachingClassId; }
|
||||
public String getTeachingClassName() { return teachingClassName; }
|
||||
public void setTeachingClassName(String teachingClassName) { this.teachingClassName = teachingClassName; }
|
||||
public Date getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
|
||||
public Date getUsedAt() { return usedAt; }
|
||||
public void setUsedAt(Date usedAt) { this.usedAt = usedAt; }
|
||||
public Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.sztzjy.linkCommerce.entity.dto;
|
||||
|
||||
public class StudentDemoSessionCreateRequest {
|
||||
private String teachingClassId;
|
||||
|
||||
public String getTeachingClassId() { return teachingClassId; }
|
||||
public void setTeachingClassId(String teachingClassId) { this.teachingClassId = teachingClassId; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.sztzjy.linkCommerce.entity.dto;
|
||||
|
||||
public class StudentDemoSessionExchangeRequest {
|
||||
private String ticket;
|
||||
|
||||
public String getTicket() { return ticket; }
|
||||
public void setTicket(String ticket) { this.ticket = ticket; }
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.sztzjy.linkCommerce.entity.dto;
|
||||
|
||||
public class StudentDemoSessionLogin {
|
||||
private String token;
|
||||
private String userId;
|
||||
private String name;
|
||||
private String username;
|
||||
private String roleId;
|
||||
private String schoolId;
|
||||
private String classId;
|
||||
private String className;
|
||||
private boolean demoMode;
|
||||
private String demoTeachingClassId;
|
||||
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getRoleId() { return roleId; }
|
||||
public void setRoleId(String roleId) { this.roleId = roleId; }
|
||||
public String getSchoolId() { return schoolId; }
|
||||
public void setSchoolId(String schoolId) { this.schoolId = schoolId; }
|
||||
public String getClassId() { return classId; }
|
||||
public void setClassId(String classId) { this.classId = classId; }
|
||||
public String getClassName() { return className; }
|
||||
public void setClassName(String className) { this.className = className; }
|
||||
public boolean isDemoMode() { return demoMode; }
|
||||
public void setDemoMode(boolean demoMode) { this.demoMode = demoMode; }
|
||||
public String getDemoTeachingClassId() { return demoTeachingClassId; }
|
||||
public void setDemoTeachingClassId(String demoTeachingClassId) { this.demoTeachingClassId = demoTeachingClassId; }
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.sztzjy.linkCommerce.entity.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class StudentDemoSessionTicket {
|
||||
private String ticket;
|
||||
private Date expiresAt;
|
||||
private String teachingClassId;
|
||||
private String className;
|
||||
|
||||
public String getTicket() { return ticket; }
|
||||
public void setTicket(String ticket) { this.ticket = ticket; }
|
||||
public Date getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
|
||||
public String getTeachingClassId() { return teachingClassId; }
|
||||
public void setTeachingClassId(String teachingClassId) { this.teachingClassId = teachingClassId; }
|
||||
public String getClassName() { return className; }
|
||||
public void setClassName(String className) { this.className = className; }
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.sztzjy.linkCommerce.mapper;
|
||||
|
||||
import com.sztzjy.linkCommerce.entity.StudentDemoSession;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Mapper
|
||||
public interface StudentDemoSessionMapper {
|
||||
int insertSelective(StudentDemoSession record);
|
||||
|
||||
StudentDemoSession selectByTicketHash(@Param("ticketHash") String ticketHash);
|
||||
|
||||
int markUsedIfActive(@Param("id") String id, @Param("usedAt") Date usedAt);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.sztzjy.linkCommerce.service;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionLogin;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionTicket;
|
||||
|
||||
public interface StudentDemoSessionService {
|
||||
StudentDemoSessionTicket create(JwtUser teacher, String teachingClassId);
|
||||
|
||||
StudentDemoSessionLogin exchange(String ticket);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.sztzjy.linkCommerce.service;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
|
||||
public interface StudentTeachingClassResolver {
|
||||
String resolveRequired(JwtUser user);
|
||||
|
||||
String resolveRequested(JwtUser user, String requestedTeachingClassId);
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
package com.sztzjy.linkCommerce.service.impl;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.config.security.TokenProvider;
|
||||
import com.sztzjy.linkCommerce.entity.SchoolClass;
|
||||
import com.sztzjy.linkCommerce.entity.StudentDemoSession;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionLogin;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionTicket;
|
||||
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
|
||||
import com.sztzjy.linkCommerce.mapper.StudentDemoSessionMapper;
|
||||
import com.sztzjy.linkCommerce.service.StudentDemoSessionService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class StudentDemoSessionServiceImpl implements StudentDemoSessionService {
|
||||
private static final long TICKET_EXPIRATION_MILLIS = 5 * 60 * 1000L;
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
@Autowired
|
||||
public SchoolClassMapper schoolClassMapper;
|
||||
@Autowired
|
||||
public StudentDemoSessionMapper studentDemoSessionMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public StudentDemoSessionTicket create(JwtUser teacher, String teachingClassId) {
|
||||
requireTeacher(teacher);
|
||||
SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(StringUtils.trimToEmpty(teachingClassId));
|
||||
if (schoolClass == null || !"TEACHING".equals(schoolClass.getClassType())
|
||||
|| !StringUtils.equals(teacher.getUserId(), schoolClass.getCreatedBy())
|
||||
|| !StringUtils.equals(teacher.getSchoolId(), schoolClass.getSchoolId())) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Only the creator can demonstrate this teaching class");
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
Date expiresAt = new Date(now.getTime() + TICKET_EXPIRATION_MILLIS);
|
||||
String ticket = nextTicket();
|
||||
StudentDemoSession session = new StudentDemoSession();
|
||||
session.setId(UUID.randomUUID().toString());
|
||||
session.setTicketHash(hash(ticket));
|
||||
session.setTeacherUserId(teacher.getUserId());
|
||||
session.setTeacherName(teacher.getName());
|
||||
session.setSchoolId(teacher.getSchoolId());
|
||||
session.setTeachingClassId(schoolClass.getSchoolClassId());
|
||||
session.setTeachingClassName(schoolClass.getClassName());
|
||||
session.setExpiresAt(expiresAt);
|
||||
session.setCreateTime(now);
|
||||
studentDemoSessionMapper.insertSelective(session);
|
||||
|
||||
StudentDemoSessionTicket result = new StudentDemoSessionTicket();
|
||||
result.setTicket(ticket);
|
||||
result.setExpiresAt(expiresAt);
|
||||
result.setTeachingClassId(session.getTeachingClassId());
|
||||
result.setClassName(session.getTeachingClassName());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public StudentDemoSessionLogin exchange(String ticket) {
|
||||
String normalizedTicket = StringUtils.trimToEmpty(ticket);
|
||||
if (StringUtils.isBlank(normalizedTicket)) {
|
||||
throw new ServiceException(HttpStatus.BAD_REQUEST, "Demo ticket is required");
|
||||
}
|
||||
StudentDemoSession session = studentDemoSessionMapper.selectByTicketHash(hash(normalizedTicket));
|
||||
Date now = new Date();
|
||||
if (session == null || session.getUsedAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().after(now)
|
||||
|| studentDemoSessionMapper.markUsedIfActive(session.getId(), now) != 1) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Demo ticket is invalid or expired");
|
||||
}
|
||||
JwtUser demoUser = new JwtUser();
|
||||
demoUser.setUserId("demo:" + session.getId());
|
||||
demoUser.setName(StringUtils.defaultIfBlank(session.getTeacherName(), "Teacher") + "(演示)");
|
||||
demoUser.setUsername("demo:" + session.getTeacherUserId());
|
||||
demoUser.setRoleId(4);
|
||||
demoUser.setSchoolId(session.getSchoolId());
|
||||
demoUser.setDemoMode(true);
|
||||
demoUser.setDemoTeachingClassId(session.getTeachingClassId());
|
||||
demoUser.setSourceTeacherId(session.getTeacherUserId());
|
||||
|
||||
StudentDemoSessionLogin result = new StudentDemoSessionLogin();
|
||||
result.setToken(TokenProvider.createDemoToken(demoUser));
|
||||
result.setUserId(demoUser.getUserId());
|
||||
result.setName(demoUser.getName());
|
||||
result.setUsername(demoUser.getUsername());
|
||||
result.setRoleId("4");
|
||||
result.setSchoolId(session.getSchoolId());
|
||||
result.setClassId(session.getTeachingClassId());
|
||||
result.setClassName(session.getTeachingClassName());
|
||||
result.setDemoMode(true);
|
||||
result.setDemoTeachingClassId(session.getTeachingClassId());
|
||||
return result;
|
||||
}
|
||||
|
||||
private void requireTeacher(JwtUser teacher) {
|
||||
if (teacher == null || teacher.getRoleId() != 3 || StringUtils.isBlank(teacher.getUserId()) || StringUtils.isBlank(teacher.getSchoolId())) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Teacher only");
|
||||
}
|
||||
}
|
||||
|
||||
private String nextTicket() {
|
||||
byte[] bytes = new byte[32];
|
||||
RANDOM.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
|
||||
private String hash(String value) {
|
||||
try {
|
||||
byte[] bytes = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
||||
for (byte valueByte : bytes) {
|
||||
builder.append(String.format("%02x", valueByte));
|
||||
}
|
||||
return builder.toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to hash demo ticket", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.sztzjy.linkCommerce.service.impl;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.entity.SchoolClass;
|
||||
import com.sztzjy.linkCommerce.entity.TeachingClassStudent;
|
||||
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
|
||||
import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper;
|
||||
import com.sztzjy.linkCommerce.service.StudentTeachingClassResolver;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StudentTeachingClassResolverImpl implements StudentTeachingClassResolver {
|
||||
@Autowired
|
||||
public SchoolClassMapper schoolClassMapper;
|
||||
@Autowired
|
||||
public TeachingClassStudentMapper teachingClassStudentMapper;
|
||||
|
||||
@Override
|
||||
public String resolveRequired(JwtUser user) {
|
||||
requireStudent(user);
|
||||
String teachingClassId;
|
||||
if (user.isDemoMode()) {
|
||||
teachingClassId = StringUtils.trimToNull(user.getDemoTeachingClassId());
|
||||
if (teachingClassId == null) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Demo teaching class is required");
|
||||
}
|
||||
} else {
|
||||
TeachingClassStudent membership = teachingClassStudentMapper.selectActiveByStudentUserId(user.getUserId());
|
||||
teachingClassId = membership == null ? null : StringUtils.trimToNull(membership.getTeachingClassId());
|
||||
if (teachingClassId == null) {
|
||||
throw new ServiceException(HttpStatus.BAD_REQUEST, "Student has no active teaching class");
|
||||
}
|
||||
}
|
||||
validateTeachingClass(user, teachingClassId);
|
||||
return teachingClassId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveRequested(JwtUser user, String requestedTeachingClassId) {
|
||||
String resolved = resolveRequired(user);
|
||||
String requested = StringUtils.trimToNull(requestedTeachingClassId);
|
||||
if (requested != null && !StringUtils.equals(resolved, requested)) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Teaching class does not match current student context");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void validateTeachingClass(JwtUser user, String teachingClassId) {
|
||||
SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(teachingClassId);
|
||||
if (schoolClass == null || !"TEACHING".equals(schoolClass.getClassType())
|
||||
|| !StringUtils.equals(user.getSchoolId(), schoolClass.getSchoolId())) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Teaching class is unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireStudent(JwtUser user) {
|
||||
if (user == null || user.getRoleId() != 4 || StringUtils.isBlank(user.getUserId())) {
|
||||
throw new ServiceException(HttpStatus.FORBIDDEN, "Student only");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.sztzjy.linkCommerce.mapper.StudentDemoSessionMapper">
|
||||
<resultMap id="BaseResultMap" type="com.sztzjy.linkCommerce.entity.StudentDemoSession">
|
||||
<id column="id" jdbcType="VARCHAR" property="id" />
|
||||
<result column="ticket_hash" jdbcType="VARCHAR" property="ticketHash" />
|
||||
<result column="teacher_user_id" jdbcType="VARCHAR" property="teacherUserId" />
|
||||
<result column="teacher_name" jdbcType="VARCHAR" property="teacherName" />
|
||||
<result column="school_id" jdbcType="VARCHAR" property="schoolId" />
|
||||
<result column="teaching_class_id" jdbcType="VARCHAR" property="teachingClassId" />
|
||||
<result column="teaching_class_name" jdbcType="VARCHAR" property="teachingClassName" />
|
||||
<result column="expires_at" jdbcType="TIMESTAMP" property="expiresAt" />
|
||||
<result column="used_at" jdbcType="TIMESTAMP" property="usedAt" />
|
||||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByTicketHash" resultMap="BaseResultMap">
|
||||
select id, ticket_hash, teacher_user_id, teacher_name, school_id, teaching_class_id,
|
||||
teaching_class_name, expires_at, used_at, create_time
|
||||
from student_demo_session where ticket_hash = #{ticketHash,jdbcType=VARCHAR} limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertSelective" parameterType="com.sztzjy.linkCommerce.entity.StudentDemoSession">
|
||||
insert into student_demo_session
|
||||
(id, ticket_hash, teacher_user_id, teacher_name, school_id, teaching_class_id, teaching_class_name, expires_at, used_at, create_time)
|
||||
values (#{id,jdbcType=VARCHAR}, #{ticketHash,jdbcType=VARCHAR}, #{teacherUserId,jdbcType=VARCHAR},
|
||||
#{teacherName,jdbcType=VARCHAR}, #{schoolId,jdbcType=VARCHAR}, #{teachingClassId,jdbcType=VARCHAR},
|
||||
#{teachingClassName,jdbcType=VARCHAR}, #{expiresAt,jdbcType=TIMESTAMP}, #{usedAt,jdbcType=TIMESTAMP},
|
||||
#{createTime,jdbcType=TIMESTAMP})
|
||||
</insert>
|
||||
|
||||
<update id="markUsedIfActive">
|
||||
update student_demo_session set used_at = #{usedAt,jdbcType=TIMESTAMP}
|
||||
where id = #{id,jdbcType=VARCHAR} and used_at is null and expires_at > #{usedAt,jdbcType=TIMESTAMP}
|
||||
</update>
|
||||
</mapper>
|
||||
@ -0,0 +1,79 @@
|
||||
package com.sztzjy.linkCommerce.controller.stu;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionCreateRequest;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionExchangeRequest;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionLogin;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionTicket;
|
||||
import com.sztzjy.linkCommerce.service.StudentDemoSessionService;
|
||||
import com.sztzjy.linkCommerce.util.ResultEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class StudentDemoSessionControllerTest {
|
||||
|
||||
@Test
|
||||
void teacherCanCreateDemoTicketForSelectedTeachingClass() {
|
||||
TestController controller = new TestController(teacher());
|
||||
controller.studentDemoSessionService = mock(StudentDemoSessionService.class);
|
||||
StudentDemoSessionTicket ticket = new StudentDemoSessionTicket();
|
||||
ticket.setTicket("one-time-ticket");
|
||||
ticket.setTeachingClassId("class-1");
|
||||
ticket.setExpiresAt(new Date());
|
||||
when(controller.studentDemoSessionService.create(controller.user, "class-1")).thenReturn(ticket);
|
||||
StudentDemoSessionCreateRequest body = new StudentDemoSessionCreateRequest();
|
||||
body.setTeachingClassId("class-1");
|
||||
|
||||
ResultEntity<StudentDemoSessionTicket> result = controller.create(body, mock(HttpServletRequest.class));
|
||||
|
||||
assertEquals(HttpStatus.OK, result.getStatusCode());
|
||||
assertEquals("one-time-ticket", result.getBody().getData().getTicket());
|
||||
verify(controller.studentDemoSessionService).create(controller.user, "class-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exchangeDoesNotAcceptClassOrTeacherFromBrowser() {
|
||||
StudentDemoSessionController controller = new StudentDemoSessionController();
|
||||
controller.studentDemoSessionService = mock(StudentDemoSessionService.class);
|
||||
StudentDemoSessionLogin login = new StudentDemoSessionLogin();
|
||||
login.setToken("demo-token");
|
||||
when(controller.studentDemoSessionService.exchange("one-time-ticket")).thenReturn(login);
|
||||
StudentDemoSessionExchangeRequest body = new StudentDemoSessionExchangeRequest();
|
||||
body.setTicket("one-time-ticket");
|
||||
|
||||
ResultEntity<StudentDemoSessionLogin> result = controller.exchange(body);
|
||||
|
||||
assertEquals(HttpStatus.OK, result.getStatusCode());
|
||||
assertEquals("demo-token", result.getBody().getData().getToken());
|
||||
verify(controller.studentDemoSessionService).exchange("one-time-ticket");
|
||||
}
|
||||
|
||||
private JwtUser teacher() {
|
||||
JwtUser user = new JwtUser();
|
||||
user.setUserId("teacher-1");
|
||||
user.setRoleId(3);
|
||||
user.setSchoolId("school-1");
|
||||
return user;
|
||||
}
|
||||
|
||||
private static class TestController extends StudentDemoSessionController {
|
||||
private final JwtUser user;
|
||||
|
||||
private TestController(JwtUser user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JwtUser currentUser(HttpServletRequest request) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
package com.sztzjy.linkCommerce.service.impl;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.config.security.TokenProvider;
|
||||
import com.sztzjy.linkCommerce.entity.SchoolClass;
|
||||
import com.sztzjy.linkCommerce.entity.StudentDemoSession;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionLogin;
|
||||
import com.sztzjy.linkCommerce.entity.dto.StudentDemoSessionTicket;
|
||||
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
|
||||
import com.sztzjy.linkCommerce.mapper.StudentDemoSessionMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class StudentDemoSessionServiceImplTest {
|
||||
|
||||
@Test
|
||||
void createsShortLivedTicketForTeachersOwnTeachingClass() {
|
||||
StudentDemoSessionServiceImpl service = buildService();
|
||||
when(service.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(teachingClass());
|
||||
|
||||
StudentDemoSessionTicket result = service.create(teacher(), "class-1");
|
||||
|
||||
assertNotNull(result.getTicket());
|
||||
assertTrue(result.getTicket().length() >= 40);
|
||||
assertEquals("class-1", result.getTeachingClassId());
|
||||
assertEquals("Teaching Class 1", result.getClassName());
|
||||
assertTrue(result.getExpiresAt().after(new Date()));
|
||||
|
||||
ArgumentCaptor<StudentDemoSession> stored = ArgumentCaptor.forClass(StudentDemoSession.class);
|
||||
verify(service.studentDemoSessionMapper).insertSelective(stored.capture());
|
||||
assertEquals("teacher-1", stored.getValue().getTeacherUserId());
|
||||
assertEquals("school-1", stored.getValue().getSchoolId());
|
||||
assertEquals("class-1", stored.getValue().getTeachingClassId());
|
||||
assertNotEquals(result.getTicket(), stored.getValue().getTicketHash());
|
||||
assertEquals(64, stored.getValue().getTicketHash().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTeacherTryingToCreateDemoForAnotherTeachersClass() {
|
||||
StudentDemoSessionServiceImpl service = buildService();
|
||||
SchoolClass anotherTeachersClass = teachingClass();
|
||||
anotherTeachersClass.setCreatedBy("teacher-2");
|
||||
when(service.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(anotherTeachersClass);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.create(teacher(), "class-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAdministrativeClassForDemo() {
|
||||
StudentDemoSessionServiceImpl service = buildService();
|
||||
SchoolClass administrativeClass = teachingClass();
|
||||
administrativeClass.setClassType("ADMIN");
|
||||
when(service.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(administrativeClass);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.create(teacher(), "class-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exchangesUnusedTicketExactlyOnceForDemoStudentToken() {
|
||||
StudentDemoSessionServiceImpl service = buildService();
|
||||
StudentDemoSession session = activeSession();
|
||||
when(service.studentDemoSessionMapper.selectByTicketHash(any())).thenReturn(session);
|
||||
when(service.studentDemoSessionMapper.markUsedIfActive(eq("session-1"), any(Date.class))).thenReturn(1);
|
||||
|
||||
StudentDemoSessionLogin result = service.exchange("ticket-from-browser");
|
||||
JwtUser tokenUser = TokenProvider.getJWTUser(result.getToken());
|
||||
|
||||
assertEquals(4, tokenUser.getRoleId());
|
||||
assertTrue(tokenUser.isDemoMode());
|
||||
assertEquals("class-1", tokenUser.getDemoTeachingClassId());
|
||||
assertEquals("teacher-1", tokenUser.getSourceTeacherId());
|
||||
assertEquals("class-1", result.getDemoTeachingClassId());
|
||||
assertEquals("Teaching Class 1", result.getClassName());
|
||||
verify(service.studentDemoSessionMapper).markUsedIfActive(eq("session-1"), any(Date.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTicketThatWasAlreadyExchanged() {
|
||||
StudentDemoSessionServiceImpl service = buildService();
|
||||
when(service.studentDemoSessionMapper.selectByTicketHash(any())).thenReturn(activeSession());
|
||||
when(service.studentDemoSessionMapper.markUsedIfActive(eq("session-1"), any(Date.class))).thenReturn(0);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.exchange("ticket-from-browser"));
|
||||
}
|
||||
|
||||
private StudentDemoSessionServiceImpl buildService() {
|
||||
StudentDemoSessionServiceImpl service = new StudentDemoSessionServiceImpl();
|
||||
service.schoolClassMapper = mock(SchoolClassMapper.class);
|
||||
service.studentDemoSessionMapper = mock(StudentDemoSessionMapper.class);
|
||||
return service;
|
||||
}
|
||||
|
||||
private JwtUser teacher() {
|
||||
JwtUser user = new JwtUser();
|
||||
user.setUserId("teacher-1");
|
||||
user.setName("Teacher One");
|
||||
user.setUsername("teacher.one");
|
||||
user.setSchoolId("school-1");
|
||||
user.setRoleId(3);
|
||||
return user;
|
||||
}
|
||||
|
||||
private SchoolClass teachingClass() {
|
||||
SchoolClass schoolClass = new SchoolClass();
|
||||
schoolClass.setSchoolClassId("class-1");
|
||||
schoolClass.setClassName("Teaching Class 1");
|
||||
schoolClass.setSchoolId("school-1");
|
||||
schoolClass.setClassType("TEACHING");
|
||||
schoolClass.setCreatedBy("teacher-1");
|
||||
return schoolClass;
|
||||
}
|
||||
|
||||
private StudentDemoSession activeSession() {
|
||||
StudentDemoSession session = new StudentDemoSession();
|
||||
session.setId("session-1");
|
||||
session.setTeacherUserId("teacher-1");
|
||||
session.setTeacherName("Teacher One");
|
||||
session.setSchoolId("school-1");
|
||||
session.setTeachingClassId("class-1");
|
||||
session.setTeachingClassName("Teaching Class 1");
|
||||
session.setExpiresAt(new Date(System.currentTimeMillis() + 60_000));
|
||||
return session;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.sztzjy.linkCommerce.service.impl;
|
||||
|
||||
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
|
||||
import com.sztzjy.linkCommerce.config.security.JwtUser;
|
||||
import com.sztzjy.linkCommerce.entity.SchoolClass;
|
||||
import com.sztzjy.linkCommerce.entity.TeachingClassStudent;
|
||||
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
|
||||
import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class StudentTeachingClassResolverImplTest {
|
||||
|
||||
@Test
|
||||
void normalStudentResolvesTheirActiveTeachingClassMembership() {
|
||||
StudentTeachingClassResolverImpl resolver = buildResolver();
|
||||
TeachingClassStudent membership = new TeachingClassStudent();
|
||||
membership.setTeachingClassId("class-1");
|
||||
when(resolver.teachingClassStudentMapper.selectActiveByStudentUserId("student-1")).thenReturn(membership);
|
||||
when(resolver.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(teachingClass("class-1"));
|
||||
|
||||
assertEquals("class-1", resolver.resolveRequired(normalStudent()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void demoStudentUsesSignedTeachingClassWithoutMembershipLookup() {
|
||||
StudentTeachingClassResolverImpl resolver = buildResolver();
|
||||
when(resolver.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(teachingClass("class-1"));
|
||||
|
||||
assertEquals("class-1", resolver.resolveRequired(demoStudent()));
|
||||
|
||||
verify(resolver.teachingClassStudentMapper, never()).selectActiveByStudentUserId("demo:session-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void demoStudentCannotRequestDifferentTeachingClass() {
|
||||
StudentTeachingClassResolverImpl resolver = buildResolver();
|
||||
when(resolver.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(teachingClass("class-1"));
|
||||
|
||||
assertThrows(ServiceException.class, () -> resolver.resolveRequested(demoStudent(), "class-2"));
|
||||
}
|
||||
|
||||
private StudentTeachingClassResolverImpl buildResolver() {
|
||||
StudentTeachingClassResolverImpl resolver = new StudentTeachingClassResolverImpl();
|
||||
resolver.schoolClassMapper = mock(SchoolClassMapper.class);
|
||||
resolver.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private JwtUser normalStudent() {
|
||||
JwtUser user = new JwtUser();
|
||||
user.setUserId("student-1");
|
||||
user.setSchoolId("school-1");
|
||||
user.setRoleId(4);
|
||||
return user;
|
||||
}
|
||||
|
||||
private JwtUser demoStudent() {
|
||||
JwtUser user = new JwtUser();
|
||||
user.setUserId("demo:session-1");
|
||||
user.setSchoolId("school-1");
|
||||
user.setRoleId(4);
|
||||
user.setDemoMode(true);
|
||||
user.setDemoTeachingClassId("class-1");
|
||||
return user;
|
||||
}
|
||||
|
||||
private SchoolClass teachingClass(String id) {
|
||||
SchoolClass schoolClass = new SchoolClass();
|
||||
schoolClass.setSchoolClassId(id);
|
||||
schoolClass.setSchoolId("school-1");
|
||||
schoolClass.setClassType("TEACHING");
|
||||
return schoolClass;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue