社区陪诊系统这个题目,在近两年的毕业设计和简历项目里出现频率非常高。很多人一看到“基于SpringBoot+Vue”就开始发怵,觉得前端后端都得碰,工作量太大。实际上,如果你把需求拆开,会发现它只是一个典型的管理系统加一个小型交易闭环:用户下单、陪诊员接单、服务完成、评价结算。这个复杂度刚好适合用SpringBoot+Vue这套前后端分离方案来承载,既不会简单到没东西可讲,也不会复杂到做不完。
这篇内容我按自己实际开发这类项目的经验来写,覆盖从需求分析、数据库设计、后端接口实现到前端页面联调、打包部署的完整链路。后端的SpringBoot项目怎么初始化、JWT登录怎么做、Redis缓存加在哪些位置,前端的Vue路由怎么传参、Axios怎么封装、打包后布局为什么乱,都会提到。适合准备做毕设、想练全栈项目,或者刚从前端转后端第一次接触Java项目的人参考。
1. 项目定位与整体设计思路
1.1 陪诊系统到底在解决什么问题
先把业务想清楚再写代码,这是我从一开始就坚持的顺序。陪诊服务的核心场景是:老人或者不熟悉医院流程的患者,看病时需要有人帮忙挂号、取号、排队、取药、记录医嘱。家属通常要上班,没法全程陪同,于是就有了“按小时或者按半天购买陪诊服务”的需求。
这里的关键点在于“社区”两个字。它意味着用户不是面向所有陌生人,而是基于社区服务站点来组织陪诊员,像网格员一样被分配到某个社区附近。对应的系统设计就必须包含几个基本角色:患者或家属(下单方)、陪诊员(服务提供方)、社区管理员(审核和监管)、系统管理员(平台维护)。角色身份不同,能看到的数据和操作的功能也不同,这直接决定了后端需要做RBAC权限控制,前端需要做动态路由和菜单权限。
另一个容易忽略的点是“服务过程透明”。家属在上班时也想随时知道老人现在到哪个环节了,是排队中、看诊中还是取药中。所以系统除了基础的下单接单流程,最好加入订单状态流转记录,让用户能实时看到进度。这个需求落到表结构设计上,就是订单主表加订单状态变更记录表,每次状态变化都落一条记录。
1.2 为什么选SpringBoot+Vue这套组合
很多人在技术选型时会纠结,觉得用若依这种开源脚手架改改更快,或者后端也想用Python。从我实际做项目的角度说,SpringBoot+Vue仍然是这类社区服务系统最稳的选择。
SpringBoot的优势在于生态成熟。做接口开发,依赖引入简单,Spring MVC的注解一套就能把Controller写得很干净;做数据访问,MyBatis-Plus一引入,单表CRUD基本不用写SQL;做权限,Spring Security加JWT虽然配置麻烦点,但是网上能找到大量现成方案。这些对于一个需要兼顾“设计实现”和“满足老师/面试官提问”的项目来说,非常重要。
Vue这边,组件化开发很顺手,Element Plus(或Element UI)组件库能快速把后台管理界面搭出来,Vue Router负责前端路由跳转,Pinia或者Vuex管理用户登录信息和全局状态。前后端通过JSON格式交互,职责清晰,接口调试也方便。如果是第一次做前后端分离,Vue的学习曲线也算最友好的那一档。当然Vue和React的区别面试会问,但在实际项目中选Vue就是看中它中文文档全、社区问答多、组件库成熟,遇到问题基本都能搜到答案。
1.3 功能模块与角色权限设计
这个系统的功能模块我习惯拆成五块:用户管理、陪诊服务管理、订单管理、评价管理、消息通知管理。
用户管理包含用户注册登录、个人信息维护、陪诊员资质审核。陪诊员不是注册后就能接单的,需要管理员审核身份证、健康证明、技能证书,审核通过后才会出现在可预约列表里。服务管理包含陪诊服务项目定义,比如半天陪诊、全天陪诊、代办取药、预约检查陪同,不同项目对应不同价格和说明。订单管理是整个系统的核心,从用户创建订单开始,到陪诊员接单、开始服务、结束服务、用户确认、评价结算,一整个生命周期都要覆盖。评价管理比较简单,用户对陪诊员的服务打分和文字评价。消息通知管理,订单状态变化后通过站内消息通知相关方,也可以预留短信或微信通知的扩展点。
角色权限我用一张表就能梳理清楚:
| 角色 | 核心权限 | 典型操作 |
|---|---|---|
| 患者/家属 | 下单、支付、查看订单进度、评价 | 创建订单、取消订单、提交评价 |
| 陪诊员 | 接单、更新服务进度、查看我的订单 | 接单、开始服务、点击结束服务 |
| 社区管理员 | 审核陪诊员、管理本社区订单和用户 | 审核资质、查看服务记录、处理投诉 |
| 系统管理员 | 全局配置、数据统计、系统监控 | 管理服务项目、用户禁用、报表查看 |
权限设计上不需要做到Spring Security的细粒度方法级权限,做到角色级别的拦截就可以。前端根据登录角色动态生成菜单,后端在接口上加角色校验注解或者拦截器判断,双保险,防止有人绕过前端直接调接口。
2. 技术选型、数据库设计与关键参数
2.1 后端依赖怎么选,版本坑有哪些
我创建SpringBoot项目时踩过最大的坑就是版本。SpringBoot 3.x推出后,很多人新建项目直接选最新版,结果发现JDK版本要求17以上,有些旧教程里的javax.包全变成了jakarta.,MyBatis-Plus的适配也有问题。如果你的JDK环境是8,老老实实用SpringBoot 2.7.x,别追新。
推荐一套组合:SpringBoot 2.7.18,JDK 8,MyBatis-Plus 3.5.x,MySQL 5.7或8.0,Redis,jjwt 0.9.x,Lombok。这套组合我跑了多个项目都很稳。如果你非要用SpringBoot 3,就要接受JDK 17和jakarta命名空间迁移,同时确保MyBatis-Plus用的版本是3.5.3之后的兼容版本。
Maven依赖用starter方式引入,核心pom配置大概是这样:
<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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</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.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这里有个小技巧,Maven仓库如果下载慢,可以把仓库地址换成国内镜像,在settings.xml里配置。这个问题卡住过很多新手,其实不是依赖写错,是没下载下来。
2.2 前端技术栈与环境准备
前端我建议直接用Vue CLI或者Vite创建项目。Vue 3配Vite是当前主流,依赖安装快,启动也快。如果你之前只学过Vue 2,第一次上手Vue 3需要适应下组合式API的写法,但其实核心概念还是那些:组件、路由、状态管理、生命周期。
环境准备阶段最容易翻车的是Node.js版本。Vite需要Node.js 16以上,老项目可能要求低版本,新项目用太老的Node会直接报错。建议装Node.js 18 LTS版本,一把梭不纠结。
创建命令很简单:
npm init vue@latest或者走传统方式:
npm install -g @vue/cli vue create community-escort-web依赖安装用npm install,项目启动用npm run serve(Vite下是npm run dev)。如果安装时卡在某个包,先删掉node_modules和package-lock.json重新装,多半是锁文件损坏。
前端技术栈清单:
| 用途 | 推荐方案 |
|---|---|
| UI组件 | Element Plus |
| 路由 | Vue Router 4 |
| 状态管理 | Pinia |
| HTTP请求 | Axios |
| 代码规范 | ESLint + Prettier |
如果你对Element Plus的样式印象深刻,应该知道它的表单和表格组件直接能覆盖管理系统大部分场景。下单页的表单校验、订单列表的分页表格、陪诊员审核页的详情弹窗,靠组件库都能解决,不用自己手搓。
2.3 核心表结构设计
数据库设计是整个项目的地基。很多同学表建得不合理,后面接口写起来各种别扭。我建议按业务域拆表,下面这几张是必须要有的。
用户表(sys_user),统一存所有角色账号,用role_type字段区分患者、陪诊员、社区管理员、系统管理员。陪诊员额外的资质信息放单独一张表,冗余姓名、身份证信息到用户表,避免每次联表查询。
订单表(escort_order)是核心表,字段至少包括:订单编号、下单用户ID、陪诊员ID、服务项目ID、服务日期、开始时间、结束时间、患者姓名、患者联系电话、医院名称、医院地址、病情描述、订单状态、支付金额、创建时间、更新时间。金额建议用decimal(10,2),千万不要用float,精度会出问题。订单编号我习惯用时间戳加随机数生成,或者用“日期+自增ID”格式,方便人眼识别和排查问题。
服务项目表(service_item):项目名称、服务内容描述、原价、折扣价、服务时长、状态。这个表可以直接做成数据字典风格的配置表,管理员后台能增删改。
评价表(evaluation):订单ID、陪诊员ID、评分(1-5分)、评价内容、回复内容、评价时间。评分字段用tinyint,范围限制0到5。一个订单只允许评价一次,所以订单ID要加唯一索引。
订单状态记录表(order_status_log):订单ID、旧状态、新状态、操作人ID、备注、创建时间。每次订单状态变更都插一条记录,前端时间线组件直接查这张表渲染,用户就能看到完整流程。
陪诊员资质表(escort_certification):用户ID、身份证号、身份证照片、健康证照片、审核状态、审核意见、提交时间、审核时间。管理员审核状态可以做成0待审核、1通过、2拒绝。
这些表建好之后,用物理外键还是逻辑外键,我一直推荐逻辑外键。数据库只存关联ID,真正的外键约束在应用层控制,这样后续分库分表或者归档数据时不容易被数据库的约束卡住。
2.4 订单状态机:所有业务流转的骨架
状态机是整个订单模块最容易让新手混乱的地方,但也是面试官最爱问的点。订单状态我建议这样定义:
| 状态值 | 状态名称 | 含义 | 可执行操作 |
|---|---|---|---|
| 0 | 待支付 | 用户已提交订单但未支付 | 取消订单、支付 |
| 1 | 待接单 | 支付成功,等待陪诊员接单 | 系统派单、陪诊员抢单 |
| 2 | 已接单 | 陪诊员已接单,准备服务 | 开始服务 |
| 3 | 服务中 | 陪诊员已开始陪诊 | 结束服务 |
| 4 | 待确认 | 陪诊员已结束服务 | 用户确认完成 |
| 5 | 已完成 | 用户确认完成 | 评价、查看详情 |
| 6 | 已取消 | 用户或管理员取消 | 无 |
| 7 | 退款中 | 用户申请退款 | 管理员处理退款 |
| 8 | 已退款 | 退款完成 | 无 |
状态流转不能乱跳,比如用户不能从“待接单”直接跳到“已完成”。实现上最简单的方式是在Service层写一个状态变更的公共方法,进入方法先判断当前状态和目标状态是否满足流转条件,满足才更新,不满足直接抛业务异常。代码大概长这样:
public void changeStatus(Long orderId, Integer targetStatus) { EscortOrder order = orderMapper.selectById(orderId); if (order == null) { throw new BusinessException("订单不存在"); } Integer current = order.getOrderStatus(); boolean allowed = statusTransitionMap.getOrDefault(current, Collections.emptyList()) .contains(targetStatus); if (!allowed) { throw new BusinessException("订单状态不允许从 " + current + " 变更为 " + targetStatus); } order.setOrderStatus(targetStatus); orderMapper.updateById(order); OrderStatusLog log = new OrderStatusLog(); log.setOrderId(orderId); log.setOldStatus(current); log.setNewStatus(targetStatus); log.setOperatorId(LoginUserUtil.getUserId()); orderStatusLogMapper.insert(log); }状态流转Map可以在项目启动时初始化成static常量,把所有允许的路径写死,越界操作直接拒绝。这是个小细节,但能挡住很多逻辑漏洞。
3. 后端核心实现与踩坑记录
3.1 项目初始化与统一返回结构
在IDEA里新建SpringBoot项目很简单,选择Spring Initializr,改好Group和Artifact,勾选Web、Security、MySQL、Redis这些依赖,生成后把多余的文件清理掉,按controller/service/mapper/entity/common配置包分层。
这里有个容易被忽略的点是统一返回结构。如果每个接口返回的JSON格式都不统一,前端Axios拦截器就没法统一处理。我的做法是封装一个Result对象,固定包含code、message、data三个字段,code为200表示成功,401表示未登录,403表示无权限,500表示服务异常。
@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; } }全局异常处理也需要提前写好,不然到处都是try-catch,代码丑不说,前端拿到一堆看不懂的默认异常信息。
我自定义了BusinessException,业务上能预判的错误(比如金额不对、状态不能流转)都主动抛这个异常。Controller里的业务代码就不会被异常处理淹没,看起来清爽很多。全局异常处理器用@RestControllerAdvice注解标注,@ExceptionHandler分别处理BusinessException和Exception。BusinessException返回业务提示信息,Exception记录日志并返回“系统繁忙,请稍后重试”。
3.2 JWT认证与拦截器实现
登录认证是SpringBoot项目逃不掉的一环。社区陪诊系统不需要复杂OAuth2,JWT加拦截器足够。登录接口验证用户名密码后生成JWT返回前端,前端每次请求在请求头里带Authorization字段,后端拦截器从请求头取出token验证身份。
JWT工具类主要做三件事:生成token、解析token、判断token是否过期。生成时把用户ID、用户名、角色放进去,过期时间我根据项目需要设置为24小时。
public class JwtUtils { private static final String SECRET_KEY = "your-secret-key-please-change"; private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000L; public static String generateToken(Integer userId, String username, String role) { return Jwts.builder() .claim("userId", userId) .claim("username", username) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }拦截器里做的事也简单:判断请求路径是否是登录路径或静态资源白名单,不是就取token解析,解析失败返回401,解析成功把用户信息放进ThreadLocal,方便后续Service获取当前操作用户。
SecurityConfig那里我做了简化,没有用Spring Security的完整表单登录流程,而是放行所有请求,把真正的安全校验交给自定义拦截器。很多教程不推荐这种做法,但从这个项目体量来看,自己接管JWT拦截比折腾Spring Security的过滤器链更可控,也更好理解。如果你想突出Spring Security技能,也可以在SecurityConfig里配置SessionCreationPolicy.STATELESS,加上JWT过滤器,两者结合。
3.3 陪诊下单和接单的核心接口
下单流程是整个系统的核心链路,我完整走一遍。
用户在前端选好服务项目、日期,填写患者信息和医院地址,提交订单。后端收到请求后,先校验用户是否登录,再校验服务项目是否在有效期内,然后是计算金额。我这里的价格计算规则是:优先看陪诊员的个人定价,没有个人定价就使用服务项目的默认价格;优惠券功能为了控制复杂度,我没有做,这个扩展后续可以加。金额确认后,订单状态为待支付,这里因为是毕设演示,支付模块我用的是模拟支付,不会真接微信支付宝。
接单流程是陪诊员端的主场景。陪诊员登录后看到待接单订单列表,列表是按服务日期和地址排序的。点击接单时,后端不能简单看一眼订单状态就改为已接单,要考虑并发情况。如果两个陪诊员同时点了同一个订单,就可能出现两位陪诊员都显示“接单成功”,但订单只属于其中一人的尴尬。
解决方法是更新时带条件,用数据库乐观锁:
Integer rows = orderMapper.acceptOrder(orderId, userId, oldStatus); if (rows == 0) { throw new BusinessException("订单已被其他陪诊员接走"); }对应的SQL是:
UPDATE escort_order SET order_status = 2, companion_user_id = #{userId}, accept_time = NOW() WHERE id = #{orderId} AND order_status = 1这个写法的核心在WHERE条件带order_status=1,更新条数为0就说明状态已经被别人抢先改掉了,立即抛出异常。不需要分布式锁,不需要redis锁,就能把这个并发问题解决掉,效率还高。
服务结束流程类似。陪诊员点击结束服务,把状态改为待确认,系统给用户的站内消息表插一条“陪诊员已完成服务,请确认”。用户确认完成之后,订单变成已完成状态,此时才允许评价。这里要注意一点,用户确认不能省掉,它起到“服务结果把关”的作用,不然陪诊员自己点开始点结束就完成订单,家属完全不知情,体验会很差。
3.4 Redis缓存用在哪几个位置
Redis在这个项目里不是必须的,但加上可以让面试多一个聊点。我实际用在了三个位置。
第一是验证码缓存。注册时发送验证码,把验证码存Redis,并设置5分钟过期。用Redis而不是数据库,是因为这类临时数据不需要持久化,还能自动过期。
第二是陪诊员热门列表缓存。首页显示推荐的陪诊员,这个列表数据变化不频繁,查询频率却很高。我可以把陪诊员ID列表缓存到Redis,缓存里没有数据时再查数据库,查完重新设置缓存。缓存更新策略采用先删缓存再更新数据库的简单方式,对一致性要求不是极高,完全够用。
第三是陪诊员接单量的计数。每次接单成功后,使用Redis的incr命令把当天的接单量加1,前端展示“今日已接N单”的实时统计。这个数据如果每次都查数据库,既慢又没必要,Redis做计数器再合适不过。
public void incrementDailyAcceptCount(Integer userId) { String key = "companion:daily:accept:" + userId + ":" + DateUtil.today(); redisTemplate.opsForValue().increment(key); redisTemplate.expire(key, Duration.ofDays(2)); }使用Redis前的序列化配置也要注意。默认的JdkSerializationRedisSerializer会把数据存成一堆二进制内容,不好看也不好排查。建议改成使用Jackson的GenericJackson2JsonRedisSerializer,key用StringRedisSerializer。这个细节经常被忽略,但配置好之后用Redis Desktop Manager看数据会舒服很多。
3.5 注解使用心得:SpringBoot常用注解一页纸
很多面试题会问SpringBoot常用注解,我在项目里用到的核心注解就这么几个。Controller层用@RestController标注返回JSON,@RequestMapping定义路径,@GetMapping、@PostMapping分别处理GET和POST请求,@PathVariable获取URL路径参数,@RequestBody接收JSON对象,@RequestParam接收查询参数。Service层用@Service标记业务类,@Transactional声明事务,@Autowired注入依赖,或者直接用构造器注入更推荐。配置类用@Configuration标记,@Bean注入自定义组件,@ConfigurationProperties绑定配置对象。
MyBatis-Plus里常用的是@TableName指定表名、@TableId指定主键、@TableField指定字段名和自动填充标记。创建时间和更新时间字段,用@TableField(fill = FieldFill.INSERT)自动填充创建时间,用@TableField(fill = FieldFill.INSERT_UPDATE)自动填充更新时间,配合MetaObjectHandler实现类,插入和更新时自动设置值,省去一遍遍手动set。这个做法代码量减少不多,但绝对能避免“忘了set创建时间”的低级错误。
4. 前端核心页面与联调经验
4.1 创建Vue项目和路由配置
前端项目骨架我用的是Vue 3加Vite。创建好之后第一件事是安装依赖,装Element Plus、Vue Router、Pinia、Axios。
安装Element Plus时要注意,Vue 3必须用Element Plus而不是Element UI,后者只支持Vue 2,这个混淆坑了很多刚接触的人。安装命令:
npm install element-plus npm install vue-router@4 npm install pinia npm install axios路由配置分两块:公开路由和需要登录的路由。公开路由包含首页、登录页、注册页,其余页面全部走登录守卫。为了不让业务代码里到处都是v-if判断角色,我把动态路由和静态路由分开,用户登录后根据角色信息动态添加路由,这个做法在“若依vue开源项目在idea中部署”那类项目里很常见。
const routes = [ { path: '/login', component: Login, meta: { public: true } }, { path: '/register', component: Register, meta: { public: true } }, { path: '/', component: Layout, redirect: '/home', children: [] } ]路由守卫放在main.js或单独router文件里:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.public) { next() } else if (!token) { next('/login') } else { next() } })路由参数有两种最常用场景。一种是从订单列表点击进入订单详情,通过路径参数传递订单ID,跳转时把订单ID带上。另一种是列表页往表单页传对象,这种数据量稍大,放到路由参数不好维护,我会用Pinia的临时状态或者直接在跳转后用订单ID去查详情接口。我的习惯是能用ID查后端就绝不传整个对象,这样刷新页面后数据也还在。
4.2 Axios封装与登录态保持
Axios封装是前端项目的标配。我的封装逻辑是:创建一个axios实例,设置baseURL为后端接口地址,设置请求超时时间,比如10秒;请求拦截器里从localStorage取出token,拼到请求头的Authorization字段;响应拦截器里判断响应状态码,200直接返回data,401跳转登录页,其他错误用Element Plus的Message提示错误信息。
const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('未登录或登录已过期')) } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )登录态保持我直接用localStorage存token和用户信息。每次刷新页面时,App.vue或路由守卫里会调用一次“获取当前用户信息”接口,把最新的用户数据重新放到Pinia。这样即使手动改了localStorage里的角色信息,刷新后也会被后端返回的真实身份覆盖,安全性更有保障。
这里有个小坑:后端JWT过期后,前端拿到的所有接口都会返回401,如果Axios拦截器里没有做跳转,页面会停留在“看起来正常但所有接口都请求失败”的状态。把401统一处理成跳转登录页是最省事的方案,但要注意登录页本身不能走401逻辑,否则会跳转死循环。所以在响应拦截器里先判断当前路由不是/login再跳转。
4.3 下单页面和服务评价页怎么做
下单页面是这个系统里交互最复杂的页面,包含三块内容:服务项目选择、服务时间选择、患者信息填写。
服务项目选择我使用Element Plus的卡片列表展示,点击某个服务项目卡片后,页面下方显示对应的价格,同时调用后端接口获取按日期条件筛选后的可用陪诊员列表。日期选择器限制只能选今天之后的时间,禁选过去的日期。患者信息部分做了表单校验,姓名、联系电话、医院名称必填,病情描述为选填,但这些信息最终在订单详情页要展示给陪诊员,所以不建议太简略。
<el-form ref="formRef" :model="orderForm" :rules="rules" label-width="100px"> <el-form-item label="患者姓名" prop="patientName"> <el-input v-model="orderForm.patientName" placeholder="请输入患者姓名" /> </el-form-item> <el-form-item label="联系电话" prop="patientPhone"> <el-input v-model="orderForm.patientPhone" placeholder="请输入联系电话" /> </el-form-item> <el-form-item label="医院名称" prop="hospitalName"> <el-input v-model="orderForm.hospitalName" placeholder="请输入医院名称" /> </el-form-item> <el-form-item label="服务日期" prop="serviceDate"> <el-date-picker v-model="orderForm.serviceDate" type="date" value-format="YYYY-MM-DD" /> </el-form-item> </el-form>提交订单时调用创建订单接口,成功后把订单ID存起来,跳转到支付页面。支付页面我放了一个模拟支付按钮和一个后台二维码图,点击“模拟支付成功”后调用后端支付回调接口,把状态从待支付改成待接单,然后跳转到订单详情页。这个流程虽然模拟,但和真实支付流程的接口设计很接近,后续接微信支付时只需要替换掉支付调用部分。
服务评价页用评分组件加文本域,评分默认5分,用户可改。提交评价前需要判断当前订单是否已完成,已完成才能调评价接口。评价成功后,订单详情页出现已评价样式,评价内容回显,不可修改。
4.4 打包部署后布局异常的排查
这是我从热搜词里看到很多人遇到的高频问题。开发环境布局好好的,npm run build打包后扔到服务器上,页面布局就乱了。常见原因有三个。
第一个是静态资源路径问题。打包后index.html里引用的CSS和JS路径默认是根路径,如果你的项目部署在子路径下,比如nginx的location /admin/,那这些资源就找不到。解决办法是在vite.config.js里配置base字段,Vue CLI项目则配置publicPath。部署在子路径时设置base为相对路径或者完整子路径,这一点极易踩坑。
第二个是字体和图标文件404。element-plus等组件库内部会引用一些图片和字体资源,打包时路径处理不好就会404,导致图标变成方块。检查打包后的static目录里有没有对应资源,再看网络请求的路径是否正确。
第三个是CSS作用域问题。有些人和我一样图方便,在好几个组件里都写了全局样式,开发时没问题,打包后样式被压缩,优先级发生变化,布局就乱了。解决办法是重新检查style标签是否没写scoped。全局样式统一放到App.vue或单独的css文件里管理。
部署上线我比较习惯的方式是后端打包成jar,前端打包成dist目录,再用Nginx做反向代理,把/api请求转发到后端端口:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; 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,少了它你从订单列表点进详情页刷新一下就会白屏。这也是高频问题之一。
5. 系统测试、常见问题与经验复盘
5.1 接口自测与联调遇到的问题
接口写完后不能直接丢给前端联调,我习惯先用Postman或者Apifox把核心接口完整走一遍,从注册登录到下单接单,再到服务完成评价,一条完整业务链路跑通后再喊前端联调。这里记录几个我实际遇到过的通用问题。
第一个是跨域问题。前端开发时访问http://localhost:5173,后端接口在http://localhost:8080,端口不同就触发跨域。解决方法有三:后端加CorsFilter、前端配置Vite代理、或者上线后用Nginx同源访问。开发环境我推荐前端配置Vite代理,生产环境走Nginx。调试跨域问题时,先看浏览器请求有没有被CORS拦截,再看后端响应头里有没有Access-Control-Allow-Origin,排查起来很快。
第二个是LocalDateTime序列化问题。Java 8的LocalDateTime默认序列化成数组格式,前端拿到的是不知所云的结构。解决方法是统一配置Jackson:
@Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }如果图省事,也可以直接在实体类字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解。两种写法都可以,但我更推荐全局配置,不然每个字段都要加注解,太烦。
第三个是分页参数问题。MyBatis-Plus的分页返回结果是IPage对象,默认字段是records、total、size、current,前端分页组件刚好能对上,但需要确保配置了分页插件,否则分页不生效,直接查全表。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }5.2 并发和状态冲突怎么办
社区陪诊系统的并发量不会很高,但订单状态流转、接单抢单是并发问题的重点区域。
抢单场景我前面已经介绍过,使用带状态条件的更新语句解决。另一个常见冲突是用户下单后超时未支付,订单一直占着位置。我设置了订单创建时间和支付时间,页面展示支付倒计时,后端不一定要做定时任务强制关闭,可以在用户查询订单时,如果发现待支付订单已超过30分钟,就自动修改为已取消。这种懒计算方案对小型项目完全够用,省去引入定时任务框架的复杂度。
还有一个问题是陪诊员接单后如果临时有事取消订单,需要管理员介入。这里我把取消权限严格控制:订单在待接单状态,用户可以取消;订单在已接单状态,用户不能直接取消,需要申请管理员取消或者陪诊员同意。这样设计是为了防止恶意取消干扰陪诊员排班,逻辑上更接近真实社区服务场景。
5.3 提醒通知模块:SpringBoot整合ActiveMQ的取舍
关于消息通知,我在这个项目里使用的是SpringBoot整合ActiveMQ的异步消息方案。为什么用它而不是RocketMQ或RabbitMQ?原因很简单,ActiveMQ对单机小项目更加轻量,安装配置也简单,SpringBoot有官方starter支持,跑在默认的虚拟机模式下不需要额外安装服务,非常适合毕设演示和中小型项目。
我用它的场景是订单状态变更后的站内消息通知。比如用户下单成功后,系统发出一个“订单创建成功,等待陪诊员接单”的消息;陪诊员接单后,系统发消息通知用户“您约的陪诊员已经接单,请注意查收服务信息”。
使用ActiveMQ时,pom引入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency>配置连接工厂和队列名称后,消息生产者就是调用JmsTemplate.convertAndSend,消息消费者是写一个@JmsListener注解的方法,整体链路不复杂。如果你不想引入消息队列,也可以用Spring的ApplicationEvent发布事件变相实现异步通知,但面试时能主动聊消息队列的使用场景,是一个明显的加分项。
5.4 从毕设到生产环境:可扩展点清单
这个项目做完之后,如果你还想继续完善,有几个方向可以做。
第一个是真实支付对接。模拟支付改成微信支付或支付宝支付,后端增加支付回调接口,加入签名验证逻辑。这个改动可以单独做一个支付模块,面试时提到会显得有真实落地意识。
第二个是接入地图服务,做成陪诊员位置追踪。可以用腾讯地图或者高德地图的API,把陪诊员的实时位置展示给用户,这样家属能直观看到陪诊员和患者的距离,解决等待焦虑。
第三个是消息通知渠道扩展。站内消息之外,增加短信通知或者微信公众号模板消息通知,这些都有现成云服务,接入不算难。
第四个是数据报表。管理员后台增加订单量趋势图、陪诊员服务排行、用户满意度统计等图表。前端用ECharts,后端增加一些统计接口,整体工作量不大,但整个项目会显得更完整。
如果想把项目做成一个能讲的“有深度”的毕设,我建议多在这些非CRUD模块上花时间。只做增删改查很容易被问穿,但能聊消息队列解耦、乐观锁防并发、Redis缓存热点数据、前后端分离权限控制,面试官会觉得你真的思考过生产问题。
6. 关于这个项目,我再多说几句
这个系统我做下来最大的体会是,技术难度其实都被框架消化了,真正费心思的是把业务状态和用户预期对齐。比如我把“用户下单”和“陪诊员接单”之间拆成两个明确状态,用户能看到等待过程,而不是下单后就觉得自己预约成功了。前端加一个订单时间线组件,用户能一眼看到进度,这种体验上的细节比多写十个接口更能打动使用者和评审老师。
最后再分享一个小经验。做这类SpringBoot+Vue的前后端分离项目时,前后端联调是最容易拖慢进度的环节,建议提前把接口文档写清楚,字段名、类型、状态码含义都列出来。我用Apifox离线文档分享给前端同学,开发效率高很多。技术本身不难,难的是把流程安排得井井有条。希望这篇内容能帮到正在做社区陪诊系统的你。