ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Vue的宿舍维修管理系统设计与实现全解析

基于SpringBoot+Vue的宿舍维修管理系统设计与实现全解析 最近正直毕设季后台总有人问我“有没有好做的Java Web毕设题目”。说实话Java Web方向的经典题目就那么几个图书管理、商品管理、教务系统、考勤系统做得太多答辩时撞车概率极高。这次给你拆解一个我觉得各方面都比较均衡的选题——基于SpringBootVue的宿舍维修管理系统从项目设计、数据库表结构、核心接口实现到前端页面联动完整地把这套毕设源码的骨架讲清楚。这个题目好在哪里业务完整度适中既能体现前后端分离的开发模式又能展示你对权限控制、工单流转、数据统计这些真实业务场景的理解而且宿舍环境是每个人都熟悉的需求好理解、好讲解答辩时不会卡壳。如果你手头拿到的是一份“SpringBootVue宿舍维修管理系统完整源码SQL脚本接口文档”这样的资源包这篇文章可以作为你的导读手册帮你把源码从“能跑”变成“讲得清、改得动、答得妙”。如果你打算自己从零写那就更对路了照着这个思路一步步实现即可。1. 项目整体设计与技术选型思路1.1 业务模块梳理与角色权限设计宿舍维修管理系统名字听起来简单但真正的业务场景拆开来看其实是一个比较典型的“工单系统”学生发现宿舍设施损坏提交报修单宿管员或者系统管理员审核并派单给维修工维修工接单、上门维修、填写处理结果学生确认维修效果并评价管理端对整个过程进行数据统计和监控。所以核心业务流程是一条单向链路学生报修 → 审核派单 → 维修处理 → 学生确认 → 评价归档。围绕这条链路系统至少要拆出这么几个角色学生提交报修、查看自己的工单状态、对完成的工单进行确认和评价。维修工查看分配给自己的工单、更新维修进度、填写维修结果。宿管员审核报修单、分派维修工、处理异常工单。系统管理员用户管理、维修类型管理、数据统计、全局配置。权限设计我建议直接采用RBAC模型。不需要做得太复杂三张基础表就够了用户表存用户信息和角色标识角色表定义角色名称和可访问的路由标识中间表做关联。毕设答辩时老师问到“权限控制怎么实现的”你就按这个思路答前端用路由守卫控制页面访问后端用拦截器校验接口权限前后端双重控制。如果你用的源码里权限设计更简单比如只在用户表里存了一个role字段也没关系够用就行但要能讲清楚不同角色登录后看到不同菜单和页面是怎么实现的这个细节很加分。数据层面除了核心的用户、工单、维修类型、评价表建议把通知模块也加上。学生提交报修后可以给他推一条“报修受理”消息工单状态变化时再推一条。通知功能实现起来不复杂一张表加几个查询接口就够了但在答辩时能成为“消息推送机制”的亮点。整个系统的功能边界要控制住不要盲目加模块毕设的核心是让每条业务链路完整闭环而不是功能堆砌。1.2 技术选型为什么是SpringBootVue组合“为什么选SpringBootVue”是答辩必问题你自己心里得先有一个明确的答案不能只回一句“大家都用这个”。后端用SpringBoot核心原因是它的自动化配置机制。传统的SSH或SSM项目需要写一大堆XML配置环境搭建就劝退很多人了。SpringBoot通过starter机制把常用的依赖封装好比如spring-boot-starter-web自带Tomcat和内嵌Servlet容器spring-boot-starter-data-jpa或者mybatis-spring-boot-starter直接管理数据库访问开发环境从零到能跑起来只需要几分钟。做毕设项目不需要花大量时间在环境配置上把精力省下来放在业务代码上这才是SpringBoot的核心价值。前端用Vue在于它渐进式的开发体验。用Vue CLI或Vite初始化一个工程引入Element UI组件库表格、表单、对话框、分页这些管理后台常用的组件都是现成的。Vue的双向绑定机制处理“表单填写报修信息”这种高频交互场景特别舒服数据模型变了页面自动更新不需要手动操作DOM。整个宿舍维修管理系统的前台界面不算复杂无外乎就是列表页、表单页、详情页和几个图表Vue完全可以胜任。再往后就是前后端分离的协作模式。后端专心提供RESTful API把业务逻辑、数据校验、权限控制这些做扎实前端专心做页面交互和状态管理。两边通过JSON格式的数据通信。这种开发方式的好处是职责清晰你做毕设时可以先跑通后端所有接口用Postman把测试数据都调通再回头写前端页面不会出现“前端在等后端接口”的卡顿。这里要说明的是前后端分离项目的部署策略也值得提前想清楚。最简单的方案是前端代码打包后直接放到后端的static目录下由SpringBoot统一提供服务这样只需要部署一个Java进程。另一种方案是前端单独部署在Nginx上API请求通过反向代理转发给后端服务。两种方案在毕设答辩时都可以讲但实际部署细节有差异后文我会专门说。2. 核心功能拆解与难点击破2.1 报修工单状态机的设计与流转规则整个系统里最值得花时间设计的就是工单的状态流转。很多同学做这类系统容易把状态设计成简单的字符串字段比如status存“待维修”然后在代码里到处写if判断后来改需求时就非常痛苦。更规范的做法是一开始就把状态机设计清楚明确每个状态由谁触发、能流转到哪个状态代码里只保留状态流转的入口所有非法流转在入口处就拦截掉。我设计的工单状态大致分这几个阶段状态值状态名称说明谁可以操作0待审核学生提交报修后的初始状态宿管员审核1待派单审核通过等待指派维修工宿管员派单2维修中维修工已接单并开始处理维修工/宿管员3待确认维修工提交处理结果学生确认4已完成学生确认维修完成系统自动/学生5已取消审核不通过或学生自行撤销宿管员/学生这里有几个细节值得注意。首先状态流转一定不能跳步比如维修工不能把一个还在“待审核”状态的工单变成“维修中”因为这个工单还没经过派单环节维修工甚至都不应该看到它。其次状态变更需要记录时间和操作人尤其是“维修中”变为“待确认”的时间点这个时间戳是后续计算平均维修时长的重要数据来源。还有学生提交报修后如果长时间没人处理系统需要一个超时提醒机制这个可以用SpringBoot的Scheduled定时任务来实现每天扫描一次超过24小时还停留在“待审核”状态的工单自动给宿管员发站内通知。在设计这个状态机的时候我建议你把状态流转规则表先画出来每个状态对应的下一步状态和操作角色都列清楚再动手写代码。这个表格本身就是很好的答辩素材老师一问“业务流程你怎么设计的”直接拿表出来讲非常加分。2.2 图片上传与静态资源映射宿舍报修的场景里“文字说不清楚”是常态插座坏了、漏水了光靠描述很难让维修工准确判断问题。所以图片上传几乎是这个系统的刚需功能。但很多新手在做这个功能时会把事情想复杂以为要接什么云存储服务。其实毕设项目用本地存储方案完全够用。实现思路不复杂。前端用Element UI的el-upload组件选图上传请求发到后端的/upload接口后端接收MultipartFile后把文件保存到项目的一个固定目录下比如D:/upload或者项目根目录的upload文件夹文件名用UUID生成避免重名覆盖保存完后返回文件的访问路径前端拿到路径后把这个路径拼在报修表单的字段里一起提交后续前端要显示图片时只需要向后端请求这个静态资源路径。这里有一个特别容易踩的坑SpringBoot默认不会帮你把本地目录映射成可访问的静态资源。也就是说你文件确实存到本地了但前端通过http://localhost:8080/upload/xxx.jpg访问时得到404因为Spring Boot找不到这个路径对应的资源。解决办法是写一个配置类继承WebMvcConfigurer重写addResourceHandlers方法把/upload/**这个URL前缀映射到你的本地目录。代码大致是这样的Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将/upload/**映射到本地文件的物理路径注意末尾要有分隔符 registry.addResourceHandler(/upload/**) .addResourceMapping(/upload/**) .addResourceLocations(file:D:/upload/); } }另外两个小细节一是上传文件的大小限制SpringBoot默认单文件最大1MB超过会被拦截报错信息还容易让人摸不着头脑需要在配置里调大二是文件类型校验最好只允许jpg、png等图片格式防止有人上传恶意文件这个在后端接口里做白名单校验就够了。2.3 数据统计模块的SQL与接口设计管理后台的统计看板是这个系统容易被低估、但其实很出彩的模块。宿舍报修记录在数据库里一累积就是数据如何把这些数据转化为直观的信息就是SQL能力最好的体现点。统计类需求通常集中在四个维度按宿舍楼统计报修数量、按状态统计工单分布、维修完成率、平均维修时长。对应到SQL就是用GROUP BY和聚合函数进行计算。以“按宿舍楼统计报修数量”为例你需要维护一个宿舍楼维度。这里有两种设计思路简单一点的做法是报修工单表里存students表关联的宿舍号然后根据宿舍号前缀或者关联表分组统计更规范的做法是单独建一张dormitory表工单表存dormitory_id这样统计时直接JOIN两张表。毕设项目用第二种方案更好JOIN操作是高频面试题答辩时能顺手展示表关联的设计能力。对应SQL大概是这样的SELECT d.building_name, COUNT(r.id) AS repair_count FROM repair_order r LEFT JOIN dormitory d ON r.dormitory_id d.id WHERE r.create_time BETWEEN #{startTime} AND #{endTime} GROUP BY d.building_name ORDER BY repair_count DESC;平均维修时长这个指标稍微复杂一点数据来源是工单表里两个时间字段的差值。比如工单进入“维修中”的时间是start_time变为“待确认”的时间是end_time时长就是这两个时间戳的差。SQL里可以用TIMESTAMPDIFF函数计算分钟差再套AVG函数取平均值。如果数据量不大也可以把工单列表查出来后在Java层计算平均两种方式都行。我建议在SQL里算因为SQL表达的是你“对业务指标有清晰的理解”而且代码更简洁。3. 实操过程与核心环节实现3.1 环境准备与项目初始化动手之前先把环境备好。Java项目建议使用JDK 8这是目前兼容性最好的版本很多老项目跑在JDK 11或17上会出现依赖不兼容。如果你的源码是基于SpringBoot 2.x的那用JDK 8就对了如果源码是SpringBoot 3.x那必须JDK 17以上这个版本匹配问题很多同学第一次跑毕设源码时会踩坑。数据库版本选MySQL 5.7或8.0都可以前后端SpringBoot和MySQL的版本兼容性在毕设项目里基本不会有太大问题。前端工具链方面Node.js建议选14到16的稳定版本npm或者yarn看你习惯Vue CLI全局装了就能跑。后端项目我建议直接用Spring Initializr生成骨架选好Maven构建、Spring Web、MyBatis、MySQL驱动这几个依赖。如果你拿到的源码已经是完整项目那就不用重生成重点是把Maven依赖下载速度和仓库配到阿里云镜像不然下载依赖能把你卡到怀疑人生。项目结构方面我强烈建议用标准的controller、service、mapper、entity、common五层结构controller层接收请求和参数校验service层写业务逻辑mapper层操作数据库entity层对应表结构common层放统一返回类和全局异常处理。这个层次和Spring Boot的官方推荐实践一致答辩时讲起来也特别流畅。前端工程用Vue CLI初始化跑一下vue create dormitory-repair-web选择Vue 2还是Vue 3取决于你的源码。如果你的后端源码年代较早大概率配的是Vue 2 Element UI的组合那你前端也用Vue 2以免Element Plus的API差异导致页面渲染异常。初始化完成后第一件事是把axios请求库装好封装一个统一的request工具把baseURL、token拦截、响应拦截都在里面处理好后面所有接口调用都走这一个入口。3.2 数据库脚本与核心表结构设计拿到一份毕设源码第一件该做的事情永远是打开SQL脚本看表结构。因为整个系统的一切逻辑都建立在数据表之上表设计合理代码自然顺理成章。宿舍维修管理系统最核心的几张表我上面大致提过这里给你看两个重点表的SQL脚本可以作为你检查源码数据模型的参照。用户表结构大致是这样的CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 登录密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL COMMENT 角色0学生 1维修工 2宿管 3管理员, dormitory_id bigint(20) DEFAULT NULL COMMENT 所属宿舍楼ID, phone varchar(20) DEFAULT NULL COMMENT 联系电话, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;报修工单表是系统的核心所有业务流转都发生在这张表上字段设计要把每一步状态流转需要用到的信息都覆盖到CREATE TABLE repair_order ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, order_no varchar(32) NOT NULL COMMENT 工单编号, student_id bigint(20) NOT NULL COMMENT 报修学生ID, repair_type_id bigint(20) NOT NULL COMMENT 维修类型ID, description text COMMENT 故障描述, images varchar(1000) DEFAULT NULL COMMENT 图片路径多张用逗号分隔, dormitory_id bigint(20) DEFAULT NULL COMMENT 宿舍楼ID, room_no varchar(20) DEFAULT NULL COMMENT 房间号, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待审核 1待派单 2维修中 3待确认 4已完成 5已取消, assignee_id bigint(20) DEFAULT NULL COMMENT 维修工ID, audit_time datetime DEFAULT NULL COMMENT 审核时间, assign_time datetime DEFAULT NULL COMMENT 派单时间, start_time datetime DEFAULT NULL COMMENT 维修开始时间, finish_time datetime DEFAULT NULL COMMENT 维修完成提交时间, confirm_time datetime DEFAULT NULL COMMENT 学生确认时间, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 报修提交时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修工单表;这张表有两个细节值得留心。工单编号order_no建议用日期加序号的方式生成比如20250512001这种格式既保证唯一性又带有时间信息比纯自增主键更符合业务直觉。状态字段用tinyint存数值而不是直接存中文好处是状态流转逻辑在Java代码里清晰可控不会因为字符串拼写不一致导致数据错乱。你拿到源码后先对一下这两张表如果结构大致相似说明业务模型是正常的可以直接进入下一步。3.3 后端核心接口实现后端接口的设计我建议所有的响应都统一用一个Result类包装包含code、message、data三个字段。前端拿到响应后第一个判断code是否为200再决定是取data还是弹出错误提示。这个响应结构的统一是前后端联调顺畅的前提。很多源码里每个接口返回的数据格式都不一样前端接起来异常痛苦这就是设计时没有统一定义的后果。先从登录接口说起。用户登录成功之后后端会签发一个token返回给前端前端的每次请求都在Header里带上这个token后端通过拦截器校验token从而识别当前登录用户。毕设项目里token的实现方案一般有两种一种是使用JWT无状态、自带过期时间需要引入JJWT依赖另一种是直接把随机字符串存到Redis里做服务端session管理。如果你的项目没有引入Redis直接上JWT更简单。JWT的基本工作流程是登录时根据userId和角色生成token拦截器每次从Header里取token解析成功就放行解析失败返回401。这个机制能有效保护需要登录才能访问的接口。报修提交这个接口是整个系统最常用的接口实现的逻辑要完整。前端传来的表单数据包含报修类型、故障描述、图片路径、宿舍楼、房间号等信息后端要做好这些字段的校验不能允许description为空。下一步要组装一张工单记录生成唯一的order_no把status设置为0待审核然后保存入库。保存成功后还应该给宿管员发一条站内通知提醒有新的报修待处理。这个通知的载体用前面说的通知表即可等宿管员下次登录后台时可以在通知列表里看到。完整流程串起来的代码如下Override Transactional(rollbackFor Exception.class) public Result submitRepair(RepairOrderReq req, User currentUser) { // 基础参数校验 if (StringUtils.isBlank(req.getDescription())) { return Result.error(故障描述不能为空); } RepairOrder order new RepairOrder(); order.setOrderNo(generateOrderNo()); order.setStudentId(currentUser.getId()); order.setRepairTypeId(req.getRepairTypeId()); order.setDescription(req.getDescription()); order.setImages(String.join(,, req.getImages())); order.setDormitoryId(req.getDormitoryId()); order.setRoomNo(req.getRoomNo()); order.setStatus(0); // 默认没有派给任何维修工 repairOrderMapper.insert(order); // 给宿管员发一条待办通知 notificationService.sendToRole(ROLE_DORM_ADMIN, 新报修工单 order.getOrderNo() 待审核); return Result.success(order.getId()); }工单状态流转的接口设计同样重要。审核、派单、接单、完成、确认这几个动作虽然名称不同但核心逻辑都是“把工单从某个状态更新到另一个状态”。你可以把状态流转的核心逻辑抽取成一个方法在方法参数里传入目标状态和当前登录用户内部先做状态机校验校验通过再更新工单并记录响应时间字段。比如派单时更新assignee_id和assign_time完成时更新finish_time。这样一套简洁统一的流转逻辑既好维护也便于在答辩中解释。3.4 前端页面搭建与接口对接前端页面结构按照角色来切分比较清晰登录页不区分角色登录成功后根据返回的角色信息跳转到对应首页学生端界面有“我要报修”“我的报修单”“消息通知”三个主要页面管理端有“报修审核”“派单管理”“人员管理”“统计看板”等页面维修工则看到“我的任务”和“历史任务”。所有页面都通过Vue Router统一管理路由同时配合路由守卫做权限控制在访问每个路由之前检查当前用户角色是否在允许列表里不在就跳转到登录页或者403页面这个机制就是前面说的前端权限控制。页面和接口对接的关键点在于Axios的封装配置。统一的request工具要在Axios实例里设置baseURL指向后端地址在请求拦截器里从localStorage取token并注入Header。例如import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 业务错误统一提示这里可以用UI库的Message组件 return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { // 未登录或登录过期跳回登录页 router.push(/login) } return Promise.error(error) } )如果你的前端是在Vue CLI的开发服务器下运行端口默认8080后端也是8080这就出现了跨域问题。开发阶段最简单的解决办法是在Vue CLI的vue.config.js里配置代理转发请求/api开头的接口时转发到localhost:8080这样前端请求的URL和页面在同一个域下浏览器不会触发跨域拦截。配置类似这样module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }每个页面的实现细节就不逐个罗列了说两个最容易让前端同学翻车的点。列表页的分页参数要跟后端的PageHelper或者其他分页插件对齐。比如后端接收的请求参数是pageNum和pageSize前端传参就必须用这两个字段名传on或length都会导致查询无效而且报错信息可能很隐晦。状态显示要做到“存数值、显示中文”后端返回的status是数字前端用状态映射表做一次转换再去渲染。这里可以定义一个字典对象const statusMap {0: 待审核, 1: 待派单, 2: 维修中, 3: 待确认, 4: 已完成, 5: 已取消}渲染时取statusMap[status]就行。表格列要加双引号包装不至于在页面上显示一个干巴巴的数字。4. 常见问题排查与答辩准备4.1 运行期常见报错与解决方案运行毕设源码的过程中几乎每个人都会遇到几个典型的报错问题。我根据自己的实战经验把出现概率最高的四类问题整理成一个速查表你照着排查就行报错现象根因分析解决步骤项目启动时报数据库连接失败数据库账号、密码或IP和配置文件不一致或MySQL未启动检查application.yml里的spring.datasource配置确认用户名密码正确数据库已创建MySQL服务已启动登录接口报401或无权限token生成失败或拦截器放行路径配置错误检查JWT签名密钥是否一致检查拦截器里excludePathPatterns是否正确放行了login接口和静态资源前端页面请求接口404前端代理没生效或后端接口路径不匹配先直接用Postman请求后端接口确认存活再检查vue.config.js代理配置尤其注意contextPath是否拼接重复上传图片后页面无法回显静态资源映射没有配置检查WebMvcConfig里的addResourceHandlers确认物理路径和URL前缀对应正确关于“SpringBoot版本太高”的问题多说一句。一些老源码项目基于SpringBoot 2.4.x而你本地Maven仓库拉到了最新依赖可能会导致启动失败或接口行为变化。解决优先级从高到低是锁定项目的SpringBoot版本为源码所依赖的版本、注意JDK版本匹配、不要随意升级大版本。像SpringBoot 3.x相比2.x就修改了不少底层实现不少老写法无法兼容所以守株待兔、按源码版本走才是正路。另外一个比较常见的坑是前端打包后部署时“刷新页面404”。这是因为Vue Router默认使用HTML5 History模式所有路由都由前端接管后端没有针对这些路径配置转发规则服务器收到请求后找不到对应文件就会返回404。解决办法是给后端加一个controller把所有非静态资源的请求都转发到index.html入口或者在后端配置统一把404请求指向index页面。部署时如果直接把前端dist目录里的内容拷到SpringBoot的static目录下建议在application.yml里设置好静态资源路径同时确保Vue Router用hash模式或后端有转发规则。4.2 答辩高频问题与项目亮点提炼答辩是毕设的最后一关技术评分通常取决于你对系统设计逻辑的熟悉程度。评委老师不会要求你背代码但一定希望你对自己做出来的东西有通透的理解。我帮你想了几个高频问题和对应的回答思路你可以提前组织好语言。“你的系统权限是怎么控制的”这个问题建议从后端和前端的配合来回答。后端使用拦截器对需要登录的接口统一鉴权解析token里的用户角色校验该角色是否有权限访问当前接口前端用路由守卫控制页面级访问不同角色登录后只能看到自己角色对应的菜单和页面。两层控制结合保证了数据安全也提升了用户体验。“工单状态是怎么流转的”这道题是核心题。你要拿状态机模型来解释说明状态从提交到归档的每条路径、每次流转的触发条件、以及状态变更记录了对应用的时间字段。如果方便可以在白板上把状态流转图线画出来面试官一看就知道你真的理解了业务而不是背了一堆名词。“统计报表的数据是怎么来的”如果评委对统计看板感兴趣你就从SQL层面讲。说明哪些指标用到了GROUP BY、JOIN、时间函数数据源是哪几张表。这里特别强调一点统计逻辑尽量做成后端接口前端只负责展示。这样不管以后数据量变大还是统计口径变化都只需要改后端SQL不用动前端代码。把接口和页面解耦是很专业的系统设计思路。项目亮点方面建议重点包装三点一是工单状态机的结构化设计体现了对业务流的理解二是前端权限控制和后端接口鉴权的双重安全机制三是通知消息驱动的待办闭环——报修、审核、派单、维修、确认每个环节都有通知触达整个流程不再是一个黑盒用户随时知道自己的需求进行到哪一步了。这三个点已经足够让你的项目在同类毕设中显得有设计感答辩时讲透其中一个就能撑起主要分数。最后再分享一个亲测有效的建议拿到源码后不要急着跑起来先花一个晚上把表结构和接口文档通读一遍画一张系统的功能清单和接口清单把每个接口的路径、入参、出参、业务作用都整理出来。跑通之后再对着源码一行一行读核心业务代码尤其是状态流转和权限校验部分自己亲手改一个功能比如加一个维修类型或者调整一个状态名称。这个过程会让你对系统的理解有一个从“会跑”到“懂做”的质变答辩时的自信和气场是不一样的。这个项目是我个人认为毕设选题里性价比最高的一类业务真实、链路完整、技术主流、扩展空间大。做完之后如果你想继续演进还可以考虑接入WebSocket实现维修进度的实时推送、用ECharts让统计看板更漂亮、或者加一个小程序端让学生随手拍照报修。这些都是加分项但在做之前先把主流程吃透、跑通、讲清楚这才是毕设真正的根基。
返回列表