ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue+SpringBoot图书馆座位预约系统:从毕设到实战项目的全栈开发指南

Vue+SpringBoot图书馆座位预约系统:从毕设到实战项目的全栈开发指南 简介本资源是一套完整的图书馆座位预约系统毕业设计实现方案面向计算机类专业本科生、课程设计学习者及初入Web开发的实践者解决高校场景下座位资源线上化管理与实时预约的核心需求。压缩包共82个文件涵盖38个Vue前端组件含Admin后台与User用户端、22个SpringBoot后端Java业务逻辑与控制器、4个JS工具脚本、1个完整SQL建表语句seat.sql及系统运行截图等关键材料整体仅257KB轻量易部署。已有1134人下载学习适合作为毕设原型、课程设计参考或SpringBootVue全栈入门项目。读者可直接运行前后端代码查看管理员排座、学生预约/签到/释放座位、实时座位状态渲染等完整功能项目结构清晰含vue.config.js配置、SpringBoot多环境yaml、README与项目说明文档便于理解模块划分与二次开发。1. 项目背景与核心价值从“毕设”到“可落地的实战项目”最近几年但凡涉及到“管理系统”的计算机专业毕业设计图书馆座位预约系统绝对能排进热门选题前三。原因很简单它需求明确、贴近生活、技术栈成熟而且功能模块清晰非常适合作为学生综合能力的展示。但问题也随之而来网上流传的源码质量参差不齐很多项目只是简单实现了“增删改查”界面粗糙、逻辑漏洞百出甚至数据库设计都不规范直接拿来用答辩时被老师问几个深入点的问题就可能露馅。我手上这个“基于vuespringboot的图书馆座位预约系统”项目最初也是作为个人毕设完成的。但和那些“玩具级”的代码不同我在开发时就有意识地把它当作一个“准生产级”的小型应用来打磨。它不仅仅是一个交差的作业更是一个完整展示了前后端分离架构、数据库设计、业务逻辑和用户体验考量的实战案例。对于正在寻找毕设选题、或者想通过一个完整项目深入学习Vue和SpringBoot的同学来说这个项目提供的不是一堆冰冷的代码而是一条清晰的、可以复现并深入学习的路径。这个系统的核心价值在于“真实感”。它模拟了大学图书馆座位预约的真实场景学生需要选座、预约、签到、暂离、退座管理员需要管理座位、审核预约、处理违规。这里面涉及的状态流转、并发控制、数据一致性等问题都是你在学习Web开发时必须面对的经典问题。通过剖析这个项目的源码和设计你能学到的远不止如何调用几个API而是如何将一个现实需求系统地拆解、设计并最终实现成一个可运行的软件系统。2. 技术栈选型解析为什么是VueSpringBootMySQL看到“VueSpringBootSQL数据库”这个组合你可能觉得毫无新意甚至有点“老套”。但恰恰是这种成熟、稳定的技术栈才是毕设和入门实战项目的最优解。选型的理由不在于炫技而在于高效、稳妥地达成目标。2.1 前端Vue.js 2.x 的务实之选项目前端采用了Vue 2.x而不是最新的Vue 3。对于毕设和大多数中小型管理后台来说这是一个非常务实的选择。生态成熟Vue 2拥有极其丰富的生态系统Element UI、Vue Router、Vuex、Axios等周边库经过多年打磨稳定可靠遇到任何问题几乎都能在社区找到解决方案。这对于时间紧张的毕设阶段至关重要。学习曲线平缓Vue 2的Options API对于初学者更为友好数据、方法、计算属性分门别类结构清晰易于理解和上手。项目中使用的是Vue CLI搭建的工程化环境这让你能接触到现代前端开发的核心流程模块化、组件化、打包构建。UI框架Element UI项目界面基于Element UI构建。选择它是因为它专为后台管理系统设计组件丰富、文档详尽、风格统一。你不需要花大量时间在CSS雕花上就能快速搭建出专业、美观的界面。例如座位管理页面用el-table展示预约对话框用el-dialog表单验证用el-form的rules规则极大地提升了开发效率。2.2 后端SpringBoot 的“约定大于配置”后端采用SpringBoot这是Java领域微服务和企业级应用的事实标准框架。快速启动SpringBoot最大的优势是自动化配置和起步依赖。你不需要再被繁琐的XML配置和依赖冲突折磨。在pom.xml中引入spring-boot-starter-web,spring-boot-starter-data-jpa,spring-boot-starter-security等依赖一个具备Web、数据访问、安全基础的后端服务就准备好了。数据持久层Spring Data JPA项目使用JPAHibernate实现作为ORM框架。相比原生的JDBC或MyBatisJPA通过注解映射实体类与数据库表能进行对象化操作大大简化了CRUD代码。例如定义Seat座位、User用户、Reservation预约记录实体它们之间的关系如一个用户有多个预约通过OneToMany等注解声明代码可读性极高。业务逻辑清晰服务层Service封装核心业务规则如预约时的座位状态检查、签到超时判断、违规记录生成等。控制器层Controller则干净地处理HTTP请求和响应遵循RESTful风格返回统一的JSON数据给前端。2.3 数据库MySQL的经典与可靠数据库选择了MySQL这是最广泛使用的关系型数据库之一。设计与规范项目的SQL文件不仅仅是一堆建表语句。它体现了清晰的数据库设计思想。例如表结构设计user表用户、seat表座位、reservation表预约记录是核心。此外通常还会有violation违规记录、schedule座位开放时间安排等表。字段与约束合理的数据类型如datetime用于预约时间tinyint用于状态标志、主外键约束确保数据完整性、索引优化在reservation表的seat_id和reserve_time上建索引加速查询。关系明确通过外键清晰地表达了“用户预约座位”的一对多或多对一关系。SQL脚本的价值提供的database.sql文件是你理解整个系统数据模型的蓝图。运行它你就能瞬间拥有一个结构完整、带有初始测试数据的数据库这是项目能“一键运行”的基础。这个技术栈组合确保了项目在技术上的完整性、稳定性和可学习性每一个部分都有大量的学习资源和最佳实践可供参考。3. 系统核心功能模块与业务流程拆解一个完整的座位预约系统其核心在于“状态”的管理和流转。下面我们深入每个模块看看代码是如何实现这些业务逻辑的。3.1 用户端核心流程预约、使用、离开用户端的体验必须流畅且符合直觉。核心流程如下查询与选座用户进入系统前端通过地图或列表展示图书馆区域和座位状态空闲、已预约、使用中、暂离、故障。这里涉及一个关键接口GET /api/seats?date2023-10-27zoneA。后端逻辑是查询seat表并关联reservation表计算每个座位在指定日期的状态。发起预约用户选择空闲座位和时段如 14:00-18:00点击预约。前端调用POST /api/reservations。后端关键校验Service层逻辑检查座位是否存在且状态为“空闲”。检查该用户是否有未完成的预约或正在进行的预约防止重复占座。检查该座位在目标时段是否已被他人预约并发控制重点。这里通常采用“乐观锁”或“悲观锁”机制。项目中更可能采用在数据库层面利用事务和SELECT ... FOR UPDATE或在应用层使用分布式锁如Redis来保证同一座位同一时段只能被成功预约一次。所有校验通过后在reservation表中插入一条新记录状态为“已预约”并将对应座位的状态更新为“已预约”。签到用户在预约开始时间前后一定时间窗口内如前后15分钟到座位附近扫码或手动点击“签到”。调用PUT /api/reservations/{id}/check-in。后端校验预约记录是否存在、用户是否匹配、当前时间是否在签到窗口内。校验通过后将预约记录状态改为“使用中”座位状态同步改为“使用中”。暂离与返回用户临时离开可点击“暂离”座位状态变为“暂离”并开始计时如30分钟。超时未返回系统自动释放座位并记录一次违规。这是一个典型的定时任务场景可以使用Spring的Scheduled注解定时扫描状态为“暂离”且超时的记录。退座/签退用户使用完毕点击“退座”。系统将预约记录状态改为“已完成”座位状态恢复为“空闲”。3.2 管理端核心功能配置、监控与裁决管理端是系统的“大脑”负责规则的制定和秩序的维护。座位管理CRUD操作可以设置座位编号、区域、类型如普通、带插座、状态启用/禁用。预约管理查看所有预约记录支持按时间、用户、座位、状态筛选。对于异常预约如长时间未签到管理员可以手动强制取消。用户管理用户信息的维护以及违规管理。这是系统的“牙齿”。当用户发生“预约未签到”、“暂离超时”等行为时系统自动在violation表中生成记录。累计达到一定次数管理员可以执行惩罚如冻结其预约权限若干天。相关逻辑在对应的Service方法中通常会在更新预约状态为“违规”的同时插入违规记录。数据统计通过图表展示每日预约量、座位使用率、热门时段、用户活跃度等。这需要编写复杂的查询语句通常使用JPA的Query注解写原生SQL或JPQL对reservation表进行分组聚合查询。3.3 数据库表核心设计要点光看代码不理解数据关系是徒劳的。这里重点剖析几个核心表的设计思路reservation表预约记录表这是系统的核心事实表。CREATE TABLE reservation ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL COMMENT 用户ID, seat_id int NOT NULL COMMENT 座位ID, reserve_date date NOT NULL COMMENT 预约日期, start_time time NOT NULL COMMENT 预约开始时间, end_time time NOT NULL COMMENT 预约结束时间, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0已预约1使用中2暂离3已完成4已取消5违规, check_in_time datetime DEFAULT NULL COMMENT 实际签到时间, check_out_time datetime DEFAULT NULL COMMENT 退座时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seat_date (seat_id,reserve_date), -- 复合索引加速按座位和日期的查询 KEY idx_user_status (user_id,status), -- 加速查询用户当前预约 CONSTRAINT fk_reservation_seat FOREIGN KEY (seat_id) REFERENCES seat (id), CONSTRAINT fk_reservation_user FOREIGN KEY (user_id) REFERENCES user (id) ) ENGINEInnoDB COMMENT预约记录表;状态字段status用一个TinyInt存储状态码是所有业务流程流转的枢纽。后端实体类中会定义一个枚举Enum与之对应使代码更清晰。时间字段区分reserve_date预约哪一天和start_time/end_time那一天的几点到几点方便按天统计和查询。check_in_time和check_out_time是实际发生的时间用于计算实际使用时长和判断是否准时。索引设计idx_seat_date索引对于“查询某个座位在某天是否有预约”这个高频操作至关重要能极大提升性能。idx_user_status则优化了“查询用户当前有效预约”的速度。4. 项目部署与运行指南从源码到可访问的系统拿到源码压缩包后如何让它跑起来以下是详细的步骤和可能遇到的坑。4.1 环境准备清单后端环境JDK 8或11建议11与SpringBoot 2.x兼容性好。Maven 3.6用于管理依赖和构建项目。IDEIntelliJ IDEA推荐或 Eclipse。前端环境Node.js 14 和 npm。安装后在命令行输入node -v和npm -v检查。数据库环境MySQL 5.7或8.0。确保已安装并启动MySQL服务。4.2 后端SpringBoot项目配置与启动导入项目用IDEA打开解压后的后端项目文件夹通常包含pom.xml的目录IDEA会自动识别为Maven项目并开始下载依赖。数据库配置找到src/main/resources/application.yml(或application.properties) 文件。这是SpringBoot的核心配置文件。你需要修改数据库连接信息spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_seat_db?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root # 你的MySQL用户名 password: your_password # 你的MySQL密码 jpa: hibernate: ddl-auto: update # 首次启动可设为create或update生产环境务必设为none或validate show-sql: true # 开发时开启方便看生成的SQL关键点ddl-auto: update会让Hibernate根据你的实体类自动更新表结构适合初次运行。但如果你已经运行了提供的database.sql建议设为none或validate避免覆盖已有数据。解决依赖问题如果pom.xml中的依赖下载失败或标红检查网络或尝试更换Maven镜像源为阿里云。在IDEA中可以右键点击pom.xml文件选择“Maven” - “Reload project”。启动应用找到主启动类通常命名为XxxApplication带有SpringBootApplication注解右键运行。控制台看到Tomcat started on port(s): 8080类似的日志说明后端启动成功。4.3 前端Vue项目配置与启动安装依赖在终端中进入前端项目目录包含package.json的文件夹运行npm install。这个过程会下载所有Node模块国内网络可能较慢可以使用淘宝镜像npm install --registryhttps://registry.npmmirror.com。配置API代理前端需要调用后端接口。在开发环境下为了避免跨域问题Vue CLI项目通常在vue.config.js文件中配置代理module.exports { devServer: { proxy: { /api: { // 将所有以/api开头的请求转发到后端 target: http://localhost:8080, // 你的后端地址 changeOrigin: true, pathRewrite: { ^/api: // 重写路径去掉/api前缀根据后端接口实际情况调整 } } } } }请根据你后端实际的接口路径前缀来调整这个配置。如果后端接口就是http://localhost:8080/api/xxx那么target就是http://localhost:8080pathRewrite可能不需要重写或重写为^/api: /api。启动前端服务运行npm run serve。成功后终端会输出App running at: - Local: http://localhost:8081等信息。此时用浏览器打开这个本地地址就能看到系统登录界面了。4.4 首次登录与测试通常提供的SQL脚本会初始化一些测试数据包括管理员和普通用户账号。常见的默认账号可能是管理员admin / admin123普通用户student / 123456请查看SQL文件或项目文档确认。登录后分别以用户和管理员身份操作体验完整的预约和管理流程。5. 源码深度剖析与关键代码解读只看运行效果不够我们得深入代码看看核心逻辑是如何实现的。这里挑几个典型场景分析。5.1 后端预约服务的并发控制实现这是系统的技术难点。假设两个用户几乎同时预约同一个座位如何避免“超售”我们来看ReservationService中可能的一段代码逻辑Service Transactional // 声明式事务管理 public class ReservationServiceImpl implements ReservationService { Autowired private SeatRepository seatRepository; Autowired private ReservationRepository reservationRepository; Autowired private RedisTemplateString, String redisTemplate; // 使用Redis分布式锁 Override public ApiResult createReservation(ReservationRequest request) { Long seatId request.getSeatId(); Long userId request.getUserId(); LocalDate date request.getDate(); LocalTime startTime request.getStartTime(); LocalTime endTime request.getEndTime(); // 1. 基于座位ID生成分布式锁的key String lockKey reservation:lock:seat: seatId : date.toString(); String lockValue UUID.randomUUID().toString(); // 尝试获取锁设置过期时间防止死锁 Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 2. 在锁的保护下执行核心校验和创建逻辑 // 2.1 检查座位是否存在且可用 Seat seat seatRepository.findById(seatId) .orElseThrow(() - new BusinessException(座位不存在)); if (!SeatStatus.AVAILABLE.equals(seat.getStatus())) { throw new BusinessException(该座位当前不可用); } // 2.2 检查该座位在目标时段是否已有预约关键查询 ListReservation existingReservations reservationRepository .findBySeatIdAndDateAndTimeOverlap(seatId, date, startTime, endTime); if (!existingReservations.isEmpty()) { throw new BusinessException(该时段座位已被预约); } // 2.3 检查用户是否有未完成预约 ListReservation userActiveReservations reservationRepository .findByUserIdAndStatusIn(userId, Arrays.asList(ReservationStatus.RESERVED, ReservationStatus.IN_USE)); if (!userActiveReservations.isEmpty()) { throw new BusinessException(您已有进行中的预约请先完成); } // 3. 所有校验通过创建预约记录 Reservation reservation new Reservation(); reservation.setUserId(userId); reservation.setSeatId(seatId); reservation.setReserveDate(date); reservation.setStartTime(startTime); reservation.setEndTime(endTime); reservation.setStatus(ReservationStatus.RESERVED); reservation.setCreateTime(LocalDateTime.now()); reservationRepository.save(reservation); // 4. 更新座位状态 seat.setStatus(SeatStatus.RESERVED); seatRepository.save(seat); return ApiResult.success(预约成功, reservation.getId()); } finally { // 5. 释放锁确保释放的是自己加的锁 String currentValue redisTemplate.opsForValue().get(lockKey); if (lockValue.equals(currentValue)) { redisTemplate.delete(lockKey); } } } else { // 获取锁失败说明有其他请求正在处理同一座位的预约 throw new BusinessException(系统繁忙请稍后重试); } } }代码解读与思考分布式锁Redis在高并发场景下使用数据库行锁SELECT ... FOR UPDATE可能会成为性能瓶颈且不利于水平扩展。这里采用Redis分布式锁将并发控制粒度细化到“具体座位具体日期”性能更好。锁的Key设计包含了座位ID和日期确保同一天同一座位的预约请求串行化。事务边界整个预约创建、座位状态更新操作都在一个Transactional事务内保证了数据一致性。即使中间步骤出错所有操作也会回滚。关键查询findBySeatIdAndDateAndTimeOverlap这个方法需要自定义Repository查询。它的SQL逻辑是检查是否存在一条预约记录其时间段[start_time, end_time]与请求的时间段[request.start_time, request.end_time]有重叠。这是判断座位是否被占用的核心。5.2 前端座位状态的可视化与交互前端如何展示复杂的座位状态并实现流畅的交互以SeatMap.vue组件为例template div classseat-map div v-forzone in zones :keyzone.id classzone h3{{ zone.name }}/h3 div classseats-grid div v-forseat in zone.seats :keyseat.id :class[seat, seat-status-${seat.status}, { seat-selected: selectedSeatId seat.id }] clickonSeatClick(seat) div classseat-number{{ seat.number }}/div div classseat-status-text{{ getStatusText(seat.status) }}/div !-- 可能还有插座图标等 -- /div /div /div !-- 预约对话框 -- el-dialog :visible.syncreserveDialogVisible title预约确认 !-- 对话框内容 -- /el-dialog /div /template script import { getSeatsByZone, reserveSeat } from /api/seat; export default { data() { return { zones: [], // 从后端获取的区域和座位数据 selectedSeatId: null, reserveDialogVisible: false, selectedTimeRange: [] }; }, mounted() { this.loadSeatMap(); // 可以开启一个定时器定期刷新座位状态 // this.refreshTimer setInterval(this.loadSeatMap, 30000); // 每30秒刷新一次 }, methods: { async loadSeatMap() { try { const response await getSeatsByZone({ date: this.selectedDate }); this.zones response.data; } catch (error) { this.$message.error(加载座位图失败); } }, onSeatClick(seat) { if (seat.status ! AVAILABLE) { this.$message.warning(该座位${this.getStatusText(seat.status)}无法预约); return; } this.selectedSeatId seat.id; this.reserveDialogVisible true; }, async handleReserveConfirm() { // 组装预约请求数据 const params { seatId: this.selectedSeatId, date: this.selectedDate, startTime: this.selectedTimeRange[0], endTime: this.selectedTimeRange[1] }; try { await reserveSeat(params); this.$message.success(预约成功); this.reserveDialogVisible false; this.loadSeatMap(); // 刷新座位图 } catch (error) { this.$message.error(error.message || 预约失败); } }, getStatusText(status) { const statusMap { AVAILABLE: 空闲, RESERVED: 已预约, IN_USE: 使用中, AWAY: 暂离, DISABLED: 故障 }; return statusMap[status] || 未知; } }, beforeDestroy() { // 清除定时器 // if (this.refreshTimer) clearInterval(this.refreshTimer); } }; /script style scoped .seat { width: 80px; height: 80px; border: 1px solid #ccc; border-radius: 4px; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; margin: 5px; transition: all 0.3s; } .seat:hover { transform: scale(1.05); box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .seat-status-AVAILABLE { background-color: #e8f5e9; } /* 绿色系 */ .seat-status-RESERVED { background-color: #fff3e0; } /* 橙色系 */ .seat-status-IN_USE { background-color: #ffebee; } /* 红色系 */ .seat-status-AWAY { background-color: #e3f2fd; } /* 蓝色系 */ .seat-status-DISABLED { background-color: #f5f5f5; color: #9e9e9e; cursor: not-allowed; } .seat-selected { border: 3px solid #409eff; } /style前端实现要点组件化与数据驱动整个座位图被抽象成一个可复用的Vue组件。座位状态通过CSS类名如seat-status-AVAILABLE动态绑定实现视觉区分。状态管理虽然这个组件内状态简单但如果涉及全局用户状态如当前用户信息、预约记录等通常会引入Vuex进行集中式状态管理。用户体验点击座位时的视觉反馈:hover效果、选中边框、状态提示、加载中的等待动画、操作成功/失败的全局消息提示使用Element UI的$message这些细节共同构成了良好的用户体验。实时性考虑注释掉的定时刷新逻辑在实际项目中可以考虑使用WebSocket实现真正的座位状态实时推送避免用户看到过期信息。6. 项目扩展与优化思路让你的毕设脱颖而出如果只是跑通基础功能可能还不足以在答辩中拿到高分。以下是一些可以深入研究和扩展的方向能让你的项目报告和答辩更有深度。6.1 功能增强点智能推荐座位根据用户历史偏好如喜欢靠窗、有插座、当前人流热力图推荐最合适的空闲座位。这需要在前端展示推荐度后端算法可以很简单如根据座位属性打分也可以引入简单的协同过滤。预约规则灵活化在管理后台增加“规则管理”模块。允许管理员动态配置每日可预约次数、最长预约时长、最短预约时长、开放预约的时间范围如只能预约未来3天的、不同用户角色如研究生和本科生的权限差异等。这些规则需要在预约校验的Service层进行集中判断。扫码签到/蓝牙签到集成二维码生成库如qrcode.js每个座位生成唯一动态二维码。用户用手机扫码即可签到比手动点击更真实。甚至可以模拟蓝牙信标iBeacon签到研究室内定位技术。数据可视化大屏为图书馆管理员提供一个数据仪表盘使用ECharts等库实时展示今日预约总量、各区域使用热力图、当前在馆人数曲线、违规统计排行等。6.2 技术深度优化缓存优化座位状态、图书馆区域信息等不常变化但高频访问的数据可以引入Redis缓存。在SeatService中先查缓存缓存未命中再查数据库并写入缓存。注意在座位状态变更时预约、签到、退座需要清除或更新对应的缓存。接口性能与安全防刷与限流使用Guava RateLimiter或Spring Cloud Gateway等工具对/api/reservationsPOST接口进行限流防止恶意脚本刷预约。SQL注入防护项目使用JPA本身已通过参数化查询很大程度上避免了SQL注入。但仍需确保所有自定义的Native QueryQuery也使用参数绑定而不是字符串拼接。XSS防护确保前端Vue已对用户输入进行转义后端也可以统一配置过滤器或使用框架如Spring Boot的spring-boot-starter-security的默认防护。部署与监控前后端分离部署将前端Vue项目通过npm run build打包成静态文件dist目录部署到Nginx。后端SpringBoot打包成Jar通过java -jar运行或部署到Tomcat。Nginx同时作为静态资源服务器和反向代理将/api请求转发到后端。基础监控在SpringBoot项目中集成Spring Boot Actuator暴露健康检查、指标等信息。使用logback-spring.xml配置结构化日志方便排查问题。6.3 答辩准备要点理解并准备阐述以下几点能让答辩老师眼前一亮系统的核心矛盾与解决方案清楚说明“如何解决同一座位同一时段被多人预约”的并发问题分布式锁事务。数据库设计范式与权衡解释你的表结构设计如第三范式以及为了查询性能可能做的反范式化设计比如在reservation表里冗余seat_zone字段。前后端分离的优势谈谈为什么选择这种架构职责清晰、独立开发、部署、技术选型灵活等。遇到的真实问题与解决过程准备一两个你在开发或部署中实际遇到的坑比如跨域问题、定时任务不触发、打包后前端路由404并说明你是怎么排查和解决的。这比单纯讲功能更有说服力。这个“图书馆座位预约系统”项目就像一块包含了Web开发全流程的“压缩饼干”。从需求分析、技术选型、数据库设计、前后端编码、到部署测试它覆盖了一个初级开发者需要掌握的大部分技能。希望这份详细的解读能帮助你不仅“运行”起这个项目更能“吃透”它并以此为基础构建出属于你自己的、更优秀的应用。本文还有配套的精品资源点击获取
返回列表