
毕业设计选医院门诊挂号系统的同学我猜你多半是冲着这个题简单、资料多、容易过去的。说实话这个选题确实适合作为JAVA方向毕设但它真正考察的技术点比看上去多得多SSM框架的整合、JSP服务端渲染、事务与并发控制、数据库状态机设计、多角色权限控制、甚至还有定时任务和部署每一个单独拎出来都是面试题里常问的东西。这篇内容我按自己带项目的思路来写把挂号系统从需求拆解到数据库设计、核心业务、JSP页面细节、权限控制、部署答辩全部走一遍全程以复现和讲清楚为目标不涉及任何多余的空话。照着做你不仅能跑通项目还能在答辩时把每个设计决策的原因说清楚。1. 门诊挂号系统到底在解决什么问题1.1 线下挂号的痛点与线上化目标在动手写代码之前先把需求想明白。医院门诊挂号这件事线下场景里最典型的几个痛点分别是号源不透明患者不知道今天还有没有号、排队时间长窗口和自助机前堆着一堆人、退号困难想取消得再跑一趟、医生排班变化通知不及时。线上化之后系统要解决的核心问题其实不是替代窗口而是把号源变成可查询、可预订、可回退的透明数据。在这个定位下系统的业务目标就很清楚了患者角色登录后能查科室、查医生排班、在线挂号、在线退号、查自己的挂号记录。医生角色查看自己的排班和已挂号患者列表方便接诊时确认。管理员角色维护科室信息、维护医生信息、安排医生排班、查看挂号统计。从技术视角看这个过程本质就是把号源数量这个核心数据在并发访问下安全地减一、加一、查询。想通这一点后面整个系统的设计都会围绕它展开。1.2 系统边界哪些功能该做哪些坚决不做很多同学拿到这个题就忍不住加功能在线支付、电子病历、排队叫号大屏、预约取号二维码……如果你是奔着毕设高分去的这些功能可以加一两个亮点但前提是核心链路先跑通。我更建议把功能边界控制在一句话能说清的范围内必须做登录注册、科室管理、医生管理、排班管理、在线挂号、在线退号、挂号记录查询。加分但不必须公告管理、挂号统计图表、定时取消过期未取号订单、验证码。坚决不做支付对接、真实号源同步、电子病历、问诊对话。为什么毕设答辩时老师最忌讳的就是功能很多但每个都讲不透。挂号系统的核心是号源和状态流转你把这个讲明白比堆十个花哨功能强得多。功能边界定好之后技术选型就顺理成章了。2. 技术选型复盘为什么是SSM JSP 而不是 Spring Boot Vue2.1 SSM 的定位稳定、好讲、够用导师给了java_ssm医院门诊挂号系统这个方向那技术栈基本就是 Spring SpringMVC MyBatis JSP。有同学问现在公司都用 Spring Boot 前后端分离为什么还写 SSM我的看法是毕业设计和企业项目目标不同。企业要的是快速交付和易于维护毕设要的是你理解框架在干什么。SSM 恰好是能讲清楚原理的组合Spring 管理 bean 和事务SpringMVC 负责请求分发MyBatis 负责 SQL 与对象映射。你答得出为什么用 MyBatis 而不是 JDBC事务注解加在哪一层、为什么答辩就已经赢了一半。2.2 JSP 在这个项目里的真实分工JSP 在这个项目里不是简单的前端页面它是服务端渲染的一部分。Controller 里返回 ModelAndView把数据塞进 ModelJSP 页面里用 JSTL 和 EL 表达式直接渲染。这样做的优势是整个项目只有一个 Tomcat 应用不需要搭前端工程部署简单特别适合单人完成的毕设。实际开发时JSP 放的位置要规范/WEB-INF/jsp 目录下按模块分文件夹比如 admin、doctor、patient、common。放到 /WEB-INF 下的好处是浏览器不能直接访问必须经过 Controller 跳转这既是安全措施也是规范要求。页面里尽量少写 Java 代码片段Scriptlet统一用 JSTL 标签和 EL 表达式不然答辩时老师看到满屏的% %会直接扣印象分。2.3 环境与版本搭配建议这套技术栈对版本很敏感我踩过的坑直接列给你组件推荐版本备注JDK1.8语法特性对老框架兼容最好Tomcat8.5 或 9.0千万别用 Tomcat 10包名变了跑不起来MySQL5.7 或 8.0用8.0记得配驱动区分 com.mysql.jdbc.Driver 和 com.mysql.cj.jdbc.DriverMaven3.6用于管理 jar 包依赖IDEA2020社区版也能跑配置都差不多一个更稳妥的做法是SnakeYAML 别乱升级MyBatis 用 3.5.xSpring 用 5.2.x。所有 jar 用 Maven 管理不要在 WEB-INF/lib 里手动塞包否则换环境必出问题。SSM 整合时最经典的坑是 Mapper 接口扫描没配好导致启动报错后面部署章节我再细说。3. 数据库设计核心是号源而不是挂号单3.1 七张核心表的职责划分我先给你一张表清单这是整个系统里最值得讲清楚的部分。表名职责关键字段t_user登录账号含三种角色id, username, password, role(1患者/2医生/3管理员)t_department科室id, dept_name, introductiont_doctor医生基本信息id, name, dept_id, title, 头像URL, 简介t_schedule排班表核心中的核心id, doctor_id, dept_id, schedule_date, shift(上午/下午), total_number, remaining_number, begin_time, end_timet_patient患者信息id, user_id, real_name, id_card, phone, age, gendert_registration挂号单id, patient_id, schedule_id, doctor_id, reg_code(取号码), status(0取消/1已预约/2已取号/3已就诊/4已退号), create_timet_notice公告可选id, title, content, create_time这张表结构里最容易被忽略、却最重要的两张表是 t_schedule 和 t_registration。可以说整个系统的成败都在这两张表身上。3.2 排班表的设计细节为什么它才是核心很多第一版设计的同学会把号数直接写到医生表或者挂号单表里这是错误思路。正确做法是单独建排班表t_schedule把某个医生在某天某个时段出诊作为一种可排班的资源每个排班记录自带号源总数和剩余数。排班表的设计可以再加一个 unique 约束unique(doctor_id, schedule_date, shift)。这个约束非常关键它的作用是防止管理员给同一位医生在同一个上午重复排班也防止代码层漏判导致数据重复。号源字段 total_number 和 remaining_number 是挂号系统的命脉。total_number 在创建排班时设定比如普通门诊上午 40 个号remaining_number 在每次挂号时递减退号时递增。剩余数通过 SQL 条件更新保证不会扣成负数这是后面防超挂的逻辑基础。3.3 状态字段所有关键业务都用 int 状态控制数据库设计最容易犯的错是用业务描述存状态比如挂号单里存已挂号已完成已取消这种字符串。我的建议是全部用 int 存代码里定义常量类。挂号单的四态- 0已取消患者自己取消但未退号注意与4区分 - 1已预约挂号成功未取号 - 2已取号到院取号后 - 3已就诊医生接诊后标记 - 4已退号完成退号流程号源已返还这里有个容易混淆的点0 和 4 都是取消区别是号源是否已返还。0 是管理员或系统取消但未走退号流程4 是走完退号流程号源已加回来。实际项目里可以把 0 只留给系统异常场景患者主动取消统一走 4这样状态机简单得多。为什么全部用 int因为数据库层面的数字判断是最高效的也方便代码里用常量或枚举做语义映射。答辩时把这一套状态机讲出来属于加分项。4. 核心业务逻辑挂号、退号与防超挂的实现4.1 挂号流程的完整链路挂号动作的业务链路比想象中长因为它同时涉及两张表的修改t_registration 插入一条挂号单同时 t_schedule 的 remaining_number 要递减。这两个操作必须在一个事务里完成否则会出现号扣了但单子没生成或者反之的脏数据。我用 Spring 的声明式事务实现核心逻辑放在 service 层Override Transactional(rollbackFor Exception.class) public Result register(Long scheduleId, Long patientId) { Schedule schedule scheduleMapper.selectByIdForUpdate(scheduleId); if (schedule null) { return Result.error(排班不存在); } if (schedule.getRemainingNumber() 0) { return Result.error(该时段号源已满); } // 插入挂号单 Registration reg new Registration(); reg.setPatientId(patientId); reg.setScheduleId(scheduleId); reg.setDoctorId(schedule.getDoctorId()); reg.setRegCode(generateRegCode(schedule, patientId)); reg.setStatus(1); registrationMapper.insert(reg); // 扣减号源 int rows scheduleMapper.decreaseRemaining(scheduleId); if (rows 0) { throw new RuntimeException(号源扣减失败请重试); } return Result.success(挂号成功, reg); }注意几个细节事务注解里 rollbackFor 必须写成 Exception.class因为 Spring 默认只对 RuntimeException 回滚selectByIdForUpdate 是加行级锁的查询后面讲并发时细说号源扣减影响行数为 0 时必须抛出异常让事务回滚否则单子插了号没扣属于严重的数据不一致。4.2 超挂问题的本质并发不是 SQL 写错很多初次接触挂号系统的同学觉得防超挂很简单查一下 remaining_number 0 就扣呗。单用户场景下确实没问题但一旦并发来了就超卖了。为什么会超卖从原理看两个线程同时执行查询剩余数 → 判断大于 0 → 扣减它们读到的剩余数可能都是 1于是都认为自己能挂号最终两个挂号单都插入成功号源却变成了 0 或 -1。这就是典型的并发竞争问题本质不是 SQL 写错而是先检查后操作这种读改写模式缺少原子性保护。常规解决方案有两种第一种数据库行级锁。查询时使用SELECT ... FOR UPDATE把排班记录锁住事务提交后才释放其他线程必须排队等待。代码就是上面写的 selectByIdForUpdate。它实现简单但要注意 FOR UPDATE 必须放在事务里才生效并且会降低并发吞吐。第二种乐观锁或条件更新。给排班表加 version 字段更新时校验版本号或者用带条件更新update t_schedule set remaining_number remaining_number - 1 where id ? and remaining_number 0返回值影响行数等于 0 说明没抢到号。毕设场景我更推荐第二种它对并发原理的展示更直观也不依赖长事务。SQL 长这样UPDATE t_schedule SET remaining_number remaining_number - 1 WHERE id #{scheduleId} AND remaining_number 0MyBatis 的 update 返回 int代码里判断rows 1才继续插入挂号单。你可以把这个条件更新的含义翻译成只有当还有号的时候才允许扣数据库本身承担了最后的防线。4.3 退号与号源返还的边界处理退号看起来比挂号简单改一下状态把号加回去。但如果退号时没有校验状态就可能出现重复退号同一个挂号单被退两次号源被返还两回系统结算全乱。我把退号逻辑的约束条件写清楚Transactional(rollbackFor Exception.class) public Result cancelRegistration(Long regId) { Registration reg registrationMapper.selectById(regId); if (reg null) { return Result.error(挂号记录不存在); } // 只有已预约/已取号状态才能退 if (reg.getStatus() ! 1 reg.getStatus() ! 2) { return Result.error(当前状态不允许退号); } reg.setStatus(4); registrationMapper.updateStatus(reg); // 号源返还 int rows scheduleMapper.increaseRemaining(reg.getScheduleId()); if (rows 0) { throw new RuntimeException(号源返还失败); } return Result.success(退号成功); }边界处理是三个第一状态校验必须放在事务里否则并发下两次退号都能读到 status1第二号源返还是加一操作不存在扣负数的问题但也要检查 update 返回值因为若排班被删除则行数为 0第三可以设计退号时限比如超过预约日期当天就不能退这个属于业务规则在 service 层加日期判断即可。4.4 定时任务清理到期未取号的挂号单这个是很多成品项目里没有、但实际很需要的功能患者预约了号但当天没来取号就一直被占用着导致后面想挂的人挂不上。在实际系统里这叫爽约治理做法是定时扫描超时的挂号单自动标记取消并返还号源。用 Spring Task 就能实现不需要额外引入 Quartz毕设够用。核心代码Component public class RegistrationTimeoutTask { Scheduled(cron 0 0/30 * * * ?) Transactional(rollbackFor Exception.class) public void autoCancelTimeoutRegistrations() { // 找到预约时间早于当前时间且状态仍为1未取号的记录 ListRegistration list registrationMapper.findTimeoutOrders(new Date()); for (Registration reg : list) { reg.setStatus(0); registrationMapper.updateStatus(reg); scheduleMapper.increaseRemaining(reg.getScheduleId()); } } }要注意 cron 表达式在 Spring 中是六位秒 分 时 日 月 周很容易按 Quartz 的七位去写然后报错。另一点定时任务里的事务粒度是整个方法如果数据量大可以改成每处理一条提交一次但毕设数据量小整体事务更简单。加这个功能之后你在答辩时可以说考虑了实际业务中的爽约场景属于产品思维的加分项。5. JSP 页面容易卡住的几个细节5.1 科室-医生-排班三级联动挂号页最常见的交互是先选科室再选医生再选这个医生的排班时段。三个下拉如果每次刷新页面体验很差而且 JSP 服务端渲染做这种渐进查询很笨拙正确做法是局部刷新。我的实现方案是JSP 页面用 jQuery 发 Ajax 请求后端 Controller 返回 JSON前端渲染这些 JSON 数据生成选项。后端接口示例RequestMapping(/doctor/listByDept) ResponseBody public Result listByDept(Long deptId) { ListDoctor doctors doctorService.listByDept(deptId); return Result.success(doctors); }前端代码要点$(#deptSelect).change(function () { var deptId $(this).val(); if (!deptId) return; $.get(ctx /doctor/listByDept, {deptId: deptId}, function (res) { var options ; $.each(res.data, function (i, d) { options option value d.id d.name ( d.title )/option; }); $(#doctorSelect).html(options); }); });三个细节注意第一JSP 里拿项目根路径统一用${pageContext.request.contextPath}拼 URL否则部署到非根路径时 Ajax 全部 404第二ResponseBody 要引入 Jackson 依赖否则 Controller 返回对象时序列化失败第三切换科室时要顺手清空医生和排班下拉框否则会残留上一个科室的数据这是一个很常见又很烦人的小 bug。5.2 实操题JSP 页面里的图片坐标定位是怎么一回事网络热词里有jsp图片如何对坐标定位这个问题在实际做医院挂号系统时它对应的真实场景一般是两个。第一个是医生简介页要放一张排班示意图或者管理员要给科室上传楼层导览图需要在图片某个坐标位置放一个可点击标记第二个是用户头像上传后需要裁剪定位。实现思路不复杂因为 JSP 最终渲染出来就是 HTML定位这件事靠的是 CSS 和 JS跟 JSP 本身没有关系。如果是要在背景图上放置可点击区域我会建议用 CSS 绝对定位。div styleposition: relative; display: inline-block; img src${pageContext.request.contextPath}/images/dept_map.png usemap#deptMap / map namedeptMap area shaperect coords50,80,150,180 href${pageContext.request.contextPath}/dept/detail/1 alt内科诊区 / area shapecircle coords200,120,30 href${pageContext.request.contextPath}/dept/detail/2 alt外科诊区 / /map /divcoords 里的四个数字rect 是左上角X, 左上角Y, 右下角X, 右下角Ycircle 是圆心X, 圆心Y, 半径。确定这些坐标的最笨但最有效的方法浏览器 F12 打开控制台鼠标悬停在图片目标位置直接读 DevTools 显示的坐标值然后微调。如果是实现点击图片自动填充坐标到表单这种交互可以用 JavaScript 监听 click 事件从 event.offsetX / event.offsetY 拿坐标存进隐藏域。这个方法在管理员指定科室在平面图上的位置这类场景非常实用。5.3 为什么会有页面加载完后刷新一次的需求热搜词里jsp页面让加载完后刷新一次也是真实痛点。我遇到这个需求的场景是提交挂号单成功后页面跳转到列表页但列表数据总是显示旧的数据还有一种是验证码图片在第一次加载时偶尔不出来需要在页面加载后刷新一次。先说验证码场景最简单的方式是给验证码 标签设置 onload 事件或者用 setTimeout 重新赋一下 src注意 src 后面加时间戳参数避免浏览器缓存window.onload function () { var codeImg document.getElementById(codeImg); if (codeImg codeImg.src codeImg.src.indexOf(captcha) 0 !codeImg.complete) { var ts new Date().getTime(); codeImg.src ctx /captcha?timestamp ts; } };再说数据刷新场景。不要在 JSP 页面里用window.location.reload()无脑刷新否则用户填写到一半的表单会被清空而且容易造成重复提交。正确做法是提交成功后后端用 Redirect重定向而不是 Forward转发跳转到列表页也就是经典的Post/Redirect/Get模式。return redirect:/registration/myList;浏览器会重新发起一次 GET 请求自然拿到的是最新数据。如果你用了转发Forward页面 URL 不变化刷新时浏览器会再次提交表单重复挂号就发生了。这个细节我在实际排查用户刷新页面就多出一条挂号记录的 bug 时用过很多次值得写在纸上贴到电脑前。5.4 表单校验里判断字符串是否不是字母和数字这类小问题挂号表单里总有身份证号、手机号、挂号单编号这些字段。搜索引擎里java 判断字符串中是否不是字母和数字是很高频的搜索词换到业务里其实就是格式校验。最省事的是用正则。判断字符串是否只包含字母和数字public static boolean isLetterOrDigit(String str) { if (str null || str.isEmpty()) { return false; } return str.matches([a-zA-Z0-9]); }判断是否包含非字母数字字符也就是题目说的是否不是字母和数字public static boolean containsNonLetterOrDigit(String str) { if (str null || str.isEmpty()) { return false; } for (int i 0; i str.length(); i) { char c str.charAt(i); if (!Character.isLetterOrDigit(c)) { return true; } } return false; }需要说明的是Character.isLetterOrDigit 对中文也返回 true如果你只想判断英文字母和数字必须用正则[a-zA-Z0-9]而不是 Character 方法。手机号校验可以更严格^1[3-9]\\d{9}$身份证号是 18 位前 17 位数字或 X 结尾^\\d{17}[0-9X]$。把这些校验写成工具类放在 common 包比散落到各个 controller 里好维护得多。6. 登录与权限三种角色如何共用一套登录逻辑6.1 用户表与角色字段这个系统有三种角色患者、医生、管理员。最简单可靠的设计是在 t_user 表里直接加 role 字段1 代表患者2 代表医生3 代表管理员不需要单独建角色表和权限表。因为权限差异主要通过拦截器和菜单动态渲染实现并不需要细粒度的权限点管理如果搞 RBAC 五张表反而让人觉得你在堆技术。登录成功后把用户对象和角色放进 session。JSP 页面里根据 role 动态显示不同的菜单栏c:if test${sessionScope.loginUser.role 3} lia href${pageContext.request.contextPath}/admin/dept/list科室管理/a/li lia href${pageContext.request.contextPath}/admin/schedule/list排班管理/a/li /c:if c:if test${sessionScope.loginUser.role 2} lia href${pageContext.request.contextPath}/doctor/registration/list就诊患者列表/a/li /c:if这个按角色动态渲染菜单的优点是简单清晰缺点是权限判断分散在页面里所以 Service 层做核心业务时还需要再校验一次角色不能只靠页面隐藏。6.2 拦截器做权限隔离为什么 Controller 里的判断不够如果只在 Controller 里写 if (role ! 3) return error代码会重复且容易被遗漏。标准做法是定义 SpringMVC 拦截器实现 HandlerInterceptor在 preHandle 里统一处理登录状态和角色权限。LoginInterceptor 的核心逻辑Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { // 未登录跳转到登录页 response.sendRedirect(request.getContextPath() /login); return false; } // 从 handler 里取出注解或请求路径判断权限 return true; }更精细的做法是配合注解RequireRole(3)使用HandlerMethod 可以拿到方法上的注解然后比对当前用户角色。这个方案能让你在 Controller 里直接标注解权限逻辑仍然集中在拦截器里答辩讲起来也算一个亮点。spring-mvc.xml 里配置拦截器路径mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/captcha/ bean classcom.hospital.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors我提醒一个容易踩的坑JSP、CSS、JS、图片这些静态资源也要被拦截如果你只想保护 Controller 路径可以把拦截路径精确到 /admin/、/doctor/、/patient/或者在 exclude-mapping 里放行 /static/。6.3 密码加密与 session 安全问题医院的挂号系统涉及患者隐私信息密码安全在答辩时几乎是必问项。一定不能明文存储密码最低要求是 MD5 加盐。我见过太多直接把密码字段存明文提交的毕设老师只要看一眼数据库截图就能看出来。加盐 MD5 的实现思路很简单注册时生成随机盐值比如 UUID 前 8 位把盐和密码拼接后取 MD5数据库存 salt 和 hashedPassword 两个字段登录时再用输入的密码和数据库里的 salt 拼接后加密比对。BCrypt 是更好的方案加盐逻辑自动处理但需要引依赖毕设用 MD5 自己实现盐也说得过去前提是你要能讲清为什么要加盐不加盐的话相同密码的 hash 值相同黑客用彩虹表一查就破。session 安全方面做好三件事就好第一登录成功用session.setAttribute退出用session.invalidate()销毁整个 session不要自己手动删 attribute第二密码相关的修改操作如改密成功后强制重新登录或让旧 session 失效第三页面隐藏很重要但服务层权限校验更不能少因为 Ajax 请求可以直接绕过页面可见性访问接口。7. 部署与答辩从本地跑通到讲清楚项目7.1 本地部署的完整步骤很多同学项目写了一大半卡在别人电脑上跑不起来这一步。我给出一个在全新环境下从零跑通的清单安装 JDK 1.8配置 JAVA_HOME 和 PATH命令行java -version验证。安装 MySQL 5.7创建数据库hospital_db执行 sql 脚本导入表结构和基础数据。用 IDEA 打开 Maven 项目等待依赖下载完成。settings.xml 里配阿里云镜像否则下载慢到怀疑人生。修改 jdbc.propertiesurl、username、password。MySQL 8 的驱动类名和时区配置跟 5.7 不一样。配置 Tomcat在 IDEA 里添加 Tomcat ServerDeployment 选 war exploded artifactApplication context 改为 /hospital。启动前重点检查mapper 接口是否加了 Mapper 注解或配置了 MapperScan少了它会报 Invalid bound statementweb.xml 的 contextConfigLocation 和 DispatcherServlet 映射是否和 spring 配置文件对应。启动后访问http://localhost:8080/hospital/login管理员账号能登录后台流程跑通就算部署成功。第 6 步的 MapperScan 是 SSM 整合最经典的报错点。解决方案是在 Spring 配置类或 xml 里加mybatis:scan base-packagecom.hospital.mapper/或者在启动类上写 MapperScan二选一即可。7.2 导师最爱问的 6 个问题答辩环节导师手里的问题其实很有套路。我把高频问题整理成表附带回答思路问题回答思路为什么选 SSM 而不是 Spring BootSSM 更偏底层原理能展示 Spring IoC/AOP、SpringMVC 分发机制和 MyBatis ORM 的理解适合教学场景怎么防止同时很多人挂号导致超挂条件更新语句 事务 行级锁讲清检查-扣减必须原子挂号单的状态为什么用 int 不用字符串状态机设计数字更省空间、便于索引和比较代码里用常量映射可读性高事务注解加在哪一层为什么Service 层。Controller 负责参数组装DAO 只做单表操作跨表事务必须在 Service 聚合你有考虑数据一致性问题吗挂号与扣号在一个事务退号返还也在一个事务定时任务清理也有事务保护这个系统能应对并发吗单机 Tomcat 场景优化到数据库乐观锁即可加 Nginx 负载均衡和 Redis 缓存是扩展方向可以说但别展开过度回答的时候要短答 细节先一句话正面回答再补一层实现细节或异常处理。千万不要绕导师问的每个问题都已经圈定了范围。7.3 写完这个项目之后我认为最重要的一件事带过的同学里做完整个系统后真正学到东西的不是那些页面做得最漂亮的而是能把自己写过的每个 mapper 方法、每条核心 SQL 都重新过一遍、反复推演这里挂了会怎样的人。去医院门诊挂号这套题表面上是 CRUD 和页面展示内核是对业务状态和数据一致性的认知。这个认知最直接的体现就是你能不能在黑板上画出挂号单从创建到退号的完整状态流转能不能写清楚扣号 SQL 里remaining_number 0这个条件代替了什么。这些都是很小的点但它们恰恰是区分背项目和懂项目的分界线。把这篇内容里的核心链路亲手敲一遍再带上自己的理解和踩坑记录去答辩你会发现自己比大多数同题目的同学都踏实不少。