ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Delphi销售管理系统毕业设计:通用数据模型、FireDAC与开单事务

2026/9/20 3:46:03 拓冰建站 浏览量
Delphi销售管理系统毕业设计:通用数据模型、FireDAC与开单事务 简介面向计算机专业毕业设计选题的 Delphi 通用销售管理系统完整论文资料以 PDF 形式呈现适合需要完成信息管理系统类毕设、参考传统单机 C/S 架构开发流程的本科与高职学生。文档围绕销售管理系统的需求分析、可行性研究、数据库设计与前端实现展开依次讲解 Delphi 6.0 语言概述、数据库系统简介、经济与时间可行性论证、数据库建立与连接、系统总体设计与详细设计并给出登录界面、销售管理、查询统计、图表分析及主界面五个模块的界面设计与代码实现要点末尾附结论、致谢与参考文献结构完整可直接对照写作。压缩包共 1 个 PDF 文件大小约 5.46MB便于阅读与打印。目前已有 104 人学习可作为选题参考、论文框架模板以及 Delphi 数据窗口、ADO 访问数据库的入门范例。1. Delphi 写销售管理系统卡住毕业设计的往往不是代码量答辩现场最尴尬的一幕通常是老师随手点了新增销售单程序弹出一个Cannot open file sales.db或者连续开两张单把库存扣成了负数。Delphi 做销售管理系统这类毕业设计代码量从来不是瓶颈三五百行就能把界面铺满真正卡人的是通用两个字——采购单、销售单、退货单要不要各写一套窗体字段改一个要不要重编译库存和订单怎么保证在同一个事务里。不少同学用 Delphi 7 起步拖控件快但一碰到并发扣减、单据配置、报表对账就露怯。这篇按一线做企业内部管理系统的路子把数据模型、FireDAC 连接参数、单据可配置方案、开单事务和几个答辩加分技巧讲透读完能直接落到自己的作品里也能回答老师你这个通用体现在哪的追问。适合正在做 Delphi 销售管理系统毕业设计、或者要接手一套存量 Delphi 管理软件的人。2. 通用销售管理系统的数据模型与 FireDAC 连库参数2.1 把通用翻译成表结构主数据、单据、配置三层通用销售管理系统里的通用落到数据库上就是三件事主数据可维护商品、客户、仓库、供应商单据结构统一一张主表 一张明细表支撑所有业务类型行为参数可配置单据类型、字段显示、审批开关。很多同学一上来给每个业务建一套表t_sale_order、t_purchase_order、t_return_order三份结构几乎一样的表字段一改三处同步改到第三周自己都记不清哪张表有Discount。我一般会收敛成t_billt_bill_item用BillType区分业务金额方向、库存方向这类差异用配置表描述真正需要独立字段的再挂一张扩展表。表名作用关键约束t_goods商品主数据Code 唯一Price 以分存储t_customer客户档案支持信用额度、账期t_stock商品库存主键 GoodsIDQty 不允许为负t_bill / t_bill_item单据主表与明细OrderNo 唯一索引t_field_map字段显示与校验配置联合主键 (BillType, FieldName)t_op_log操作日志记录操作人、IP、时间这张清单看着朴素但把通用从口号变成了可查询的数据。答辩时老师问加一个新单据类型要不要改代码你可以指着 t_field_map 说加行数据就行窗体字段是运行时读配置生成的。2.2 建库脚本订单唯一约束与库存表设计先把最小可跑的骨架建出来SQLite 适合单机演示MySQL 适合答辩时多机联调DDL 差别不大这里以 SQLite 为例。注意金额字段用整数存分浮动类型在反复累加后会飘报表对账时能差出几毛钱老师一眼就能看出问题。-- 商品与库存库存单独成表避免和商品主数据混在一起被误改 CREATE TABLE t_goods ( GoodsID INTEGER PRIMARY KEY AUTOINCREMENT, Code VARCHAR(32) NOT NULL, Name VARCHAR(64) NOT NULL, Spec VARCHAR(64), Price INTEGER NOT NULL DEFAULT 0, -- 单位分 IsActive INTEGER NOT NULL DEFAULT 1 ); CREATE UNIQUE INDEX ux_goods_code ON t_goods(Code); CREATE TABLE t_stock ( GoodsID INTEGER PRIMARY KEY, Qty INTEGER NOT NULL DEFAULT 0, -- 当前库存 SafeQty INTEGER NOT NULL DEFAULT 0, -- 安全库存低于此值预警 UpdatedAt TEXT ); -- 单据主表OrderNo 唯一Status 用整数枚举便于索引 CREATE TABLE t_bill ( BillID INTEGER PRIMARY KEY AUTOINCREMENT, BillType VARCHAR(16) NOT NULL, -- SALE / PURCHASE / RETURN OrderNo VARCHAR(32) NOT NULL, CustomerID INTEGER, OrderAmt INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0, -- 0草稿 1已确认 2已出库 9已作废 CreatedAt TEXT NOT NULL, CreatedBy VARCHAR(32) ); CREATE UNIQUE INDEX ux_bill_no ON t_bill(OrderNo); CREATE INDEX ix_bill_type_status ON t_bill(BillType, Status); -- 单据明细 CREATE TABLE t_bill_item ( ItemID INTEGER PRIMARY KEY AUTOINCREMENT, BillID INTEGER NOT NULL, GoodsID INTEGER NOT NULL, Qty INTEGER NOT NULL, Price INTEGER NOT NULL, Amount INTEGER NOT NULL ); CREATE INDEX ix_item_bill ON t_bill_item(BillID);ux_bill_no这个唯一索引值得单独说单号由程序按前缀 yyyyMMdd 4 位流水生成多端同时开单时有概率撞号交给数据库拦比在代码里加锁稳。撞号时 FireDAC 会抛唯一约束异常捕获后重取流水重试一次即可别让用户看到英文报错。2.3 FireDAC 连接参数SQLite 与 MySQL 的差别在哪Delphi 7 时代大家用 ADO 或 BDE现在新版本直接上 FireDAC跨 SQLite、MySQL、SQL Server 只换连接参数。SQLite 单机演示最省事路径一定要用程序所在目录拼不要写相对路径双击 exe 和从 IDE 运行时当前目录不一样这就是昨天还好好的今天就打不开库的最大来源。// SQLite单机演示首选无需装服务 FDConn.Params.Clear; FDConn.Params.Add(DriverIDSQLite); FDConn.Params.Add(Database ExtractFilePath(ParamStr(0)) data\sales.db); FDConn.Params.Add(StringFormatUnicode); // 中文不乱码的前提 FDConn.Params.Add(OpenModeCreateUTF8); // 文件不存在时按 UTF8 新建 FDConn.LoginPrompt : False; // 别弹登录框答辩时很尴尬 FDConn.Connected : True; // MySQL多机联调或老师要求要有服务器 FDConn.Params.Clear; FDConn.Params.Add(DriverIDMySQL); FDConn.Params.Add(Server127.0.0.1); FDConn.Params.Add(Port3306); FDConn.Params.Add(Databasesales); FDConn.Params.Add(User_Nameroot); FDConn.Params.Add(Password123456); FDConn.Params.Add(CharacterSetutf8mb4); // 老 utf8 存不下 emoji 和生僻字 FDConn.Params.Add(CompressTrue); FDConn.Connected : True;参数含义逐个对齐OpenModeCreateUTF8让 SQLite 在库文件不存在时自动建StringFormatUnicode决定字符串以何种格式递交缺了它在某些驱动版本下中文会变成问号MySQL 的CharacterSet必须和建库字符集一致服务端 utf8mb4、客户端 utf8插入生僻字照样报Incorrect string value。连接成功后建议在数据模块的OnConnect里跑一句PRAGMA foreign_keysONSQLite否则外键约束形同虚设。3. 单据可配置字段映射表与 ListBox 自绘商品选择器3.1 单据类型 字段映射让同一套代码跑通采购单和销售单配置驱动的核心是把界面上有哪些字段、标签叫什么、属于哪种数据类型存进数据库窗体在运行时读出来生成控件。这样做的好处很直接新增退货单不用动代码只在 t_field_map 里插几行老师临时要求金额字段加个必填校验改配置比改源码快得多。字段类型我一般只定义为四种STRING、INT、MONEY、DATE够覆盖绝大多数单据字段再多就是过度设计。CREATE TABLE t_field_map ( BillType VARCHAR(16) NOT NULL, -- SALE / PURCHASE FieldName VARCHAR(32) NOT NULL, -- 物理列名动态控件按它命名 Caption VARCHAR(32) NOT NULL, -- 界面标签 DataType VARCHAR(16) NOT NULL, -- STRING / INT / MONEY / DATE IsShow INTEGER NOT NULL DEFAULT 1, IsRequired INTEGER NOT NULL DEFAULT 0, SortNo INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (BillType, FieldName) ); INSERT INTO t_field_map VALUES (SALE,OrderNo,销售单号,STRING,1,1,10); INSERT INTO t_field_map VALUES (SALE,CustomerID,客户,INT,1,1,20); INSERT INTO t_field_map VALUES (SALE,OrderAmt,单据金额,MONEY,1,0,30); INSERT INTO t_field_map VALUES (SALE,Remark,备注,STRING,1,0,40);排序字段 SortNo 用 10、20 递增日后中间插字段不用重排所有行这个小习惯能省掉不少 UPDATE 语句。3.2 动态生成录入界面的核心代码下面这段是运行时根据配置生成编辑控件的最小实现控件命名统一加edt_前缀提交时按字段名反查取值不用为每个单据写一遍赋值代码。procedure TfrmBill.BuildEditor(const ABillType: string); var Q: TFDQuery; Ed: TControl; Lb: TLabel; Top, LineH: Integer; begin while pnlEditor.ControlCount 0 do // 先清空旧控件避免切换单据类型时堆叠 pnlEditor.Controls[0].Free; Top : 8; LineH : 28; Q : TFDQuery.Create(nil); try Q.Connection : DM.Conn; Q.SQL.Text : SELECT FieldName, Caption, DataType, IsRequired FROM t_field_map WHERE BillType :bt AND IsShow 1 ORDER BY SortNo; Q.ParamByName(bt).AsString : ABillType; Q.Open; while not Q.Eof do begin Lb : TLabel.Create(Self); Lb.Parent : pnlEditor; Lb.Left : 8; Lb.Top : Top 4; Lb.Caption : Q.FieldByName(Caption).AsString; Ed : CreateEditorByType(Q.FieldByName(DataType).AsString); Ed.Parent : pnlEditor; Ed.Left : 96; Ed.Top : Top; Ed.Width : 180; Ed.Name : edt_ Q.FieldByName(FieldName).AsString; if Q.FieldByName(IsRequired).AsInteger 1 then Ed.Hint : 必填; // 提交时统一收集 Hint 做校验 Top : Top LineH; Q.Next; end; finally Q.Free; end; end;逻辑说明先清空面板防止重复生成查询按 SortNo 排序保证界面顺序稳定控件名由字段名拼出提交时用pnlEditor.FindComponent(edt_OrderNo)直接定位不用维护映射数组。CreateEditorByType按类型返回 TEdit、TDBEdit、TDateTimePicker 或 TComboBoxMONEY 类型返回带掩码的 TEdit把输入限制在两位小数内。3.3 用 ListBox 自绘做商品选择列表开单时选商品如果只显示商品名操作员得靠记忆猜规格和库存出错率高。TListBox 的lbOwnerDrawFixed风格配上OnDrawItem能把三行信息塞进一行高度名称加粗、规格和库存在下方、价格右对齐这就是常说的 delphi listbox自绘。要注意 TColor 是 BGR 顺序写颜色时别按 RGB 想。procedure TfrmGoodsPick.lbGoodsDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState); var Lb: TListBox; G: TGoodsInfo; begin Lb : TListBox(Control); G : FGoodsList[Index]; // 外部维护的商品数组与列表索引一一对应 if odSelected in State then Lb.Canvas.Brush.Color : $00F5E6D3 // BGR呈浅蓝灰比系统高亮色柔和 else Lb.Canvas.Brush.Color : clWhite; Lb.Canvas.FillRect(Rect); // 必须自己填背景否则残留旧文字 Lb.Canvas.Font.Style : [fsBold]; Lb.Canvas.TextOut(Rect.Left 6, Rect.Top 3, G.Name); Lb.Canvas.Font.Style : []; Lb.Canvas.Font.Color : clGray; Lb.Canvas.TextOut(Rect.Left 6, Rect.Top 20, G.Spec 库存 IntToStr(G.Stock) 件); Lb.Canvas.Font.Color : clMaroon; Lb.Canvas.TextOut(Rect.Right - 80, Rect.Top 3, FormatFloat(0.00, G.Price)); end;两个容易踩的点控件Style属性必须设为lbOwnerDrawFixedItemHeight至少 38否则两行文字会被裁掉商品数量多时不要在 OnDrawItem 里查数据库先把数据取到FGoodsList自绘只做绘制否则拖动滚动条会明显掉帧。搜索框配合自绘列表时用Items.BeginUpdate/EndUpdate包住批量更新能消掉闪烁。4. 开单、扣库存与定时任务销售主流程落地4.1 一张销售单的事务边界怎么划销售单保存涉及四步写入主表、明细、库存扣减、库存流水。这四步必须同生共死否则会出现单子建了库存没扣的对账黑洞。FireDAC 的事务对象要显式指定别依赖自动提交自动提交在多表写入时一旦中途报错前面几条已经落库了。function TOrderService.SaveSaleOrder(const AOrder: TOrderDTO): Boolean; begin Result : False; DM.Conn.StartTransaction; try InsertBill(AOrder); // 1. 写主表Status 1 已确认 InsertItems(AOrder); // 2. 写明细 for var I : 0 to AOrder.Items.Count - 1 do begin if not DeductStock(AOrder.Items[I]) then raise Exception.CreateFmt(商品 %s 库存不足, [AOrder.Items[I].GoodsName]); WriteStockLog(AOrder.Items[I], AOrder.OrderNo); // 3. 写流水便于追溯 end; DM.Conn.Commit; Result : True; except on E: Exception do begin DM.Conn.Rollback; LogError(AOrder.OrderNo, E.Message); // 日志单独走另一连接或写文件 raise; // 继续向上抛界面层统一提示 end; end; end;StartTransaction到Commit之间不要弹对话框、不要更新界面进度条事务持有时间越长SQLite 越容易报database is lockedMySQL 上则表现为行锁等待。日志写入如果用同一个连接回滚会把日志一起回滚掉这是新手最常见的自坑点改成独立连接或直接写文本文件。4.2 扣减库存与超卖判断的代码库存扣减不要先 SELECT 再 UPDATE中间那条缝就是超卖的入口。正确做法是一条带条件的 UPDATE让数据库来判断够不够再用受影响行数决定成败。这个思路在 SQLite 和 MySQL 上都通用且不需要显式加锁。function TOrderService.DeductStock(const AItem: TOrderItemDTO): Boolean; var Q: TFDQuery; Rows: Integer; begin Q : TFDQuery.Create(nil); try Q.Connection : DM.Conn; Q.SQL.Text : UPDATE t_stock SET Qty Qty - :qty, UpdatedAt :ts WHERE GoodsID :gid AND Qty :qty; // 条件写在 WHERE 里天然防超卖 Q.ParamByName(qty).AsInteger : AItem.Qty; Q.ParamByName(gid).AsInteger : AItem.GoodsID; Q.ParamByName(ts).AsString : FormatDateTime(yyyy-mm-dd hh:nn:ss, Now); Rows : Q.ExecSQL; // 返回受影响行数 Result : Rows 1; finally Q.Free; end; end;Qty :qty这个条件是整个函数的灵魂库存只剩 3 件、要出 5 件时UPDATE 影响 0 行函数返回 False上层抛异常触发回滚全程不会出现负库存。参数化写法同时挡住了 SQL 注入商品名里带单引号也不会把语句拆坏。安全库存预警可以另开一个查询Qty SafeQty的记录在开单界面上用红底提示不要塞进这条 UPDATE 里。4.3 TTimer 自动释放超时未付款订单销售单确认后常见做法是锁库存客户半小时不付款就把单子作废、库存回吐。这个任务用 TTimer 做轮询最省事但必须处理重入问题——上一轮还没跑完定时器又触发了库存会被扣两次这就是调试时排查 delphi timer.working 状态的意义所在。procedure TfrmMain.TimerReleaseTimer(Sender: TObject); begin if FReleasing then Exit; // 重入保护上一轮未结束直接返回 FReleasing : True; TimerRelease.Enabled : False; // 先停表处理完再开避免堆积 try ReleaseExpiredBills(30); // 30 分钟未付款的已确认单据 finally TimerRelease.Enabled : True; FReleasing : False; end; end; procedure TfrmMain.ReleaseExpiredBills(AMinutes: Integer); var Q: TFDQuery; begin Q : TFDQuery.Create(nil); try Q.Connection : DM.Conn; // 只处理草稿/已确认状态已出库的不动 Q.SQL.Text : SELECT BillID, OrderNo FROM t_bill WHERE Status IN (0,1) AND CreatedAt :limit; Q.ParamByName(limit).AsString : FormatDateTime(yyyy-mm-dd hh:nn:ss, Now - AMinutes / 1440); Q.Open; while not Q.Eof do begin CancelBillAndRestoreStock(Q.FieldByName(BillID).AsInteger); Q.Next; end; finally Q.Free; end; end;TimerRelease.Interval设 60000 到 300000 之间比较合适太密会频繁开事务太疏释放不及时。生产环境里这种定时任务通常挪到后台服务或数据库调度里做桌面程序一关就没了毕业设计范围内用 TTimer 说明思路足够但要在论文里写清这个边界老师反而会觉得你考虑过部署形态。5. 答辩加分与排错XMLDoc 注释、IP 归属地与常见报错对照5.1 给方法和参数加注释提示代码可读性在答辩里占分不低。Delphi 支持 XMLDoc 注释在方法或参数上方输入///后按回车IDE 会自动补出summary、param、returns骨架写完后把鼠标悬停在调用处就能看到提示这就是很多资料里说的 delphi 给方法和参数增加提示描述。要点是注释块必须紧贴声明行、中间不能空行否则提示不生效。/// summary按单号查询销售单主表只返回一条记录/summary /// param nameAOrderNo销售单号格式 SOyyyyMMdd4位流水/param /// returns已打开的查询对象调用方负责 Free/returns function TOrderService.GetByOrderNo(const AOrderNo: string): TFDQuery;param的名字必须和形参完全一致大小写不敏感但拼错就没有提示。把服务层和 DTO 都补上注释再用 IDE 的类视图浏览一遍会发现不少命名含糊的字段顺手改掉比写文档划算。5.2 操作日志里解析 IP 归属地管理系统的操作日志只记录 IP 说服力有限加上归属地能明显提升完整度。纯真 IP 库qqwry.dat是常见选择文件结构不复杂前 8 字节是索引区首尾偏移之后每条索引 7 字节前 4 字节起止 IP、后 3 字节记录偏移记录区里再存结束 IP 和国家、地区字符串偏移遇到 0x01、0x02 标志要做重定向。用 TFileStream 读二进制、按小端序拼回整数再和查询 IP 做区间比较即可。实际项目里我一般把它封成一个GetIPLocation(IP: string): string函数日志写入时调用一次结果一起入库避免每次看日志都重读文件。5.3 商品图片任意角度旋转与报错对照商品图片需要摆正或做报表水印时Delphi 自带 Canvas 只能做 90 度整数倍旋转任意角度得走 GDI 或 Graphics32。用 GDI 的思路是先把坐标系原点平移到图片中心旋转指定角度再平移回去绘制三行变换加上一次 DrawImage 就能出结果关键 API 是 TGPGraphics 的 TranslateTransform 和 RotateTransform。热词里常提到的 delphi graphicex 属于另一类封装思路适合需要频繁做滤镜和缩放的场景毕业设计用原生 GDI 已经够。现象常见原因处理方式Cannot open file sales.db用了相对路径当前目录变了用 ExtractFilePath(ParamStr(0)) 拼绝对路径中文显示为问号字符集与 StringFormat 不匹配驱动端 utf8mb4连接加 StringFormatUnicodedatabase is locked事务未提交或长事务中弹窗缩短事务范围日志写独立连接库存扣成负数先 SELECT 再 UPDATE改成带 Qty :qty 条件的 UPDATEListBox 自绘闪屏未开双缓冲DoubleBuffered : True批量更新用 BeginUpdate定时任务重复执行未做重入保护布尔标志加定时器先停后开服务端连接被拒MySQL 未授权远程访问建专用账号并按需授权别用 root 直连排错时优先怀疑三样路径、字符集、事务边界这三类占了毕业设计里报错的大半。把日志按天写文件、关键操作落库出问题时能顺着单号把主表、明细、库存流水三条线串起来看比盯着代码猜快得多。本文还有配套的精品资源点击获取