实验室管理与排课系统作为高校信息化建设的重要组成部分,其技术架构的选择直接影响系统的稳定性和扩展性。本系统采用前后端分离架构,前端使用Flask框架构建轻量级Web界面,后端采用Java生态中成熟的SSM(Spring+SpringMVC+MyBatis)技术栈,通过RESTful API进行数据交互。这种架构组合既保证了前端开发的灵活性,又充分发挥了Java在企业级应用中的优势。
选择Flask作为前端框架主要基于以下考虑:
后端选择SSM框架则出于:
数据库同时支持MySQL和SQLServer,主要考虑:
实验室资源管理采用树形结构组织,包含以下核心实体:
java复制@Entity
@Table(name = "lab_resource")
public class LabResource {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name; // 资源名称
private String type; // 设备/场地/耗材
private String spec; // 规格参数
private Integer status; // 使用状态
private LocalDateTime maintainTime; // 维护时间
@ManyToOne
@JoinColumn(name = "parent_id")
private LabResource parent; // 父级资源
@OneToMany(mappedBy = "parent")
private Set<LabResource> children = new HashSet<>();
// 省略getter/setter
}
资源状态变更采用状态模式实现:
java复制public interface ResourceState {
void handleRequest(LabResource resource);
}
@Component
@Scope("prototype")
public class AvailableState implements ResourceState {
@Override
public void handleRequest(LabResource resource) {
// 可用状态下的业务逻辑
}
}
// 其他状态实现类:OccupiedState、MaintenanceState等
排课核心算法采用贪心策略结合约束满足:
排课核心代码结构:
java复制public class SchedulingAlgorithm {
private List<Course> courses;
private List<LabRoom> labs;
public ScheduleResult generate() {
// 1. 按优先级排序课程
sortCoursesByPriority();
// 2. 递归分配实验室
return backtrack(0, new ScheduleResult());
}
private ScheduleResult backtrack(int index, ScheduleResult current) {
if (index >= courses.size()) {
return current.clone();
}
Course course = courses.get(index);
for (LabRoom lab : getAvailableLabs(course)) {
if (canAssign(course, lab)) {
current.assign(course, lab);
ScheduleResult result = backtrack(index + 1, current);
if (result != null) {
return result;
}
current.unassign(course, lab);
}
}
return null;
}
// 其他辅助方法...
}
采用JJWT库实现安全的认证流程:
java复制public class JwtUtil {
private static final String SECRET = "lab-secret-key";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(UserDetails user) {
return Jwts.builder()
.setSubject(user.getUsername())
.claim("roles", user.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static Authentication parseToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
String username = claims.getSubject();
List<String> roles = claims.get("roles", List.class);
return new UsernamePasswordAuthenticationToken(
username, null,
roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
);
}
return null;
}
}
采用时间片位图算法提升冲突检测效率:
java复制public class TimeConflictChecker {
private static final int DAYS = 7;
private static final int SLOTS = 48; // 每半小时一个时段
private long[][][] bitmap; // [roomId][day][slot]
public boolean checkConflict(LabBooking booking) {
int day = booking.getDayOfWeek();
int startSlot = timeToSlot(booking.getStartTime());
int endSlot = timeToSlot(booking.getEndTime());
long roomBitmap = bitmap[booking.getRoomId()][day];
long mask = ((1L << (endSlot - startSlot)) - 1) << startSlot;
return (roomBitmap & mask) != 0;
}
public void markOccupied(LabBooking booking) {
int day = booking.getDayOfWeek();
int startSlot = timeToSlot(booking.getStartTime());
int endSlot = timeToSlot(booking.getEndTime());
long mask = ((1L << (endSlot - startSlot)) - 1) << startSlot;
bitmap[booking.getRoomId()][day] |= mask;
}
private int timeToSlot(LocalTime time) {
return time.getHour() * 2 + time.getMinute() / 30;
}
}
采用Docker Compose定义服务拓扑:
yaml复制version: '3'
services:
web:
build: ./flask-web
ports:
- "5000:5000"
depends_on:
- api
api:
build: ./spring-api
ports:
- "8080:8080"
environment:
- DB_URL=jdbc:mysql://db:3306/lab_system
depends_on:
- db
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=lab_system
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
采用多级缓存提升系统响应速度:
java复制@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return manager;
}
}
java复制@Repository
public class LabCacheRepository {
private final RedisTemplate<String, Object> redisTemplate;
public void cacheLabStatus(Long labId, LabStatus status) {
redisTemplate.opsForValue().set(
"lab:status:" + labId,
status,
1, TimeUnit.HOURS);
}
public LabStatus getCachedStatus(Long labId) {
return (LabStatus) redisTemplate.opsForValue()
.get("lab:status:" + labId);
}
}
采用分布式锁防止超订:
java复制public class BookingService {
private final RedissonClient redisson;
@Transactional
public BookingResult createBooking(BookingRequest request) {
String lockKey = "lock:lab:" + request.getLabId();
RLock lock = redisson.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 检查余量
int remaining = getRemainingCapacity(request.getLabId());
if (remaining < request.getStudentCount()) {
return BookingResult.fail("容量不足");
}
// 创建预约记录
return doCreateBooking(request);
}
return BookingResult.fail("系统繁忙");
} finally {
lock.unlock();
}
}
}
使用ECharts实现课表可视化:
javascript复制function renderTimetable(data) {
const chart = echarts.init(document.getElementById('timetable'));
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const option = {
tooltip: { /* ... */ },
calendar: { /* ... */ },
series: [{
type: 'custom',
renderItem: function(params, api) {
const dayIndex = api.value(0);
const start = api.value(1);
const end = api.value(2);
const course = api.value(3);
return {
type: 'rect',
shape: {
x: params.coordSys.x + dayIndex * 100,
y: start * 10,
width: 90,
height: (end - start) * 10
},
style: {
fill: getCourseColor(course)
},
textContent: {
type: 'text',
style: {
text: course.name,
fill: '#fff'
}
}
};
},
data: data.map(item => [
item.dayIndex,
item.startSlot,
item.endSlot,
item.course
])
}]
};
chart.setOption(option);
}
xml复制<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<mapper namespace="com.lab.mapper.LabMapper">
<cache eviction="LRU" flushInterval="60000" size="512"/>
</mapper>
java复制@Repository
public class LabRepository {
@EntityGraph(attributePaths = {"equipments", "maintainRecords"})
List<LabRoom> findAllWithDetails();
}
java复制public interface SchedulingStrategy {
ScheduleResult generate(List<Course> courses, List<LabRoom> labs);
}
@Service
@Primary
public class BasicSchedulingStrategy implements SchedulingStrategy {
// 基础实现
}
@Service
@ConditionalOnProperty(name = "scheduling.strategy", havingValue = "advanced")
public class AdvancedSchedulingStrategy implements SchedulingStrategy {
// 高级实现
}
java复制@Component
public class LabStatusListener {
@EventListener
public void handleLabStatusChange(LabStatusEvent event) {
// 发送通知、更新缓存等
}
}
@Service
public class LabService {
private final ApplicationEventPublisher publisher;
public void changeStatus(Long labId, Status newStatus) {
// 状态变更逻辑...
publisher.publishEvent(new LabStatusEvent(labId, newStatus));
}
}
在系统实际运行中,我们发现实验室设备状态同步是个关键痛点。通过引入WebSocket实现实时状态推送后,设备使用冲突减少了约40%。具体实现是在Spring中配置STOMP协议支持:
java复制@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/lab-status")
.setAllowedOrigins("*")
.withSockJS();
}
}