资讯动态

中小商家本地报价管理系统:从SQLite建模到本地部署

发布时间:2026/10/22 10:27:07 来源:尧图企业网站定制
前几天一位做家装维修的师傅来找我说他白天干活不累最累的是晚上对着手机翻聊天记录一条条找“当初给谁报过什么价、报的是哪一版、最后怎么谈的”。我帮他理需求时发现这个痛点在中小商家身上太普遍了。后来我把这套方案叫做“中小商家本地报价管理系统”把报价从随口说、随手写变成有编号、有状态、可查询、可打印的标准动作。这套系统适合谁装修队、广告图文店、水电维修、空调清洗、家政保洁凡是按项目或者按服务报价、三五个人到十几个人、没有专门IT人员的小店都能用。它要解决的事情很具体报价算得准不准、能不能一键找到某个客户的历史报价、发出去的报价当前在哪个阶段、有没有过期没人跟。下面这篇总结不是我闭门造车写出来的方案而是跟着几个店主从需求梳理、数据建模、功能落地到现场部署走了一遍之后的复盘希望对正在做同类工具或者想给自己店里搭一套的人有点用。1. 项目概述与需求拆解中小商家到底需要什么1.1 先看真实的业务现场我刚开始做这个项目时以为核心难点在“算钱”。真去店里蹲了两天后才发现算钱只是最表层的需求。大多数中小商家的报价场景是这样的客户从微信发来一张照片或者说一句“我家屋顶漏水了来看看吧”老板现场看完后开始估。人工费、材料费、车费顺手在手机备忘录里记几个数字或者在纸质单子上随手一画。回去之后用语音或文字把总价发给客户客户说“太贵了”老板再降一点或者加个项目再凑一点。到这里报价已经不是最初那个报价了。更麻烦的是这种报价没有任何留痕。一周后客户回头问“上次你说的三千到底含不含材料”老板自己都说不清楚。我蹲点的时候亲眼见过一个店主翻了十几分钟聊天记录最后查到的是一个两年前的价格和今年的成本完全对不上。还有一个普遍现象同一个服务不同的人报出来的价不一样。老板自己有老客户感情分员工接电话时按心情报价有些学徒工干脆乱报。价格不统一直接导致两个后果一是利润被悄悄吃掉二是客户发现同样东西两个价信任感瞬间没了。所以我把需求拆成了三块。第一把客户和报价单结构化客户不只是微信里的一个头像报价单也不只是一条语音第二把报价的明细和金额计算固化下来让谁录都能录出同样的结果第三把状态跟进去让“报完价就石沉大海”的情况每天自动暴露出来。1.2 系统定位只做报价不做一个大而全的管理系统这是我前期最想提醒同行的一件事。很多做进销存、做ERP的人一看到这种需求本能反应就是“这块报价功能可以并到订单系统里顺便把库存、采购、财务都做了”。但中小商家根本消化不了我见过太多小店买了一套功能庞大的软件最后只用其中一个模块其余按钮都不知道是干嘛的。这个项目从一开始就定了一个边界只做报价闭环。客户资料、产品服务价格库、报价单生成与打印、报价单状态跟踪、数据备份这五件事做完项目就算及格。库存、采购、应收账款、项目排期统统不做哪怕它们看起来和报价离得很近。原因很简单报价是小店里最高频、最影响成交的动作把这一件事做到顺手商家才愿意长期用。一旦让操作员每天面对十几个复杂表单第二天就会被闲置。还有个很现实的考虑中小商家的决策人往往就是老板本人老板最怕的是员工流动把数据带走或者把流程搞乱。系统越简单越容易在店里形成习惯习惯一旦形成后面再想扩展订单和回访都是顺理成章的事。1.3 核心功能与验收标准在设计功能清单时我没有按模块去压任务而是把功能都翻译成了容易验证的验收标准。下面这张表是我在项目里反复调整后定下的底版功能模块具体能力验收标准客户管理新建客户、手机号、地址、来源备注能按客户名快速检索并且看到该客户全部历史报价产品/服务库名称、规格、单位、默认单价、分类报价时输入关键字能带出不需要每次手打报价单新建选择客户、添加行项目、修改数量单价、整单折扣自动算总额重新打开后仍可继续编辑打印导出A4/A5打印、PDF生成、表格文件导出打印版和微信转发版至少两套样式状态跟踪草稿、已发送、已确认、已拒绝、已过期首页能列出三天内没有回音的报价单备份恢复一键备份、一键恢复、历史备份文件管理换电脑或重装系统后能原样恢复数据这张表最有价值的地方不是功能多而是每一行都能在半天之内验证完。中小商家对软件验收不会写复杂的测试用例他们就认一件事我能不能在两分钟以内给客户发一张好看的报价单。能就继续用不能做得再专业也是白搭。2. 整体思路与技术选型本地存储为什么更适合小店场景2.1 坚持本地化而不是云端SaaS项目标题里最让我在意的词是“本地”。我先说说我为什么不推荐一上来就做云平台。第一成本。小店的利润本来就薄你让他每个月付几十上百的订阅费他第一反应是“我自己做个表格不行吗”。本地部署不依赖订阅代码跑在店里一台普通电脑上数据文件就是本地的数据库文件没有额外费用。第二信任。很多店主对数据上传这件事非常敏感。客户电话、门牌地址、报过的价格在他看来都是自己吃饭的家底。你让他把这些数据交给一个不知名的云服务商他要反复问你“我的数据安不安全”“你公司倒了怎么办”。本地部署则简单得多数据就躺在店里谁也拿不走备份文件拷进U盘就能带走。第三网络。店里网络断掉的时候报价业务不能停。客户就在面前等着你不可能说“对不起我们的系统连不上网稍后再报”。本地运行的系统天然抗断网只要电脑能开机报价就能继续做。有些朋友会说现在用手机随地报价才是趋势。我不否认但中小商家现在的真实工作习惯往往是现场勘查靠手机记录回到电脑前报价最后用微信发图片给客户。所以一个稳定运行的本地系统加上方便的手机转发比强行做一个要登录、要同步的手机App更符合实际。2.2 一套能落地的技术选型我在这类项目里常用的方案是后端用 Python 的 Flask数据库用 SQLite前端用最普通的 HTML 加少量 JavaScript整体部署在一台店里的旧电脑上局域网内其他设备通过浏览器访问。这套组合在很多人眼里不够“新潮”但它的好处非常明显。SQLite 是一个单文件数据库整个数据库就是一个文件。备份的时候把这个文件拿走就完了恢复的时候把它放回去就完了。对没有IT人员的店铺来说这种“实体感”特别重要。Flask 足够轻量不需要服务器管理器代码复制过去就能跑。前端不做单页复杂交互因为操作员主要是点几个按钮需要学习成本低。相比之下用 Electron 做桌面应用也能做到本地部署打包之后确实是双击打开。但 Windows 环境里的常见问题是旧电脑运行慢、杀毒软件可能报警、打包体积大。我最后选择了“本地Web服务 浏览器访问”的方式理由不是桌面端不行而是对项目来说维护成本更低。操作员打开浏览器输入一个固定地址看到的就是界面和用网页版系统没有区别。如果你以后遇到的需求是“必须双击桌面图标打开”再把 Electron 壳加上也不迟。项目初期真没必要被这些壳拖住。这套选型的核心逻辑是怎么简单怎么来怎么方便备份怎么来怎么不花钱怎么来。2.3 为什么不能只靠表格工具有店主问过我电子表格也挺好用的想怎么改就怎么改为什么要用系统这个问题我认真回答过很多次。表格工具的优点是自由缺点也正是自由。它没有一个天然的“客户主档”同一个客户在统计表里叫“王姐”在另外一张表里叫“老王”想合并都合不动。它没有一个强制的报价编号单据顺序只能靠手工维护一排序就乱。它可以有公式但公式一旦被误删整张表的计算结果就会失灵而老板往往过很久之后才发现利润算错了。最关键的是协作问题。店里如果只有老板一个人用表格确实够用但只要多一个员工表格的并发冲突和版本混乱就立刻显现。两个人同时打开同一个文件后保存的人覆盖先保存的人这种事我见过太多次。报价管理系统把数据和操作结构化之后员工只需要在固定表单里填内容系统负责编号、算价、存历史。这不是限制而是保护。3. 数据模型与业务规则设计先想清楚字段和状态机3.1 核心实体关系与建表语句数据模型我是这么设计的客户表、产品服务表、报价单主表、报价单明细表、编号计数器表。一共有五张核心表尽量少不要加戏。先看客户表CREATE TABLE customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, address TEXT, source TEXT, remark TEXT, created_at TEXT DEFAULT (datetime(now,localtime)) );再看产品服务表CREATE TABLE service_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, unit TEXT, default_price INTEGER NOT NULL, category TEXT, enabled INTEGER DEFAULT 1 );这里我把default_price设计成了整数单位是“分”。为什么不用带小数的浮点数因为涉及钱的计算用浮点数很容易出现 0.1 0.2 不等于 0.3 的问题。界面显示的时候再转成元内部计算全部用整数分稳得很。报价单主表CREATE TABLE quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, quote_no TEXT NOT NULL UNIQUE, customer_id INTEGER NOT NULL, total_amount INTEGER DEFAULT 0, discount_amount INTEGER DEFAULT 0, tax_amount INTEGER DEFAULT 0, final_amount INTEGER DEFAULT 0, status TEXT DEFAULT draft, valid_until TEXT, remark TEXT, created_at TEXT DEFAULT (datetime(now,localtime)), FOREIGN KEY (customer_id) REFERENCES customers(id) );报价单明细表CREATE TABLE quote_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, quote_id INTEGER NOT NULL, item_id INTEGER, item_name TEXT NOT NULL, item_spec TEXT, unit TEXT, price INTEGER NOT NULL, quantity REAL NOT NULL, amount INTEGER NOT NULL, FOREIGN KEY (quote_id) REFERENCES quotes(id) );这里有一个我特别想强调的设计明细表里除了item_id还要冗余存一份item_name、item_spec、unit、price。理由很简单报价单代表的是“当时那一刻的报价”。三个月后你把产品服务库里的名字改了价格也调了如果明细表只存一个关联ID历史报价单打印出来就会跟着变成今天的名字这绝对不行。存快照历史报价单才能永久保持原样。3.2 报价单状态机状态我设计了六个草稿、已发送、已确认、已拒绝、已过期、已成交。刚开始我以为可以少一点把“已过期”这个状态去掉系统只要显示创建时间就好。后来真正用起来才发现没有过期状态首页那个待跟进的列表就没有灵魂。状态流转规则如下草稿创建后没发给客户只有本地员工能看。已发送通过微信或打印发给客户了记录发送时间并设置有效期。已确认客户表示“可以”这个报价进入待执行状态。已拒绝客户明确不要了应该记录拒绝原因。已过期设置了有效期到期当天系统自动标红。已成交确认后转化成实际服务或订单报价流程正式关闭。设计上有一条很关键从草稿到已发送这个动作必须由员工操作确认系统不自动跳。因为有的报价单创建出来只是老板自己算账用的发给没发给客户差别很大只能让操作者在界面上点一下“发送并通知客户”。首页的“待跟进”视图本质上就是把状态等于“已发送”且当前日期大于有效期的报价单全部列出来。对一个店主来说这张列表就是他的催款线索也是他的客户回访名单。3.3 金额计算与舍入规则金额计算逻辑我反复调了几次最后定下来的规则是每个行项目的小计等于数量乘以单价结果取到分。整单原始金额等于所有行项目小计之和。整单折扣可以填金额比如“总价减200”也可以填比例比如“打九折”。最终金额等于原始金额减去折扣金额如果涉及税还要单独加税。听起来很简单但实际项目里踩过一个坑如果直接拿浮点数相乘再相加偶尔会冒出 0.009 这样的尾差导致系统算出来的总额和客户一看就知道不对劲。后来我把所有价格都改成用整数分存储所有运算用整数完成问题彻底消失。用 Python 表达核心逻辑大概是这个样子from decimal import Decimal def calc_quote(items, discount_type, discount_value): total 0 for item in items: # 单价和数量都以分为单位进入运算避免浮点误差 total int(Decimal(str(item[price])) * Decimal(str(item[quantity]))) if discount_type percent: discount int(total * Decimal(str(discount_value)) / 100) else: discount min(int(discount_value), total) final_amount total - discount return total, discount, final_amount注意代码里特意用str转一次Decimal是为了避免从外部传入浮点数时产生精度污染。在实际业务中还要把这部分封装成独立函数报价单新增和修改都必须走同一个计算函数避免不同入口算出不同金额。4. 核心功能逐一落地报价单从生成到打印4.1 报价单编号的并发与合法性报价单编号看起来是个小事做不好会导致灾难。我见过同一个系统里出现两张一摸一样的“20260601-01”报价单打印出来之后客户拿编号来咨询员工根本分不清是哪一张。我采用的编号规则是日期 两位业务员首字母 当天流水号比如20260601-ZS-001。这个规则的主要目的是方便在微信里被搜索到客户报出编号员工马上能在系统里定位。当天流水号不能直接“查一下当前最大编号再加一”因为两个员工同时开单时各自都读到 001插进去就会出现重复。正确的做法是在 SQLite 里用一张专门的计数器表通过事务原子更新来获取编号def generate_quote_no(cursor, today): cursor.execute( UPDATE quote_counters SET value value 1 WHERE date ? RETURNING value, (today,) ) row cursor.fetchone() if row is None: cursor.execute(INSERT INTO quote_counters(date, value) VALUES (?, 1), (today,)) return f{today}-ZS-001 return f{today}-ZS-{row[0]:03d}用UPDATE ... RETURNING的好处是数据库在同一个事务里完成读取和递增即使两个人同时提交也只会拿到两个不同的流水号。能走这条逻辑就尽量不要用“select max 1”。4.2 打印与PDF导出的细节报价单最终是要给人看的打印和导出这部分我踩过最多的坑。第一坑是打印样式。直接调浏览器的打印功能默认会把整个页面都打出来工具栏、按钮、输入框全在里面。正确的做法是专门写打印样式media print { body * { visibility: hidden; } .print-area, .print-area * { visibility: visible; } .print-area { position: absolute; left: 0; top: 0; width: 100%; } page { size: A4; margin: 10mm; } }在界面设置一个“打印预览”按钮点击后打开新模式只显示报价单内容加一个打印按钮。这样操作员所见即所得不会把乱七八糟的界面发给客户。第二坑是导出表格乱码。如果直接导 CSV 并用电子表格软件打开中文很可能变成乱码原因是文件编码和软件默认编码不匹配。如果导出的是带样式的表格文件长数字编号会被自动转成科学计数法看起来特别不专业。解决方法是把编号列、电话列都显式设置成文本格式再输出。第三坑是中文字体。直接在浏览器里打印不同电脑上字体表现差异很大。客户那边打开文件的时候字体丢了也是常事。最稳妥的方案是生成PDF再发送。我后来在系统里默认优先导出PDF打印HTML只作为店内内部存档使用给客户看的清单一律走PDF。4.3 手机端怎么处理我在前面说过不做手机App但现实中客户交流几乎全在微信里。最后的解决方案是报价单生成后在电脑上导成PDF再提供一个“转图片”的快捷按钮PDF 或者图片直接保存到本地。操作员用微信文件传输助手发到手机或者用电脑版微信直接把文件拖给客户。有些人会觉得这个流程太原始但对小店来说这已经是零学习成本的操作。不需要教员工怎么用复杂App他们本来就会微信发文件。本地系统只要能快速生成好看的PDF和图片就等于完成了移动端全覆盖。4.4 备份恢复救命功能不能只是摆设很多小店系统用了几个月后突然硬盘坏了才想起来从来没备份过。数据库文件一旦损坏前面的所有报价记录全部归零。所以我把备份做成了系统里的一个一级按钮不藏在设置深处。备份的逻辑不是简单复制数据库文件直接用文件复制容易在写入中间时拷出不完整文件。SQLite 有专门的安全备份指令VACUUM INTO backup_20260601.db;不过要注意这条指令会一次性把全量数据写进新文件如果数据量很大建议还是用定时任务在业务低峰期执行。我实际设置的是每天晚上十点自动生成一个备份文件保留最近30个自动清理更早的。恢复的时候直接把备份文件替换到数据目录重启服务数据就能原样回来。另外备份文件不能只存在同一台电脑上硬盘坏的时候这些都一起完蛋。至少要每周把备份目录整体复制到U盘或者老板的手机一次。系统会显示每个备份文件的大小和生成时间老板一眼就能看出备份有没有正常跑。5. 常见问题与真实排查记录5.1 常见问题速查表项目上线后我积累了一张问题速查表这里直接分享出来遇到问题可以先对照一下现象可能原因解决办法打印出来没有中文字体浏览器用了系统英文默认字体导出PDF后再打印或统一指定中文字体栈导出表格里日期变成乱码日期被识别成系统日期格式将日期列导出为文本字符串报价编号重复没有用原子计数器改用UPDATE ... RETURNING生成流水号金额和手算差几分钱使用了浮点数运算单价和金额全部改存整数分备份文件恢复后缺数据备份时数据库正在被写入用VACUUM INTO或停止服务后备份首页卡顿打开很慢没有给常用字段建索引给quotes.customer_id、quotes.status加索引客户名字重复没有唯一约束新增时先检索同名弹窗提示合并这张表里的问题我几乎在真实店铺里都撞到过。大家的经验大同小异运维阶段排错少一点“灵光一闪”多一点“查表和合理实验”。5.2 一次上门实施实录我陪着一位店主走了一遍完整实施流程整个过程大概三小时。第一件事不是装系统而是先和老板理“价格底表”。他翻出手写的报价公式把经常做的十个项目全部录进系统标注默认单价和折扣底线。这个过程非常重要因为系统再好里面的价格也是错的后面所有报价都会跟着错。第二件事是把历史报价做一次轻量导入。我建议只录最近三个月还有可能复用的报价更早的不用硬录。花了一天时间录三年前的报价只会拖慢启动而且那些报价基本已经没用了。导入完成后给每个历史客户建一条联系人记录后面再报价就能直接搜索带出。第三件事是培训。我没有开会讲课而是让老板自己用“老板视角”走一遍新建客户、添加项目、填数量、算总价、导出PDF、发给客户。然后让店里负责开单的员工用“员工视角”再走一遍。整个过程大约四十分钟员工就基本能上手了。真正花时间的是老板对价格库的确认那才是全系统里最需要人判断的地方。实施当天最容易出问题的不是软件而是打印机。很多小店的打印机是老旧型号驱动乱纸张规格和目标样式不匹配。我建议在系统里预设几组常见打印纸张模板并在实施现场先打一张测试页确认字体和页边距之后再打印正式报价单。5.3 没有人愿意看操作手册我本来写了一份完整的操作手册后来发现没人看。中小商家的员工流动性大、现场忙碌大家只想快速完成操作。后来我把培训材料压缩成一张贴在显示器旁边的“三步小抄”第一步点“新建报价”选客户填项目。第二步核对金额点“保存并预览”。第三步导出PDF发给客户状态改成已发送。这三步之外的内容全部放进界面内的浮动提示里。比如第一次添加行项目时旁边会出现一行小字“价格会取价格库默认值可手动修改。”这样操作者不需要翻说明界面本身就完成了教学。做这类系统不要追求文档精美要追求少写文档、界面自解释。6. 复盘与扩展方向哪些坚持对了哪些该留给以后6.1 哪些设计一定要坚持走过一遍之后我特别后悔的事情是前期差点把项目做成大系统。如果一开始就上了权限管理、审批流、多级审核、销售统计图表我大概率会在第三个月被需求拖死。中小商家的核心是“快”老板自己一眼能看完整条报价链路审批流纯粹是给自己添堵。坚持只做报价闭环让客户管理和产品服务库成为辅助模块这个判断现在是验证过了的。报价做顺手之后老板才会主动问“能不能让报价直接变成工单”到那时候再做订单化整个系统的成长路径就顺了。另外一个值得坚持的点是历史快照。所有报价单明细都保存生成那一刻的价格、规格、项目名不随当前价格库变动而变动。这个设计在后期带来的信心非常大客户拿着一年前的报价单来问系统还能原样打印出当时的内容员工不会一脸尴尬。6.2 后续可以扩展的方向项目已经不是当初那个只能算价的小工具了但扩展点还是应该克制。我目前比较认可的几个扩展方向是第一报价单转工单。报价被确认后一键转化为执行工单工单里可以直接看到原始报价不需要重新录入。这是最顺滑的下一步能直接减少重复劳动。第二客户回访提醒。报价三个月后自动提醒“可以回访一次”这个功能不需要复杂营销只需要在待办里多一条记录小店就能把老客户激活起来。第三数据统计。统计“本月发了多少报价、成交率是多少、平均折扣是多少”这类老板关心的指标。但要注意统计图不能做成大屏展示要能直接嵌入到每天打开的首屏。还有一个方向是局域网多端。如果店里有多台电脑或者想在前台Pad上操作可以让服务绑定局域网地址。这个在技术上是小改动却能让店里的操作体验从“排队用一台电脑”升级成“随手在哪都能录”。6.3 最想分享的一条经验如果只留一句话给后来的同行我想说给中小商家做的系统功能多少不重要数据能不能找回来、报价能不能追溯才最重要。这个项目做到后期真正让店主觉得离不开的不是某几个按钮而是他能随时回答客户那句“当初你报的是多少钱”。只要历史报价能在一秒内被翻出来并且打印出来和当初一模一样这个系统就已经赢了。技术选型、代码结构、页面布局当然都要做好但别让这些细节过多消耗精力把精力集中到数据可靠性和操作顺手度上才是这类项目的正解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑