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); } } }