尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot前后端分离预约挂号系统:分时段号源防超卖设计

SpringBoot前后端分离预约挂号系统:分时段号源防超卖设计 每年三四月份打开校园毕设群和开发社区被问得最多的一个题目就是“SpringBoot前后端分离的网上预约挂号系统”。这个选题火得很有道理一是背景好讲“互联网医院”“智慧门诊”是实打实的行业方向二是业务链完整——从用户注册、科室医生展示、医生排班到在线挂号、退号能覆盖数据库设计、接口开发、前端交互、部署上线的全流程。但也正因为业务场景熟悉很多人反而把这个项目做浅了——挂个号就往里插一条订单记录号源怎么管理、超卖怎么防止、分时段预约怎么建模、退号之后号源怎么回补一问三不知。这篇博文就以基于SpringBoot的互联网医院分时段预约平台这个典型毕设项目为对象把它从0到1的关键设计思路、核心代码实现和实际踩坑点完整拆开讲一遍。内容会覆盖总体架构、数据库建模、后端接口、前端联调、上线演示全程用我在真实项目里落地过的方案尽量少说空话。正在做计算机毕业设计的同学、想转医疗信息化方向的后端新手都可以直接参考这套思路去搭自己的项目。1. 为什么预约挂号系统是毕设常青树也是最容易做“浅”的项目1.1 这个选题到底在考核什么能力预约挂号系统表面看是“用户挑医生、选时间、提交订单”三件事实际上它是一个小型的交易系统。任何一个交易系统都有三个绕不开的命题资源建模、状态流转、并发控制。放在挂号场景里这三个命题就变成了资源建模医生在某一天某个时段有多少个号号源属于排班还是属于时段明细状态流转挂号单从待支付到已预约、已完成、已取消、已退号每一步的触发条件和数据变化是什么并发控制大量用户同时抢一个热门医生的号怎么保证不会超卖。如果一开始没想清楚这三件事后期改起来非常痛苦。我在帮人看代码时见过不少项目把“挂号”做成了直接往订单表里插记录不管排班剩余数量也不做任何状态校验这种实现演示的时候确实能跑通但只要老师追问一句“号源不够了怎么办”直接就露馅。1.2 网上抄来的项目为什么总差一口气毕设圈有个现象很多人在GitHub上找一个现成的挂号项目改个页面就交上去。这种做法最大的问题不是“抄袭”而是很多开源项目本身也只做了界面和基本CRUD真正核心的分时段排班模型和号源防超卖处理恰恰是缺失的。老师是见过真项目的他问的问题往往不在这套系统“能不能挂号”而在于“复杂场景下你怎么设计”。所以这篇博文的核心不在于教你怎么“做出一套能用的系统”而在于把设计决策讲清楚为什么排班要拆两张表为什么扣号源要用数据库原子更新而不是先查询再更新为什么退号要做事务回补1.3 这套系统的参考定位我做的这个版本前后端分离SpringBoot做后端服务Vue做前端页面MySQL存业务数据Redis做缓存和可选的号源预占JWT做登录鉴权。功能上包括医生排班管理、分时段号源生成、用户在线挂号、订单状态管理、退号与号源回补、简单的用户和管理员登录。整套项目如果全职投入三周左右能做完按毕设节奏每天两三个小时一个半月比较稳妥。下面每一章我会把最影响项目质量的设计点单独拿出来讲。2. 技术选型与总体架构设计前后端分离不只是“拆两个项目”2.1 技术栈清单与选型理由先把我的技术选型摆出来不是因为这些技术最新而是因为它们在毕设场景里最容易落地、出问题最好查、老师看着也眼熟。层级技术选型理由后端框架SpringBoot 2.7.x稳定资料多对JDK 8友好适合教学和毕设持久层MyBatis-Plus单表CRUD不用写SQL分页插件好用学习成本低数据库MySQL 8.0主流关系型数据库事务特性完善缓存Redis 5缓存科室医生列表、验证码进阶可做号源预占鉴权方案JWT SpringBoot拦截器前后端分离下无状态鉴权的标准方案比Session简单直观接口文档Knife4jSwagger增强自动生成接口文档答辩演示时很加分前端Vue 2 Element UI AxiosVue 2资料多Element UI组件全适合快速搭建管理端和用户端构建部署Maven npm Nginx后端打jar包前端build后走Nginx反向代理这里重点说两个容易被忽略的决策不用Spring Security而是用拦截器JWT我不反对Spring Security但它对新手不友好SecurityConfig里稍微配置错一点项目就起不来或者接口全被拦截。挂号系统的权限模型并不复杂拦截器里校验token、按路径匹配角色完全够用而且代码逻辑一目了然答辩时也讲得清楚。Redis在这个项目里不是必需品但强烈建议加至少把“科室列表”“医生列表”这种几乎不变的字典数据缓存到Redis展示你对缓存的理解。至于号源预占这种高并发设计可以放在“可扩展点”里讲不一定全部实现。2.2 前后端分离的物理结构与通信方式前后端分离的重点不在于“用了Vue”就算分离而在于二者独立开发、独立部署、只通过HTTP接口通信。实际项目里我建议把工程分成三个目录hospital-admin前端用户端C端挂号页面hospital-serverSpringBoot后端服务hospital-doc数据库脚本和部署文档。放在一个总的仓库里管理提交方便给对方看代码也清晰如果有精力也可以拆成两个Git仓库但毕设没必要。后端统一的接口前缀是/api前端Axios实例统一配置baseURL到/api这样开发环境下用Vite或者Vue CLI的proxy把/api代理到localhost:8080生产环境由Nginx把/api反向代理到SpringBoot服务避免跨域这个无底洞。这套方案我在第5章还会详细说提前预告一句跨域问题90%不是后端CORS配置不对而是前后端环境代理没弄对。2.3 后端包结构与模块边界很多同学写SpringBoot项目Controller里又是校验又是业务逻辑又是SQL包结构乱七八糟。一个好的包结构应该是按“业务域”而不是按“技术类型”划分的。我用的包结构如下com.hospital ├── controller # 请求入口只做参数接收和结果封装 ├── service # 业务逻辑层核心规则都在这 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体 ├── dto # 请求/响应对象避免直接暴露实体 ├── common # 通用返回体、异常处理、常量 ├── config # 配置类CORS、MyBatis-Plus、Redis等 ├── interceptor # JWT拦截器 └── utils # JWT工具类、日期工具类等关键原则就一句话Controller里别写业务Mapper里别写复杂逻辑。例如“取消挂号”这个功能Controller只接收订单号和用户ID调registrationService.cancel(orderNo, userId)真正的事务回补逻辑全部在Service层。这样不管你自己维护还是老师抽查代码都会舒服很多。3. 分时段预约的核心业务建模号源、排班与订单状态机3.1 什么是分时段预约为什么不能只搞一个“上午/下午”分时段预约的本质是把医生一天的门诊时间切成多个时间段每个时间段有独立的号源数量。比如某个科室医生周一上午8:00-12:00门诊我可以切成8:00-8:30、8:30-9:00……每半个小时一批号每批20个号。这样做的好处是患者不用一早就去排队取号按照预约时段来院即可医院也能平摊人流。但如果只是做毕业设计切成“上午/下午两批”也不是不行只是系统含金量会低一档。因为“上午/下午”本质上就是两个号池无法体现你对于时空粒度和资源细分的理解。我强烈建议做半小时或一小时时段难度增加并不大但接口、页面、演示效果都会上一个档次。3.2 核心表结构排班表与时段明细表理解了分时段概念数据库建模的核心就不难了。我用了五张核心业务表t_user用户表患者、医生表、管理员都用这一个表用角色字段区分t_department科室表t_doctor医生表关联科室t_schedule医生排班主表记录某医生某天上午/下午出诊t_schedule_slot排班时段明细表记录排班下的具体时段和号源t_registration_order挂号订单表。排班表和时段明细表是关键我给出SQL-- 排班主表 CREATE TABLE t_schedule ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, doctor_id BIGINT NOT NULL COMMENT 医生ID, work_date DATE NOT NULL COMMENT 出诊日期, period_type TINYINT NOT NULL COMMENT 时段类型1上午 2下午, total_slots INT NOT NULL DEFAULT 0 COMMENT 总号源数, remaining_slots INT NOT NULL DEFAULT 0 COMMENT 剩余号源数, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0默认 1已约满 2停诊, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_period (doctor_id, work_date, period_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表;时段明细表我把它和号源绑定在一起CREATE TABLE t_schedule_slot ( id BIGINT NOT NULL AUTO_INCREMENT, schedule_id BIGINT NOT NULL COMMENT 排班ID, start_time VARCHAR(10) NOT NULL COMMENT 开始时间如08:00, end_time VARCHAR(10) NOT NULL COMMENT 结束时间如08:30, total_count INT NOT NULL COMMENT 该时段总号数, remain_count INT NOT NULL COMMENT 该时段剩余号数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可约 1已满 2停用, version INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_schedule_id (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT排班时段明细表;注意t_schedule里的remaining_slots是冗余的也可以用SQL实时统计子表。我故意留了它目的有两个一是列表页显示“总共还剩多少个号”时不用聚合查询子表效率高二是它作为兜底的总号池防止时段明细分了太多导致主表总数对不上。也就是说真正扣号源时要同时扣t_schedule_slot.remain_count和t_schedule.remaining_slots保持总池和子池一致。这条规则我用在事务里扣减顺序是先扣主表总号池通过条件remaining_slots 0来保证再扣子表时段号池通过remain_count 0来保证。两个都成功了挂号才继续。3.3 挂号订单状态机设计订单状态是指“挂号单”这个业务对象从创建到失效的完整生命周期。我设计了五个状态用数字常量管理状态值含义说明0待支付号源已锁定但订单未支付超时30分钟自动释放1已预约支付成功演示项目里常模拟支付号源真正占用2已完成就诊时间已过表示完成就诊3已取消用户主动取消待支付或已预约状态下可取消4已退号已预约的号在规则允许时间内退掉号源返还为什么要单独区分“待支付”和“已预约”因为号源锁定是一个临时的资源占用如果不做这个状态用户点了一下挂号号就被永久占了过了几天他不管了号一直浪费掉。真实医院里是“先支付定金或直接付款才算真约到号”。在毕设演示中很多同学不想接支付那至少要做到创建订单后进入“待支付”提供一个“模拟支付”按钮点击后状态变“已预约”。这个小小的状态机设计能让答辩老师瞬间觉得你对业务场景有真实理解。3.4 订单表字段设计CREATE TABLE t_registration_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号如RU202403011200001, user_id BIGINT NOT NULL COMMENT 挂号用户ID, doctor_id BIGINT NOT NULL COMMENT 医生ID, schedule_id BIGINT NOT NULL COMMENT 排班ID, slot_id BIGINT NOT NULL COMMENT 时段明细ID, visit_date DATE NOT NULL COMMENT 就诊日期, period_type TINYINT NOT NULL COMMENT 上午/下午, slot_time VARCHAR(20) NOT NULL COMMENT 如08:00-08:30, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已预约 2已完成 3已取消 4已退号, price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 挂号费, pay_time DATETIME DEFAULT NULL, cancel_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_schedule_id (schedule_id), UNIQUE KEY uk_user_slot (user_id, slot_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT挂号订单表;这里有两个容易忽略的设计点uk_user_slot这个唯一索引是防止同一用户在同一时段重复挂号的最后兜底。即便Service层漏了校验数据库也会报错不会产生脏数据。order_no不要用自增ID当订单号我自己生成规则是“RU yyyyMMddHHmmss 6位随机数”虽然极端情况下可能重复但加唯一索引后如果冲突就重新生成一次完全可接受。4. 后端接口落地从登录鉴权到号源扣减的完整链路4.1 JWT登录鉴权与拦截器登录接口用的是JWT无状态方案。用户输用户名密码校验通过后后端生成一个token里面包含用户ID、用户名、角色过期时间我设置的是24小时public String createToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }然后写一个拦截器AuthInterceptor在preHandle里从请求头Authorization拿到token解析验证把用户信息塞到request的Attribute里后续Controller直接取Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; }拦截器要配置放行路径/api/auth/login、/api/department/list、/api/doctor/list、/api/schedule/list这类查询接口可以匿名访问但挂号、退号、订单查询必须登录。管理员接口再写一个角色校验比如AdminInterceptor或者直接在拦截器里判断role值。这个方案比Spring Security轻量得多也符合毕设项目体量。4.2 医生排班发布接口排班发布是管理端的核心功能它的逻辑是管理员选一个医生、选日期、选上午/下午、填总号数和时段粒度后端自动生成t_schedule主表记录同时按粒度生成t_schedule_slot子表记录。我按30分钟粒度生成8:00到11:30、14:00到17:00的时间段代码大致如下Transactional(rollbackFor Exception.class) public void createSchedule(ScheduleCreateRequest request) { // 校验医生存在、日期是否已排过 Schedule schedule new Schedule(); schedule.setDoctorId(request.getDoctorId()); schedule.setWorkDate(request.getWorkDate()); schedule.setPeriodType(request.getPeriodType()); schedule.setTotalSlots(request.getTotalSlots()); schedule.setRemainingSlots(request.getTotalSlots()); scheduleMapper.insert(schedule); ListScheduleSlot slots buildSlots(request, schedule.getId()); slotMapper.batchInsert(slots); }buildSlots方法里需要根据开始时间、结束时间、每时段号数去切分。这里有一个细节如果总号数是60每个时段20个号那一个上午三个时段但如果总号数不能整除多余的号就放到最后一个时段。这种“余数兜底”的逻辑实现简单却能避免演示时“总号数对不上”的尴尬。4.3 查询可预约列表前端首页需要展示“某科室某医生的可预约排班”我设计了一个聚合查询接口GET /api/schedule/available?doctorIddate返回的数据结构包含排班主信息以及子时段列表。在SQL层面可以用t_schedulejoint_schedule_slot一次性查出来然后按schedule分组返回给前端。查询接口有一个地方容易踩坑日期过滤不要用字符串拼接。MySQL里work_date是DATE类型传参时要用LocalDate类型或正确的日期字符串yyyy-MM-dd否则可能出现时区或隐式转换问题。MyBatis-Plus里直接用LambdaQueryWrapper条件写Schedule::getWorkDate, date不要手动拼SQL。4.4 号源扣减与创建订单事务原子更新这是全项目最核心的一段代码也是答辩老师最爱追问的地方。我先说最常见、但绝对不能用的写法// 错误示范先查再更新存在并发超卖风险 ScheduleSlot slot slotMapper.selectById(request.getSlotId()); if (slot.getRemainCount() 0) { slot.setRemainCount(slot.getRemainCount() - 1); slotMapper.updateById(slot); // 创建订单... }这种写法在“同时只有一个用户操作”时没问题但只要两个请求同时读到remain_count1两个都会判定“还有号”然后都去减一结果剩余号数变成-1这就是超卖。解决方式有同步锁、乐观锁、悲观锁、Redis分布式锁但最简单有效的是数据库原子更新update iddeductSlot UPDATE t_schedule_slot SET remain_count remain_count - 1, version version 1 WHERE id #{slotId} AND remain_count 0 /update这条SQL的意思是在更新时判断剩余号数大于0才允许减1。MySQL的行锁会保证同一时刻只有一条update能成功另一个请求的update受影响行数为0就能判断“号已抢完”。这是一种利用数据库的行级锁乐观条件来实现的原子扣减比先查后更新安全得多。对应的Service方法整体带事务Transactional(rollbackFor Exception.class) public RegisterOrder book(BookRequest request) { Long slotId request.getSlotId(); // 1. 校验用户和排班时段有效性 // 2. 扣子表时段号源 int slotRows slotMapper.deductSlot(slotId); if (slotRows 0) { throw new BusinessException(该时段号源不足请选择其他时段); } // 3. 扣主表总号源 int scheduleRows scheduleMapper.deductRemaining(request.getScheduleId()); if (scheduleRows 0) { throw new BusinessException(该排班总号源不足); } // 4. 判断该时段是否约满约满则更新状态 // 5. 生成订单号并插入订单记录状态为待支付 RegisterOrder order new RegisterOrder(); order.setOrderNo(generateOrderNo()); order.setStatus(0); orderMapper.insert(order); // 6. 往Redis里放一条“订单超时未支付”的延迟处理消息异步操作 return order; }这里有一个微妙的点先扣子表时段号源再扣主表总号源两者都成功才能继续。如果扣了子表但主表扣失败理论上不会发生因为排班总数等于各时段之和事务会整体回滚子表扣减也会回滚。这个就是Transactional的作用。“订单超时未支付释放号源”这个功能毕设里可以用两种方式做简单方式启动一个Spring定时任务每分钟扫描一次t_registration_order把创建时间超过30分钟且状态为“待支付”的订单改成“已取消”同时给对应时段增加remain_count和remaining_slots。进阶方式使用Redis的延迟队列或RabbitMQ延迟消息发送一条“订单超时取消”的消息。对于毕设来说Spring的Scheduled定时任务就够了。但要注意定时任务必须用事务把“取消订单回补号源”包起来否则回补失败会导致号源莫名变少。4.5 退号与号源回补退号逻辑基本上就是挂号的逆操作Transactional(rollbackFor Exception.class) public void cancelOrder(Long userId, String orderNo) { RegisterOrder order orderMapper.selectByOrderNo(orderNo); // 校验归属和状态 if (!order.getUserId().equals(userId)) { throw new BusinessException(无权操作该订单); } if (order.getStatus() ! 0 order.getStatus() ! 1) { throw new BusinessException(当前状态不可取消); } // 回补号源 slotMapper.refundSlot(order.getSlotId()); scheduleMapper.refundRemaining(order.getScheduleId()); // 更新订单状态 order.setStatus(order.getStatus() 0 ? 3 : 4); // 待支付变已取消已预约变已退号 order.setCancelTime(LocalDateTime.now()); orderMapper.updateById(order); }回补号源的SQL也要注意UPDATE t_schedule_slot SET remain_count remain_count 1, version version 1 WHERE id ...这样不管是正常扣减还是回补都走同一个字段不会出现并发下的数据错乱。5. 前端页面开发与前后端联调的避坑经验5.1 Vue工程结构与页面划分前端我用的是Vue 2 Vue Router Vuex/Pinia Element UI。页面规划上划分为公共部分和两个端用户端的首页科室导航、医生列表、排班选择、挂号确认、我的订单管理端的医生管理、排班管理、订单查看。路由权限上利用路由守卫判断本地存的token和role管理端路由要求role为管理员否则跳回首页。如果你使用Vue CLI创建项目推荐在vue.config.js里配置devServer代理module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端开发环境所有/api请求都转发到后端不需要在后端开启CORS的allowCredentialstrue之类的复杂配置。生产环境部署时再用Nginx把/api反向代理到后端前端请求路径完全不用改。5.2 挂号下单页的交互细节挂号页是用户操作最密集的地方交互设计直接影响演示效果。我的流程是选择科室 - 选择医生 - 选择日期 - 选择上午/下午 - 选择具体时段 - 确认订单信息 - 模拟支付。每一步都是单选列表选完自动进入下一步而不是把所有信息堆在一个页面里。这一步在后端接口上对应一个关键点用户在确认订单页停留的时候号源并没有锁定。真正的锁定是用户点击“提交订单”之后才发生的。所以前端不能把后端返回的“剩余号数”当成永恒的提交订单时后端还是重新扣减扣不到就弹“号源已约满”。我在项目里做了个细节优化在前端页面用一个倒计时提示“当前时段剩余X个号数据每30秒刷新”这样既真实又能演示你对实时性的理解。5.3 联调中典型问题排查前后端分离项目联调阶段的问题我整理成一张表都是实际踩过的坑现象原因解决前端请求报404后端接口路径是/api/schedule/list前端请求写成了/schedule/list统一接口前缀检查proxy配置请求到不了后端前端报跨域生产环境忘了配Nginx代理Nginx里location /api { proxy_pass http://后端地址; }日期显示差一天或格式不对JSON序列化LocalDateTime格式没有配置在application.yml统一配置JackSon日期格式登录后刷新页面状态丢失Vuex内存存储刷新即清空把token存localStorage路由守卫时读取页面一直转圈不渲染Axios响应拦截器没处理后端返回结构体里的code字段统一在Axios拦截器判断code非200时Message提示并reject6. 部署、演示与答辩让毕设从“能跑”变成“能讲”6.1 本地打包与运行全流程部署这块不用上Docker毕设没那个必要但你要讲清楚全流程。后端用Maven打包成hospital-server.jar放到服务器或本机mvn clean package -DskipTests java -jar hospital-server.jar --spring.profiles.activeprod前端在项目目录执行npm install npm run builddist目录就是静态资源。生产环境用Nginx托管配置大致如下server { listen 80; server_name localhost; location / { root /opt/hospital/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意try_files这一行作用是让前端路由在刷新时不会404。6.2 初始化数据脚本的作用我强烈建议准备一套完整的初始化SQL包含10个科室、20个医生、未来一周的排班数据。演示最尴尬的事情就是老师想看挂号结果数据库里没有今天的排班。你可以在脚本里用日期函数动态生成未来7天的数据INSERT INTO t_schedule (doctor_id, work_date, period_type, total_slots, remaining_slots, version, status) SELECT doctor_id, DATE_ADD(CURDATE(), INTERVAL n DAY), 1, 30, 30, 0, 0 FROM t_doctor, (SELECT 0 n UNION ALL SELECT 1 UNION ALL SELECT 2 ... UNION ALL SELECT 6) tmp WHERE doctor_id BETWEEN 1 AND 20;这样不管哪天演示数据库里永远有未来七天的可预约排班这个细节很多人没想到。6.3 答辩时最容易被追问的5个问题我在毕设评审和帮人模拟答辩时发现老师几乎都会往这几个方向问提前准备好答案比临场编要稳得多问题1为什么用JWT而不用Session答案思路前后端分离后前端可能部署在多台服务器上Session默认存在单台内存里无法共享JWT是无状态的token里包含用户信息后端只需要验证签名天然支持分布式。缺点是token无法主动失效所以我在Redis里存了token黑名单登出时加入黑名单。问题2多个用户同时抢一个号你的系统怎么保证不超卖答案思路不在Java代码里先查再更新而是用数据库的原子更新UPDATE ... SET remain_count remain_count - 1 WHERE remain_count 0。MySQL的行锁会让这条语句串行执行只有一个请求能更新成功受影响行数为0就说明已经没号了。同时用事务保证号源扣减和订单创建的一致。问题3你Redis用来做什么不用Redis行不行答案思路我用Redis做了两件事缓存科室和医生列表降低数据库压力存储用户token状态。对于号源防超卖核心保障是数据库原子更新不是Redis。但如果要支持真正的秒杀级流量可以前置Redis预占号源用Lua脚本保证原子性这是可扩展方向。问题4医生排班和号源为什么要拆成两张表答案思路排班是“医生某天出诊”的抽象只保存一个出诊事实号源明细是具体到时间段的资源池。拆开之后一个排班可以灵活配置不同数量和长度的时段比如上午有30分钟的时段也有1小时的时段互不影响。问题5定时任务回补号源时如果服务刚好挂了怎么办答案思路定时任务只处理“待支付且超过30分钟”的订单即使这次挂了下次扫描还会扫到因为订单状态没有被更新同时号源回补和订单状态更新在同一个事务里不会出现回补了一半的中间状态。6.4 最后两星期赶工的实操建议如果你现在还处于“啥都没写”的状态只剩两周时间我建议按这个优先级来先搭项目骨架跑通登录和科室列表然后把排班和号源表建好管理员可以创建排班接着把核心挂号流程做成“一个接口跑通”——创建订单扣号源最后快速做几个Vue页面把接口串起来。至于统计报表、用户病历、消息通知这些非核心模块能砍就砍先把主干立住。我个人在给这个项目做验收时最大的体会是老师不会看你做了多少页面而是看你能不能把“预约挂号”这件事讲清楚。哪怕界面朴素一点只要你能现场画出表关系图、讲明白号源是怎么扣的项目的说服力反而比那些界面花哨但业务空洞的好得多。这一点希望你现在就开始重视别等到答辩前夜才来补业务逻辑。
返回列表