尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
多功能在线报价系统开发实战:PHP+MySQL实现从数据表到PDF导出
简介多功能在线报价系统源码是一套基于ASP与SQL数据库的Web应用面向需要在线询价、报价管理的企业或个人开发者系统围绕产品、品牌、分类、账户与报价记录五大模块构建支持动态价格调整、批量折扣、订单跟踪、支付整合及简单的CRM能力覆盖从商品维护到报价单输出的完整流程。资源压缩包为rar格式共16个文件包含8个ASP页面用于后台管理和报价逻辑3个CSS文件负责前台样式2个HTM框架页构建后台导航另有SQL脚本用于数据库初始化以及INC/ASA辅助配置组件整体仅22KB轻量且便于部署。目前已有2663人学习/下载。开发者将其解压到IIS环境后可快速搭起一套可运行的报价系统通过阅读源码能理解经典ASP的数据交互、会话管理与前端渲染方式也便于后续加入第三方支付、物流接口或优化用户体验代码结构清晰后台与前台分离是初中级Web开发者学习业务系统搭建的实用参考。1. 多功能在线报价系统源码它到底在解决什么前两天有个做五金贸易的朋友找我说他们跟单员还在用 Excel 手工算价客户要 30 件 A 产品、50 件 B 产品先查价格表再套折扣再用计算器加一遍最后填进 Word 模板改成 PDF。一天能报 20 张单但错率很高遇到阶梯价和含税价就更麻烦。他要的正是类似多功能在线报价系统源码这样的东西——一套能维护产品、自动算价、生成报价单、跟踪客户确认状态的 Web 系统。本文不聊宏大的数字化赋能就把这套系统从数据表设计到计价函数、PDF 导出、常见坑、再到多用户权限按一线落地的方式讲清楚。适合做 B2B 销售、工程项目报价、贸易跟单场景的开发者不管你是拿现成源码二次开发还是从零自己搭都能按这套思路做。下面的实现以 PHP MySQL 为主前端用原生 HTML JavaScriptPDF 用 FPDF/TCPDF 这类开源库。你换成 Java Spring Boot 或 Python Django 也没问题核心是数据结构和计价逻辑语言只是载体。我会给出可复现的 SQL 和代码片段并解释每个参数为什么这么设计。2. 把需求拆成数据结构报价单主表、明细表与价格策略设计报价系统的多功能主要体现在阶梯价、多种折扣、多币种、含税不含税切换、报价单状态流转、历史版本追溯。如果一开始数据表没拆好后面加功能就要改表结构线上数据迁移很痛苦。我一般先把报价流程跑一遍再定表。2.1 报价单主表状态、客户与金额快照报价单主表记录一笔报价的全局信息。除了基本字段有两个容易被忽略的设计客户名称快照和金额快照。客户资料是随时可改的但报价单是历史凭证客户改名后旧报价单不能跟着变所以要单独存customer_name。金额快照更关键如果结算价是在生成报价单那一刻算好的就必须把当时的 subtotal、tax、total 都存进主表而不是报价后每次查询都在线计算。用一个quote_order表来承担这个职责CREATE TABLE quote_order ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, quote_no VARCHAR(32) NOT NULL COMMENT 报价单编号, customer_id INT UNSIGNED NULL COMMENT 客户ID可为空, customer_name VARCHAR(120) NOT NULL COMMENT 客户名称快照, currency CHAR(3) NOT NULL DEFAULT CNY COMMENT 币种, exchange_rate DECIMAL(10,4) NOT NULL DEFAULT 1.0000 COMMENT 汇率相对本币, discount_mode TINYINT NOT NULL DEFAULT 1 COMMENT 1行折扣 2整单折扣 3无折扣, discount_value DECIMAL(5,4) NOT NULL DEFAULT 0.0000 COMMENT 折扣率0.1500表示15%, subtotal DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 不含税小计, tax_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0000 COMMENT 税率, tax_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 税额, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 含税总价, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已发送 2已接受 3已拒绝 4已过期, valid_days SMALLINT NOT NULL DEFAULT 15 COMMENT 报价有效期天数, remark VARCHAR(500) NULL, created_by INT UNSIGNED NOT NULL COMMENT 创建人用户ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_quote_no (quote_no), KEY idx_customer_id (customer_id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的关键点是discount_mode和discount_value。很多初版设计只放一个折扣字段结果行折扣和整单折扣混在一起算后面代码里全是if分支判断到底是哪一种极其容易翻车。明确折扣模式之后计价函数就能走清晰的分支。DECIMAL而不是FLOAT存金额是为了避开二进制浮点误差。subtotal、tax_amount、total_amount三个字段都直接落库以后查询报表也不需要在JOIN明细时实时聚合性能更好。2.2 报价明细表一条报价单对应多行产品行明细表存的是报价单里每一行产品。设计时要注意报价明细里的产品名称、规格、单价必须是生成报价单那一刻的快照不能用JOIN product去实时取。这里是最多客户扯皮的来源——你改了产品表的基础价历史报价单的金额跟着变了客户拿着旧报价来对账你却找不出当初报的是多少钱。CREATE TABLE quote_item ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, quote_id INT UNSIGNED NOT NULL COMMENT 主表ID, product_id INT UNSIGNED NULL COMMENT 产品ID可为空, product_name VARCHAR(200) NOT NULL COMMENT 产品名称快照, spec VARCHAR(200) NULL COMMENT 规格型号, unit VARCHAR(20) NOT NULL DEFAULT 件 COMMENT 单位, quantity DECIMAL(12,2) NOT NULL COMMENT 数量支持小数如米/公斤, unit_price DECIMAL(12,2) NOT NULL COMMENT 成交单价快照不含税, line_discount DECIMAL(5,4) NOT NULL DEFAULT 0.0000 COMMENT 行折扣, line_amount DECIMAL(12,2) NOT NULL COMMENT 行金额, sort_no SMALLINT NOT NULL DEFAULT 0, KEY idx_quote_id (quote_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;quantity用DECIMAL(12,2)而不是INT是考虑钢材按吨、线缆按米、五金按公斤这些场景整数型根本不够用。unit_price也快照进表即使以后产品调价也不影响这张报价单的历史金额。行折扣line_discount单独存是因为实际业务里经常给某一行产品让两个点但不能影响整单其他行。折扣存成0.1500这种比率而不是存折扣后价格这样无论怎么改基础价算出来的结果都是统一的逻辑。2.3 价格策略从哪来产品表与价格区间产品表是报价系统的基础数据。但多功能系统里产品价格往往不是固定一个数而是按数量阶梯变化100 件以下按单价 20 元100—499 件按 18 元500 件以上按 15 元。这种阶梯价如果直接在产品表里建price_100、price_500字段加一档就要改表结构很蠢。正确做法是单独建一个价格策略表。CREATE TABLE product ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(200) NOT NULL, spec VARCHAR(200) NULL, base_price DECIMAL(12,2) NOT NULL COMMENT 基准价不含税, currency CHAR(3) NOT NULL DEFAULT CNY, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE price_policy ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, product_id INT UNSIGNED NOT NULL, min_qty DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 最小数量含, max_qty DECIMAL(12,2) NULL COMMENT 最大数量不含NULL表示不限, price DECIMAL(12,2) NOT NULL COMMENT 该区间单价, KEY idx_product_qty (product_id, min_qty, max_qty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;max_qty允许为NULL代表上无止境这样最新一档就不用填个 9999999 这种魔法数字。查询时用区间匹配取min_qty 数量 AND (max_qty IS NULL OR max_qty 数量)的那一行。注意区间右边界取不含否则 100 件恰好落在两档交界时和不同的写法会产生两行记录这也是个隐藏坑。产品表里仍然保留base_price的目的是兜底当某产品没配任何阶梯价时直接按基准价报价保证系统不会因为漏配策略而报不出价。这个兜底逻辑在报价函数里要写清楚。3. 报价计算与折扣逻辑用 PHP 实现一个可维护的计价服务数据结构定了之后核心就是计价服务。这一段是报价系统的灵魂也最容易写出无法维护的大函数。我建议把计价独立成一个类输入报价明细和折扣参数输出subtotal、discount_amount、tax_amount、total_amount不掺数据库操作方便单元测试。3.1 计价流程先取阶梯价再算行金额再算总价以 PHP 为例写一个QuoteCalculator。它的输入是一个数组每行包含product_id、quantity、unit_price、line_discount整单参数包含discount_mode、discount_value、tax_rate、currency、exchange_rate。?php class QuoteCalculator { /** * 计算报价单金额 * param array $items [[product_id1,quantity100,unit_price18.00,line_discount0.0000], ...] * param string $mode row|order|none * param float $discount 整单折扣率0.1500表示15% * param float $taxRate 税率0.13表示13% * return array [subtotal..., order_discount..., tax_amount..., total_amount...] */ public function calculate(array $items, string $mode, float $discount, float $taxRate): array { $subtotal 0.0; foreach ($items as $item) { $qty (float) $item[quantity]; $price (float) $item[unit_price]; $lineDiscount (float) ($item[line_discount] ?? 0.0); $lineAmount $qty * $price * (1 - $lineDiscount); $subtotal $lineAmount; } $orderDiscount 0.0; if ($mode order $discount 0) { $orderDiscount $subtotal * $discount; } $taxableBase $subtotal - $orderDiscount; $taxAmount $taxableBase * $taxRate; $totalAmount $taxableBase $taxAmount; return [ subtotal round($subtotal, 2), order_discount round($orderDiscount, 2), tax_amount round($taxAmount, 2), total_amount round($totalAmount, 2), ]; } }这段代码的逻辑是按先算行金额、再累加小计、再扣整单折扣、再算税额的顺序走。line_discount作用于单行整单折扣只在$mode order时生效。你也许会发现这里没有对外币换算做处理——我的习惯是报价明细和主表统一以本币记账汇率只在最终显示时报出一个参考值不参与行金额计算否则每次查汇率都会改变报价历史客户会质疑。round(..., 2)只在最后输出时做一次中间累不加时不做四舍五入避免每行都舍入导致累计误差。如果项目对精度要求苛刻比如财务系统建议换成bcadd字符串高精度运算但一般报价系统用round已经足够。3.2 折扣参数行折扣、订单折扣与整单折扣只能选一个很多报价系统的翻车现场发生在折扣叠加。销售在系统里给某行打了 9 折又在整单层面填了 5% 折扣最后系统按两个乘下来客户一看金额和自己算的对不上就得重出报价单。所以我在主表里放了一个discount_mode明确当前报价单属于哪种折扣模式。// 选型示例从数据库加载报价主表后按模式计算 $itemResult []; foreach ($quoteItems as $item) { $itemResult[] [ product_id $item[product_id], quantity $item[quantity], unit_price $item[unit_price], line_discount $mode row ? $item[line_discount] : 0.0, ]; } $calculator new QuoteCalculator(); $result $calculator-calculate($itemResult, $mode, $discountValue, $taxRate);这里的关键判断是当discount_mode row时行折扣按明细里的数据走整单折扣强制为 0当discount_mode order时所有行的line_discount全部置 0只扣整单折扣none则什么都不扣。这样做避免了折上折的争议。为什么不做叠加我见过业务方提出行折扣后总额再打 9 折的需求听起来很灵活但对报价单的可解释性是个灾难客户拿到报价单没法用简单的单价乘数量验证。作为开发者你要做的是给业务方一个明确选择而不是做一个万能的折扣组合器。真遇到特殊场景可以在备注里说明而不是写死在计价逻辑里。3.3 价格精度与舍入浮点数直接累加的隐患如果用FLOAT类型存金额累计到一定程度就会出现0.1 0.2 0.30000000000000004这种问题。报价单上显示 0.02 元误差客户不会直接说但财务对账时会卡死。所以数据库字段用DECIMALPHP 端也要注意。常见做法是金额用整数分计算即把元转成分所有计算都是整数最后再除以 100。这个方案最稳但改造成本高。折中方案是数据库层用DECIMAL(12,2)PHP 层每个金额字段都用round(,2)在关键节点收敛。// 推荐最终金额统一用整数分计算 $totalCents 0; foreach ($items as $item) { $lineCents (int) round($item[quantity] * $item[unit_price] * 100); $lineCents (int) round($lineCents * (1 - $item[line_discount])); $totalCents $lineCents; } $totalAmount $totalCents / 100; echo number_format($totalAmount, 2, ., );round($item[quantity] * $item[unit_price] * 100)把当前行金额先放大 100 倍并四舍五入成整数分再参与折扣运算最后行与行之间累加越到后面越不容易出现离谱的小数位。number_format($totalAmount, 2)输出时保证两位小数不会丢精度。4. 在线生成报价单表单校验、PDF 导出与模板打印报价系统的在线体现在两个环节一是销售在网页上填报价单二是客户收到的是一份正式电子文档。很多源码只做了后台录入漏了前端实时算价和可打印的 PDF 输出那算不上完整。这一章讲清楚从表单到 PDF 的全过程。4.1 报价表单设计与前端校验必填项、数量非零、金额只读报价表单最好做成明细行 底部汇总的结构。前端用原生 JavaScript 就能满足不需要引大框架。核心是数量变化后实时重算行金额和总价但最终金额以服务端计算为准——前端只是给销售一个即时反馈防止他填到最后一刻才发现总额不对。table idquoteItems tr tdinput nameproduct_name[] classform-control required/td tdinput namequantity[] typenumber min0.01 step0.01 classform-control qty required/td tdinput nameunit_price[] typenumber min0.01 step0.01 classform-control price required readonly/td tdinput typetext classline-total form-control readonly/td /tr /table script document.querySelector(#quoteItems).addEventListener(input, function (e) { if (e.target.classList.contains(qty)) { const row e.target.closest(tr); const qty parseFloat(e.target.value) || 0; const price parseFloat(row.querySelector(.price).value) || 0; row.querySelector(.line-total).value (qty * price).toFixed(2); recalcTotal(); } }); function recalcTotal() { let sum 0; document.querySelectorAll(.line-total).forEach(input { sum parseFloat(input.value) || 0; }); document.querySelector(#grandTotal).textContent sum.toFixed(2); } /scriptunit_price是readonly的它由后台根据产品或阶梯价自动带出销售不能手输——这是防止随意报价再让财务背锅的第一道闸。数量输入框的min和step是为了阻止负数和小数位过长的值比如数量 0.001 件在钢材单位里也许合理但在五金件里就是业务错误。前端只拦明显违规真正的价格校验还在服务端后面会讲。4.2 服务端生成报价单 PDF用 FPDF/TCPDF 输出正式文档在线报价最好的收尾是生成 PDF 直接发客户。PHP 生态里 FPDF 轻量、TCPDF 功能全。以 FPDF 为例中文需要额外加载支持中文的 TTF 字体否则会显示乱码或方块。下面是一个最小可用的 PDF 报价单生成函数。?php require_once(fpdf.php); class QuotePDF extends FPDF { public function buildQuote($order, $items) { // 加载中文字体建议用 FreeSerif 或思源黑体避免版权问题 $this-AddFont(freeserif, , FreeSerif.ttf, true); $this-SetFont(freeserif, , 12); $this-AddPage(); $this-Cell(0, 10, iconv(UTF-8, UTF-8, 报价单编号 . $order[quote_no]), 0, 1); $this-Cell(0, 10, 客户 . $order[customer_name], 0, 1); $this-Cell(0, 10, 有效期至 . date(Y-m-d, strtotime($order[created_at] . . $order[valid_days] . days)), 0, 1); $this-Ln(5); $this-SetFont(freeserif, , 10); $header [产品名称, 规格, 数量, 单价, 行金额]; $w [70, 30, 25, 30, 35]; for ($i 0; $i count($header); $i) { $this-Cell($w[$i], 8, $header[$i], 1, 0, C); } $this-Ln(); foreach ($items as $item) { $this-Cell($w[0], 8, $this-truncate($item[product_name], 20), 1); $this-Cell($w[1], 8, $item[spec], 1); $this-Cell($w[2], 8, $item[quantity] . . $item[unit], 1, 0, R); $this-Cell($w[3], 8, number_format($item[unit_price], 2), 1, 0, R); $this-Cell($w[4], 8, number_format($item[line_amount], 2), 1, 0, R); $this-Ln(); } $this-Ln(5); $this-Cell(0, 8, 小计 . number_format($order[subtotal], 2), 0, 1, R); if ($order[discount_amount] 0) { $this-Cell(0, 8, 折扣- . number_format($order[discount_amount], 2), 0, 1, R); } $this-Cell(0, 8, 税额 . number_format($order[tax_amount], 2), 0, 1, R); $this-SetFont(freeserif, B, 12); $this-Cell(0, 10, 合计 . number_format($order[total_amount], 2) . . $order[currency], 0, 1, R); } private function truncate($str, $len) { if (mb_strlen($str) $len) { return mb_substr($str, 0, $len) . ...; } return $str; } }要点AddFont里加载的 TTF 文件路径要放在 FPDF 的 font 目录下文件名和字体名必须一一对应iconv那行是为了把内部编码转成 PDF 能处理的格式如果你的工程全是 UTF-8可以不加。列宽$w的总和要等于页面可用宽度A4 默认 210mm 减边距 20mm 后剩 190mm这里 7030253035190正好不溢出。truncate方法是为了防止超长产品名把单元格撑破PDF 表格没有 HTML 那种自动换行必须自己截断或主动换行。生产环境建议把truncate换成多行处理逻辑比如用MultiCell但对大多数报价单截断加省略号更简洁。4.3 模板打印与发送报价流程状态流转PDF 是发给外部客户的内部销售之间沟通有时只要一个可打印网页。我的方案是报价详情页做成 HTML 排版好的打印模板点打印按钮调window.print()系统自动调起浏览器打印对话框用户可选另存为 PDF。这样就不用每改一个格式都去动 FPDF 代码适合快速响应业务需求。style media print { .no-print { display: none; } body { font-size: 12pt; } table { width: 100%; border-collapse: collapse; } table td, table th { border: 1px solid #333; padding: 4px; } } /style button classno-print onclickwindow.print()打印此报价单/button table theadtrth产品名称/thth规格/thth数量/thth单价/thth金额/th/tr/thead tbody ?php foreach ($items as $item): ? tr td? htmlspecialchars($item[product_name]) ?/td td? htmlspecialchars($item[spec]) ?/td td? $item[quantity] ? ? $item[unit] ?/td td? number_format($item[unit_price], 2) ?/td td? number_format($item[line_amount], 2) ?/td /tr ?php endforeach; ? /tbody /table发送报价的动作是改变状态。HTML 里放一个发送确认按钮调接口把status从 0 改成 1并记录发送时间。这里不需要推真实邮件因为很多企业内部系统的发送只是走线下或企业 IM。你把状态机留好后面接邮件网关时就只是加一个mail()调用的事。5. 避坑多功能报价系统开发中的 5 个常见问题与排查这套系统看起来简单但我接手的几个项目里坑都集中在数据的准确性和历史一致性上。下面五条是我踩过或见过的典型问题。5.1 报价单编号重复并发下用自增 ID 拼号不可靠现象两个销售同时点保存生成的报价单编号都是20250607001后写的覆盖先写的或者数据库唯一索引直接报错。原因很多源码用SELECT MAX(id)1来生成流水号但这条语句在高并发下不是原子操作两个连接同时读到同一个 max 值。解决改用独立序列表或者用UNIX_TIMESTAMP()加后四位随机数再配合唯一索引做兜底。$quoteNo Q . date(YmdHis) . str_pad(mt_rand(0, 9999), 4, 0, STR_PAD_LEFT);这个写法在同秒内冲突概率是万分之一再加数据库UNIQUE KEY uk_quote_no真冲突时重新生成一次即可。如果有更高要求用 RedisINCR做全局自增是最稳的。5.2 阶梯价取错数量边界条件没处理好现象客户报价 100 件系统却取了 50—99 档的价格导致单价偏高客户流失。原因SQL 里用了quantity min_qty AND quantity max_qty恰好数量等于 100 时100 落在上一档的闭区间和下一档的开区间交界取到了错误的行。解决统一区间定义min_qty qty AND (max_qty IS NULL OR max_qty qty)即下闭上开。然后在程序里加一个单元测试分别验证 99、100、101 三个边界值。function getPrice(PDO $pdo, int $productId, float $qty): float { $stmt $pdo-prepare( SELECT price FROM price_policy WHERE product_id ? AND min_qty ? AND (max_qty IS NULL OR max_qty ?) ORDER BY min_qty DESC LIMIT 1 ); $stmt-execute([$productId, $qty, $qty]); $row $stmt-fetch(); return $row ? (float) $row[price] : 0.0; }ORDER BY min_qty DESC LIMIT 1是为了防止区间本身有重叠多条记录命中时取最宽松那档。平时入库时可以通过唯一索引(product_id, min_qty)来避免重叠但我仍建议保留这个查询排序给异常数据兜底。5.3 客户改价后历史报价单金额跟着变了现象销售在产品维护里把某产品基础价从 20 改到 22然后发现上个月发给客户的报价单明细金额也变成了 22。原因报价明细表没有快照字段或者查询时JOIN product取实时价格历史报价就变成了活数据。解决坚持两条铁律报价明细写入时把product_name、spec、unit_price、discount全部连同写入查询详情时只读明细表绝不JOIN product显示当前价。已经在产的老系统要迁移数据写个脚本把当前报价单里的unit_price回填到明细表即可。5.4 PDF 中文乱码FPDF 默认字体不支持中文现象生成的 PDF 里中文全变成方块或者空白只有英文和数字正常。原因FPDF 内置字体只有 Helvetica、Times 等都是西文编码不支持 UTF-8 中文。直接从网上下载的 TTF 字体没注册进 FPDF 也一样乱码。解决下载一个支持中文的 TTF比如免费商用的思源黑体或 FreeSerif放到 FPDF 的font目录然后调用$pdf-AddFont(freeserif, , FreeSerif.ttf, true)之后所有SetFont都要用这个字体名不能用默认字体。注意第二个参数true表示 unicode 模式必须传否则Cell输出中文仍会乱码。5.5 税额算错含税价与不含税价混用现象明细显示单价 100 元税率 13%最后总价是 113 元业务方却说应该是 100 元含税。原因产品表里有的价格是含税入库的有的不含税报价时没统一约定系统默认又加了一遍税。解决一定要在系统配置里明确产品表的价格基数。我的做法是产品表统一存不含税基准价报价单里单独有tax_rate字段前端显示时提示含税总价 小计 × (1 税率)同时在每个产品价格旁边标注不含税。这个约定要写进需求文档防止业务人员手工录错。如果老数据已经混了就加一个is_tax_included标志位查询时做一次反向换算再参与计价。6. 进阶多用户权限、报价历史与数据统计的做法基础报价跑通之后要做成多功能还得补三块多用户权限、历史追责、业务统计。这部分我按最小但可用的方案给。6.1 操作日志与报价历史版本谁改了什么报价单被销售反复修改是常态。没有日志出了差错就会变成你说我改了我说我没改扯不清。我一般建一个quote_log表记录每一次状态变更和金额修改。CREATE TABLE quote_log ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, quote_id INT UNSIGNED NOT NULL, action VARCHAR(50) NOT NULL COMMENT create/update/send/accept/reject, user_id INT UNSIGNED NOT NULL, old_total DECIMAL(12,2) NULL, new_total DECIMAL(12,2) NULL, remark VARCHAR(255) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_quote_id (quote_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每次更新报价单时先在事务里把旧总价读出来写入 log再更新主表。更重量级的需求可以做整单 JSON 快照把明细整个塞进remark字段这样任何时候都能还原当时的报价方案。对中小规模报价系统JSON 快照比建明细版本表省事得多。6.2 权限控制销售只能看自己的主管看全部最简单的方案是基于角色的权限判断。用户表加role字段取值sales、manager、admin。查询报价单列表时sales 只能看created_by 当前用户ID的记录。$stmt $pdo-prepare( SELECT * FROM quote_order WHERE (:role manager OR created_by :uid) ORDER BY created_at DESC LIMIT 50 ); $stmt-execute([:role $user[role], :uid $user[id]]);这段 SQL 的原理是如果是 manager条件:role manager恒真就不过滤创建人如果是 sales则必须匹配created_by :uid。注意LIMIT 50只是演示生产环境要加分页。如果源码用的是现成框架用框架自带的授权中间件会更规范但核心逻辑不变。6.3 报表统计成交率、平均折扣、产品 Top报价系统的价值最后要落到报表上。业务方最常问三个问题报价发了多少、成交率多少、哪些产品被报得最多。下面两条 SQL 覆盖了后两个。-- 成交率已接受订单数 / 已发送订单数 SELECT COUNT(CASE WHEN status 2 THEN 1 END) AS accepted_count, COUNT(CASE WHEN status 1 THEN 1 END) AS sent_count, ROUND(COUNT(CASE WHEN status 2 THEN 1 END) / NULLIF(COUNT(CASE WHEN status 1 THEN 1 END), 0) * 100, 1) AS accept_rate FROM quote_order WHERE created_at DATE_SUB(NOW(), INTERVAL 30 DAY); -- 产品报价次数 Top10 SELECT product_name, SUM(quantity) AS total_qty, COUNT(*) AS quote_cnt FROM quote_item JOIN quote_order ON quote_order.id quote_item.quote_id WHERE quote_order.created_at DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY product_name ORDER BY total_qty DESC LIMIT 10;NULLIF(..., 0)是防止分母为零让整个 SQL 报错。如果一张报价单被发送多次按状态统计时要注意已接受只认最新状态不要按时间累加否则成交率会超过 100%。最后一件事我在自己的项目里吃过没做快照的亏客户拿着三个月前的报价来说我们答应 15 元单价系统里查不到最后只能赔钱。后来我强制把所有报价明细字段快照落库并且任何修改都写日志客户再追溯时直接导出当时的 PDF 原档就再也没扯过皮。这是这套系统里最不值钱却最救命的一条设计。报价系统的核心不是炫技是每一笔报价都能说清楚来龙去脉。希望这篇文章能帮你在实现和踩坑路上少走一段弯路拿去就能落地。本文还有配套的精品资源点击获取
RELATED

相关推荐

Cocos Creator回合制战斗系统:调度器与状态机设计实战

Cocos Creator回合制战斗系统:调度器与状态机设计实战

简介:CocoKnightManager 是一套基于 Cocos Creator 的开源回合制游戏系统设计,面向希望快速搭建回合制战斗框架的开发者与学习者。它围绕模块化与数据驱动思路,将回合管理器、行动规则引擎、状态机等核心组件拆分实现,帮助解决回合…

📅 2026/9/25 21:06:51
Substrate 区块链开发实战:从模块化架构到 Pallet 开发与升级

Substrate 区块链开发实战:从模块化架构到 Pallet 开发与升级

1. 从零认识 Substrate:它到底是什么,能解决什么问题第一次听到 Substrate 这个词,很多人会以为是某个前端框架或者构建工具,其实它是一套用于构建区块链底层网络的开发框架。你可以把它理解成“区块链世界的操作系统内核”——它…

📅 2026/9/25 21:06:51
CRMEB 标准版系统(Java)v3.2 公测版发布

CRMEB 标准版系统(Java)v3.2 公测版发布

CRMEB 标准版系统(Java)v3.2 公测版发布!此次,v3.2主要围绕真实使用过程中的关键环节进行了集中优化:部署更省事了:两个部署包合并成一个JAR,一个文件就能启动;后台接入AI MCP能力&a…

📅 2026/9/25 21:06:51
MORE NEWS

更多资讯

📰

网页特效实战指南:从滚动视差到粒子动画的性能落地技巧

上次给客户做官网的时候,对方提了一个让我印象很深的需求:首页要"酷",最好一打开就让人觉得技术很强、设计很新。我给了他一个粒子背景加滚动视差,他当场拍板。后来那个站上线不到一个月,数据反馈里停留时长…

📰

Atlas 300V推理卡实战:YOLOv5部署全流程与性能调优

前一阵群里好几个朋友在问“Atlas 300V 24G 是运算加速卡吗”,紧接着又有人追着问“这卡能不能拿来部署 YOLO”。这两个问题凑在一起,正好是我最近大半个月一直在折腾的事情。借这个机会,把从硬件选型、环境搭建、模型转换到推理调优的完整过…

📰

Atlas 300V Pro 24G推理卡YOLO部署全链路实战指南

1. Atlas 300V Pro 24G:先回答那个最纠结的问题标题里带了"atlas"这个词,说实在的,范围特别大。Atlas在华为昇腾产品线里几乎是一整个宇宙——训练卡有Atlas 800、Atlas 900,推理卡有Atlas 200、Atlas 300系列&#xff…

📰

Atlas 300V上部署YOLOv5/v8:从环境配置到推理调优实战

Atlas 300V 24G,严格来说不太适合叫显卡,而是一张专门给AI推理用的运算加速卡。做视觉检测的同行最近问到我最多的一个问题就是:手里刚好有这张卡,想把YOLO模型部署上去,但卡在环境配置、模型转换和推理接口那一堆文档…

📰

50+营销Skill装进AI Agent:从提示词到工程化能力

1. 这个项目到底在解决什么问题第一次看到“把 50 多种营销 Skill 装进 AI Agent”这个标题,我脑子里冒出来的第一个念头是:终于有人把营销这件事从“提示词玄学”往“工程化能力”的方向推了一步。过去大半年,我身边做增长、做内容、做投放的…

📰

02_实验一_安装交叉编译工具链

实验一 安装交叉编译工具链——给 x86 电脑配一位"ARM 翻译官"对应课件:《第3章 移植U-Boot》3.4 节,Slide 28-31 系列说明:本系列基于华清远见 FS-MP1A(STM32MP157A)开发板,对应课件《第3章 移植…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬