
简介医院挂号小程序毕业设计资料包面向需要完成毕业设计或课程设计的计算机专业学生。项目基于微信小程序与SSM框架配合MySQL数据库实现患者管理、医生管理、科室管理、专家信息管理、预约信息管理、取消预约申请管理等核心功能业务覆盖完整可直接用于课题参考。后台管理端基于SSM框架开发管理员可在浏览器中登录对系统数据进行维护操作简便。包体共858个文件包含后端源码、前端页面、小程序界面文件、数据库脚本、演示视频、毕业论文文档以及大量界面截图压缩包约46.83MB结构清晰。已有180人学习。论文含可行性分析、系统分析与设计、数据库设计等章节视频演示各功能操作便于快速搭建环境、掌握SSM与小程序开发思路。整体适合作为同类型挂号系统的毕业设计范本。1. 医院挂号小程序毕业设计SSM微信小程序这套组合究竟能做出什么“医院挂号小程序-毕业设计基于微信小程序SSMMySQL开发”——这句话近期在毕业设计选题库里出现频率不低。它讲的是一套前后端分离的预约挂号系统微信小程序端提供科室浏览、医生查询、时段选择、提交挂号与记录管理服务端用SSMSpringSpringMVCMyBatis实现业务接口MySQL负责用户、科室、医生、排班、挂号记录这些数据的存取。对计算机专业的毕业生来说这个方向比单纯做个网页管理系统多了一层小程序端的约束和适配问题也比只写后端接口的选题多了一个能直接演示的入口。这篇笔记会把架构拆开给出建库脚本、后端接口、小程序页面和避坑清单目标是让你照着把它跑起来。2. 技术栈拆解SSM、微信小程序、MySQL各自承担的角色与数据流拿到这个标题第一步不是急着写代码而是先把三个技术角色摆清楚。很多毕业生栽在“什么是SSM都说不清”上答辩老师一问就露馅。SSM不是指某一个小程序插件而是Spring、SpringMVC、MyBatis三个框架的组合它们和MySQL、微信小程序之间有一条清晰的调用链路。2.1 SSM三层Spring管Bean、SpringMVC管接口、MyBatis管SQLSSM里三个框架的活儿分得很细Spring是容器负责创建和装配对象也就是把Controller、Service、Mapper这些类都交给它管同时把事务边界画在Service层SpringMVC负责接收HTTP请求把URL映射到Controller方法上再把返回对象序列化成JSON回给前端MyBatis负责和MySQL打交道Mapper接口定义方法XML文件写SQL。一次最普通的挂号请求数据流是这样的小程序端调用wx.request发起POST请求带着patientId、doctorId、scheduleId这些参数URL先落到SpringMVC的DispatcherServlet由它找到RegistrationController里对应的方法Controller不写业务直接调ServiceService里开启事务先查排班是否还有号再调MyBatis的Mapper执行UPDATE和INSERTMyBatis把JDBC的ResultSet映射成Java对象逐层返回最后SpringMVC通过ResponseBody把结果变成JSON小程序解析后渲染页面。这里我要强调一个SSM新手容易犯的错Controller里不要直接调Mapper那等于把三层结构压成一层。论文里写“三层架构”没问题代码里也要真的分三层。Service层哪怕只是透传也必须存在因为事务注解Transactional只能加在Service方法上直接写在Controller会被Spring的事务代理忽略这是SSM的经典翻车点。2.2 微信小程序端的运行限制域名白名单、https与登录取巧微信小程序不是普通网页前端它运行在微信的沙箱环境里有几条硬性规则请求的URL必须是HTTPS域名开发阶段可以用“不校验合法域名”绕过坑我后面细说每个请求都要经过微信的域名校验不在白名单里的域名直接报fail用户信息不能直接拿到手机号必须通过wx.login换取code再拿code去后端换openid。这三个限制直接决定了你的接口设计方式。本地开发时最常见做法是让后端监听http://localhost:8080小程序开发者工具里勾选“详情→本地设置→不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。注意这个勾选只对开发者工具生效真机预览时还是要靠合法域名所以很多人的项目在模拟器里好好的一装到手机上就全挂了。登录这块我不建议在毕业设计里走复杂的微信授权流程简化的做法是首页让用户输入手机号和密码后端校验成功后把userId和token返回小程序存在Storage里。这个方案的优点是答辩时逻辑清楚论文里也好写“基于Token的认证机制”。如果你想把微信登录也做进去就加一个wx.login获取code的步骤把code发给后端后端调微信接口拿openid再拿openid去t_user表里匹配——成本高一点但显得项目更完整。2.3 为什么毕业设计钟爱SSM可写、可讲、可持续扩展“为什么不直接用Spring Boot”这是答辩老师最爱问的问题。你要能答上来Spring Boot确实把配置自动化了但SSM把每个组件的装配过程暴露出来方便在论文里写“系统架构设计”这一章比如SpringIoC容器怎么初始化、SpringMVC的DispatcherServlet怎么拦截请求、MyBatis的SqlSessionFactory怎么构建。这些在Spring Boot里都是黑匣子一句话带过论文篇幅凑不够也体现不出你对框架的理解。从实际交付角度看SSM项目部署在Tomcat上和微信小程序端是完全解耦的后端可以单独启动测试小程序端只需要知道接口地址。项目数据表结构清晰一个科室、一个医生、一个排班、一个挂号记录扩展性也有——想加管理员端就再加一个角色字段和几个管理接口想加支付就再加一张订单表。这些在毕业设计评分表里都是“功能完整性”的加分项。3. 数据库设计与SSM后端落地从建表到挂号接口完整实现后端部分我按“先建库再搭工程最后写接口”的顺序来。这套顺序本身也对应论文里的数据库设计章节你截图放到论文里就是现成的素材。3.1 核心表设计用户、科室、医生、排班、挂号记录五张表我做的方案里建立了一个名为hospital_db的库涉及五张表。t_user存患者信息t_department存科室t_doctor存医生并外键关联科室t_schedule存某位医生在某天的上下午可挂号总数与已挂号数t_registration存挂号单。先执行建库脚本CREATE DATABASE IF NOT EXISTS hospital_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hospital_db; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, phone VARCHAR(11) NOT NULL UNIQUE COMMENT 登录手机号, password VARCHAR(64) NOT NULL COMMENT MD5加密存储, name VARCHAR(20) NOT NULL, id_card VARCHAR(18) COMMENT 身份证号挂号时需要, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE t_department ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 科室名称如心内科, location VARCHAR(100) COMMENT 楼层位置如门诊楼3层, description VARCHAR(255) ) ENGINEInnoDB; CREATE TABLE t_doctor ( id INT AUTO_INCREMENT PRIMARY KEY, department_id INT NOT NULL, name VARCHAR(20) NOT NULL, title VARCHAR(20) COMMENT 职称主任医师/副主任医师/主治医师, introduction TEXT, avatar VARCHAR(200) COMMENT 头像URL可空, KEY idx_department (department_id) ) ENGINEInnoDB; CREATE TABLE t_schedule ( id INT AUTO_INCREMENT PRIMARY KEY, doctor_id INT NOT NULL, work_date DATE NOT NULL COMMENT 出诊日期, period VARCHAR(10) NOT NULL COMMENT AM上午 / PM下午, total INT DEFAULT 20 COMMENT 该时段最大挂号量, booked INT DEFAULT 0 COMMENT 已挂号数量, UNIQUE KEY uk_doctor_date (doctor_id, work_date, period) ) ENGINEInnoDB; CREATE TABLE t_registration ( id INT AUTO_INCREMENT PRIMARY KEY, patient_id INT NOT NULL COMMENT 患者关联t_user, doctor_id INT NOT NULL, schedule_id INT NOT NULL COMMENT 关联t_schedule冗余方便统计, reg_date DATE NOT NULL, period VARCHAR(10) NOT NULL, fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 挂号费, status TINYINT DEFAULT 0 COMMENT 0已挂号 1已取消 2已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_patient (patient_id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB;这段脚本有几个细节说明所有表都用InnoDB引擎因为挂号涉及事务MyISAM不支持行锁事务这是血泪经验t_user.phone设为唯一键注册时可以直接用INSERT的DuplicateKey判断手机号是否已存在省一次查询t_schedule做了联合唯一索引防止同一医生同一天同一时段被插入两条排班t_registration把reg_date和period做了冗余查记录列表时不用再连表查schedule这是用空间换查询速度。另外字符集务必用utf8mb4而不是utf8utf8mb4是真正的四字节UTF-8否则存emoji或者特殊符号会报错中文偶尔也会出现乱码。3.2 搭建SSM骨架pom依赖、Spring配置与web.xml后端工程我用Maven管理依赖。pom.xml核心依赖就六个spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java、jackson-databind。这里要特别提醒Spring版本和JDK版本必须匹配。我用的是Spring 5.2.x JDK 1.8这个组合最稳换了Spring 6就要JDK17起步很多毕业生的Tomcat还停在9乱配会产生兼容性事故。properties spring.version5.2.22.RELEASE/spring.version mybatis.version3.5.6/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.10/version /dependency /dependencies注意mysql-connector-java我用了5.1.49对应MySQL 5.7如果你装的是MySQL 8.0驱动要换成com.mysql.cj.jdbc.Driver且版本改为8.0.x。这一点很容易踩坑驱动类名不同连接串写法也不同MySQL 8还要额外指定useSSLfalseserverTimezoneAsia/Shanghai不然启动就报时区错误。接下来是Spring和MyBatis的整合配置。老工程习惯写XML配置我建议你也写XML不要用全注解配置因为XML配置在论文里截图非常直观。核心是让Spring扫描mapper接口、构建SqlSessionFactory、管理事务public class RootConfig { // 用Java配置替代部分XMLSpring支持混合方式 }说句实话纯Java配置类有个缺点太分散。我实际操作时用的是spring-mybatis.xmlweb.xml组合。spring-mybatis.xml里最关键的是这三段context:component-scan base-packagecom.hospital.service/ bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/hospital_db?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword valueroot/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.hospital.mapper/ /bean bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertxManager/这段配置我会重点解释mapperLocations指向classpath下的Mapper XML目录MyBatis启动时会把XML里的SQL和Mapper接口绑定mapUnderscoreToCamelCase设置为true是关键开关没有它数据库的create_time字段无法自动映射到Java的createTime属性后面接口返回的字段全是null这是高频翻车点。连接串里的useUnicodetruecharacterEncodingutf8也要写否则中文参数传输时大概率出现乱玛。3.3 后端接口实现登录、科室列表、医生列表、提交挂号现在开始写控制器。我一般把统一返回结构封装成一个ResultT类包含code、msg、data三个字段小程序端只认这个结构。Controller层用SSM常用注解最顺手RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody UserLoginDTO dto) { User user userService.login(dto.getPhone(), dto.getPassword()); if (user null) { return Result.error(400, 手机号或密码错误); } return Result.success(user); } }RestController等于Controller加ResponseBody省得每个方法都写返回JSON的注解。RequestBody把前端传的JSON字符串反序列化成DTO对象这里有个细节前端传的字段名必须和DTO属性名保持一致后端用驼峰命名前端JSON也要用驼峰否则反序列化出来全是null查半天都查不出是字段名不匹配。登录的逻辑里还有一层加密密码不能明文存库我习惯用MD5加盐Spring自带的DigestUtils.md5DigestAsHex就能搞定不需要引额外依赖。科室列表和医生列表接口相对简单重点是提交挂号这个接口它是整个项目的核心也是最容易出并发问题的地方Service public class RegistrationServiceImpl implements RegistrationService { Autowired private RegistrationMapper registrationMapper; Autowired private ScheduleMapper scheduleMapper; Transactional Override public boolean create(RegistrationDTO dto) { // 1. 尝试占用排班号源 int rows scheduleMapper.increaseBooked(dto.getScheduleId(), 1); if (rows 0) { throw new BusinessException(该时段号源已满); } // 2. 插入挂号记录 Registration reg new Registration(); reg.setPatientId(dto.getPatientId()); reg.setDoctorId(dto.getDoctorId()); reg.setScheduleId(dto.getScheduleId()); reg.setRegDate(dto.getRegDate()); reg.setPeriod(dto.getPeriod()); reg.setFee(dto.getFee()); registrationMapper.insert(reg); return true; } }对应的Mapper XML里increaseBooked是一条原子UPDATE语句update idincreaseBooked UPDATE t_schedule SET booked booked 1 WHERE id #{scheduleId} AND booked lt; total /update这个设计的逻辑是MySQL的行锁保证同一时刻只有一个请求能更新同一条schedule记录booked total作为剩余条件的判断如果受影响行数为0说明号源已经挂满直接抛异常。这个方案比“先SELECT再UPDATE”安全得多后者在高并发下会超卖。论文里可以把这条SQL单独拿出来讲解属于系统设计中的“并发控制”亮点。Transactional保证步骤1和步骤2要么都成功要么都回滚不会出现号源被占但挂号记录没插上的情况。到此后端核心接口就跑通了可以用Postman做简单冒烟测试。写完接口不意味着完事还要在web.xml里配置DispatcherServlet和Spring的ContextLoaderListener这两处配置路径写错会导致Tomcat启动后接口404我在避坑章再展开。4. 微信小程序端复现登录、列表加载与挂号提交小程序端的页面结构我按“登录→首页科室列表→医生列表→排班选择→确认挂号→挂号记录”这条主流程来搭。前端页面数量控制在六个左右撑起“完整业务闭环”的人设代码量也不至于失控。4.1 小程序目录结构与app.json页面注册一个微信小程序的目录结构包含全局配置文件app.json、全局逻辑app.js、样式app.wxss以及若干页面目录。每个页面目录下有四个同名文件wxml、wxss、js、json。先说app.json它决定了整个小程序的页面栈和窗口表现{ pages: [ pages/login/login, pages/index/index, pages/doctors/doctors, pages/schedule/schedule, pages/confirm/confirm, pages/records/records ], window: { navigationBarTitleText: 医院挂号, navigationBarBackgroundColor: #07c160, navigationBarTextStyle: white }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/records/records, text: 我的挂号 } ] } }pages数组里第一项是启动页我把登录页放在第一位——但这会带来一个小问题登录成功后需要通过wx.switchTab跳转到tab页面而tabBar里的页面必须已经注册。wx.redirectTo不能跳tab页面这个API限制非常容易踩坑。tabBar目前只有两个页面首页和挂号记录医生列表、排班、确认页都是普通页面用wx.navigateTo跳转。页面目录的JSON文件虽然看起来是空的但它可以覆盖全局配置比如隐藏导航栏或者改页面标题。我建议每个页面的json都加一句usingComponents: {}这是微信小程序的规范模板不写虽然也能跑但如果后面引入组件库这里就是注册入口。4.2 请求封装统一baseUrl、返回码拦截与微信登录处理小程序原生wx.request的写法啰嗦而且每个页面重复写一遍success/fail会非常乱。我一般封装一个Promise风格的请求模块放在utils/api.js里const BASE_URL http://localhost:8080; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json }, success(res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.removeStorageSync(token); wx.redirectTo({ url: /pages/login/login }); } else { reject(res.data.msg || 请求失败); } }, fail(err) { reject(err); } }); }); } const api { login: (data) request(/api/user/login, POST, data), getDepartments: () request(/api/department/list, GET), getDoctors: (departmentId) request(/api/doctor/list, GET, { departmentId }), getSchedules: (doctorId) request(/api/schedule/list, GET, { doctorId }), createRegistration: (data) request(/api/registration/create, POST, data), getMyRegistrations: (patientId) request(/api/registration/list, GET, { patientId }) }; module.exports api;BASE_URL默认用http://localhost:8080这只是为了让开发者工具能直接跑。真机调试时要改成你电脑的局域网IP比如http://192.168.1.101:8080并且手机和电脑要在同一个WiFi下。这种切换我一般写在一个常量里答辩演示前改一次就行。请求封装还有一层作用统一处理后端返回的code。后端规定code 200是正常400是业务错误比如号源满了401表示登录过期。小程序端在封装的reject逻辑里把msg抛出来页面调用的地方统一wx.showToast显示避免每个页面重复写错误提示。4.3 挂号主流程页面实现科室→医生→排班→提交登录页的逻辑比较简单表单收集手机号和密码调api.login成功后把返回的userInfo存到Storage里然后wx.switchTab到首页。重点放在从科室列表到提交挂号的这条链路上我以排班选择页为例说明完整做法// pages/schedule/schedule.js const api require(../../utils/api); Page({ data: { doctorId: 0, schedules: [] }, onLoad(query) { const doctorId parseInt(query.doctorId); this.setData({ doctorId }); this.loadSchedules(doctorId); }, loadSchedules(doctorId) { api.getSchedules(doctorId) .then(list { this.setData({ schedules: list }); }) .catch(err { wx.showToast({ title: err, icon: none }); }); }, onSelectSchedule(e) { const schedule e.currentTarget.dataset.schedule; wx.navigateTo({ url: /pages/confirm/confirm?doctorId${this.data.doctorId}scheduleId${schedule.id}regDate${schedule.work_date}period${schedule.period} }); } });WXML端面的绑定逻辑是wx:for遍历schedules数组每个排班卡片展示日期、上午/下午、剩余号量。剩余号量计算逻辑放在wxml的标签里用{{schedule.total - schedule.booked}}直接算这是小程序数据绑定的优势省去了后端额外返回一个remain字段。但注意如果排班数据量变大这种计算会重复执行需要迁移到js里预处理好再setData这里只是毕业设计量级问题不大。确认挂号页才是真正提交流程的半场。用户选好时段后确认页展示医生信息、日期时段、挂号费点击提交按钮时从Storage里取出userInfo组装参数submitRegistration() { if (this.data.submitting) return; this.setData({ submitting: true }); const userInfo wx.getStorageSync(userInfo); const query this.data; api.createRegistration({ patientId: userInfo.id, doctorId: query.doctorId, scheduleId: query.scheduleId, regDate: query.regDate, period: query.period, fee: query.fee }).then(() { wx.showToast({ title: 挂号成功, icon: success }); setTimeout(() { wx.redirectTo({ url: /pages/records/records }); }, 1500); }).catch(err { wx.showToast({ title: err || 挂号失败, icon: none }); this.setData({ submitting: false }); }); }这里的this.data.submitting开关很关键。小程序页面里用户连点两次提交按钮会发起两个相同请求后端虽然靠事务能保证不超卖但会留下两条日志且用户体验很怪。设置一个布尔值在请求期间锁定按钮是最简单的防重提交方案。wx.redirectTo跳转到挂号记录页而不是navigateTo是因为确认页在提交成功后已经没有回退价值redirectTo会把当前页从栈中替换掉避免用户按返回键又回到提交页重复提交。挂号记录页要展示不同状态的记录我习惯用filter标签切换“全部/已挂号/已完成/已取消”每种状态对应一个status的数字映射在js里维护一个状态字典。列表底部加一个“下拉加载更多”的逻辑数据分页onReachBottom触发时page加1把下一页数据concat到当前数组后面每页的pageSize设为10条这样不太费后端。这类操作在毕业设计里属于锦上添花但答辩时演示出来很加分。5. SSM小程序项目避坑五个翻车现场与排查方法毕业设计项目里功能可以不大但稳定性必须够。答辩现场最怕的就是“我在寝室能跑换个环境就崩”。下面是五个最常出现的翻车现场每个都按“现象→原因→解决”梳理。5.1 接口500但后端日志干净SpringMVC扫描包漏了注解现象小程序请求一直报500但后端Tomcat控制台没有任何异常堆栈日志干干净净。这个问题玄学在于它不是每次都报错而是访问某个接口固定报错其他接口正常。原因SpringMVC的context:component-scan只扫描了com.hospital.controller包而某个Controller里依赖的Service实现类在com.hospital.service.impl包没有扫描到导致Spring创建Controller时找不到依赖。但Tomcat日志没有打堆栈是因为Spring注释了部分异常实际上点开“详情”能看到BeanCreationException的根因。解决把component-scan的base-package改成com.hospital或者在spring-mvc.xml里增加一个context:component-scan base-packagecom.hospital/。这里要理解Spring父子容器的关系Controller走的是SpringMVC的子容器Service和Mapper走父容器如果子容器扫描了Service实现会导致事务失效所以扫描范围要刻意控制。5.2 MyBatis查询返回字段全为null驼峰映射没开启现象数据库表里字段有值用Navicat能查到但调用接口返回的JSON里每个字段都是null。查MyBatis日志发现SQL执行正常、参数正确就是返回的Java对象属性是空的。原因数据库字段是下划线风格如create_time、doctor_idJava实体类属性是驼峰风格createTime、doctorIdMyBatis默认不会自动把两者对应起来也不会报错只会默默地填null。这是个典型的黑匣子问题整个链路看起来都对但最终输出是坏的。解决在mybatis-config.xml或spring-mybatis.xml的SqlSessionFactoryBean里开启mapUnderscoreToCamelCasetrue这个配置上面写过。另一种方案是在每个Mapper XML的resultMap里手动映射字段但那样表一多代码就膨胀不值得。开启驼峰映射后重新加载工程即可。检查时看一眼SQL日志是否打印SELECT id, doctor_id, create_time ...如果SQL字段和下划线风格正确多半就是这个开关没开。5.3 真机预览所有请求失败合法域名和局域网IP冲突现象开发者工具里一切正常数据加载、挂号、取消都流畅。一旦用手机微信扫码预览所有接口请求全部失败提示“request:fail url not in domain list”或者直接超时。原因微信小程序的真机环境强制校验域名白名单本地开发的http://localhost:8080不在任何白名单里。开发者工具里的“不校验合法域名”只对模拟器生效真机不认。就算你把localhost换成局域网IP浏览色也依然提示域名不合法。解决分两步。短期方案是微信公众平台后台“开发管理→开发设置→服务器域名”里添加后端域名但这个域名必须备案且走HTTPS毕设阶段一般没有现成域名应急方案是在开发者工具里点“真机调试”按钮通过调试二维码进入调试模式下真机也不校验域名可以用来做演示长期方案还是把后端部署到云服务器用Nginx做HTTPS反代再配一个解析好的域名。毕业设计答辩通常用“真机调试”就够了。5.4 MySQL中文变问号建库字符集与连接串不配套现象通过小程序提交的中文姓名、科室名存到数据库后就变成“???”但用Navicat手动插入中文没问题。更奇怪的是系统里原有的中文数据查询正常新插入的中文就会变问号。原因这个坑有两种来源。第一种是建库时没指定字符集默认用了库的latin1或者utf8的旧配置第二种是JDBC连接串里没加characterEncodingutf8导致客户端连接MySQL时用了服务器默认的编码。我的一个经验Navicat手动插入中文正常说明数据库端的字符集没问题问题多半出在连接串身上。解决建库语句里显式写DEFAULT CHARACTER SET utf8mb4不要依赖MySQL默认。连接串加上useUnicodetruecharacterEncodingutf8。还有一个容易被忽略的点小程序端wx.request请求头的Content-Type要设置为application/json;charsetutf-8虽然我封装里只写了application/json但保险起见还是把charset显式带上。改完连接串后需要重启Tomcat不是热重载能生效的。5.5 列表下拉加载更多重复请求小程序onReachBottom触发时机现象挂号记录页下拉到页面底部触发加载更多但一次性触发了两次请求列表数据重复拼接出现两条一模一样的记录。原因onReachBottom默认在页面滚动到接近底部时触发但如果在极短时间内连续触发且每次触发都在调接口、接口还没返回时再次触发就会出现重复请求。有些Android机型的触发高度阈值和iOS不同更容易连发。解决在Page实例上维护一个isLoading标志位加载期间对后续触发直接return数据返回后再解锁。另外在onReachBottom里判断当前已加载条数是否小于总条数避免发起多余的请求。具体实现是onReachBottom() { if (this.data.loading || this.data.page * this.data.pageSize this.data.total) { return; } this.setData({ loading: true }); this.loadNextPage(); }不过要注意onReachBottom的触发条件是页面的滚动容器是整个页面。如果你用了自定义的scroll-view并设置固定高度onReachBottom就不工作需要在scroll-view的bindscrolltolower事件里实现。这两种方案的差异经常被忽略程序员在模拟器上测得好好的真机一滚动就失灵。6. 联调验收与答辩演示把毕业设计交付成完整闭环整套系统跑通以后离“能交”还差一步你需要一份自测清单和一套演示预案。先说自测。用Postman把后端每个接口都过一遍重点测试三个场景正常挂号返回成功、号源已满时返回业务错误、重复预约同一医生同一天返回提示。这三条是答辩老师最关注的并发与边界处理。改完代码用mvn package重新打war包部署到干净的Tomcat路径下再跑一遍冒烟测试——很多项目的坑在于只在IDEA里能跑部署到Tomcat就404或类冲突。演示视频的录制我吃过亏。第一次录视频用手机横屏拍电脑屏幕画面抖动还反光。后来改用OBS录开发者工具窗口和浏览器窗口分辨率设置成1920x1080录之前把开发者工具里的“不校验合法域名”勾选确认一遍。视频里不要只演示快乐路径最好穿插一个失败场景比如故意选一个已满的时段提示“该时段号源已满”这能展现出你做了防御性设计比一直演示成功更有说服力。视频时长控制在15分钟以内开头3秒先展示系统架构图让评审老师快速知道你做了什么。答辩演示时有个小技巧先打开数据库的t_registration表实时展示数据写入再打开小程序进行挂号操作提交成功瞬间马上刷新数据库表展示新插入的记录。这三步连贯起来数据流就清清楚楚地呈现在评委面前了。我最后还想提醒一句SSM项目里不要用外置Tomcat的server.xml改端口来迎合代码统一用8080端口。另外所有前端请求的baseUrl建议改成相对路径加代理或者写一个config文件统一管理避免答辩现场临时改IP改出低级错误。这套项目从选题到交付真正花时间的不是写代码而是把“为什么这样设计”想明白希望帮到你。本文还有配套的精品资源点击获取