ARTICLE DETAIL

资讯详情

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

Spring Boot+Vue汽车4S店保养系统:Java全栈项目实战解析

Spring Boot+Vue汽车4S店保养系统:Java全栈项目实战解析 1. 为什么4S店保养系统是Java全栈练手项目的黄金选题做Java开发这几年我经常被问到一个问题课程设计或者简历项目到底做什么好我的答案一直很固定——Spring Boot Vue 的汽车4S店保养服务管理系统。这个选题看起来普通但它是目前市面上少有的、能把Java后端、前端、数据库、权限控制、业务流程全部串起来的完整项目覆盖的知识点非常均衡。先说为什么这个选题值钱。4S店保养业务天然就是一个多角色、多状态、有金额计算、有并发冲突的真实场景。它不像图书管理系统那样只做增删改查也不像电商那样对并发要求极高到难以驾驭。保养系统的复杂度恰好卡在一个绝佳位置学生或初中级开发者能啃下来面试官又能从中挖出大量可追问的点。我自己带过不少新人认真做完这个项目再去面试被问到的基本都是自己写过的东西而不是八股文。从技术栈覆盖度来看这个项目几乎把 Java 生态里最常用的东西都装进去了技术组件在项目中的位置Spring Boot后端基础框架提供依赖注入、自动配置、事务管理MyBatis Plus数据持久层单表CRUD基本不用写SQLSpring Security / JWT登录认证与接口权限控制Vue 3 Vue Router Pinia前端SPA应用组件化开发Axios前端HTTP请求封装统一处理token和错误码MySQL业务数据存储核心业务表大概8-12张Redis可选验证码存储、热点数据缓存、预约防重这套组合做出来就是一个标准的前后端分离项目也就是热搜词里一直在说的springboot vue前后端分离。你在简历上写这个项目技术栈那一栏会非常饱满。再说适合谁。如果你是正在准备课程设计或毕业设计的在校生这个项目能让你完整走一遍需求分析、数据库设计、接口定义、前后端联调、部署上线的全过程。如果你已经工作但想系统补全Spring Boot Vue的技能树这个项目的代码量适中——后端大概3000-5000行前端大概2000-3000行周末加几个晚上就能跑通核心流程。如果你本身在汽车后市场行业做IT那就更直接了这套业务模型很多小型的汽修门店系统都能复用。这里我特别想强调一个点选择项目时业务复杂度比技术栈更重要。很多人喜欢堆技术——微服务、消息队列、分布式事务全往上放结果面试时被问到你为什么要用MQ直接卡壳。而4S店保养系统的好处是每一项技术都能找到合理的业务理由为什么工单需要状态机因为保养流程有严格的先后顺序为什么结算要事务因为扣库存、记流水、更新工单状态必须同时成功。技术是跟业务长在一起的说出来的话才有说服力。所以这篇文章我就按自己当年实操的完整路径来写从业务模型拆解开始到后端核心实现、前端页面结构、权限安全设计最后是部署和面试准备。全程都是我自己踩过的坑和验证过的方案可以直接照着落地。2. 系统拆解从预约到结算的完整保养链路拿到题目先别急着写代码把业务流程想清楚是第一步。4S店保养服务的核心链路可以压缩成一句话客户预约 - 到店接车 - 技师派工养护 - 质检确认 - 结算离店 - 后续回访。每一环都是一个独立的业务模块模块之间通过工单Work Order这张表串联。2.1 六步主流程的角色与状态流转我把整条链路拆成六步每一步对应哪些角色、产生什么数据、触发什么状态变化用下面这张表理清楚环节操作角色核心动作产生数据在线预约客户选择门店、保养套餐、预约时间预约单接车检查前台接待登记车辆信息、当前里程、外观状态车辆档案、接车单派工保养车间主管分配技师、确认保养项目工单状态施工中质检确认质检员检查完工质量、记录更换配件质检记录结算离店财务/前台计算工时费配件费、收款结算单售后回访客服电话/短信回访、记录满意度回访记录这六步中最核心的实体是工单表work_order。所有业务动作都围绕工单展开它是整个系统的心脏。我设计的工单状态枚举如下public enum WorkOrderStatus { PENDING(0, 待接车), INSPECTED(1, 已接车), REPAIRING(2, 保养中), QUALITY_CHECKING(3, 质检中), COMPLETED(4, 已完成), SETTLED(5, 已结算), CANCELLED(6, 已取消); private final int code; private final String desc; // getter、构造器省略 }状态流转的规则是只能从当前状态迁移到指定的下一个状态。比如保养中不能直接跳已结算必须经过质检中。这个规则在后端Service里要写成显式的校验逻辑不能只靠前端控制。原因很简单Postman直接调接口就能绕过前端按钮如果后端不做状态机校验就会出现工单未质检就结算的脏数据。2.2 核心数据表结构设计直接抄作业根据上面的链路我设计的核心表一共9张它们的关系我用一段DDL骨架来说明-- 客户表 / 车辆表 / 保养套餐表基础资料 CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, ... ); CREATE TABLE vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, plate_no VARCHAR(20) NOT NULL UNIQUE, -- 车牌号 brand VARCHAR(30), model VARCHAR(50), mileage DECIMAL(10,2), -- 当前里程 ... ); CREATE TABLE maintenance_package ( id BIGINT PRIMARY KEY AUTO_INCREMENT, package_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, -- 套餐价格 duration_hours INT, -- 预计工时 ... ); -- 工单主表 工单明细表多对多关系拆分 CREATE TABLE work_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, customer_id BIGINT NOT NULL, vehicle_id BIGINT NOT NULL, package_id BIGINT, technician_id BIGINT, -- 负责技师 status INT NOT NULL DEFAULT 0, -- 见状态枚举 appoint_time DATETIME, -- 预约时间 total_amount DECIMAL(10,2) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, ... ); CREATE TABLE work_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, item_name VARCHAR(100) NOT NULL, -- 保养项目如更换机油 item_type INT, -- 1工时项 2配件项 quantity INT DEFAULT 1, unit_price DECIMAL(10,2), ... ); -- 结算表 / 回访表 CREATE TABLE settlement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL UNIQUE, total_amount DECIMAL(10,2), pay_method INT DEFAULT 0, -- 0现金 1微信 2支付宝 3银行卡 operator_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, ... );有三个地方我要特别提醒第一金额字段必须用DECIMAL绝对不能用double。double在二进制里是近似值0.10.2算出来是0.30000000000000004一旦涉及结算金额这就是致命bug。热搜词里有java怎么保证数据一致性金额精度就是最基础的一种一致性。第二工单号和预约时间要建立联合索引。因为业务上最常见的查询是某天有哪些工单某个车牌近半年的历史工单这两个查询频率极高不加索引的话数据量一上来就慢。第三车辆表和客户表为什么要分开一个客户名下可能有多辆车家里两台车保养很正常车辆是跟着车走而不是跟人走的。如果一开始偷懒把车牌号直接塞到客户表里后面加第二辆车时会非常痛苦。设计表结构时多花十分钟考虑一对多的拆分省下的重构时间是几倍。2.3 哪个环节最容易做崩预约并发与超卖问题很多人把增删改查做完就以为系统完工了但真实业务场景里最容易翻车的是预约环节。4S店的保养工位有限一个时间段只能接有限数量的车。如果两个客户同时抢同一个时间段后端不做控制就会出现超卖——预约单都创建成功了但工位只有一个到店就得排队吵架。我当时处理这个问题用的是Redis预占 数据库唯一约束的双保险策略// 预约时先通过Redis原子操作占位 String key appoint: shopId : appointTime; Long remain redisTemplate.opsForValue().decrement(key); if (remain 0) { // 名额已满回补并抛出业务异常 redisTemplate.opsForValue().increment(key); throw new BizException(该时段预约已满); } // 然后插入预约记录数据库层用唯一索引兜底 // 例如创建预约时以 shop_id appoint_time 分组作为唯一业务键这里有个细节decrement是原子操作两个并发请求同时进来时JVM内存里可能都读到remain1但Redis层面只有一个能减成功这是天然防超卖的核心。数据库的唯一索引是最后一道防线即使Redis挂了重复插入也会报错而不是静默产生脏数据。这个设计虽然只加了十几行代码但面试时是最能打的亮点之一。如何解决预约超卖这个问题能同时考察你的并发思维、Redis使用能力和数据库约束意识比背一百道八股文都有用。3. Spring Boot后端分层架构、表设计以及几个容易写崩的点后端是整个系统的地基。我的习惯是严格分四层Controller接收请求、Service处理业务逻辑、Mapper操作数据库、Entity映射表结构。这一层没有太多花活真正容易出问题的往往是几个看似简单的细节。3.1 项目目录结构与分层职责我推荐的目录结构是下面这样简单清晰且符合大多数人认知com.shop.system ├── controller // 接口层只做参数接收和结果返回 │ ├── AuthController.java │ ├── WorkOrderController.java │ └── ... ├── service // 业务层核心逻辑都在这 │ ├── WorkOrderService.java │ └── impl/ ├── mapper // MyBatis Plus Mapper接口 ├── entity // 数据库实体类 ├── dto // 数据传输对象用于接口入参 ├── vo // 视图对象用于接口返回 ├── common // 通用类Result、异常、工具类 └── config // 配置类CORS、MyBatis Plus、拦截器Controller层我要求自己只做三件事取出参数、调用Service、返回Result。判断逻辑一概不写。这样做的直接好处是将来接口被人用Postman随便调时所有校验都能在Service层拦住前端只是看起来控制了流程。Result统一返回体也很重要我习惯的格式是public class ResultT { private Integer code; // 0成功非0失败 private String msg; private T data; public static T ResultT success(T data) { ... } public static T ResultT error(String msg) { ... } }前端Axios拦截器拿到响应后先判断code是否为0再决定是渲染数据还是弹出错误提示。这个约定一旦定下来前后端联调时扯皮的几率会小很多。3.2 手写MyBatis Plus自动填充和逻辑删除MyBatis Plus几乎是Spring Boot项目的标配了我用它的核心原因只有一个字快。单表CRUD零SQL但有两个配置建议直接加上因为几乎所有表都需要。第一个是字段自动填充。每张表都有create_time和update_time手动在每个插入语句里写太蠢了。用MetaObjectHandler实现Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }同时实体类的字段上加注解Data public class WorkOrder extends ModelWorkOrder { TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }第二个是逻辑删除。客户可能删除预约记录但工单和结算单属于业务凭证物理删除会让财务对账查无实据。TableLogic注解加在deleted字段上MyBatis Plus执行delete时会自动转成update。建议所有核心业务表都这么做代价极小但备份和追溯能力提升一大截。3.3 事务边界不对钱就算错了结算模块是最容易出事务问题的。一次结算动作涉及三步插入结算记录、更新工单状态为已结算、扣减配件库存。这三步必须在一个事务里任何一步失败都要回滚。Service public class SettlementServiceImpl implements SettlementService { Override Transactional(rollbackFor Exception.class) public Settlement settle(Long orderId, Integer payMethod) { // 1. 校验工单状态必须是COMPLETED否则抛异常 WorkOrder order workOrderMapper.selectById(orderId); if (order.getStatus() ! WorkOrderStatus.COMPLETED.getCode()) { throw new BizException(工单未完成不能结算); } // 2. 根据工单明细计算总金额 BigDecimal total calculateAmount(orderId); // 3. 插入结算单 Settlement settlement new Settlement(); settlement.setOrderId(orderId); settlement.setTotalAmount(total); settlement.setPayMethod(payMethod); settlementMapper.insert(settlement); // 4. 更新工单状态 order.setStatus(WorkOrderStatus.SETTLED.getCode()); workOrderMapper.updateById(order); // 5. 扣减配件库存如果保养单包含配件 reduceStock(orderId); // 合并实际需求将外部依赖与性能优化统一交给外层 return settlement; } }这里有一个极其隐蔽且高频的坑Transactional默认只在RuntimeException下生效。如果你的代码抛的是受检异常比如SQLException而rollbackFor没指定事务根本不会回滚这就是热搜词里java怎么保证数据一致性最常见的反面教材。所以我在所有写操作上都会显式加rollbackFor Exception.class宁可多写几个字也绝不赌默认行为。另一个坑是**Transactional失效**。同一个类内部A方法调B方法B上面挂的Transactional是不生效的因为Spring代理的入口在A内部的this.b()调用走的是原始对象不是代理对象。解决方案很简单把需要事务的B方法放到另一个Service类里或者自己注入自己。别问我怎么知道的——我第一次写结算逻辑时就在这个坑里趴了半天。3.4 日期时间处理的统一Java的日期时间处理是经典坑点。后端用LocalDateTime前端传过来的是字符串2025-03-18 14:30:00如果不做全局配置Spring Boot默认的Jackson反序列化会出现各种格式不一致的报错。我的做法是在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 servlet: multipart: max-file-size: 10MB max-request-size: 10MB同时给前端返回的时间字段都统一为字符串格式避免前端拿到时间戳后还要自己处理时区。前后端时间问题看似小实际联调时消耗的时间占比非常高早统一早省心。4. Vue前端页面结构、Axios封装以及联调避坑指南后端接口就绪后前端就开始进场了。我用的是Vue 3 Vite Pinia Element Plus这套组合。Element Plus对后台管理系统极其友好表格、表单、弹窗、日期选择器开箱即用能省下大量造轮子的时间。4.1 前端项目结构与路由划分前端我按功能模块拆页面目录结构如下src ├── api // 接口请求定义一个模块一个文件 │ ├── auth.js │ ├── order.js │ └── settlement.js ├── assets ├── components // 通用业务组件如车辆信息卡片 ├── layout // 后台框架布局侧边栏 顶部栏 ├── router // 路由配置 路由守卫 ├── store // Pinia状态管理 ├── views // 页面 │ ├── login.vue │ ├── dashboard.vue // 仪表盘 │ ├── appointment/ │ ├── workorder/ │ ├── settlement/ │ └── customer/ └── utils └── request.js // Axios封装路由设计采用懒加载权限路由和固定路由分开const routes [ { path: /login, component: () import(/views/login.vue), meta: { public: true } }, { path: /, component: () import(/layout/index.vue), redirect: /dashboard, children: [ { path: workorder/list, component: () import(/views/workorder/list.vue), meta: { roles: [admin, technician] } // 可访问角色 } ] } ]热搜词里有vue动态路由和vue路由参数这里都涉及到了。我的实现思路是前端先根据登录用户的角色动态过滤可访问的路由表再通过router.addRoute()注册。菜单渲染也从路由表生成这样权限控制逻辑只写一遍路由、菜单、页面三层保持一致。4.2 Axios封装别让token散落在每个请求里没有封装的Axios就是灾难——每个请求都要手写header带token、手写判断错误码、手写格式化参数。我封装一版后业务代码里只写getOrderList(params)这种两行的调用。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) // 请求拦截器统一携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error)) // 响应拦截器统一处理业务错误和登录过期 request.interceptors.response.use( response { const res response.data if (res.code 0) { return res.data } // 登录过期或token无效 if (res.code 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request封装完以后所有关于token放哪、过期了怎么跳登录、错误提示怎么弹的问题全部收敛到一个文件里。这是我在项目中体会最深的一点前端的工程质量很多时候不是写在页面里而是写在基础设施里。登录态管理我用的是Piniaexport const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, setUserInfo(info) { this.userInfo info localStorage.setItem(userInfo, JSON.stringify(info)) }, logout() { this.token this.userInfo null localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })4.3 核心页面的组件拆分思路以工单管理页为例一个业务页面通常包含四个部分筛选区按车牌号、状态、时间段过滤工单列表。表格区展示工单号、客户、车辆、套餐、技师、状态、金额等核心字段。操作按钮根据工单状态动态展示——待接车的可以取消完成状态的可以结算其他状态只读。详情抽屉/弹窗点击行展开工单详情包括明细列表、质检信息、结算信息。组件拆分时我遵循的原则是页面只负责组装业务逻辑下沉到子组件。比如工单状态标签我单独封装一个OrderStatusTag.vue根据status值渲染不同颜色的标签结算按钮单独封装SettleDialog.vue内部处理金额展示、支付方式选择、确认回调。这样任何页面需要复用直接import即可。Element Plus的表格和表单组件确实省力但有一个坑要注意表格分页的current-page和page-size要跟后端的PageRequest对齐否则会出现前端点了第3页后端拿到的还是第1页的诡异bug。我习惯在utils/page.js里封装一个统一的formatPageParams函数所有分页请求都走它杜绝各写各的。4.4 联调时最常见的三个问题前后端联调是新手最痛苦的一环我遇到的典型问题有三个第一跨域问题。开发时前端跑在5173端口后端跑在8080端口直接请求必然跨域。解决方案是后端加CORS配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意addAllowedOrigin(*)在setAllowCredentials(true)时是无效的必须用addAllowedOriginPattern(*)。这个坑我调了很久才发现。第二时间格式不一致。后端返回LocalDateTime默认是ISO格式2025-03-18T14:30:00而前端需要的是2025-03-18 14:30:00。配合前面说到的Jackson全局配置我统一在DTO的JsonFormat上再加一层兜底JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime appointTime;第三枚举值前后端映射。前端拿到工单状态1时不能指望所有页面都记得1是已接车。我的做法是让后端额外提供一个枚举字典接口GET /api/dict/work-order-status返回[{code: 1, name: 已接车}, ...]前端根据字典渲染。这样状态文案修改只需要后端改一处前端全部同步更新。5. 权限与安全不是说加了登录就完事4S店保养系统里有三种核心角色管理员、技师、前台/财务。每种角色的数据权限完全不一样如果只在登录时判断一次身份后面所有接口都能被任意调那跟没做权限没区别。5.1 基于JWT 拦截器的权限模型我的权限方案不引入Spring Security的重量级依赖除非你有复杂权限要求用的是JWT HandlerInterceptor代码量小、逻辑直白、面试也好解释。JWT登录流程// 登录成功后签发token有效期为7天 String token Jwts.builder() .setSubject(userId.toString()) .claim(username, username) .claim(role, user.getRole()) // 角色 .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器负责解析token、校验角色、放行或拒绝Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和字典接口 if (handler instanceof HandlerMethod) { HandlerMethod hm (HandlerMethod) handler; if (hm.hasMethodAnnotation(AllowAnonymous.class)) { return true; } } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { throw new BizException(401, 未登录); } // 解析并校验token Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(authHeader.substring(7)) .getBody(); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } }角色校验我用自定义注解实现在需要限制角色的方法上标注Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }再用另一个拦截器检查RequireRole角色不匹配就返回403。比如结算接口只允许管理员和财务操作技师调就报无权限。5.2 防XSS攻击不只是前端过滤的事热搜词里有一条springboot项目全局过滤器处理上传pdf文件时xss攻击说明这个点是大家普遍关心的。很多人以为XSS是前端该管的事——前端确实能在输入框上做过滤但攻击者可以绕过前端直接通过Postman发送恶意脚本字符串。如果后端不做处理这些恶意内容存进数据库下次任何前端页面渲染时都可能执行。我的做法是用全局过滤器统一清洗所有请求参数Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; XssHttpServletRequestWrapper wrapper new XssHttpServletRequestWrapper(httpRequest); chain.doFilter(wrapper, response); } }XssHttpServletRequestWrapper要重写getParameter、getParameterValues、getHeader把script、iframe、onerror等危险字符串全部转义或剔除。再配合前端路由守卫和Vue模板默认的转义机制双层防护才算到位。需要注意HTML转义要区分场景客户备注这种纯文本字段转义没问题但如果业务上确实需要支持富文本例如维修说明那就要用白名单策略只允许b、br这类安全标签其余一律过滤。这个细节在面试时如果主动提到会显得对安全理解很透彻。5.3 垂直越权怎么防比XSS更隐蔽但危害更大的是垂直越权。举个例子工单详情接口是GET /api/workorder/{id}如果前端把工单id传进去后端不做任何判断就返回数据那么客户A登录后完全可以猜到或遍历id查看客户B的工单隐私。解决思路是查询数据时把当前登录人的身份作为查询条件传进去而不是只依赖前端传的id。// 客户查询自己的工单 public PageResult listMyOrders(PageRequest pageReq, Long customerId) { LambdaQueryWrapperWorkOrder wrapper new LambdaQueryWrapper(); wrapper.eq(WorkOrder::getCustomerId, customerId); // ... } // 在Controller里从Request取当前登录人 Long userId (Long) request.getAttribute(userId);工单状态由待接车改为施工中这类操作更是要校验操作人是不是该工单分配的技师。这些校验代码每行都不复杂但漏掉任何一个系统安全性就整体降级。6. 部署上线与面试答辩项目做完了怎么讲才值钱系统开发完不算完能部署上线、能经得起面试追问才是这个项目的完整价值。6.1 前后端分离部署Nginx Spring Boot我常用的部署方案是后端打成jar包直接跑前端build后交给Nginx托管Nginx反向代理/api开头的请求到后端服务。server { listen 80; server_name shop.example.com; # 前端静态资源 location / { root /var/www/vue-dist; index index.html; try_files $uri $uri/ /index.html; # 解决Vue Router刷新404问题 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这行一定要写否则前端路由用了history模式时刷新页面会出现404。这是部署环节最常见的坑之一。后端打包指令mvn clean package -DskipTests java -jar target/system-0.0.1.jar --spring.profiles.activeprod生产环境的数据库密码、JWT密钥等配置我用application-prod.yml里通过环境变量引用避免把敏感信息硬编码进项目仓库。6.2 项目亮点怎么总结项目做完后面试官问你这个项目有什么亮点你不可能把功能列表念一遍。我建议从以下五个维度提炼业务建模完整从预约到回访的六步闭环状态机流转有严格校验订单数据可追溯。这是项目骨架的含金量。并发预约占位用Redis原子操作解决热点时段超卖问题数据库唯一约束兜底保证极端情况下数据也不错。统一安全体系JWT认证 角色鉴权 全局XSS过滤 垂直越权防护四层安全措施都有明确代码落地。金额计算合规全程BigDecimal杜绝浮点误差结算流程在事务保护下完成任何异常都完整回滚。全栈交付能力前端组件化、Axios统一封装、Nginx部署说明你具备独立的整站交付能力。这五个点每一个都能展开讲三五分钟。面试官追问细节时你手里全是自己一行一行写出来的代码答起来自然就有底气。6.3 面试高频追问清单最后我列一份我实际被问到过的问题清单按照由浅入深排列供你自查难度问题参考回答方向基础工单状态为什么用枚举而不是数字裸奔可读性、类型安全、展示文案统一管理基础预约并发超卖怎么解决的Redis原子操作 数据库唯一约束兜底进阶事务失效的可能原因有哪些方法自调用、非RuntimeException、传播行为错误进阶你怎么保证金额计算准确DECIMAL存储、BigDecimal计算、事务保证一致进阶JWT和传统Session方案比有什么优缺点无状态、跨域友好服务端无法主动踢人、泄露风险深入垂直越权如何被检测和防护未知身份绑定查询条件、后端二次校验深入工单列表查询慢怎么优化索引设计、分页插件、必要时引入Redis缓存看到没这个项目答下来面试官对你的技术判断基本就清晰了。它不像网上那些复制粘贴的秒杀系统代码是别人写的你也讲不清楚也不像简单的图书馆管理问了两个问题就见底。4S店保养系统正好卡在有话可说和全都能讲透的中间位置。我个人带项目的经验是代码量不用贪多把这条保养链路真正吃透每一步都知道为什么要这么设计这个项目就值回票价了。如果你也想找一个课程设计或简历项目不妨就按这套思路走一遍。过程中有任何卡壳的地方回头看这篇文章里列的坑多半能省下不少浪费在盲搜上的时间。
返回列表