做前端做了两三年,最焦虑的事情往往不是框架又更新了,而是简历里能写的项目,翻来覆去还是“基于 Vue 的中后台管理系统”。这类项目不是没有价值,而是面试官看过的实在太多了。真正能拉开差距的,是一个业务完整、有多端协作、能体现后端能力的项目。这篇文章要聊的,就是用 SpringBoot + Vue + Uniapp 三端联动,从零实现一个医疗小程序实战项目。
先给一个判断:医疗小程序这个方向,不是因为医疗行业本身多热门才值得做,而是因为它的业务复杂度正好卡在“适合练手”和“足够写简历”之间。它有预约、排班、状态流转、角色权限,业务链条完整;但又不像大型电商那样动辄几十张表、一堆分布式组件。对前端转全栈的开发者来说,这是性价比很高的练习项目。
这篇文章会从数据库设计讲起,一路写到 SpringBoot 后端接口、Uniapp 小程序端、Vue 管理后台,最后给出联调验证的步骤和常见坑的排错清单。读完以后,你可以照着把项目跑通,更重要的是理解每一步“为什么这么做”,而不是背出一堆脚手架代码。
1. 这篇文章真正要解决的问题
先聊一个很多前端同学的困境。日常业务开发中,前端能接触到的内容往往很有限:写页面、调接口、处理组件状态。接口是怎么设计的?数据库表是怎么拆的?预约这种业务的状态是怎么流转的?这些东西如果不在项目中亲手做一遍,光靠看八股文是记不住的。而在跳槽面试中,后端基础又是高频考点,这就形成了一个死循环:不会所以不写,不写所以不会。
全栈项目恰好能打破这个循环。它逼你站在全局视角思考问题:一个预约请求从前端页面发起,经过后端接口校验,落到数据库表中,最后回到前端展示状态。任何一个环节出问题,你都得自己排查。这个过程积累的东西,比单纯刷题扎实得多。项目里遇到的问题越多,你在面试时能讲出的真实细节就越多。
那为什么选医疗小程序?三个原因:
- 业务完整。科室、医生、排班、预约、取消、状态更新,天然存在复杂的关联和状态机,能锻炼数据建模能力。
- 端足够多。用户端是小程序,管理端是 Web 后台,后端是 SpringBoot。这意味着你能在一套业务里同时掌握 C 端和 B 端的开发差异。
- 话题有辨识度。面试官看到“医疗预约小程序”比看到“商城管理系统”更愿意多问两句,因为业务里有真实的设计决策可以聊。
当然,什么人不适合也要说清楚。如果你连 Vue 的基本语法都还不熟,建议先做一个简单的后台管理项目再上手这个。本项目的知识点密度较高,直接硬啃容易劝退。如果你已经能独立完成一个单体管理系统,那这个项目就是你从“能写页面”走向“能设计系统”的必经一步。
2. 技术栈选型与医疗业务场景拆解
2.1 为什么是 SpringBoot + Vue + Uniapp
先说后端。Java 是后端就业市场占有率最高的语言之一,SpringBoot 又是 Java 后端的事实标准。虽然 Node 后端也能做全栈,但如果目标是“提升就业几率”,SpringBoot 的覆盖面明显更广,社区资料、面试题、岗位数量都更充足。对前端转后端的同学来说,Java 的学习曲线虽然比 Node 陡一点,但它的生态成熟度能让你在遇到问题时更容易搜到答案。
再说前端。用户端选择 Uniapp,是因为它可以一套代码同时编译到微信小程序、H5 和 App,开发成本和上线成本都比较低。管理后台选择 Vue 3 + Element Plus,作为传统的 Web 管理端非常顺手。前端与后端通过 RESTful 接口通信,这就是标准的前后端分离架构。
这套技术栈还有一个隐藏优势:你只需要掌握 JavaScript 和 Java 两门语言,就能覆盖三条业务线。对前端转全栈的人而言,学习曲线比“前端 + Go + 小程序原生”的组合平滑很多。这也意味着你不需要在项目启动前先去补一堆陌生语言,可以立刻把精力放在业务逻辑上。
2.2 医疗小程序的典型业务模块
一个完整的医疗小程序项目,通常包含三个端,对应三类角色:
- 用户端小程序:患者注册登录、浏览科室和医生、查看医生排班、预约挂号、查看我的预约和就诊记录。
- 管理后台:管理员维护科室和医生信息、设置排班、查看和审核预约记录、统计预约数据。
- 后端服务:提供所有数据访问和业务逻辑,统一处理身份认证、参数校验、状态流转。
本文不会把三个端的所有功能都写完,而是挑最核心的“预约挂号”作为主线,因为它把科室、医生、用户、预约四张核心表都串起来了。把这条主链跑通之后,其他功能都是锦上添花,而且你会发现后面每个功能都是在重复同一套方法论:设计表、写接口、做页面、联调验证。
2.3 前后端分离与多端复用的架构思路
整个项目的调用链是这样的:
小程序端用户选择科室 → 请求后端获取该科室的医生列表 → 用户选择医生和日期 → 提交预约请求 → 后端校验号源是否充足、是否重复预约 → 生成预约记录 → 用户端展示“待就诊 / 已完成 / 已取消”状态 → 管理后台可以查询用户的预约数据。
这中间最值得关注的设计点是“状态字段不要用中文硬编码”。预约状态应该用数字表示,比如 0 待就诊、1 已完成、2 已取消。后端用枚举或常量管理,前端用字典映射显示文案。这个细节虽然小,但在多人协作项目中非常重要,也是面试官喜欢问的点。如果状态用字符串直接存“待就诊”,后续一旦要改文案,就得动数据库,代价非常高。
3. 数据库设计:核心表结构与建表 SQL
3.1 表结构设计原则
医疗预约系统的数据模型,重点是处理好三个关系:
- 科室与医生:一个科室有多个医生,一对多。
- 医生与排班:一个医生在不同时间段有不同排班,一对多。
- 用户与预约:一个用户可以有多个预约记录,但同一个医生同一天不能重复预约。
设计时遵循几个基本原则:每张表都有主键;时间字段统一用 datetime;状态字段使用 int/tinyint;关联字段加上索引;字符集统一使用 utf8mb4,因为要存用户昵称这类可能包含生僻字或表情的文本。不要小看这些基础规范,很多新手项目挂在面试官面前时,第一眼暴露问题的就是表设计。
3.2 建表 SQL 实现
下面是核心表的建表 SQL,可以直接在 MySQL 中执行。medical_mini是数据库名,执行前先创建数据库:
CREATE DATABASE IF NOT EXISTS medical_mini DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE medical_mini; -- 科室表 CREATE TABLE `department` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `name` VARCHAR(50) NOT NULL COMMENT '科室名称', `description` VARCHAR(500) DEFAULT NULL COMMENT '科室简介', `sort` INT DEFAULT 0 COMMENT '排序号,越小越靠前', `status` TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科室表'; -- 医生表 CREATE TABLE `doctor` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `department_id` BIGINT NOT NULL COMMENT '所属科室ID', `name` VARCHAR(50) NOT NULL COMMENT '医生姓名', `title` VARCHAR(50) DEFAULT NULL COMMENT '职称,如主任医师', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像地址', `introduction` TEXT COMMENT '医生简介', `status` TINYINT DEFAULT 1 COMMENT '状态:1在职 0离职', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_department_id` (`department_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='医生表'; -- 患者用户表 CREATE TABLE `patient` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `openid` VARCHAR(64) DEFAULT NULL COMMENT '小程序openid', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `status` TINYINT DEFAULT 1 COMMENT '状态:1正常 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者用户表'; -- 预约记录表 CREATE TABLE `appointment` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `patient_id` BIGINT NOT NULL COMMENT '患者ID', `doctor_id` BIGINT NOT NULL COMMENT '医生ID', `appointment_date` DATE NOT NULL COMMENT '预约日期', `time_slot` TINYINT NOT NULL COMMENT '时间段:1上午 2下午', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待就诊 1已完成 2已取消', `remark` VARCHAR(200) DEFAULT NULL COMMENT '备注', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_patient_id` (`patient_id`), KEY `idx_doctor_id` (`doctor_id`), KEY `idx_appointment_date` (`appointment_date`), UNIQUE KEY `uk_doctor_date_slot` (`doctor_id`, `appointment_date`, `time_slot`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约记录表';这段 SQL 里有几个值得注意的设计点:
- 预约表用唯一索引
uk_doctor_date_slot,从数据库层面保证“一个医生同一天同一时段不能被重复预约”。这是防止并发重复预约最直接的手段,比只在业务层判断可靠得多。 - 日期和时间分开存储。
appointment_date只存日期,time_slot存上午或下午,查询“某医生某天是否有号”非常快。 - 用户表把
openid设为唯一索引,为微信小程序登录场景做准备。
需要说明的是,这里的设计是一个简化的教学模型:一个医生一天两个时段,每个时段最多一个预约。实际生产系统会引入排班表、号源数量、余号扣减等设计。先把简单模型跑通,再逐步扩展,是练手项目最合理的路径。
4. SpringBoot 后端工程搭建与基础配置
4.1 创建工程与 Maven 依赖
你可以用 Spring Initializr 创建工程,也可以在 IDEA 里新建 Spring Boot 项目。依赖方面,最小集合是 Web、MySQL 驱动、MyBatis-Plus。下面是一个参考的pom.xml依赖片段:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>注意几点:
- 如果使用 Spring Boot 3.x,部分包名会从
javax变成jakarta,拦截器、Servlet 相关代码要对应调整。 - 上述版本号只是演示。实践时以你自己环境中验证过的组合为准,重点是理解每个依赖的作用。
- MyBatis-Plus 是 MyBatis 的增强工具,它把单表 CRUD 封装好了,能省掉大量 Mapper XML,很适合快速搭项目。它不是替代 MyBatis,而是在 MyBatis 基础上少写重复代码。
4.2 配置文件与数据源
在src/main/resources/application.yml中配置数据源:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/medical_mini?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: automap-underscore-to-camel-case让数据库的create_time自动映射到 Java 的createTime,少写很多字段映射代码。log-impl配置成StdOutImpl后,控制台会打印 SQL,联调排错时非常有用,生产环境记得去掉。如果你本机 MySQL 的 root 密码不是 123456,记得改成你实际的配置。
4.3 统一返回结果
前后端分离项目中,接口统一返回 JSON 结构,能大幅减少前后端沟通成本。定义一个通用结果类,比如Result<T>:
// 文件路径:src/main/java/com/example/medical/common/Result.java package com.example.medical.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }这里没有用复杂的错误码体系,200 表示成功。实际项目中可以定义更细的错误码,比如 401 未登录、400 参数错误、500 服务异常,但核心思想是一样的:让前端只需要判断code就能知道请求是否成功。后续所有 Controller 都返回Result<T>,前端请求封装就只需要处理一套结构。
5. 预约挂号核心接口:从需求到实现
5.1 接口设计
预约挂号主流程涉及三个接口:
GET /api/doctor/list?departmentId=1:获取某科室的医生列表。GET /api/doctor/schedule?doctorId=1&date=2025-06-10:获取医生某天剩余号源。POST /api/appointment/book:提交预约。
接口的请求体和返回结构要先约定清楚,再动手写代码。预约提交请求体可以设计成如下 JSON:
{ "patientId": 1001, "doctorId": 2003, "appointmentDate": "2025-06-10", "timeSlot": 1 }对应的 DTO 类也很简单,字段与 JSON 一一对应:
// 文件路径:src/main/java/com/example/medical/dto/AppointmentBookDTO.java package com.example.medical.dto; import lombok.Data; @Data public class AppointmentBookDTO { private Long patientId; private Long doctorId; private String appointmentDate; private Integer timeSlot; }5.2 Controller 实现
新建AppointmentController,代码示例如下:
// 文件路径:src/main/java/com/example/medical/controller/AppointmentController.java package com.example.medical.controller; import com.example.medical.common.Result; import com.example.medical.dto.AppointmentBookDTO; import com.example.medical.entity.Appointment; import com.example.medical.service.AppointmentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/appointment") public class AppointmentController { @Autowired private AppointmentService appointmentService; @PostMapping("/book") public Result<Appointment> book(@RequestBody AppointmentBookDTO dto) { return Result.success(appointmentService.book(dto)); } @GetMapping("/my-list") public Result<List<Appointment>> myList(@RequestParam Long patientId) { return Result.success(appointmentService.listMyAppointments(patientId)); } }Controller 只负责接收参数和返回结果,不写业务逻辑。业务逻辑放在 Service 层,这样接口好测试,也方便后续复用。新手最容易犯的错就是把 SQL 查询、业务判断全堆在 Controller 里,短期看功能能跑,但后续每加一个功能都要动 Controller,维护成本很高。
5.3 Service 层:防重复预约与状态校验
预约的核心业务逻辑在 Service。这里的关键点有两个:一是用户自己的重复预约检查,二是并发场景下的防超卖。先看一个常规实现:
// 文件路径:src/main/java/com/example/medical/service/impl/AppointmentServiceImpl.java package com.example.medical.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.medical.dto.AppointmentBookDTO; import com.example.medical.entity.Appointment; import com.example.medical.exception.BusinessException; import com.example.medical.mapper.AppointmentMapper; import com.example.medical.service.AppointmentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class AppointmentServiceImpl implements AppointmentService { @Autowired private AppointmentMapper appointmentMapper; @Override @Transactional(rollbackFor = Exception.class) public Appointment book(AppointmentBookDTO dto) { // 1. 检查同一用户是否重复预约了同医生的同一天 Long count = appointmentMapper.selectCount(new LambdaQueryWrapper<Appointment>() .eq(Appointment::getPatientId, dto.getPatientId()) .eq(Appointment::getDoctorId, dto.getDoctorId()) .eq(Appointment::getAppointmentDate, dto.getAppointmentDate()) .ne(Appointment::getStatus, 2)); if (count > 0) { throw new BusinessException("您已预约该医生该日期,请勿重复预约"); } // 2. 尝试插入预约记录,依靠唯一索引在数据库层面兜底 Appointment appointment = new Appointment(); appointment.setPatientId(dto.getPatientId()); appointment.setDoctorId(dto.getDoctorId()); appointment.setAppointmentDate(dto.getAppointmentDate()); appointment.setTimeSlot(dto.getTimeSlot()); appointment.setStatus(0); appointmentMapper.insert(appointment); return appointment; } @Override public List<Appointment> listMyAppointments(Long patientId) { return appointmentMapper.selectList(new LambdaQueryWrapper<Appointment>() .eq(Appointment::getPatientId, patientId) .orderByDesc(Appointment::getCreateTime)); } }这段代码做了两层防护:
- 业务层先查询一次同一患者是否重复预约。
- 数据库唯一索引保证即使两个请求同时进来,也只有一条记录能插入成功。插入失败的一方会抛出唯一键冲突异常,由全局异常处理器转换为“号源已被占用”的提示,而不是 500 错误。
真正生产级的预约系统还会配合 Redis 预扣库存、分布式锁等方式控制并发,但作为全栈练习项目,先理解“业务校验 + 唯一索引”的组合,已经能解决大部分问题。面试时能把这一层讲出来,已经比大多数只会调 CRUD 的候选人强了。
5.4 接口测试方式
后端写完可以先用 Postman 或 IDEA 自带的 HTTP Client 测试,不需要等前端:
POST http://localhost:8080/api/appointment/book Content-Type: application/json { "patientId": 1001, "doctorId": 2003, "appointmentDate": "2025-06-10", "timeSlot": 1 }预期返回:
{ "code": 200, "message": "success", "data": { "id": 1, "patientId": 1001, "doctorId": 2003, "appointmentDate": "2025-06-10", "timeSlot": 1, "status": 0 } }如果重复提交同一条数据,应该收到业务异常提示,而不是 500 错误。这一步验证通过,说明后端的核心路径是通的,接下来可以放心进入前端开发。
6. Uniapp 用户端:从页面到请求封装
6.1 项目初始化
Uniapp 项目可以通过 HBuilderX 可视化创建,也可以使用 CLI 创建。对新手来说,推荐 HBuilderX,省去很多环境配置。创建项目时选择“默认模板”即可,因为医疗小程序的首页不需要复杂的 UI 框架支撑,原生组件足够。
项目结构建议如下:
pages/ index/index.vue // 首页:科室列表 doctor/doctor.vue // 医生列表 appointment/book.vue // 预约提交页 order/order-list.vue // 我的预约列表 utils/ request.js // 请求封装页面之间通过uni.navigateTo跳转并携带参数。比如从医生列表进入预约页时,把doctorId通过url参数传递过去。
6.2 预约页面实现
以预约提交页为例,核心代码如下:
<!-- 文件路径:pages/appointment/book.vue --> <template> <view class="book-page"> <view class="card"> <text class="label">就诊日期</text> <picker mode="date" :value="date" @change="onDateChange"> <view class="picker-value">{{ date || '请选择日期' }}</view> </picker> </view> <view class="card"> <text class="label">时间段</text> <view class="slot-group"> <view v-for="slot in timeSlots" :key="slot.value" class="slot-item" :class="{ active: slot.value === timeSlot }" @click="timeSlot = slot.value" > {{ slot.label }} </view> </view> </view> <button class="submit-btn" :disabled="submitting" @click="submitBooking">提交预约</button> </view> </template> <script> import request from '../../utils/request.js' export default { data() { return { doctorId: '', date: '', timeSlot: 1, timeSlots: [ { value: 1, label: '上午' }, { value: 2, label: '下午' } ], submitting: false } }, onLoad(options) { this.doctorId = options.doctorId }, methods: { onDateChange(e) { this.date = e.detail.value }, submitBooking() { if (!this.date) { uni.showToast({ title: '请选择日期', icon: 'none' }) return } this.submitting = true request({ url: '/api/appointment/book', method: 'POST', data: { patientId: uni.getStorageSync('patientId'), doctorId: this.doctorId, appointmentDate: this.date, timeSlot: this.timeSlot } }).then(() => { uni.showToast({ title: '预约成功', icon: 'success' }) setTimeout(() => { // 如果 order-list 是底部 tabBar 页面,这里应改用 uni.switchTab uni.navigateTo({ url: '/pages/order/order-list' }) }, 1500) }).finally(() => { this.submitting = false }) } } } </script>页面的逻辑并不复杂,难点在于和现有项目风格的统一。如果你已经有一个封装好的请求层,页面里就不应该再直接调uni.request,而是统一走封装方法,这样后续做登录态、错误提示、埋点都只需要改一处。
6.3 请求封装与登录态
uni.request本身功能简单,但项目里所有请求都需要统一处理 baseUrl、token、错误提示,所以封装一层request.js非常重要:
// 文件路径:utils/request.js const BASE_URL = 'http://localhost:8080' export default function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else { uni.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) }这里把token从本地存储取出放进请求头,后续做微信登录时,只需要在登录成功后把token存进uni.setStorageSync,所有请求就都能带上认证信息了。这个封装是用户端最值得打磨的部分。页面可以后补,但请求层如果设计不合理,后面每个接口都要跟着改,返工成本很高。
7. Vue 管理后台:补齐管理端的最后一块拼图
7.1 后台选型
管理后台用 Vue 3 + Element Plus 是标准选择。千万不要认为后台不重要,它是整个业务链里不可或缺的一环。没有它,科室、医生、排班这些基础数据根本没地方维护,小程序的科室列表就是空的。
后台的核心页面包括:登录页、科室管理、医生管理、预约记录列表。对练习项目来说,不用做太复杂,能跑通增删改查就算达标。做后台的意义在于体验 B 端开发的节奏:表格、分页、搜索、表单校验、路由权限,这些是另一个高频面试话题。
7.2 预约记录列表页面
预约记录列表是后台最典型的“表格 + 搜索 + 分页”页面,代码示例如下:
<!-- 文件路径:src/views/appointment/index.vue --> <template> <div class="appointment-page"> <el-card> <el-form inline> <el-form-item label="患者ID"> <el-input v-model="query.patientId" placeholder="请输入患者ID" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="loadData">查询</el-button> <el-button @click="resetQuery">重置</el-button> </el-form-item> </el-form> </el-card> <el-card> <el-table :data="list" v-loading="loading"> <el-table-column prop="id" label="预约ID" width="80" /> <el-table-column prop="patientId" label="患者ID" width="100" /> <el-table-column prop="doctorId" label="医生ID" width="100" /> <el-table-column prop="appointmentDate" label="预约日期" /> <el-table-column label="时间段" width="100"> <template #default="{ row }"> {{ row.timeSlot === 1 ? '上午' : '下午' }} </template> </el-table-column> <el-table-column label="状态" width="100"> <template #default="{ row }"> <el-tag :type="statusMap[row.status].type">{{ statusMap[row.status].label }}</el-tag> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="query.pageNum" :page-size="query.pageSize" :total="total" @current-change="loadData" /> </el-card> </div> </template> <script> import axios from 'axios' export default { data() { return { list: [], total: 0, loading: false, statusMap: { 0: { label: '待就诊', type: 'warning' }, 1: { label: '已完成', type: 'success' }, 2: { label: '已取消', type: 'info' } }, query: { patientId: '', pageNum: 1, pageSize: 10 } } }, mounted() { this.loadData() }, methods: { async loadData() { this.loading = true const { data } = await axios.get('/api/admin/appointment/list', { params: this.query }) this.list = data.data.records this.total = data.data.total this.loading = false }, resetQuery() { this.query.patientId = '' this.query.pageNum = 1 this.loadData() } } } </script>注意,上面代码假设axios已经配置了baseURL,或者在开发环境配了代理。实际项目中,你可以在src/main.js里设置axios.defaults.baseURL,也可以在vite.config.js里配置开发代理。无论哪种方式,目标都是让前端代码里不写死接口地址,方便切换环境。
前端状态展示用statusMap做映射,而不是把后端数字当成文案直接用。前端字典和后端状态码对应关系明确,前后端各改各的也不会乱。
7.3 路由与权限
后台通常需要加一个简单的路由守卫,未登录不能访问内部页面。用 Vue Router 的全局前置守卫实现:
// 文件路径:src/router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('admin-token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })这段代码虽然简单,但回答了一个值得思考的问题:管理后台和小程序端的认证方式是有区别的。小程序端通常靠微信登录换取 token,管理后台则是用户名密码登录。两者都依赖 token 识别身份,但获取 token 的方式完全不同。这也是全栈项目里很适合在面试时展开聊的点。
8. 全栈联调:启动步骤、验证路径与常见问题
8.1 启动顺序
建议按以下顺序启动项目:
- 启动 MySQL,确认
medical_mini库和四张表已创建。 - 启动 SpringBoot 后端,确认端口 8080 启动成功,控制台无报错。
- 启动 Vue 管理后台,通过
npm install安装依赖后执行npm run dev。 - 在 HBuilderX 中运行 Uniapp 项目,选择“运行到浏览器”或“运行到微信开发者工具”。
后端启动成功后,先在浏览器访问http://localhost:8080/api/doctor/list?departmentId=1,能返回 JSON 再继续下一步。这能快速判断问题是出在后端还是前端。
8.2 验证路径
推荐按用户真实操作路径验证:
- 科室列表是否有数据。没有的话,先在数据库手动插入科室和医生数据,或在后台管理页面创建。
- 前端提交预约后,数据库
appointment表是否新增了一条记录。 - 重复提交预约,前端是否提示“请勿重复预约”。
- 后台预约列表是否能查询到刚才的数据,状态是否正确展示。
8.3 联调常见问题
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 小程序请求接口 404 | baseUrl 或端口不对 | 看浏览器开发者工具的请求地址 | 把request.js的 BASE_URL 改成后端实际访问地址 |
| 后端接口报跨域错误 | 未配置 CORS | 看控制台 CORS 报错信息 | 后端添加全局跨域配置或使用代理 |
| 数据库中文乱码 | 表和连接字符集不一致 | 查看数据库表字符集 | 统一使用 utf8mb4 |
| 预约重复提交不报错 | 唯一索引未生效 | 查看表结构是否包含唯一索引 | 补上uk_doctor_date_slot唯一索引 |
| 前端传的 patientId 是字符串 | 类型转换失败 | 看后端日志参数解析报错 | 前端转成数字或后端 DTO 字段放宽处理 |
| Vue 后台列表无数据 | 接口返回结构与前端约定不一致 | 直接浏览器访问接口看返回 JSON | 调整后端返回结构或前端解析逻辑 |
跨域是最容易卡住新手的问题。开发环境下,最简单的方式是在后端加一个全局 CORS 配置类:
// 文件路径:src/main/java/com/example/medical/config/CorsConfig.java package com.example.medical.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }allowCredentials(true)配合allowedOriginPatterns("*")在 Spring Boot 2.4 之后是允许的,但生产环境建议把允许来源限定为你的域名,而不是开放给所有来源。这里体现的工程习惯是:开发环境可以宽松,生产环境必须收紧。
9. 最佳实践:练习项目如何变成简历亮点
9.1 代码规范与分层
后端一定要分层。Controller 只做参数接收和结果返回,Service 写业务逻辑,Mapper 只访问数据库。这个分层规范在练习时容易被忽略,但在面试和实际项目中是硬要求。项目代码里最好加上全局异常处理器,把业务异常和系统异常区分开,避免把堆栈信息直接抛给前端。
前端同样要规范。页面组件按功能拆分,公共请求和工具方法放到utils里,接口地址集中管理或按模块拆分。如果你能在简历上写“设计并封装了统一请求层,支持 token 注入和统一错误处理”,比写“熟悉 Vue”有说服力得多。
9.2 业务安全与数据合规
医疗数据属于敏感数据,真实上线前必须考虑数据安全与合规要求。作为学习项目,你至少要做到几件事:密码不能明文存储,必须加盐哈希;接口不能把敏感字段完整返回给前端;后台管理接口需要权限校验,不能对所有登录用户开放全部功能。
这里要特别说明:医疗行业不是普通的 CRUD 业务,真实产品涉及资质、隐私、数据安全等一系列合规要求。本文的项目适合学习技术,不适合未经合规审查就直接上线服务真实患者。这一点在面试中主动提出来,反而能让面试官觉得你有工程风险意识。
9.3 部署与展示
练习项目做完了不要只留在本地。可以部署到云服务器,至少做到:后端用mvn package打成 jar 包运行,前端打包后放入 Nginx,小程序端提交体验版。部署过程中会遇到环境问题、端口问题、日志排查,这个环节本身就是很好的工程训练。
运维方面不需要做得很重,能跑起来、能访问、日志能看到,就足够了。如果你能在简历上写“项目已部署至云服务器,支持小程序端、Web 管理端访问”,这比在本地跑通更能证明你的交付能力。
9.4 简历与面试表达
简历上不要写“完成了预约挂号功能”,而要写清楚难度和方案。例如:
- 设计医疗预约数据模型,解决科室、医生、患者与预约记录之间的状态关联。
- 通过唯一索引与业务校验双重机制,防止并发场景下重复预约。
- 封装多端统一请求层,支持登录态注入、异常统一处理。
面试时被问“为什么选这个项目”,可以这样回答:因为这个项目让我完整走了一遍从数据库设计、后端接口、小程序端到管理后台的全流程,同时让我意识到状态设计和并发控制是预约类业务的核心难点。这样的回答比背概念更能体现真实项目经验。
10. 总结与后续学习方向
回到开头的问题:前端转全栈,到底应该怎么做?答案不是去背更多框架 API,而是亲手做一个业务完整、链路清晰的项目。这个医疗小程序把四张核心表、三条业务端、一套主流程串了起来,跑通它,你就掌握了“从页面到数据库”的完整链路。
跑通之后,后续可以往这几个方向深入:把预约状态扩展成完整的排班和号源体系;引入 Redis 处理高并发预约;用微信登录替换手工传入的 patientId;增加操作日志和后台权限管理。每加一个点,项目深度和面试可聊度都会往上走一截。
最后给一个实际建议:做这个项目时不要直接复制代码,先自己在纸上把表和接口设计画一遍,再动手写。设计过程中犯的错、改过的表结构,才是这个项目给你留下的最值钱的经验。项目本身只是起点,真正值钱的是你通过它建立起来的“从需求到落地的完整视角”。