简介:多功能在线报价系统源码是一套基于Web的完整业务系统,主要面向有报价管理需求的中小企业、自由开发者及ASP技术学习者。系统围绕产品目录、价格计算、订单跟踪与客户管理设计,支持多级定价、批量折扣和优惠策略,并允许按业务场景深度定制。压缩包共16个文件,其中8个ASP文件承担后台逻辑与前端交互,配合3个CSS样式表、2个HTML页面和INC/ASA配置文件构成运行环境,另含1个SQL数据库脚本用于初始化数据表,整体仅22KB,便于快速部署与代码走读。该资源已有2670人学习/下载,读者可从中掌握在线报价流程、ASP+SQL架构及数据库设计要点,也可在源码基础上自行接入支付接口或扩展新模块,以适配企业不同业务流程。
1. 多功能在线报价系统:从表格割据到一条线走完报价流程
多功能在线报价系统,把企业里最容易失控的报价流程收拢成一条线:从产品库选品、按客户档案生成报价单、套用价格策略、走审批、发 PDF、留历史版本。它解决的问题不是把 Excel 换成一个网页,而是让每一次报价背后都有可核对的依据——价格是谁定的、客户是谁、何时到期、发给客户的是哪一版。适合做售前的销售运营、小团队技术负责人,以及刚拿到一份报价系统源码不知道从哪读起的开发者。下面从模块拆解、数据模型、后端逻辑、前端交互一路讲到部署踩坑。
2. 拆解多功能报价系统:典型模块、技术选型与数据模型
2.1 先对清楚“多功能”是哪几个功能
在线报价系统的“多功能”,行业内并没有统一标准,但绝大多数源码包都会覆盖这六块:客户档案、产品目录、报价单编辑、价格策略、审批流、历史版本。有一些会再带上邮件发送和 PDF,功能再多一点的会有报价单转订单、收款跟踪,但那已经偏向轻量 CRM 了。
客户档案解决“报给谁”的问题;产品目录解决“报什么价”的问题;报价单编辑是核心工作台,销售在这里选产品、填数量、套折扣;价格策略负责把阶梯价和客户等级折扣落成规则,而不是靠销售口算;审批流在多级审批的公司里是刚需,销售填完不能直接发出去,要等经理确认;历史版本保证每个出去的报价都有据可查。
这里有个常见误区:功能越多越好。实际上,任何报价系统源码在二次开发时,最先砍掉的往往是审批流和价格策略,因为它们最依赖公司内部流程,不能照搬。所以拿到源码后,第一个任务不是看代码能跑什么,而是画出自己公司的报价路径:谁发起、谁审核、发给谁、是否允许改价。路径画出来,“多功能”里的取舍就清楚了。
2.2 技术选型:报价系统适合用什么栈
一个报价系统是典型的内部业务系统:数据关系明确、事务要求高、需要记录审计、页面交互中度复杂。常见的技术选型维度有后端框架、前端框架、数据库、部署方式。
我一般做这个方向时会用 Java + Spring Boot + MySQL,前端用 Vue 或 React 加一个现成的组件库。选型理由有三点:第一,报价计算和审批流转都是重逻辑,Spring Boot 的事务管理和 JPA/MyBatis 的成熟生态能省掉很多自己造轮子的时间;第二,报价系统需要历史审计,Java 生态对操作日志、SQL 审计的接入非常顺;第三,团队后续招聘和二次开发时,Spring Boot 的资料最全,新人上手成本低。
但不是所有场景都适合这套。如果只是某个小团队内部用,10 个以内用户,且没有专职后端,用 Python + Flask + SQLite 也能跑,数据量上来之前并不会有性能问题。下面用一个简短的表对比常见选择,按你自己的团队情况套。
| 后端方案 | 适合场景 | 事务/稳定性 | 上手成本 |
|---|---|---|---|
| Java + Spring Boot | 多部门、需审批、长期维护 | 强,事务管理完备 | 中上 |
| Python + Flask/FastAPI | 小团队、原型快速迭代 | 中,需自行注意事务边界 | 低 |
| Node.js + NestJS | 前后端同构、全栈团队 | 中,事务依赖 ORM | 中 |
前端不用太纠结,报价系统页面数量通常不多,一个 Vue3 工程加一个 Element Plus 或者 Ant Design Vue 就够。真正要花时间的是价格计算模块和数据模型,它们决定你交付出去的东西能撑多久。
2.3 数据库设计:一张用得上十年的报价核心表结构
在线报价系统的数据库设计,核心是五张表:用户表、客户表、产品表、报价单表、报价明细表。如果再考虑历史版本,还会有一张报价版本表。下面给出一版可以直接落库的建表 SQL,字段按最小可用设计,后续无论拿到什么源码都可以拿这张表去对照。
先说几个关键设计决策。第一,金额字段全部用 DECIMAL(12,2) 而不是 float/double,这是报价系统里无数浮点坑的根源;第二,报价明细里冗余保存当时的单价和税率快照,产品以后改价不能影响已经发出的报价;第三,主表和明细表之间用外键关联,但业务上在应用层控制读取,不要指望数据库外键帮你做级联删除。
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'SALES', -- SALES / APPROVER / ADMIN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE customers ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, contact VARCHAR(64), phone VARCHAR(32), email VARCHAR(128), address VARCHAR(255), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE products ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(128) NOT NULL, spec VARCHAR(255), base_price DECIMAL(12,2) NOT NULL, -- 基准售价,单位:元 status TINYINT NOT NULL DEFAULT 1 -- 1 上架 / 0 下架 ); CREATE TABLE quotes ( id BIGINT PRIMARY KEY AUTO_INCREMENT, quote_no VARCHAR(32) NOT NULL UNIQUE, customer_id BIGINT NOT NULL, created_by BIGINT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', total_amount DECIMAL(12,2) NOT NULL DEFAULT 0, -- 含税总金额 discount_rate DECIMAL(5,2) NOT NULL DEFAULT 1.00, -- 整单折扣 tax_rate_percent DECIMAL(5,2) NOT NULL DEFAULT 13.00, -- 增值税率快照 valid_until DATE NOT NULL, -- 报价有效期 version INT NOT NULL DEFAULT 1, -- 乐观锁版本号 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE quote_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, quote_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name_snapshot VARCHAR(128) NOT NULL, -- 报价时的产品名快照 unit_price_snapshot DECIMAL(12,2) NOT NULL, -- 报价时的单价快照 quantity INT NOT NULL, discount_rate DECIMAL(5,2) NOT NULL DEFAULT 1.00, subtotal DECIMAL(12,2) NOT NULL DEFAULT 0, tax_rate_percent DECIMAL(5,2) NOT NULL DEFAULT 13.00 ); CREATE TABLE quote_versions ( id BIGINT PRIMARY KEY AUTO_INCREMENT, quote_id BIGINT NOT NULL, version_no INT NOT NULL, snapshot_json JSON NOT NULL, -- 整单快照 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );这段 SQL 的关键在于“快照”这个概念。quotes 表里存了整单折扣、税率和大版本号;quote_items 里存了产品名和单价的快照;quote_versions 用 JSON 字段保存整单历史。产品表里的 base_price 改了,已经被 SENT 的报价单从明细快照取价,不受影响;未完成的草稿如果需要联动最新价,则可以通过重新拉取产品表数据来实现。
字段上唯一要注意的是 status 用 VARCHAR(20) 存枚举名字而不是数字。原因很简单:DRAFT、SENT 这类名字在排查问题时一眼能看懂,INT 枚举你还得翻文档。如果对性能追求极致,加索引即可,不需要为此牺牲可读性。version 字段是整个并发控制的关键,后面避坑章节会具体讲。
3. 后端核心逻辑:价格计算、状态机与历史快照
后端是报价系统的重头。无论前端页面做得多好,如果后端的价格计算不稳定、状态流转不严谨、历史不可追溯,上线之后就是不停救火。这一章把三个核心点展开讲。
3.1 价格计算:把税率、折扣、阶梯价一次算对
报价系统里最容易被低估的是价格计算。表面看是“单价乘以数量”,实际会涉及:整单折扣、明细折扣、运费、税率、含税和不含税的转换、阶梯价判断。常见做法是把价格计算收敛到一个独立的 service 类里,前端不做最终计算,数据库不做复杂计算,所有报价金额都以这个类的结果为准。
下面是一个简单的 Java 价格计算实现,按“净价乘以折扣、再加运费、再按税率计算税额”的规则来。这也是大多数贸易类公司报价时的通用顺序。
import java.math.BigDecimal; import java.math.RoundingMode; public class PriceCalculator { /** * 计算报价单某一行的含税金额 * * @param unitPrice 不含税单价(元) * @param quantity 数量 * @param discountRate 折扣率,0.9 表示 9 折,1.0 表示无折扣 * @param taxRatePercent 税率(%),13 表示 13% * @param freightFee 该行分摊运费(元) * @return 含税金额(元) */ public static BigDecimal calcLineAmount(BigDecimal unitPrice, int quantity, BigDecimal discountRate, int taxRatePercent, BigDecimal freightFee) { BigDecimal qty = BigDecimal.valueOf(quantity); BigDecimal taxRate = BigDecimal.valueOf(taxRatePercent) .divide(BigDecimal.valueOf(100), 6, RoundingMode.HALF_UP); // 1. 净价 = 单价 * 数量 * 折扣 BigDecimal net = unitPrice.multiply(qty) .multiply(discountRate) .setScale(2, RoundingMode.HALF_UP); // 2. 含税价 = (净价 + 分摊运费) * (1 + 税率) BigDecimal taxBase = net.add(freightFee); BigDecimal gross = taxBase.multiply(BigDecimal.ONE.add(taxRate)) .setScale(2, RoundingMode.HALF_UP); return gross; } }这段代码有三个设计点。第一,全程用 BigDecimal,中间结果每一步都 setScale 到 2 位小数,避免浮点误差累积;第二,税率先用 6 位小数做除法,再参与乘法,保证百分比换算不至于过早损失精度;第三,运费分摊做成按行传入而不是在总价统一加,是因为报价明细里常有“部分行包邮”的规则,按行计算后汇总更灵活。
实际项目中需要扩展的通常是两步。第一步,阶梯价:当 quantity 大于某个阈值时,unitPrice 要换成阶梯价区间对应的价格。常见做法是在产品表或单独的价格策略表里存 price_breaks,计算前先取出对应区间的价格再走同一套逻辑。第二步,含税转不含税:有的客户要“含税单价”,有的要“不含税单价”,展示端要能切换。内部统一用不含税单价做计算,展示时再套税率转换,这样历史数据不会因为切换口径而出错。
3.2 报价单状态机:草稿、待审、已发送、已确认、已过期
报价单不能是一张随便改的表格。一个正规的报价系统源码里,状态机一定是最不能改散的部分。状态机要回答三个问题:当前单据在哪个环节、谁能操作它、操作会产生什么副作用。
标准的状态流转如下:销售创建报价单,此时是 DRAFT;提交审批后是 PENDING_APPROVAL;审批通过并发送给客户是 SENT;客户确认后是 CONFIRMED;超过有效期自动进入 EXPIRED;成交结束或人工关闭是 CLOSED。这套流转覆盖了报价的完整生命周期。
public enum QuoteStatus { DRAFT, // 草稿:销售可编辑 PENDING_APPROVAL, // 待审批:销售不可编辑,审批人可驳回 SENT, // 已发送:内容固化,仅可撤回重发 CONFIRMED, // 客户已确认:等待转订单 EXPIRED, // 已过期:超过有效期 CLOSED // 已关闭:交易结束或作废 }不要让每个接口自己去判断状态。例如“更新报价单”接口里,如果只是查一下 status 再决定是否放行,早晚会漏掉一两个入口。常见做法是把所有状态迁移写成一个服务方法,统一做合法性校验:
public void changeStatus(Quote quote, QuoteStatus target) { switch (quote.getStatus()) { case DRAFT: if (target != QuoteStatus.PENDING_APPROVAL && target != QuoteStatus.CLOSED) { throw new IllegalStateException("草稿只能提交审批或关闭"); } break; case PENDING_APPROVAL: if (target != QuoteStatus.SENT && target != QuoteStatus.DRAFT) { throw new IllegalStateException("待审批只能通过或驳回"); } break; case SENT: if (target != QuoteStatus.CONFIRMED && target != QuoteStatus.EXPIRED && target != QuoteStatus.CLOSED) { throw new IllegalStateException("已发送只能确认、过期或关闭"); } break; default: throw new IllegalStateException("该状态不允许继续流转"); } quote.setStatus(target); }状态机的关键参数有两个:一个是操作角色,一个是操作时间。操作角色靠查询当前登录用户所属 role 决定,比如只有 APPROVER 角色能调用 PENDING_APPROVAL 到 SENT 的接口;操作时间则用于生成状态流转日志,哪怕不用复杂的工单系统,至少要在表里记录这次状态变更“谁、何时、从什么状态变成什么状态”。这里不额外建表的话,可以直接用数据库的 operation_log 表或者 quote_versions 表顺带记录。
3.3 历史快照:让每一份发出去的报价都有后悔药
报价系统的历史追溯,我的做法是一句话:发出去的报价不允许被后台静默修改,历史记录用来兜底。现实中经常出现的情况是,客户打电话说“你们上次报的是 190 元,怎么这次变成 210 元了”,这时候如果没有历史快照,就只能翻邮件翻聊天记录,翻不到就是一次信任事故。
实现方案并不复杂。修改报价单之前,先把当前整单序列化成 JSON 存入 quote_versions 表,再更新主表和明细表,同时 version 字段自增。取历史记录时,按 quote_id 倒序查 quote_versions,展示当时的完整快照。
// 更新报价单前的快照生成逻辑,以 MyBatis 风格的 Service 示意 public void updateQuote(Quote quote, List<QuoteItem> items) { QuoteSnapshot snapshot = buildSnapshot(quote, items); quoteVersionMapper.insert(quote.getId(), quote.getVersion(), snapshot.toJson()); quoteMapper.update(quote); // 更新主表,version + 1 quoteItemMapper.deleteByQuoteId(quote.getId()); quoteItemMapper.batchInsert(items); // 重建明细 }这里有两个细节要注意。第一,快照 JSON 里必须包含金额、税率、有效期、产品名和单价快照,而不是只存 id,否则以后查历史时关联的 PRODUCT 表已经改价,快照就失效了。第二,销售在 DRAFT 状态下反复修改不应产生过多历史版本,否则版本表会膨胀得很厉害。我一般只在状态从 DRAFT 变为 PENDING_APPROVAL、PENDING_APPROVAL 变为 SENT 时强制生成快照,此前的草稿阶段修改只更新主表,不进版本表。
4. 前端报价单编辑与导出:交互骨架和一份稳定的 PDF
4.1 报价单编辑页:实时计算与输入校验
报价单编辑页是销售每天打开最多的页面。一个合格的编辑页要有三个特征:数量、折扣改完金额马上变;必填项校验明确;不会因为误触刷新丢掉已填数据。下面这一段是报价明细计算的核心逻辑,典型的 Vue3 + Composition API 写法。
// 报价明细行:数量、折扣修改后,小计和总价同步刷新 const quoteItems = ref([ { productId: 1, unitPrice: 199.0, quantity: 10, discountRate: 1.0 }, { productId: 2, unitPrice: 89.0, quantity: 5, discountRate: 0.9 } ]) // 总金额:前端只负责展示,最终以服务端计算结果为准 const totalAmount = computed(() => { let total = 0 quoteItems.value.forEach((item) => { const net = item.unitPrice * item.quantity * item.discountRate total += net }) return total.toFixed(2) }) // 数量输入校验:不合法时给出提示,并且不参与计算 const isQuantityValid = (value) => value > 0 && Number.isInteger(Number(value))前端计算只是为了让销售在填写时有即时反馈,最终价格一定要以后端计算结果为准。很多源码包直接把前端算出来的金额提交给后端,这是不安全的做法。真正落地时,保存接口应该先由后端重新计算一轮再写库,前端传过来的金额只作展示用。这样即使有人绕过前端直接调接口,也改不了报价总金额。
编辑页还需要处理的数据是折扣输入。不同公司对折扣的表达不一样,有的人习惯输入“90”表示 9 折,有的人输入“0.9”。我一般会在页面统一做一层转换:输入框显示百分比数,提交时转为小数传给后端,避免两类人混用导致金额算错。另外,在列表页进入编辑页之前,先确认当前报价单状态是 DRAFT,否则编辑框应该全部置灰。
4.2 导出 PDF:服务端渲染而不是浏览器打印
报价单导出的终极形态是 PDF。常见但不推荐的做法是 window.print() 打印网页。这样做在本地测试感觉良好,但换一台浏览器、换一套字体、换一次分页位置,版式就崩了。真正稳定的是服务端渲染:后端用 HTML 模板生成页面,再用固定的渲染内核转成 PDF,前端只负责下载文件。
调用流程很简单。前端拿到报价单 id,请求 /api/quotes/{id}/pdf,后端返回 application/pdf 文件流。下面是一段前端下载代码:
// 下载报价单 PDF:后端返回文件流 async function exportQuotePdf(quoteId) { const token = getToken() const res = await fetch(`/api/quotes/${quoteId}/pdf`, { headers: { 'Authorization': `Bearer ${token}` } }) if (!res.ok) { throw new Error(`导出失败:${res.status}`) } const blob = await res.blob() const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = `报价单-${quoteId}.pdf` link.click() URL.revokeObjectURL(url) }后端生成 PDF 的常见选型有两种:一种是 wkhtmltopdf,配置简单,适合报价单这种版式固定、页面不复杂的场景;另一种是 headless Chrome 渲染,更适合对 CSS 要求高的页面。无论哪种,都要记住最核心的一条:PDF 模板的 CSS 尺寸用 mm 或 pt,不要用 px。报价单打印出来是 A4,px 在不同屏幕密度下换算出的物理尺寸不一致,容易导致打印内容溢出页面。还需要在模板里固定页边距、字体、表格边框,这些样式以打印媒体为准,和网页端完全分开维护。
4.3 移动端查看与只读视图:客户打开就能看,不能乱改
报价系统做到后面一定会有这个需求:销售把报价链接发给客户,客户在手机浏览器里直接打开查看,不用登录、不用下载 App。这一块的实现重点是只读视图和权限收敛。
技术方案不复杂。后端增加一个 public 报价查看接口,携带短时 token(比如 24 小时有效),返回去掉敏感字段的报价数据:公司名、产品名、数量、单价、金额、有效期。页面头部和底部加“此报价单仅供查看,最终以盖章文件为准”的水印,金额区域禁止复制。
实现只读视图时要注意敏感字段过滤。后端不要直接复用内部详情接口,而是单独写一个 DTO。内部接口返回的数据里可能有成本价、审批人备注、历史修改记录,这些一旦被客户看到,整个报价体系就露馅了。
权限控制上还有一个细节:只读视图的 token 不要用登录用户的 token,应该生成一个独立的短时凭证,记录签发的报价单编号和过期时间。报价单本身也有有效期,两者取更短的一个作为 token 过期时间。
5. 报价系统避坑手册:精度、并发、审计与 PDF 一致性
5.1 金额出现 0.30000000000000004:浮点精度问题
现象:报价单明细算出来是 0.30000000000000004,或者税费显示成 0.13000000000000001。客户看到这种金额会觉得系统在乱算。
原因:数据库或代码里用了 float / double 存金额。0.1、0.2 这类十进制小数在二进制浮点里无法精确表示,多次运算后误差累积就暴露了。
解决:全面换用 DECIMAL 和 BigDecimal。数据库金额字段用 DECIMAL(12,2),Java 计算用 BigDecimal,每一步都在合适位置 setScale 到 2。这个改造要覆盖数据库建表、实体类、计算工具类、前端展示四个地方。如果历史数据已经用了 float 字段,最好在数据迁移时重建表结构,而不是在应用层临时转换。改造后跑一遍回归测试:构造 0.1+0.2、税率 13% 的用例,确认输出 0.30 与预期一致。
5.2 好几个人同时改一张报价单:并发覆盖
现象:销售 A 打开报价单在改数量,销售 B 同时改折扣并保存成功;A 再保存时,B 改的内容被覆盖,客户最终收到的报价和审批记录不一致。
原因:更新报价单时没有做并发控制,后一次 UPDATE 直接按主键覆盖了全部字段。
解决:用乐观锁。quotes 表已经有 version 字段,更新时带上版本条件:
UPDATE quotes SET total_amount = ?, version = version + 1, updated_at = NOW() WHERE id = ? AND version = ?;如果受影响行数为 0,说明版本已变化,前端提示“该报价单已被他人修改,请刷新后重试”。验证方式也很简单:开两个浏览器窗口同时提交,后提交者应收到冲突提示。
5.3 历史报价金额跟着产品单价变:快照没有做全
现象:某个产品在台帐里改了 base_price,几天后客户打电话来质疑历史报价单金额与现在不一样,而系统里查出的历史单金额也确实变了。
原因:报价明细没有保存快照,每次详情页展示都实时 join 产品表读取最新价格。产品价格一变,历史单就跟着变。
解决:报价单创建时把产品名、单价、税率、规格全部冗余进 quote_items 表。之后无论产品台帐怎么改,已发出的报价单都从快照字段取值。草稿状态下如果想跟随最新价,由销售手动点击“刷新价格”单独触发,不能默认实时联动。这条规则建议写进代码注释的第一行,因为它是报价系统审计合规的底线。验证方式是把产品价格改掉,再打开历史报价单,金额应保持不变。
5.4 浏览器打印的 PDF 排版各异:统一服务端渲染
现象:同一张报价单,内网 Chrome 打印正常,客户用 Safari 打开后表格溢出、页脚错位、金额对不齐。
原因:window.print() 依赖浏览器自己的打印引擎,不同浏览器对 CSS 分页、字体、边距的解释都不一致,客户看到的版式根本无法控制。
解决:放弃浏览器打印,统一使用服务端渲染生成 PDF。模板中的容器宽度、字体大小、页边距全部用 mm 或 pt 固定。一次方案调整,后续所有客户拿到的文档都是同一内核渲染出的同一版式,不会再有“为什么我这边打开是好的”这种玄学问题。验证方式是用同一 PDF 文件在 Windows、macOS、手机端分别打开比对,版式必须一致。
5.5 报价过了有效期还在用:定时过期机制
现象:报价单上写着 30 天有效,合同签订时用的却是 45 天前报的价。销售不是故意违规,而是报价系统没有提醒,他没注意到已经过期。
原因:状态机只覆盖了人工操作路径,没有覆盖时间驱动。SENT 状态没有到期自动流转的逻辑。
解决:增加一个定时任务,每天凌晨扫一次:
UPDATE quotes SET status = 'EXPIRED' WHERE status = 'SENT' AND valid_until < CURDATE();前端列表对 EXPIRED 单据标红显示,编辑和转订单按钮置灰。如果需要更柔和,可以在过期前 3 天给销售发提醒,而不是直接过期。这个“提前提醒天数”建议做成配置项,各业务线需求差异很大,硬编码会招人骂。验证方式:把有效截止日期设为昨天,跑一次定时任务,状态应自动变为 EXPIRED。
6. 把报价系统部署到生产:Docker 编排与上线前检查
6.1 用 Docker Compose 一次拉起核心服务
拿到一份报价系统源码,最靠谱的起步流程不是翻代码,而是先把环境跑起来看效果。常见做法是把后端、数据库、前端分别容器化,用 Docker Compose 一次拉起。下面是一个最小可用的编排文件,按这个结构改就能用。
services: db: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: quote_system volumes: - db_data:/var/lib/mysql ports: - "3306:3306" backend: build: ./backend environment: DB_HOST: db DB_NAME: quote_system depends_on: - db ports: - "8080:8080" frontend: image: nginx:alpine volumes: - ./frontend/dist:/usr/share/nginx/html ports: - "8081:80" volumes: db_data:后端环境变量里的 DB_HOST 指向服务名 db 而不是 localhost,这是 Compose 网络里最容易踩错的地方。数据库数据通过 db_data 卷持久化,容器重建不会丢数据。前端直接挂载构建好的 dist 目录到 Nginx 默认站点目录,不需要单独写 Nginx 配置。
6.2 上线前必须做的三件事:备份、版本保留、启停演练
上线前的检查清单,我按自己的习惯排了三件事,优先级从高到低。
第一件事是全量备份数据库。用 mysqldump 导出一份完整 SQL 放到备份目录,文件名带上日期。别只依赖云厂商的快照,那种快照在关键时候可能因为配置问题恢复不了。第二件事是保留旧版本代码。部署新版本时不要覆盖旧目录,把旧目录改名加上日期后缀,出问题可以马上回滚。第三件事是停启演练。在正式切换前停掉所有容器再启动一次,确认数据卷挂载正常、后端连得上库、前端静态资源能访问。这一步能暴露大量环境变量和路径问题。
这套流程是我吃过亏之后才坚持下来的。某次上线前图省事没做备份,直接跑了升级脚本,把一批报价单状态批量改成 EXPIRED。第二天客户来问价,销售拿着系统记录说已经过期,实际上一半单据是升级脚本误改的。最后靠全量备份恢复,但业务已经断了一整天。从那以后,每次上线前再忙也要把备份、版本保留、停启演练走完一遍。希望帮到你。
本文还有配套的精品资源,点击获取