☰
自助装机硬件报价系统:兼容性规则链与配置单快照设计
2026/11/4 21:06:26 网站建设 项目流程

简介:面向电脑装机门店、硬件报价网站搭建者及ASP初学者的一套自助装机与配件报价系统源码,可用于快速搭建前台选配、报价展示和后台管理的整站流程。前台支持按CPU或主板型号自动搭配主机、实时计算总价并生成机器ID,配合今日报价的价格波动展示与带订购信息的留言模块,形成从选配到询价的闭环。压缩包共142个文件,以51个ASP动态页面、16个INC公共包含文件、1个MDB数据库和CSS样式为主,另有GIF、JPG界面素材及少量TXT说明,包大小字段显示为0B,实际以下载页为准。后台提供超级管理员、高级管理员和数据录入员的分级权限,密码经MD5处理,并集成数据库在线备份恢复压缩、SQL批量处理,以及新闻、产品、品牌和配件报价管理。已有433人学习,适合参考ASP+Access的目录组织、角色权限与报价业务实现。

1. 自助装机和硬件报价系统:客户自己选配,店员只负责核单

某装机店的老板跟我吐槽过一件事:店里三个人各维护一份 Excel 配件价目表,客户在微信里问「这套配置多少钱」,店员得挨个查价、加总,还得凭经验判断主板和 CPU 对不对得上,来回折腾半小时。后来他们换成一套自助装机和硬件报价系统,客户在页面上自己点 CPU、主板、内存,价格实时跳,装不上的组合直接标红拦住,最后生成一张能转发给朋友的配置单。

这套源码落地的就是这条链路:配件库维护、自助选配、兼容性校验、实时报价、配置单分享,后台一份价格数据全店共用。它适合两类人——中小装机商、电脑店、硬件工作室,想要一个价格和配件库都能自己改的报价工具;以及开发者,想找一个「规则校验 + 价格体系 + 前后端联动」都齐全的中型业务项目练手,比增删改查的 Demo 有嚼头得多。

2. 技术栈与目录结构:配件、配置单、报价三条线怎么切

先给结论:这类系统最怕的不是并发,是配件字段天天变。CPU 要插槽、核心数、TDP,显卡要长度、厚度、供电口,内存要代别、频率、时序,你今天用关系表的列固定下来,下个月上新品类就得改表。所以整套设计的重心,是把「稳定字段」和「差异字段」分开。下面按这个思路讲选型和分层,常见做法是后端 Spring Boot + MyBatis-Plus + MySQL,前端 Vue3 + Element Plus,前后端分离部署。

2.1 技术选型:为什么是 Spring Boot + MyBatis-Plus + Vue3

后端选 Spring Boot 的理由很实在:配件库、价格策略、规则校验这些东西都是典型的业务代码,Spring 的依赖注入能让「一条兼容性规则一个类」这种写法非常自然,规则类加个@Component就进容器了,规则链靠List<CompatibilityRule>注入自动收集,不用维护注册表。MyBatis-Plus 提供分页和条件构造器,配件库那种「按分类 + 品牌 + 价格区间 + 是否上架」的动态筛选,用LambdaQueryWrapper拼起来比手写 XML 舒服。

MySQL 8 在这套系统里有个关键作用:原生 JSON 字段。配件差异规格直接塞 JSON 列,查询时用JSON_EXTRACT或->>取值,既不用为每个品类建一张表,也不至于把规格拼成字符串解析。这一条是整个数据模型的支点,后面第 2.3 节会展开。

前端 Vue3 + Element Plus 的考虑是后台管理页面密度高:配件列表要带批量操作、价格导入要弹窗上传、配置单查询要展开行显示明细,Element Plus 的 Table、Form、Cascader 基本能覆盖九成场景,省下来的时间全砸在选配页的联动逻辑上。状态管理用 Pinia 存当前选配上下文,选配页刷新要能恢复现场,这一点比想象中重要——客户选到一半手滑刷新,上下文没了会直接跑单。

选配页本身不强依赖 SSR,因为价格和库存都是实时的,SEO 价值也不高,所以纯 SPA 部署就够了。如果你的场景是公开报价页要给搜索引擎抓,那就换成 Nuxt 或加一层预渲染,但报价准确性优先于抓取覆盖率,这是我踩过之后的判断。

2.2 目录结构:配件域、配置单域、报价域三块切开

后端按业务域分包,不按 Controller/Service/Mapper 三层平铺。平铺的问题是配件、配置单、报价三个域的 Service 混在一个包里,改报价要翻半天文件;按域分包后,每个目录自成闭环,新人接手看目录就知道系统在做什么。

目录职责关键类
part/配件库:分类、配件、规格模板、批量导入PartController、PartSpecTemplate
build/选配:选配上下文、联动候选、配置单落库BuildController、ConfigContext
rule/兼容性规则链:插槽、内存代别、尺寸、功耗、数量CompatibilityRule及实现类
quote/报价:有效价、阶梯价、优惠、快照QuoteService、PricePolicy
common/统一返回、异常、缓存键、Base62 短码Result、CacheKeys

前端目录按页面切:views/build/(选配页)、views/admin/part/(配件管理)、views/admin/quote/(报价与配置单),组件层单独抽components/PartPicker.vue,选配页和后台预览共用同一个选择器,避免两处逻辑漂移。这个拆分不是洁癖,是因为选配页和后台预览的过滤规则必须完全一致,一旦复制两份代码,早晚会出现「后台能选、前台不能选」的灵异现象。

2.3 建表:配件主表 + 规格 JSON,少改一半的表

核心就四张表:配件主表、配置单表、配置单明细表、价格历史表。配件主表只放所有品类都有的字段,差异规格进 JSON 列。

CREATE TABLE part ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT '所属分类,如 CPU/主板/显卡', brand VARCHAR(64) NOT NULL, model VARCHAR(128) NOT NULL, spec JSON NOT NULL COMMENT '差异规格,键名由分类模板约束', cost_price DECIMAL(10,2) NOT NULL COMMENT '进价,仅后台可见', sale_price DECIMAL(10,2) NOT NULL COMMENT '标准售价', stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1 上架 0 下架', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_cat_status (category_id, status) ) COMMENT='配件主表';

spec里 CPU 长这样:{"socket":"AM5","tdp":105,"cores":8};主板长这样:{"socket":"AM5","memType":"DDR5","maxCoolerHeight":160}。规则校验时用spec->>'$.socket'取值,或者查出来后在 Java 里读成 Map,看性能取舍——候选件数量小(一个分类通常几十到几百条),一次性查出来在内存里判断更快,也省得 SQL 里写一堆 JSON 路径。

配置单明细表里我坚持存一份quoted_price快照,而不是只存part_id靠实时算。原因第 5 章会细说,简单讲:客户三天后拿着分享链接来店里对账,价格变了就是纠纷。

CREATE TABLE config_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sheet_id BIGINT NOT NULL, part_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, quoted_price DECIMAL(10,2) NOT NULL COMMENT '加入配置单那一刻的有效价快照', KEY idx_sheet (sheet_id) );

价格历史表按part_id + 生效时间存,改价时插入一条新记录而不是原地覆盖,sale_price只是当前生效价的一个冗余方便查询。这样做的好处是三个月后老板问「这颗 U 上个月卖多少」,你能直接查出来;坏处是表会涨,按配件数量级算,一年也就几十万行,不叫事。

3. 兼容性校验:一条规则一个类,把「装不上」挡在下单前

兼容性校验是这套系统的灵魂,也是最容易做成 if-else 泥潭的地方。我的做法是把每条规则独立成类,实现统一接口,靠 Spring 自动收集成链。规则要能单独开关、单独定位报错,前端才能精确地在某个配件上标红,而不是甩一句「配置不兼容」让客户自己猜。

3.1 规则接口与上下文:所有规则吃同一份数据

规则统一入参是一个ConfigContext,里面是「分类 -> 已选配件列表」的映射,加上一个可选的候选件。规则只从上下文取数据,不碰数据库,这样规则可以做单元测试,也能在候选过滤和整单校验两个场景复用。

public interface CompatibilityRule { /** 规则编码,前端用它定位标红位置,也用于后台开关 */ String code(); /** 返回 empty 表示通过;返回文本表示不兼容原因,直接展示给用户 */ Optional<String> check(ConfigContext ctx); /** 默认中优先级,数字小的先跑,命中即短路 */ default int order() { return 100; } }

code()是给前端用的,比如CPU_MAINBOARD_SOCKET,前端拿到这个编码就知道该把红框画在主板那张卡上。order()决定执行顺序,插槽这种硬冲突排前面,命中就返回,不用再跑后面的功耗、尺寸规则,省一次计算也省一条更让人困惑的报错。

ConfigContext的两个方法值得单独说:first(分类)取该分类的第一个配件,optional(分类)返回Optional。后者用于「可能没选」的场景——客户还没选显卡,功耗规则就不该报错,也不该按独显去推电源。

3.2 一条具体规则:CPU 插槽匹配

看最典型的一条。CPU 和主板的插槽必须一致,这是个硬约束,错了根本点不亮。

@Component public class CpuMainboardSocketRule implements CompatibilityRule { @Override public String code() { return "CPU_MAINBOARD_SOCKET"; } @Override public int order() { return 10; } // 硬冲突,最先跑 @Override public Optional<String> check(ConfigContext ctx) { Part cpu = ctx.first("CPU"); Part board = ctx.first("MAINBOARD"); // 有一方没选就不校验,避免"未选完就满屏红" if (cpu == null || board == null) return Optional.empty(); String cpuSocket = cpu.specText("socket"); String boardSocket = board.specText("socket"); // 规格缺失时不能静默放行,要提示人工确认 if (cpuSocket == null || boardSocket == null) { return Optional.of("插槽信息缺失,请人工确认兼容性"); } if (!cpuSocket.equalsIgnoreCase(boardSocket)) { return Optional.of("CPU 插槽 " + cpuSocket + " 与主板插槽 " + boardSocket + " 不匹配"); } return Optional.empty(); } }

specText("socket")是Part上的一个小工具方法,从 JSON 里按 key 取值并做空安全。这里有个细节值得说:规格缺失时我返回的是「请人工确认」而不是Optional.empty()。早期的版本就是缺失当通过,结果一批没录插槽字段的老配件全被放过去了,客户配了套点不亮的机器,退货加差评。宁可多一次人工确认,也不要静默放行——这是血泪教训。

3.3 功耗估算与电源推荐:余量留多少才合适

功耗规则分两半:一半是校验(客户选的电源瓦数够不够),一半是推荐(没选电源时给个建议值)。两半共用同一个估算方法。

public int estimateWatts(ConfigContext ctx) { int cpuTdp = ctx.optional("CPU").map(p -> p.specInt("tdp", 65)).orElse(65); int gpuTdp = ctx.optional("GPU").map(p -> p.specInt("tdp", 0)).orElse(0); int base = 60; // 主板 + 内存 + 固态 + 风扇的粗略基数 // 按整机峰值负载的 30% 留余量,再向上取整到 10W int peak = cpuTdp + gpuTdp + base; int withHeadroom = (int) Math.ceil(peak * 1.3 / 10.0) * 10; return withHeadroom; }

参数说明:tdp缺失时 CPU 按 65W 兜底,显卡按 0(无核显的机器没选显卡时,估算值会偏保守,但至少不会给出一个明显不够的电源)。base取 60W 是个工程经验值,实际装机里主板加两三条内存加一块固态加几个风扇,峰值确实在这个量级。余量取 30% 而不是 20%,是因为电源的额定功率在高温下会缩水,而且客户后面可能加显卡。

这里翻过车:早期余量给 10%,一台 i7 加中端卡的机器推荐了 550W 电源,客户满载跑渲染偶尔重启,回来找售后。余家余量这件事,宁可推荐大一点,也别踩线。这条规则我后来固定成「估算值 + 30%,且不低于 450W」才收手。

4. 选配与报价链路:从点击 CPU 到生成一张可分享的配置单

规则讲完了,看数据怎么走。选配页一次点击会触发两件事:过滤候选(哪些还能选)、重算价格(现在多少钱)。这两件事的性能和正确性,决定了客户会不会中途关页面。

4.1 候选过滤:每选一个件,都要重跑一次局部校验

客户在「内存」分类里看到的列表,必须是「和已选主板兼容的那些」。做法是把每个候选件塞进上下文,跑一遍规则,通过的才返回。这叫局部校验,和整单校验的区别是:整单必须全部通过才允许生成配置单,局部只要不冲突就能展示。

@GetMapping("/api/build/options") public Result<List<PartVO>> options(@RequestParam Long categoryId, @RequestParam(required = false) List<Long> selectedIds) { // 1. 用已选配件还原上下文(selectedIds 来自前端,服务端要按 id 重新查库校验) ConfigContext ctx = buildService.loadContext(selectedIds); // 2. 拉取该分类下所有上架且有库存的配件 List<Part> candidates = partService.listOnSale(categoryId); // 3. 逐个塞进上下文跑规则,冲突的直接过滤掉 List<PartVO> result = candidates.stream() .filter(p -> ruleEngine.checkPartial(ctx.withCandidate(categoryId, p)).isEmpty()) .map(p -> PartVO.of(p, priceService.effectivePrice(p.getId(), ctx.channel()))) .collect(Collectors.toList()); return Result.ok(result); }

关键参数:selectedIds必须由服务端重新查库,不能信前端传来的完整对象——前端可以改价格。ctx.channel()是渠道标识(门店 / 线上 / 批发),有效价按渠道不同,这个后面说。withCandidate返回一个新的上下文副本,不是修改原对象,避免流式处理时互相污染。

还有一个必须处理的问题:候选列表会越选越少。客户先选了个 mATX 主板,再去看机箱,发现只剩三款能选,体验上会觉得「怎么东西这么少」。我的处理是在返回结果里带上被过滤掉的数量和被哪条规则过滤的,前端在上方显示一行灰字「已按主板尺寸过滤 12 款」。不显示的原因很简单,客户会怀疑系统坏了。

4.2 报价计算:叠加顺序错了,金额差一块钱

价格体系不复杂,但有严格的叠加顺序:逐件有效价 → 汇总小计 → 渠道阶梯 → 整单优惠 → 最后一步才四舍五入。顺序一旦搞反,金额就会差几分到几块,对账的时候特别难受。

public BigDecimal quote(ConfigContext ctx, QuoteOptions opt) { // 1. 逐件取有效价(可能来自渠道价 / 促销价 / 标准价),不做舍入 BigDecimal subtotal = ctx.parts().values().stream() .flatMap(List::stream) .map(p -> priceService.effectivePrice(p.getId(), opt.getChannel())) .reduce(BigDecimal.ZERO, BigDecimal::add); // 2. 渠道阶梯:满额返点,按小计匹配档位 BigDecimal afterTier = tierPolicy.apply(subtotal, opt.getChannel()); // 3. 整单优惠:券 / 活动,按金额或百分比扣减 BigDecimal discount = discountPolicy.apply(afterTier, opt); // 4. 只在最终结果上舍入,且不允许为负 return afterTier.subtract(discount) .max(BigDecimal.ZERO) .setScale(2, RoundingMode.HALF_UP); }

参数说明:effectivePrice内部按渠道 -> 当前时间 -> 是否促销三级取价,取到就返回,命中缓存。tierPolicy.apply是阶梯策略,配置在数据库里而不是写死,老板改返点规则不用发版。max(BigDecimal.ZERO)是防御,优惠券叠加算出来负数时要兜住,否则会出现「这单倒找钱」的离谱账单。

四舍五入只放在最后一步,是因为每件单独setScale(2)会把误差累加,十几件的配置单能差出几毛钱。这个问题对个人客户无所谓,对批发客户就是账目对不上的由头。

4.3 配置单落库与分享码

客户点「保存配置」,后端把明细连同当时的有效价一起写进config_item,同时生成一个短码。短码用 Base62 加唯一索引,长度 6 位足够,生成时查一次冲突重试即可,不要用自增 ID 直接暴露——那等于把「这家店昨天卖了几单」写在 URL 上。

分享出去的页面读的是配置单快照,价格就是快照里的quoted_price,同时展示「报价有效期至 X 日」。有效期是我后来加的,一开始做成永久有效,结果有客户拿三个月前的分享链接来店里要求按老价成交,谈得很不愉快。有效期设 7 天,到期后页面提示「请联系店员重新报价」,把主动权收回来。

5. 避坑与排查:上线后最常被追问的五个问题

下面五条全是真实会被问到脸上的,按「现象 → 原因 → 解决」写,能对号入座就直接抄。

5.1 兼容性相关的三个坑

坑一:选配页不拦,提交时才报错。现象是客户选了 DDR5 内存配 DDR4 主板,选配页面上毫无提示,点了保存才弹出一句「配置不兼容」,客户直接骂人。 原因是规则只在生成配置单时跑全量校验,/api/build/options那个候选接口没接规则引擎。 解决是把引擎拆成checkPartial和checkFull两档:局部校验用于候选过滤和每次变更的实时提示,全量校验用于最终提交。两档共用同一套规则类,只差「未选中的分类是否跳过」这一个参数。

坑二:散热器高度超限,规则没触发。现象是 165mm 的风冷塞进限高 160mm 的机箱,系统显示兼容。 原因是规格字段名不统一,散热器录的是height,机箱模板里定义的是maxCoolerHeight,规则取maxCoolerHeight时散热器那边取到 null,被当成「无法判断」放行了。 解决是给每个分类定义字段模板(part_category.spec_templateJSON),录入时按模板校验键名和单位,规则里取不到值一律走「需人工确认」分支。字段名的事必须在录入环节卡死,靠规则层兜不住。

坑三:内存插槽插满四条还能继续加。现象是客户选了 4 条内存,页面依然允许再加第 5 条,只是价格往上走。 原因是当时只做了兼容性校验,没做数量约束,规则层默认「同一分类可以任意多件」。 解决是补一条QuantityRule,按分类模板里的maxQty校验,主板类maxQty=1,内存类按主板规格里的memSlots动态取值。数量约束和兼容性约束是两码事,最好一开始就分开建模。

5.2 价格与数据同步的两个坑

坑四:后台改完进价,前台客户刷新还是老价。现象是老板在后台把某款显卡调价 200,前台刷新好几次价格纹丝不动,重启服务才好。 原因是配件列表整体做了缓存,缓存 key 只带分类 ID,改价时只清了「按 ID 查单件」的缓存,没清列表缓存。 解决是把价格从列表缓存里剥出来,列表只缓存「有哪些配件」,价格走独立的effectivePrice缓存,key 带上part_id + 渠道 + 价格版本号,改价时递增版本号,旧 key 自然失效。这个改法比全量清缓存精准,也不会因为一次改价把整个列表打穿。

坑五:分享出去的配置单,过几天价格变了。现象是客户拿着上周的分享链接来店里,说好的八千三,你算出来八千六,客户觉得被坑。 原因是配置单只存了part_id,页面每次打开都按实时价重算。 解决是落库时把quoted_price快照写进明细表,分享页优先读快照,同时展示有效期。改价本身没错,错在让客户看到的价格在背后偷偷变。快照这件事一定要在第一次上线就做,后面补的代价是迁移历史数据。

6. 二次开发:加一个配件分类,接一路外部价格源

这套系统最容易被改的地方是配件分类。硬件圈子品类一直在变,今天加显示器,明天可能加外设、加整机,所以分类和规格模板必须是数据驱动的,不能写死在代码里。

6.1 加一个「显示器」分类要动哪几处

理论上有三步:插分类记录、定义规格模板、加一条前端 tab。如果代码写得对,Java 侧一行不用改。

-- 1. 新增分类,spec_template 约束该分类允许出现的规格键 INSERT INTO part_category (code, name, spec_template, max_qty, sort) VALUES ('MONITOR', '显示器', JSON_OBJECT( 'size', JSON_OBJECT('type','number','unit','inch'), 'resolution', JSON_OBJECT('type','string'), 'panel', JSON_OBJECT('type','string','enum',JSON_ARRAY('IPS','VA','TN','OLED')), 'refresh', JSON_OBJECT('type','number','unit','Hz'), 'interface', JSON_OBJECT('type','string') ), 1, 90);

spec_template干了三件事:约束键名(不许乱起名)、约束类型和单位(不许把27和27寸混着录)、约束枚举(面板类型只能是那四个值)。max_qty=1表示一台机器只装一台显示器,数量规则自动生效。

前端加一个 tab 是纯配置,选配页的分类列表从/api/part/categories拉。规则层要不要加?显示器对整机兼容性几乎没有硬约束,加一条分辨率与显卡的「建议」规则就行,走警告级别而不是拦截级别——警告和拦截要分开,全都拦截会让客户什么都选不了。

验证方法很土但有效:拿三条真实配件录进去,故意把refresh录成字符串"165Hz",看后台是否报类型错误;再故意把面板类型录成PLS,看枚举校验是否拦住。两个都拦住,说明模板校验真的接上了。

6.2 接外部价格源:先落历史表,别直接覆盖售价

二次开发里最常见需求是「同步电商平台的行情价」。直接定时覆盖sale_price是灾难,因为行情价和你店里的定价策略是两回事,行情涨了不代表你要立即涨。

我的做法是分三层:拉取写price_history的source='EXTERNAL'记录,跟当前sale_price做差异对比,超过阈值(比如 5%)才在后台生成待审核提醒,人工确认后再改售价。

@Scheduled(cron = "0 30 3 * * ?") // 每天凌晨 3:30 跑,避开营业时段 public void syncExternalPrice() { List<ExternalPrice> fetched = fetchClient.pull(); // 外部数据源,失败抛异常 for (ExternalPrice ep : fetched) { Part part = partService.getBySku(ep.sku()); if (part == null || part.status() == 0) continue; // 没匹配上或已下架,跳过 BigDecimal diff = ep.price().subtract(part.salePrice()) .abs().divide(part.salePrice(), 4, RoundingMode.HALF_UP); // 差异超 5% 才生成待办,避免每天几十条无意义提醒 if (diff.compareTo(new BigDecimal("0.05")) > 0) { alertService.createPriceAlert(part.id(), ep.price(), diff); } // 无论是否超阈值,都记一条历史,后面能看趋势 priceHistoryService.append(part.id(), ep.price(), "EXTERNAL"); } }

参数说明:cron放凌晨跑,是因为抓取会占带宽也不影响营业;compareTo(new BigDecimal("0.05"))是阈值判断,用compareTo而不是equals,BigDecimal 的equals会把0.05和0.050判成不等,这是个经典陷阱。append无论是否超阈值都要写,历史数据不写就没有趋势,光看当前价说明不了什么。

验证这套同步是否正常,我一般盯三个点:一看price_history当天是否有新增记录(说明任务跑了),二看source='EXTERNAL'的记录里 SKU 匹配率(低于 80% 说明外部源的 SKU 规则变了),三看待办列表里是否有明显不合理的价差(超过 50% 基本是数据源标错价,别急着跟)。

从那以后我每次接外部数据源,都强制走一遍「先落历史、再对比、人工确认」这三步,哪怕客户催着要自动改价也不松口。自动改价省下的人力,远不够填一次错价的坑。这套系统的分类模板和规则链设计,我后来在自己做的几个报价类项目里都照搬了,改起来确实省心。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询