
简介这份Delphi自动在线升级程序源码面向使用Object Pascal进行桌面应用开发的程序员尤其适合需要在自有项目中集成版本检测、文件下载与静默更新能力的开发者。源码围绕自动升级的完整链路展开涵盖版本比对、多线程下载、哈希校验、文件替换与重启提示等环节并涉及Indy或Synapse网络库、文件操作与多线程技术的实际运用可作为理解软件更新机制的实践样本。压缩包共49个文件约3.61MB以fr3报表模板、exe可执行文件、ini配置、pas与dpr源码、dfm窗体、dcu编译单元及xml更新清单等为主其中fr3文件对应采购、入库、对账等业务单据模板源码与配置分离便于按模块查阅与二次定制。目前已有330人学习下载。读者可据此掌握断点续传、增量更新、安全校验与性能优化等改造思路并对照自身项目完善升级方案。1. 桌面端交付的最后一公里为什么手动补丁包迟早要出事做过桌面软件交付的同行大概都有过这种经历客户现场跑着一套 Delphi 写的业务系统某天发现一个字段计算逻辑写错了或者某个报表导出在特定分辨率下会崩。你改完代码、编译出新的 exe然后开始一轮漫长的沟通——发邮件、传文件、让对方关掉程序、覆盖、再打开。一个客户还好十个客户就是十倍的重复劳动而且你永远不知道对方覆盖的是不是最新那一版。Delphi 自动在线升级程序源码要解决的就是这最后一公里的问题。它让客户端启动时自己去比对版本、拉取新文件、校验完整性、替换旧文件、重启生效整个过程不需要人工介入。适合的人群很明确手里有 Delphi 桌面项目、客户分散、发版频率不低、又不想上重型安装器方案的开发者。核心难点不在“下载文件”这一步而在于版本比对策略、文件占用时的替换时机、增量与全量的取舍以及升级失败后的回滚兜底。2. 升级链路怎么拆从版本号到落地文件的完整闭环2.1 先想清楚升级的触发点和数据流自动升级的本质是一条单向数据流客户端上报当前版本 → 服务端返回差异清单 → 客户端下载 → 校验 → 替换 → 重启。这条链路里任何一个环节的假设错了后面都会翻车。我一般把触发点放在主窗体创建之前也就是Application.Initialize之后、主窗体Show之前。原因很简单如果主程序已经加载了一堆 DLL 和资源再去替换这些文件Windows 的文件锁会直接让你失败。放在启动早期被占用的文件最少替换成功率最高。数据流的具体形态取决于你选全量还是增量。全量升级就是服务端放一个完整的压缩包客户端整个下载解压覆盖增量升级是服务端维护一份文件清单每个文件带 MD5 和版本号客户端只下载 MD5 不一致的文件。前者实现简单、不容易出错后者省带宽但服务端要维护清单的生成逻辑。对于大多数中小规模的 Delphi 项目我的建议是首次上自动升级用全量跑稳了再考虑增量。因为全量的调试成本低得多出问题也好排查——大不了重新下一遍。2.2 版本清单文件的设计与生成服务端需要暴露一个版本描述文件通常是一个 JSON 或 INI。JSON 更好解析Delphi 里可以用System.JSON单元直接处理。清单里至少要包含最新版本号、最低兼容版本号、更新包下载地址、更新包 MD5、是否强制更新。{ latest_version: 2.3.1, min_compatible_version: 2.0.0, package_url: https://your-server/update/patch_231.zip, package_md5: a1b2c3d4e5f6..., force_update: false, release_notes: 修复报表导出在高分屏下的崩溃问题 }min_compatible_version这个字段容易被忽略但它很关键。如果客户端版本太老直接跳到最新版可能会导致数据结构不兼容这时候应该提示用户先升级到一个中间版本或者干脆走完整安装包。force_update用于紧急修复场景客户端收到true时不允许跳过。生成清单的脚本可以放在构建流程里每次编译完自动算 MD5、写 JSON、上传到服务器。这样避免手动维护清单时漏改版本号。2.3 客户端下载与 MD5 校验的实现客户端拿到清单后第一步是比对本地版本。本地版本可以存在注册表、INI 文件或者 exe 的版本资源里。我倾向于用 exe 的版本资源因为编译时自动写入不会出现文件和记录不一致的情况。function GetLocalVersion: string; var FileName: string; InfoSize, Wnd: DWORD; VerInfo: Pointer; VerValue: PVSFixedFileInfo; Dummy: DWORD; begin FileName : ParamStr(0); InfoSize : GetFileVersionInfoSize(PChar(FileName), Dummy); if InfoSize 0 then Exit(0.0.0); GetMem(VerInfo, InfoSize); try GetFileVersionInfo(PChar(FileName), 0, InfoSize, VerInfo); VerQueryValue(VerInfo, \, Pointer(VerValue), Dummy); Result : Format(%d.%d.%d, [VerValue^.dwFileVersionMS shr 16, VerValue^.dwFileVersionMS and $FFFF, VerValue^.dwFileVersionLS shr 16]); finally FreeMem(VerInfo); end; end;这段代码从当前 exe 的版本资源里读出主版本号。dwFileVersionMS的高 16 位是主版本低 16 位是次版本dwFileVersionLS的高 16 位是构建号。拿到本地版本后和清单里的latest_version做字符串比较——注意版本号比较不能直接按字符串比因为 2.10.0 按字符串会小于 2.9.0。正确做法是拆成整数数组逐段比较。下载环节用TIdHTTP或者THTTPClient都可以。下载完成后必须校验 MD5校验不通过就删掉重下。这一步是防止下载过程中网络中断导致文件损坏损坏的包解压后覆盖上去程序直接起不来。function DownloadFile(const Url, SavePath: string): Boolean; var HTTP: THTTPClient; FS: TFileStream; begin Result : False; HTTP : THTTPClient.Create; FS : TFileStream.Create(SavePath, fmCreate); try try HTTP.Get(Url, FS); Result : True; except on E: Exception do // 记录日志返回 False end; finally FS.Free; HTTP.Free; end; end;THTTPClient是 Delphi 较新版本自带的跨平台支持好。如果项目还在用老版本 DelphiTIdHTTP也能用但要注意 Indy 的 SSL 配置在某些 Windows 版本上需要额外处理。2.4 文件替换的时机与批处理方案文件替换是整个升级里最容易翻车的一步。主程序 exe 自己正在运行没法自己覆盖自己。常见的做法是下载完更新包后释放一个独立的 updater.exe由它来关闭主程序、解压覆盖、再启动主程序。updater.exe 的逻辑要足够简单因为它是在一个“主程序已经退出”的环境里跑的没有主程序的运行时支持。它只做几件事等待主进程退出、解压 zip、复制文件、启动主程序、自己退出。// updater.exe 的核心逻辑 begin // 1. 等待主程序退出最多等 30 秒 WaitForProcessExit(MainApp.exe, 30000); // 2. 解压更新包到临时目录 UnzipPackage(patch.zip, C:\Temp\update\); // 3. 复制文件到安装目录 CopyFiles(C:\Temp\update\, ExtractFilePath(ParamStr(0))); // 4. 启动主程序 ShellExecute(0, open, MainApp.exe, nil, nil, SW_SHOWNORMAL); end.WaitForProcessExit可以用OpenProcessWaitForSingleObject实现也可以用简单的循环检查进程是否存在。解压可以用 Delphi 自带的System.Zip单元不需要第三方库。这里有个细节复制文件时如果目标文件被占用CopyFile会失败。所以 updater 必须在主程序完全退出后才开始复制。等待超时后如果主程序还没退出应该放弃本次升级并记录日志而不是强行结束进程——强行结束可能导致用户数据丢失。3. 避坑指南升级失败的五种典型现场3.1 现象升级后程序启动报“找不到 DLL”原因更新包里漏掉了某个依赖的 DLL或者 DLL 的版本和 exe 不匹配。全量升级时如果打包脚本只复制了 exe没复制依赖项就会出现这个问题。解决打包脚本里维护一份完整的文件清单每次构建时按清单复制。清单本身也要纳入版本管理新增依赖时同步更新。另外升级前先备份旧版本的所有文件到backup目录启动失败时可以回滚。3.2 现象MD5 校验通过但解压报错原因下载的 zip 文件本身损坏但 MD5 是在服务端算的客户端下载后没重新算就通过了。或者服务端上传时文件被截断MD5 清单没同步更新。解决客户端下载完成后必须重新计算 MD5 并与清单比对不能信任服务端的值。解压时捕获异常解压失败就删除临时文件并提示用户重试。3.3 现象升级过程中断电程序再也起不来原因文件替换到一半时断电exe 被覆盖了一半或者关键配置文件丢失。解决采用“先写临时文件再重命名”的策略。所有文件先复制到目标目录下的.tmp文件全部复制成功后再批量重命名为正式文件。重命名是原子操作断电最多导致.tmp文件残留不会破坏原文件。同时保留一份完整的备份启动时检测到异常可以自动恢复。3.4 现象部分客户升级后版本号没变原因客户端读版本号读的是注册表里的记录而不是 exe 的实际版本。升级后 exe 换了但注册表没更新程序以为还是旧版本反复提示升级。解决版本号只从 exe 版本资源读取不维护额外的版本记录。如果确实需要缓存升级成功后立即更新缓存并且以 exe 版本资源为准做校验。3.5 现象升级包下载速度极慢或超时原因服务端带宽不足或者客户端网络环境差。没有断点续传每次失败都从头下载。解决对于大包实现断点续传。THTTPClient支持Range头可以在下载中断后从已下载的字节位置继续。同时设置合理的超时时间避免用户等太久。如果包不大几十 MB 以内全量下载通常可以接受不必过度设计。4. 进阶技巧让升级方案更稳的几个细节4.1 用配置文件控制升级行为不要把升级参数硬编码在代码里。服务端地址、检查间隔、是否允许用户跳过这些都应该放在一个独立的配置文件中。这样不同客户可以有不同的升级策略不用重新编译。[Update] ServerUrlhttps://your-server/update/ CheckInterval3600 AllowSkip1 BackupCount3CheckInterval控制检查频率单位秒。AllowSkip决定用户能否点“稍后再说”。BackupCount保留最近几个版本的备份方便回滚。4.2 升级日志要写到用户能拿到的地方升级失败时用户描述的现象往往很模糊——“就是打不开”。所以日志必须写到固定位置比如%APPDATA%\YourApp\update.log记录每次检查的时间、版本比对结果、下载进度、校验结果、替换结果。出问题时让用户把这个文件发过来比反复沟通高效得多。日志要滚动不能无限增长。可以按天切分保留最近 7 天。4.3 灰度发布先让一部分客户端升级服务端可以维护多个版本的清单按客户端标识比如机器码或客户编号返回不同的latest_version。先让内部测试机升级观察一天没问题再逐步放量。这样即使新版本有严重 bug影响面也可控。实现方式很简单清单接口接收一个client_id参数服务端根据这个参数决定返回哪个版本的清单。客户端不需要做任何特殊处理。4.4 回滚机制给用户一颗后悔药再完善的测试也挡不住现场环境的多样性。回滚机制是最后的兜底。具体做法是每次升级前把当前版本的所有文件复制到backup\版本号\目录升级后如果主程序启动失败可以通过 updater 检测启动后进程是否存活来判断自动从备份目录恢复。function TryRollback(const BackupDir: string): Boolean; begin Result : False; if not DirectoryExists(BackupDir) then Exit; // 复制备份文件回安装目录 CopyFiles(BackupDir, ExtractFilePath(ParamStr(0))); Result : True; end;回滚的触发条件要谨慎设置。如果主程序启动后 10 秒内退出可以认为是启动失败触发回滚。但如果用户是正常关闭程序也会被误判。所以更稳妥的做法是让主程序在成功启动后写一个标记文件updater 检测这个标记文件是否存在来判断启动是否成功。这套方案我在几个中小型 Delphi 项目上跑过最深的体会是升级逻辑本身不复杂复杂的是各种边界情况的处理。文件占用、网络中断、权限不足、杀毒软件拦截每一个都可能让升级失败。所以不要追求一次写完就完美先把主流程跑通然后根据日志逐步补边界处理。备份和回滚一定要在第一版就做进去这是唯一能在出问题时让你睡个安稳觉的东西。希望帮到你。本文还有配套的精品资源点击获取