ARTICLE DETAIL

资讯详情

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

基于JSP+Servlet的医院预约挂号系统:状态机、事务与避坑指南

基于JSP+Servlet的医院预约挂号系统:状态机、事务与避坑指南 简介基于JspServlet的医院预约挂号系统是一套面向Java Web初学者的完整实战项目适用于课程设计、毕业设计或想了解传统ServletJSP开发流程的开发者。资源包共1451个文件压缩后约31.95MB内部以js、jsp、html等前端资源为主同时包含jar依赖包、xml配置、class编译文件和sql数据库脚本便于直接导入Eclipse或IDEA配合MySQL5.7运行。系统前端覆盖首页展示、用户注册、医生查询和公告信息后端则提供医院信息、科室、医生、会员以及公告管理等模块功能相对齐全可直接在此基础上二次开发。项目采用html、css、JavaScript、jsp、servlet、bootstrap、jQuery、MySQL等传统技术栈有助于理解经典Web应用的请求响应过程。资源还附有部署教程和工具说明降低了环境配置门槛。目前已有134人学习下载对正在完成类似课题的同学具有较高的参考价值。1. 老技术做新业务基于JSPServlet的医院预约挂号系统到底还能不能打医院门诊大厅的排队队伍越来越短不是因为病人少了而是预约挂号把线下流量搬到了线上。基于JspServlet的医院预约挂号系统就是一套用经典JavaWeb三层架构实现的在线挂号平台JSP负责页面展示Servlet负责接收请求和控制跳转JDBC操作数据库完成医生排班、号源管理、用户预约、取消退号这一整条业务链路。你可能觉得Spring Boot都普及到这个份上了JSPServlet是不是该进博物馆了但现实是它在教学、课程设计和中小型医院内部系统中依然大量存在因为它把HTTP请求的生命周期、Session管理、请求转发这些底层机制摊开了给人看业务逻辑反而变得清清楚楚。这篇笔记适合三类人做JavaWeb课设或毕设的学生、想补Servlet/JSP底层原理的转行开发、以及需要在老项目上做维护或二次开发的一线工程师。跟着我往下走你会拿到一张可以直接落地的表结构、一套可运行的代码骨架以及五个你在现成项目里必然会踩的坑。2. 挂号业务的六个状态为什么先画状态机再写Servlet2.1 从放号到就诊的六次状态转换把业务当作状态机来设计很多人一拿到“医院预约挂号系统”这个题目上来就建表、写Servlet、套模板结果做到一半发现退号逻辑和号源回填对不上或者同一个号源被两个人同时挂走。我一般会先做一件事把业务拆成状态转换图再落数据库。预约挂号系统本质上不是一个CRUD系统而是一个状态机系统。一次完整的门诊预约从号源产生到就诊结束至少要经历这么几个状态号源可约、号源锁定、预约创建、取消退号、已完成、已过号。先说号源可约和号源锁定医生排班产生号源时号源是可约状态用户发起预约并且事务提交成功号源变成已预约或已锁定。这里要特意区分“预约创建”和“号源锁定”两个动作因为它们发生在同一次事务里但对应的数据表不同预约表插入一条记录排班表把剩余号源减一。取消退号是预约的逆向操作号源要从已预约回到可约同时预约记录状态改为已取消注意是“改状态”而不是“删记录”因为医院需要留痕。已完成表示患者已经取号就诊。还有一种是过号未到这个状态取决于你做的系统要不要覆盖很多医院管理系统的做法是在就诊时段结束后的半小时内由后台批处理把未取号的预约置为已过号。状态转换要落到数据库上就是在每张业务表里放一个status字段用整数或字符串枚举。比如预约表预约状态status取值BOOKED、CANCELED、COMPLETED、NOSHOW。排班表也要有status用来标记这个排班是否还在放号、是否已经停诊。宁可多写一个状态字段也不要通过“删掉记录”来表达状态取消——删记录一时爽后续想统计退号率、爽约率的时候你真的会后悔。2.2 Servlet、JSP、JDBC分别该负责哪一块三层的边界画在哪里把状态机定好之后再来看技术栈的边界。JSPServlet这套技术最被人诟病的是JSP里塞了大量Java代码页面又乱又难维护。但这个问题不是技术栈的错是分层的错。一个健康的JSPServlet项目应该按这样的职责划分Servlet只做四件事接收参数、解析参数、调用Service层、选择转发或重定向Service层做业务逻辑比如事务控制、号源扣减、状态校验DAO层只做SQL执行和结果集映射JSP只做视图渲染用EL表达式和JSTL标签输出数据不在页面里写Java脚本片段。举一个请求流转的例子。用户从前端页面点击“查询周日呼吸科的排班”浏览器发出GET请求Servlet根据WebServlet注解或web.xml配置映射到DoctorScheduleServlet它在doGet里拿到date和departmentId两个参数调用ScheduleService查询排班列表把结果放到request.setAttribute(scheduleList, list)然后request.getRequestDispatcher(/jsp/scheduleList.jsp).forward()。JSP页面用c:forEach循环输出表格。整个链路是单向的请求进Servlet逻辑进Service数据出DAO视图交给JSP。这套结构的优点是请求生命周期一目了然缺点是所有东西都要自己写。你会发现自己需要写大量样板代码获取参数、判空、类型转换、try-catch。但这恰恰是学框架之前该吃的苦。用Spring Boot时一个PostMapping加一个RequestBody就把参数封装掉了参数到底是怎么从HTTP请求里解析出来的反而成了黑匣子。JSPServlet把这些黑匣子打开给你看。至于页面样式JSP本身就是一个HTML文件可以把Element UI这类前端组件库的静态资源直接拷到webapp目录下引用图标字体、表格样式、表单控件都能直接用不需要重新发明轮子。3. 数据库设计与项目骨架四张表加一个可运行的Maven工程3.1 医生、科室、排班、预约四张表的DDL与关键索引我倾向于用四张表加一张科室表起步用户表、医生表、排班表、预约表、科室表。医生表挂在科室下排班表挂在医生下预约表同时关联用户和排班。下面是一份可以直接执行的MySQL建表脚本。-- 科室表 CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL COMMENT 科室名称, intro VARCHAR(500) COMMENT 科室简介, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表包含患者基本信息和登录凭证 CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(64) NOT NULL COMMENT MD5或BCrypt后的密文, real_name VARCHAR(30) NOT NULL COMMENT 真实姓名, id_card VARCHAR(18) COMMENT 身份证号, phone VARCHAR(15) COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生表 CREATE TABLE doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id BIGINT NOT NULL, doctor_name VARCHAR(30) NOT NULL, title VARCHAR(20) COMMENT 职称如主任医师、副主任医师, photo_url VARCHAR(200) COMMENT 医生照片的静态资源路径, intro VARCHAR(500), KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 排班表一个排班对应一个医生在某天某个时段的一批号源 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL COMMENT 出诊日期, time_slot VARCHAR(20) NOT NULL COMMENT 时段如 08:00-12:00, total_count INT NOT NULL COMMENT 总号源数, left_count INT NOT NULL COMMENT 剩余号源数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1放号中 0停诊, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_doc_date_slot (doctor_id, work_date, time_slot), KEY idx_date_dept (work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约表 CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, appoint_code VARCHAR(32) NOT NULL COMMENT 取号凭证或预约流水号, status VARCHAR(10) NOT NULL DEFAULT BOOKED, -- BOOKED已预约 CANCELED已取消 COMPLETED已完成 NOSHOW爽约 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, cancel_time DATETIME COMMENT 取消时间, UNIQUE KEY uk_patient_schedule (patient_id, schedule_id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段DDL有几个细节值得说。第一所有表都用InnoDB和utf8mb4这是为了事务和中文存储尤其要注意不要用utf8MySQL的utf8不是真正的UTF-8遇到生僻字和表情符号会报错。第二排班表上加了唯一键uk_doc_date_slot同一医生同一天同一时段只能有一个排班从数据库层面杜绝了重复排班。第三预约表上加了唯一键uk_patient_schedule同一个患者不能对同一个排班重复预约。第四排班表加version字段这是为后面处理并发超卖准备的先留在这里。这里还要提醒一个常见误区不要把排班时间只存成一个date字段加一个time字段然后把一天划分成早中晚三个时段。实际项目里医院会把一天拆成很多个时间段比如上午拆成08:00-08:30、08:30-09:00……所以time_slot直接用字符串存时段表述前端展示和后台匹配都简单索引效率也不会成为瓶颈因为单日排班数据量很小。3.2 搭起一个可运行的JavaWeb骨架Maven依赖、Servlet注册与连接池表建好后需要一个能跑的工程骨架。常见做法是用Maven建一个war包项目下面这份pom.xml只保留了核心依赖版本号你自己按本机的JDK和Tomcat版本调整。dependencies !-- Servlet APITomcat提供实现编译期需要 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTL标签库JSP页面循环和条件判断用 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 连接池避免每请求DriverManager.getConnection -- dependency groupIdcommons-dbcp/groupId artifactIdcommons-dbcp/artifactId version1.4/version /dependency /dependencies依赖里最需要注意的坑是servlet-api的scope必须是provided因为Tomcat本身带了Servlet实现你再用打包方式打进去会和容器冲突启动时常见NoSuchMethodError。DBCP连接池用1.4还是2.x看你心情但我建议把连接池配成一个全局单例而不是在每个DAO类里new一个。接下来是Servlet的注册方式。传统方案是web.xml但我更推荐用WebServlet注解省去一个配置文件也更容易在代码里看到URL和Servlet的对应关系。下面是一个登录Servlet的骨架同时展示了请求参数处理、Session写入和重定向的写法。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); PatientService service new PatientService(); Patient patient service.login(username, password); if (patient null) { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, patient); resp.sendRedirect(req.getContextPath() /scheduleList); } }这里有三处值得细说。第一req.setCharacterEncoding(UTF-8)必须放在读取第一个参数之前放在getParameter之后调用就晚了。第二登录失败用forward登录成功用sendRedirect这是避免表单重复提交的经典写法。第三Session里存的是Patient对象而不是用户名字符串后面的订单查询、个人信息页面都能直接用。数据库连接部分我不建议在DAO里用DriverManager.getConnection因为每次都要建物理连接并发一高数据库连接就耗尽。一个简单可行的方案是把DBCP连接池封装成一个工具类启动时初始化一次。上面的登录Servlet调用的PatientService内部就是用这个连接池拿连接的。DAO层负责写SQLService层负责开事务、调DAO、提交或回滚Controller层绝不碰SQL——这条边界如果守住就不会出现几百行的JSP页面。4. 核心功能实现放号、预约、退号三个关键路径4.1 查询医生排班并渲染到JSP页面从请求到表格查询排班是预约系统的门面功能用户打开首页按科室、按日期筛选医生排班看到每个时段的剩余号源然后点击“预约”按钮。下面这段从Service层到JSP的代码是典型的查询链路。// ScheduleService.listSchedules(deptId, date) public ListSchedule listSchedules(Long deptId, String date) { String sql SELECT s.id, s.doctor_id, s.work_date, s.time_slot, s.left_count, s.total_count, d.doctor_name, d.title, d.dept_id, dp.dept_name FROM schedule s JOIN doctor d ON s.doctor_id d.id JOIN department dp ON d.dept_id dp.id WHERE d.dept_id ? AND s.work_date ? AND s.status 1 ORDER BY s.work_date, s.time_slot; // 执行查询把每条结果封装成Schedule对象放入ArrayList }Servlet拿到这个List后放入request作用域并转发到JSP。JSP页面用JSTL循环渲染注意表头、数据行和“剩余号源为0时禁用按钮”这一类细节都在视图层完成。c:forEach items${scheduleList} vars tr td${s.doctorName}/td td${s.title}/td td${s.deptName}/td td${s.workDate}/td td${s.timeSlot}/td td c:choose c:when test${s.leftCount 0} a href${pageContext.request.contextPath}/appointment?scheduleId${s.id}预约/a /c:when c:otherwise span约满/span /c:otherwise /c:choose /td /tr /c:forEach查询日期参数建议前端传yyyy-MM-dd格式后端用LocalDate.parse解析不要用java.util.Date因为后者在时区和格式化上坑很多。分页在这个阶段可以不做先保证核心流程跑通数据量大了再加limit和pageNo两个参数。4.2 提交预约用一条UPDATE防止号源超卖提交预约是整个系统的核心也是最容易翻车的地方。想象一个场景某个专家上午放30个号30个人同时点预约前台的号源显示还剩2个提交的请求却可能有几十个。如果你在Service里先SELECT查left_count判断大于0再UPDATE减1这个过程会有时间差两个请求都读到left_count1都执行了插入号源就超卖了。正确做法是把判断和扣减合成一条原子UPDATE然后在同一事务里插入预约记录。下面是一段可直接参考的Service方法。Transactional // 这里注意JSPServlet项目没有Spring事务要自己用Connection控制 public Long createAppointment(Long patientId, Long scheduleId) { Connection conn null; try { conn DataSourceUtils.getConnection(); conn.setAutoCommit(false); // 第一步原子扣减号源 String lockSql UPDATE schedule SET left_count left_count - 1 WHERE id ? AND left_count 0; PreparedStatement ps conn.prepareStatement(lockSql); ps.setLong(1, scheduleId); int rows ps.executeUpdate(); if (rows 0) { throw new BusinessException(该时段号源已被挂完请选择其他时段); } // 第二步生成预约号并插入预约表 String appointCode AP System.currentTimeMillis(); String insertSql INSERT INTO appointment (patient_id, schedule_id, appoint_code) VALUES (?, ?, ?); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setLong(1, patientId); ps2.setLong(2, scheduleId); ps2.setString(3, appointCode); ps2.executeUpdate(); conn.commit(); return appointCode; } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } }这段代码的逻辑很清晰先执行UPDATE排班表的剩余号源这一步由于WHERE条件里带了left_count 0在MySQL的InnoDB引擎下会锁定这一行直到事务结束。第二个请求执行同样的UPDATE时会被阻塞等第一个请求提交后它再执行时left_count已经减掉如果减到0影响行数就是0从而拒绝本次预约。这里有个参数值得注意appointCode用System.currentTimeMillis()拼接在并发高时可能重复但我在这类小系统中见过更实际的做法。你可以用“日期随机数”或者直接使用数据库里的自增ID预约号本来只用于患者取号时展示不承担唯一约束的重任。真正的唯一约束由uk_patient_schedule承担。还需要注意事务必须手动管理JSPServlet里没有Spring的Transactional所以要么用上面的Connection控制方式要么用ThreadLocal保存当前连接保证Service里多个DAO用的是同一个Connection。用两个连接就会出现扣了号源但预约记录没插入、事务回滚不彻底的问题。4.3 我的预约与退号个人页面回显和状态回退用户预约成功后需要有一个“我的预约”页面展示个人基本信息和预约记录列表。JSP做个人信息展示页面时最容易被忽略的是数据回显。用户登录后点击“个人信息”Servlet把Patient对象塞进request并forward到页面页面上${loginUser.realName}、${loginUser.phone}直接输出到input的value里。退号比预约更讲究状态过滤。用户取消预约时先判断这条预约是否还处于BOOKED状态如果是CANCELED状态再退一次就要报错。退号操作同样是两个更新预约表把status改为CANCELED并记录cancel_time排班表把left_count加回1。这两个改动必须在一个事务里完成不然会出现号源退了但预约状态没改或者反过来。// 退号预约状态从BOOKED改成CANCELED同时释放号源 String updateAppt UPDATE appointment SET status CANCELED, cancel_time NOW() WHERE id ? AND status BOOKED; String releaseSlot UPDATE schedule SET left_count left_count 1 WHERE id ? AND left_count total_count;这里的UPDATE同样带了状态条件影响行数为0就说明这条预约已经处于取消或完成状态业务上不允许重复退号。releaseSlot里left_count total_count是个保险条件防止因为脏数据把号源退超。退号成功后才允许用户再次预约同一时段。如果你把预约记录直接DELETE而不是UPDATE status这个约束判断就无从谈起所以再次强调状态保留优于物理删除。我的预约列表页还有一个体验细节预约状态要渲染成用户能看懂的中文而不是BOOKED这样的英文枚举。JSP里可以直接 fmt: 或者用c:choose逐个映射不要把这个翻译逻辑写进SQL。5. 避坑清单中文乱码、重复提交、连接耗尽这些必须提前知道的五件事5.1 中文乱码POST乱码和GET乱码是两个不同原因现象JSP页面输入“张医生”提交到Servlet后用System.out打印出来是“?医生”或者完全乱码传到数据库也变成乱码。原因POST和GET的编码处理机制不同。POST请求体的编码取决于服务端是否调用request.setCharacterEncoding(UTF-8)而且必须在第一次读取参数前调用GET请求的参数在URL里Tomcat 8.0及以上版本默认URI编码是UTF-8但如果你用的Tomcat版本配置了URIEncodingISO-8859-1那GET参数也会乱码。最隐蔽的是Tomcat 8以下版本GET参数默认ISO-8859-1很多人把页面编码改成UTF-8后GET还是乱码就是这个原因。解决写一个Filter强制所有请求和响应用UTF-8。具体做法是在doFilter里调用request.setCharacterEncoding(UTF-8)再把chain.doFilter传下去。还要在JSP页面顶部声明pageEncodingUTF-8以及数据库连接URL上加上characterEncodingutf8。三个地方配套才彻底解决。5.2 重复提交F5刷新一下预约表多了一行现象用户提交预约后浏览器地址栏还停留在提交的URL上按F5刷新浏览器弹出“确认重新提交”的提示确认后预约表里出现了两条一模一样的记录。原因提交用的是POST提交完成之后服务端用的是forward转发到成功页面没有改变浏览器地址栏的URL。刷新时浏览器把上一次的POST请求重新发了一遍。解决用Post/Redirect/Get模式在所有写操作成功之后不要forward而是sendRedirect到一个只读的查询页面让浏览器的地址栏变成新的GET地址。这个模式在预约系统里尤其重要因为预约是写操作重复提交会产生真实的数据冗余。5.3 连接耗尽DriverManager直连与连接池混用的后果现象系统跑了一天后控制台报错“Too many connections”数据库连接数达到上限整个挂号系统卡死。原因项目里部分DAO用DataSourceUtils从连接池拿连接部分老代码直接Class.forName(com.mysql.cj.jdbc.Driver)然后DriverManager.getConnection。每次请求都建立一条物理连接用完后没有归还连接池连接池最多几十个数据库最大连接数也就一两百量一大必然打满。解决统一使用连接池并且把DataSource初始化成全局单例通过一个静态方法对外提供连接。每次用完连接后要在finally里close。注意close的是连接池里借出的连接不是真正关闭物理连接而是归还连接池。这个区别在DriverManager时代不存在所以在迁移老代码时要格外注意。5.4 页面加载完刷新一次把POST动作刷新成重复订单现象有人在JSP页面头部加了一段 想让页面加载完后自动刷新一次结果发现预约记录出现了两条或者验证码的提示一直说“验证码已失效”。原因这个meta refresh的刷新目标是当前URL。如果当前URL是预约提交的POST请求地址刷新就会重新提交表单如果当前URL是登录页刷新会导致Session中验证码被重新生成用户填的验证码和Session里的对不上。解决不要在写操作页面或者验证码校验页面使用meta refresh。如果需要自动刷新获取最新号源应该刷新一个独立的GET查询接口比如用户停留的排班列表页用setInterval定时向一个只读Servlet发AJAX请求而不是刷新整个页面。JSP页面虽然老但要自动刷新数据可以分离能查的页面交给刷新能写的页面绝不自动刷新。5.5 图片坐标定位JSP在楼层图上做可点击区域为何总是偏现象想在医生出诊楼层图上用带有USEMAP的图像映射给具体位置加一个红色标记。点击某个科室区域时需求是跳到该科室的预约页面但实际点击位置和图片上的标记位置对不上尤其是窗口大小变化后偏移得更明显。原因用标签的area做坐标定位coords属性里的坐标是相对于图片原始像素尺寸的。如果图片在页面里被CSS压缩或放大了坐标没有跟着缩放自然对不上。很多人还漏了USEMAP引用的name属性要大小写一致以及要放在 标签前面不然在部分浏览器里根本不会生效。解决常见做法是固定图片显示宽度比如统一800像素宽让图片原始尺寸和显示尺寸一致。更灵活的做法是给img注册onload事件动态计算实际显示尺寸和原始像素的缩放比例把目标坐标换算后再写入area的coords。对于挂号系统的科室楼层图我一般建议直接固定图片展示尺寸省得在页面里做复杂的坐标换算上线后只需在测试环境验证一次各科室区域的点击边界即可。6. 最后动手验收九个必测场景和一条Spring Boot迁移路径写完了代码下一个问题是怎么验证这套系统真的能跑。我通常不看代码覆盖率和单元测试报告而是直接开浏览器手动过九个场景。第一个是正常预约看号源是否减一。第二个是约满后再约看是否提示约满且不让提交。第三个是同一个用户重复约同一个排班看唯一索引是否拦住报错是否友好。第四个是预约后立即取消看号源是否加回、预约状态是否变成已取消。第五个是取消后再约看是否还能正常预约。第六个是登录后直接访问别人的预约详情URL看是否有越权。第七个是F5刷新提交页看是否产生重复预约。第八个是登录页输入特殊字符看SQL是否报错。第九个是手机号和身份证号输入全角字符看入库后展示是否正常。九项全过这个项目才算能交付。如果你做完这个项目下一步往Spring Boot迁移路线是确定的Servlet对应ControllerJSP换成模板引擎或前后端分离JDBC换成MyBatis或Spring Data JPASession继续用HttpSessionFilter换成Interceptor。迁移时最容易出错的是事务和连接管理因为Spring的Transactional帮你托管了Connection你不需要手动setAutoCommit(false)也不需要finally里close但正因为如此很多人迁移后忘了把原来的手动事务代码删干净导致事务嵌套或连接提前释放。我这些年带项目的习惯是拿到这类管理系统先花半小时把状态机画在白板上再动手指写表结构。预约挂号系统的骨血不在JSP和Servlet的语法里而在号源是怎么被扣减、退号是怎么被回填、哪些操作能并发、哪些操作必须串行。先想清楚这些用什么技术栈只是顺手的事。希望这篇笔记能帮你在做医院预约挂号系统的路上少走一两段弯路至少在下一次踩进“F5刷新重复提交”这个坑里时你能一边翻这篇笔记一边说出那句很多老程序员都会说的话“哦原来又是它。”本文还有配套的精品资源点击获取
返回列表