ARTICLE DETAIL

资讯详情

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

购车报价与文件管理系统:Spring Boot + Vue 实战

购车报价与文件管理系统:Spring Boot + Vue 实战 在汽车销售和交付环节里购车文件、配置、价格、折扣这四类数据很少是孤立存在的。客户选车时先看基础款价格再决定要不要加装科技包、冬季轮胎或充电桩销售给出一版报价后还得把优惠、置换补贴和分期权益叠进去到了交付阶段合同、车辆合格证、保险单又要归档到同一个订单下。这个场景看起来简单但实际做系统时要比写一段 CRUD 复杂得多。本文以示例车型“海獭 Racco”为业务对象设计并实现一个购车报价与文件管理系统。需要提前说明的是这里的车型名称仅用于演示业务逻辑不指向任何真实品牌或产品。文中会重点讲清楚配置项如何建模、价格如何计算、折扣如何叠加、购车文件如何归档以及整套流程在数据库和后端接口中如何落地。阅读本文需要先熟悉 Spring Boot、MySQL 和 Vue 基础概念如果只看后端也完全可以跳过前端部分。1. 先理解购车文件、配置、价格、折扣为什么需要系统化管理1.1 业务场景一份标准交付订单包含哪些数据以一台电动车为例客户从看车到提车至少会经历选配、询价、下单、签约、交付五个阶段。每个阶段都会产生一批数据车型基础信息车型编码、车型名称、基础售价、保修政策。配置项信息车身颜色、轮毂、电池容量、辅助驾驶包、内饰包、充电桩套装。价格信息基础价、配置加装价格、优惠金额、税费、最终成交价。折扣信息厂家促销、区域补贴、老客户优惠、限时权益。购车文件订单合同、购车发票、车辆合格证、保险单、交付确认单。在门店经营的初期这些数据通常散落在 Excel 表格、企业微信群和纸质合同里。销售报出的价格可能和财务算出来的不一致客户补交的证件照片也容易在聊天记录里被淹没。系统的价值不在于把表格搬到网页上而在于把“配置选择—价格计算—折扣生效—文件归档”这条链路串起来让每一版报价都有依据让每一份文件都挂在对应订单下。1.2 系统要解决的四个核心问题核心问题典型现状系统化后的目标配置管理混乱不同销售对同一车型的选装包叫法不一致统一维护车型与配置项允许前端按车型拉取可选配置报价口径不统一有人算价格时加入赠品有人不算由后端统一计算基础价、配置加价、折扣和最终价折扣规则不可控优惠活动靠口头通知容易过期或叠加错误使用折扣规则表管理有效期、优先级和叠加方式购车文件分散合同、发票、证件照片存在不同设备里上传后按订单号归档保留文件名、类型、上传时间这四个问题对应四个核心数据域车型配置、价格、折扣、订单文件。后续的数据库设计和接口设计都围绕它们展开。2. 业务建模与数据库设计2.1 车型与配置项建模车型与配置项之间是多对多关系。一个车型可以有多个可选配置一个配置项也可能适用于多个车型。为了避免每加一个车型就重复维护配置项需要拆成三张表vehicle_model车型表保存基础信息。vehicle_option配置项表保存配置名称和加装价格。vehicle_model_option车型与配置项关联表保存某个车型支持哪些配置以及该配置是否必选。这里的关键是“加装价格”放在配置项表还是关联表。如果同一配置在不同车型上的价格不同应该放在关联表如果价格全球统一放在配置项表更简单。实际项目中建议放在关联表因为配置定价经常随车型调整拆开后不会污染配置项主数据。2.2 价格与折扣规则的建模价格字段统一使用DECIMAL(10,2)不要用浮点数。金额计算必须精确浮点数在减法、乘法之后会出现微小误差虽然单个订单差距不大但累计到月报、对账阶段会非常麻烦。折扣规则需要支持多种形式否则系统上线后每来一个活动就要改代码。最低限度要支持三种满减订单金额满 200000 减 5000。折扣率订单总价打 9.5 折。直降固定减 3000。设计时增加rule_type、rule_value、priority、start_time、end_time、stackable字段。stackable表示是否允许与其他规则叠加这是很多系统最容易忽略的点。如果两个活动都是大额优惠又允许叠加最终成交价可能变成负数。2.3 订单与购车文件建模订单表不需要把配置项以 JSON 字符串直接塞进去但实际项目中常见做法是两种并存订单关联vehicle_model_option的明细表适合做库存统计。订单中保存一份配置快照 JSON适合展示历史数据。价格和配置在交付后可能调整所以订单必须保存快照。否则三个月后客户投诉当时确认过某项配置你翻数据库时发现配置表里的名称已经换了很难说清责任。购车文件表需要记录文件原始文件名、存储路径、文件类型和上传时间。文件本体建议不直接存在数据库而是存到本地磁盘目录、NFS 或对象存储数据库只保存路径和元数据。2.4 建表 SQL 示例CREATE TABLE vehicle_model ( model_code VARCHAR(32) PRIMARY KEY, model_name VARCHAR(64) NOT NULL, base_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE vehicle_option ( option_code VARCHAR(32) PRIMARY KEY, option_name VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE vehicle_model_option ( model_code VARCHAR(32) NOT NULL, option_code VARCHAR(32) NOT NULL, price DECIMAL(10,2) NOT NULL, required TINYINT NOT NULL DEFAULT 0, PRIMARY KEY (model_code, option_code) ); CREATE TABLE discount_rule ( rule_id BIGINT AUTO_INCREMENT PRIMARY KEY, rule_name VARCHAR(64) NOT NULL, rule_type VARCHAR(16) NOT NULL COMMENT FULL_REDUCE / RATE / DIRECT, rule_value DECIMAL(10,2) NOT NULL, priority INT NOT NULL DEFAULT 0, stackable TINYINT NOT NULL DEFAULT 1, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 ); CREATE TABLE purchase_order ( order_id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(64) NOT NULL UNIQUE, model_code VARCHAR(32) NOT NULL, config_snapshot JSON NULL, base_price DECIMAL(10,2) NOT NULL, option_amount DECIMAL(10,2) NOT NULL DEFAULT 0, discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0, final_price DECIMAL(10,2) NOT NULL, order_status VARCHAR(16) NOT NULL DEFAULT CREATED, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_file ( file_id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, file_name VARCHAR(128) NOT NULL, file_path VARCHAR(256) NOT NULL, file_type VARCHAR(32) NOT NULL, upload_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );这里有一个学习环境可以直接用的简化方式purchase_order.config_snapshot存 JSON省去额外建订单配置明细表的步骤。生产环境如果要做库存、选装率分析建议再建一张order_option_detail明细表。3. 后端接口实现配置查询、报价计算、文件归档3.1 项目依赖与环境后端使用 Spring Boot 3 MyBatis-Plus数据库使用 MySQL 8。以下版本组合用于说明思路实际落地前要确认当前稳定版。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency配置文件application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/seaotter_car?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 30MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto这里要特别注意serverTimezone。如果数据库连接串不指定时区而部署机器的系统时区与数据库不同start_time、end_time这类字段在查询时会与本地时间产生偏差导致折扣提前或延后生效。建议统一使用Asia/Shanghai并在应用层也设置相同时区。3.2 查询车型配置接口前端进入选配页时需要一次性拿到车型基础价、可选配置列表和每个配置的加价。接口可以这样设计RestController RequestMapping(/api/model) public class ModelController { Resource private ModelService modelService; GetMapping(/config) public ResultModelConfigVO getModelConfig(RequestParam String modelCode) { return Result.ok(modelService.getModelConfig(modelCode)); } }ModelConfigVO包含三个部分车型基础信息、必选配置、可选配置。查询时需要先查车型表再查关联表最后关联配置项表避免前端反复请求。这里推荐一个容易踩坑的点不要直接使用vehicle_option表里的加装价而要从vehicle_model_option表取价格因为同一配置在不同车型上的价格可能不同。3.3 报价计算接口报价计算是整篇文章最核心的接口。它接收车型编码、用户选择的配置编码列表、适用的折扣规则编码列表返回基础价、配置加价、优惠金额、最终价。Service public class QuoteService { public QuoteResult calculate(QuoteRequest request) { BigDecimal basePrice modelMapper.selectById(request.getModelCode()).getBasePrice(); BigDecimal optionAmount calculateOptionAmount(request.getModelCode(), request.getOptionCodes()); BigDecimal originalAmount basePrice.add(optionAmount); BigDecimal discountAmount calculateDiscount(originalAmount, request.getDiscountRuleIds()); BigDecimal finalPrice originalAmount.subtract(discountAmount); if (finalPrice.compareTo(BigDecimal.ZERO) 0) { throw new BizException(最终成交价不能小于0请检查折扣规则是否叠加异常); } return new QuoteResult(basePrice, optionAmount, discountAmount, finalPrice); } }配置加价计算时要校验每个配置编码是否真的适用于当前车型避免前端传入了其他车型的配置编码。校验不通过的情况不能静默跳过否则最终价格会比客户预期低订单成交后才发现问题。折扣计算按优先级排序先执行高优先级规则再执行低优先级规则。同一优先级下如果规则不允许叠加只取金额最大的一条。实际项目中建议把折扣计算设计成策略模式而不是一堆if...else。否则每增加一种活动类型报价服务就要重新发布。3.4 创建订单与上传购车文件创建订单接口要做三件事根据订单中的配置快照重新计算一遍价格不信任前端传过来的最终价。生成唯一订单号。保存订单数据。这里有一个非常重要的原则前端传入的价格只能用于展示不能作为落库依据。必须由后端按数据库中的配置价格和折扣规则重新计算否则销售在浏览器里改一下参数就能改成交价。上传文件接口PostMapping(/api/order/file/upload) public ResultString uploadFile( RequestParam Long orderId, RequestParam String fileType, RequestParam(file) MultipartFile file) { String originalFilename Objects.requireNonNull(file.getOriginalFilename()); String ext StringUtils.getFilenameExtension(originalFilename); String datePath LocalDate.now().toString().replace(-, ); String fileName orderId _ System.currentTimeMillis() . ext; String relativePath /uploads/ datePath / fileName; File dir new File(uploadRootPath, datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadRootPath relativePath)); return Result.ok(relativePath); }文件名要重命名不要直接使用用户上传的原始文件名避免路径穿越和重名覆盖。存储路径建议按日期分目录既方便归档也避免单个目录文件过多。4. 前端页面选配、报价试算与文件上传4.1 页面结构与交互前端使用 Vue 3 Element Plus核心页面只有一个但包含三个区域车型配置区、价格试算区、购车文件上传区。用户交互流程输入或选择车型编码页面请求/api/model/config。渲染基础价、必选配置、可选配置。用户勾选可选配置后前端调用/api/quote/calculate获取最新报价。用户确认订单调用/api/order/create生成订单。在订单详情页上传合同、发票、合格证等文件。前端不直接计算价格而是把配置变更后的结果交给后端计算。这样可以保证页面展示的价格和最终落库的价格一致。4.2 配置选择与价格试算页面中展示车型配置项的部分template el-card template #header span车型配置/span /template div v-foritem in configList :keyitem.optionCode el-checkbox :model-valueselectedOptions.includes(item.optionCode) :disableditem.required changehandleOptionChange(item, $event) {{ item.optionName }} span v-ifitem.required必选/span span¥{{ item.price }}/span /el-checkbox /div el-divider / p基础价¥{{ basePrice }}/p p配置加价¥{{ optionAmount }}/p p优惠金额¥{{ discountAmount }}/p p最终价¥{{ finalPrice }}/p /el-card /templatehandleOptionChange里不能只改勾选状态还要重新请求报价接口。如果用户快速连续勾选多个配置会产生并发请求。比较稳妥的处理是使用防抖或者用前端状态记录最后一次请求序号响应返回时只更新最新一次结果。async function handleOptionChange(option, checked) { if (checked) { selectedOptions.push(option.optionCode) } else { const index selectedOptions.indexOf(option.optionCode) if (index -1) selectedOptions.splice(index, 1) } const res await getQuote(modelCode, selectedOptions, selectedDiscounts) quoteResult.value res.data }4.3 文件上传组件Element Plus 的el-upload组件在设置action时需要把后端地址和请求头中的 Token 一起配置好。实际项目里文件上传一般不走网关或者走独立网关前端只需要关心上传成功后的返回值。el-upload :actionuploadUrl :headersuploadHeaders :data{ orderId: currentOrderId, fileType: contract } namefile :limit5 :on-successhandleUploadSuccess :on-errorhandleUploadError el-button typeprimary上传合同文件/el-button /el-upload文件上传成功后后端返回相对路径。前端要把这个路径保存到订单详情展示区域同时在下一次页面刷新时仍然能从订单接口中读出文件列表。5. 启动验证与预期结果5.1 准备数据库执行第 2 节的建表 SQL 后插入一批演示数据。例如基础车型“海獭 Racco 标准版”基础价 198000 元可选配置包括“冬季轮胎包”8000 元、“辅助驾驶包”15000 元、“充电桩套装”5000 元。INSERT INTO vehicle_model (model_code, model_name, base_price) VALUES (RACCO-STD, 海獭 Racco 标准版, 198000); INSERT INTO vehicle_option (option_code, option_name, category) VALUES (OPT-SNOW, 冬季轮胎包, 性能), (OPT-ADAS, 辅助驾驶包, 智能), (OPT-CHARGER, 充电桩套装, 能源); INSERT INTO vehicle_model_option (model_code, option_code, price) VALUES (RACCO-STD, OPT-SNOW, 8000), (RACCO-STD, OPT-ADAS, 15000), (RACCO-STD, OPT-CHARGER, 5000);5.2 启动后端在项目根目录执行mvn spring-boot:run启动成功后控制台会出现 Spring Boot 的启动日志表示端口 8080 已经在监听。5.3 调用接口验证查询车型配置curl http://localhost:8080/api/model/config?modelCodeRACCO-STD预期返回中包含基础价 198000可选配置列表 3 条。调用报价计算curl -X POST http://localhost:8080/api/quote/calculate \ -H Content-Type: application/json \ -d { modelCode: RACCO-STD, optionCodes: [OPT-SNOW, OPT-ADAS], discountRuleIds: [1] }如果没有配置任何折扣规则预期结果是{ basePrice: 198000.00, optionAmount: 23000.00, discountAmount: 0.00, finalPrice: 221000.00 }5.4 预期结果检查项预期结果查询车型配置返回车型基础价与可选配置勾选冬季轮胎包和辅助驾驶包配置加价变为 23000最终价变为 221000上传合同文件返回文件相对路径数据库 order_file 新增一行订单详情能读出配置快照、最终价、文件列表这里要提醒一点不要只验证“接口返回 200”还要验证金额数字、文件路径、数据库记录是否都正确。很多上线事故就是接口状态码正常、但业务字段错误。6. 常见问题与排查路径6.1 报价计算结果与 Excel 对不上现象同一台车系统算出的最终价和销售原有 Excel 表差了几千元。排查链路先核对基础价是否一致重点看车型编码对应的是不是同一款车。再核对配置加价前端选中的配置是否在后端按vehicle_model_option表的单价计算。最后核对折扣规则Excel 里可能存在手工优惠而系统里没有录入对应规则。多数情况下问题出在“Excel 里有人工折扣”或“配置价格不同步”。解决方式是规定所有折扣必须通过规则表配置不允许后台手工改价。如果业务上确实需要特批价格也要在建单时留下审批记录而不是直接覆盖最终价。6.2 折扣不生效现象用户下单时选择了折扣但最终价没有变化。检查方式SELECT rule_id, rule_name, rule_type, rule_value, priority, stackable, start_time, end_time, status FROM discount_rule WHERE rule_id 1;重点看三个字段status是否为 1当前时间是否在start_time和end_time之间stackable是否允许叠加。时间判断要非常小心如果数据库存的是DATETIME应用连接字符串没有指定时区可能出现偏差。6.3 上传文件后无法访问现象文件上传接口返回成功但是浏览器访问http://localhost:8080/uploads/...返回 404。原因通常是 Spring Boot 没有配置静态资源映射。解决方式是在配置类中增加Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-root-path}) private String uploadRootPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String location file: uploadRootPath /; registry.addResourceHandler(/uploads/**).addResourceLocations(location); } }生产环境一般不让应用进程直接服务文件而是上传到对象存储再用 CDN 或对象存储自带域名访问。本地映射只适合学习和联调。6.4 折扣叠加后最终价出现负数现象同时选用满减、打折、直降三种规则后最终价变成负数。处理原则在discount_rule表增加stackable字段默认只允许同优先级且同类型的规则叠加。在报价计算服务的末尾增加金额下限校验最终价不能小于 0也不能小于一个合理值。对异常情况记录日志包含订单号、规则编码、规则值、最终价方便复盘。7. 生产环境最佳实践与扩展方向7.1 上线前检查清单检查项建议数据库连接时区统一使用Asia/Shanghai避免折扣时间判断错误金额字段类型数据库用DECIMALJava 用BigDecimal前端金额展示用分转元或字符串订单价格计算后端必须重算不信任前端传入的最终价文件上传限制限制文件大小、类型文件名重命名存储路径分日期折扣规则叠加增加stackable字段和最终价下限校验日志记录报价计算过程的输入、输出和所使用规则方便对账权限配置维护、折扣维护、文件下载需要独立权限回滚配置和折扣表变更前做备份订单表不物理删除只做状态流转7.2 价格与折扣的版本化车型价格、配置价格、折扣规则都会随时间调整。如果一张表直接更新价格历史订单的报价就失去依据。生产环境建议价格调整时给vehicle_model、vehicle_model_option增加版本号。订单保存价格快照和配置快照并记录当时使用的版本号。折扣规则不要修改原有记录的有效期活动结束后直接更新end_time不要删除。这样既能保证历史可追溯也能让财务报表按版本号核对订单价格。7.3 文件存储与审计学习环境可以把文件存在本地生产环境建议使用对象存储。购车文件属于交易凭证需要额外的审计要求上传完成后记录操作人、时间、文件哈希。下载操作要记录日志避免客户证件照片被无关人员查看。合同、合格证、发票等文件要设置访问权限不能允许匿名访问。7.4 扩展方向当前系统已经覆盖了文件、配置、价格、折扣四个核心域下一步可以从三个方向扩展扩展方向要解决的问题审批流特权折扣、异常文件上传、订单改价需要多级审批规则引擎当优惠活动类型增多时用独立规则引擎替代硬编码计算消息通知订单创建后通知客户签署电子合同文件上传后通知财务复审如果只是做一个门店内部报价工具当前设计已经足够。如果要开放给客户自助下单还需要增加用户体系、电子签章、支付流水和库存占用复杂度会明显上升。建议先跑通内部流程再逐步开放。整个系统的核心并不复杂真正影响稳定性的地方在于价格必须由后端重算、折扣必须有优先级和叠加限制、文件必须归档到订单下。把这三点做好购车文件、配置、价格、折扣的管理就不会再依赖 Excel 和人工记忆。
返回列表