
简介这是一份基于PHP与iAPP框架的APP托管平台源码面向有一定Web开发基础、希望自建应用分发与更新服务的开发者可帮助免去自行搭建服务器和分发基础设施的繁琐流程。平台集成了用户管理、应用上传、版本控制、更新发布、访问控制等常用功能采用MVC分层架构模块划分清晰具备良好的可定制性与扩展空间。压缩包共26个文件约220KB核心为15个PHP脚本负责HTTP请求处理、业务逻辑与数据库交互另有HTML页面用于界面展示、PNG图标、SQL脚本用于初始化数据库以及iapp/json等配置文件用于设定运行参数整体结构紧凑部署门槛较低。目前已有365人学习浏览。借助源码中的配置、模板和数据库初始化脚本开发者既能快速搭建可用的APP托管服务也可结合文档和目录结构深入学习PHP与iAPP框架的项目实践。1. 从 IAPP 源码打包到 APP 托管Trust Web 这套 PHP 方案到底解决什么独立开发者把 APK 发到网盘链接三天两头失效小工具做好新版本老用户却一直装在旧包里反馈各种怪问题想统计下载量只知道“大概有几百人”。这些问题单拎出来都小叠在一起就成了 APP 发布链路上最烦人的一环。Trust Web 就是标题里那套 PHPiapp 开源源码所要补齐的短板用 PHP 提供版本检测、下载分发、公告下发让 IAPP 写的安卓脚本在应用内直接消费这些接口把“发布 - 更新 - 统计”收拢成一套自己能维护的服务。这类源码最常见的落地形态是轻量软件库一个 PHP 站点管 APK 文件一套 JSON 接口管版本判断一个 IAPP 客户端负责拉起更新弹窗。对 IAPP 工具作者、软件库 PHP 源码站长、需要给公司内部发测试包的技术人员来说它的价值在于不必为 50 个下载量的工具上一整套应用市场 SDK只要一台能跑 PHP 的虚拟主机就够了。下面按服务端骨架、客户端交互、下载分发、上线验证这条线把 Trust Web 这类工程从原理到可复现的代码走一遍。2. Trust Web 的服务端骨架PHP 分发架构与数据表设计2.1 先拆三层文件层、接口层、统计层不管源码包里目录叫什么名字APP 托管系统的服务端都逃不开三层职能。第一层是文件层专门存放 APK、ZIP 这类原始安装包这一层要做到“只能被程序读走不能被任意执行”第二层是接口层给 IAPP 客户端提供版本检测、公告拉取等 JSON 接口客户端只认接口不认目录第三层是统计层把每一次下载请求落成日志供后续看版本转化率、异常流量。拿到 Trust Web 源码后我一般会先对照这三层找对应文件upload 目录对应文件层api 或 index.php 路由对应接口层logs 或数据库里的记录表对应统计层。三层拆开的最大好处是换 APK 文件时不需要动接口协议IAPP 客户端永远只和接口地址打交道。很多半成品源码的问题恰恰出在这文件层和接口层混在一起下载地址直接暴露服务器真实路径换一次目录就要改一次客户端这是最需要先纠正的架构习惯。2.2 PHP 版本与数据库选型SQLite 还是 MySQLTrust Web 这类工程对运行环境的要求不高PHP 7.4 以上、Nginx 或 Apache 均可。开发机在 Windows 10 下调试时用 Nginx PHP 集成环境最省事生产环境则建议直接上 Linux 服务器避免 Windows 下路径分隔符和文件权限带来的隐藏问题。数据库方面常见做法是 SQLite 与 MySQL 二选一选型依据很简单同时在线下载数在几百以内、又不想维护数据库服务的SQLite 足够。对比项SQLiteMySQL部署成本零PHP 内置扩展即可需要单独安装维护备份方式拷贝一个 .db 文件mysqldump 或物理备份并发写入适合低频写写锁较重高并发写入能力强适用场景个人工具库、内部分发多应用聚合软件库、需要后台报表我倾向于把这个工程的数据库放在 web 根目录之外例如把 data/app.db 放在站点根目录上一级。原因是 PHP 站点一旦出现配置失误放在根目录内的 .db 文件可能被直接下载数据库外置能从物理位置上杜绝这类泄露。如果后续下载量涨到需要定时出报表再迁移到 MySQL 成本也很低接口层记得统一走 PDO 即可。2.3 版本表和下载日志表SQL 直接拿去建版本表是这套系统的核心资产IAPP 端每次启动检查的“有没有新版本”都靠它回答。下载日志表负责记录每一次文件下发用来计算版本分布和下载趋势。下面是 SQLite 下可直接执行的两张表结构MySQL 也基本兼容只需要把 INTEGER PRIMARY KEY AUTOINCREMENT 换成 INT AUTO_INCREMENT。CREATE TABLE app_version ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_id INTEGER NOT NULL DEFAULT 0, version_code INTEGER NOT NULL, version_name TEXT NOT NULL, apk_path TEXT NOT NULL, md5 TEXT DEFAULT , update_log TEXT DEFAULT , create_time INTEGER NOT NULL ); CREATE TABLE download_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_id INTEGER NOT NULL DEFAULT 0, ua TEXT DEFAULT , ip TEXT DEFAULT , create_time INTEGER NOT NULL ); CREATE INDEX idx_version_app ON app_version(app_id, version_code); CREATE INDEX idx_download_time ON download_log(create_time);version_code 是给程序比较用的数字必须单调递增不能用 1.0.2 这种字符串代替version_name 是给用户看的展示名可以写“v1.0.2”。md5 字段存 APK 的校验值IAPP 下载完后可以用来确认包没有损坏。update_log 存更新说明注意控制长度否则 JSON 接口会返回超大字段拖慢 IAPP 端的解析速度。2.4 upload 目录的权限与防解析文件层最容易出安全问题尤其是带后台的 PHP 源码只要攻击者能传一个 PHP 文件进 upload 目录整台服务器就失去边界。Nginx 下最简单的防御是在 server 块里加一条规则让 upload 目录下的 PHP 文件全部拒绝执行。location ~* ^/upload/.*\.(php|php5|phtml)$ { deny all; }Apache 环境则在 upload 目录放一个 .htaccess内容写上php_flag engine off。如果源码包自带的文件上传逻辑只校验了扩展名要补上 MIME 类型和第二层白名单校验APK 和 ZIP 之外的类型一律拒绝。规则做完后试着直接在浏览器访问http://你的域名/upload/shell.php看到 403 才算过关。3. IAPP 客户端与 PHP 接口的版本检查HTTPJSON 怎么对得起来3.1 IAPP 发起 HTTP 请求的最小示例IAPP 端做版本检查本质就是发一个 HTTP GET 请求带上当前版本号拿到服务端返回的 JSON 再决定是否弹更新框。下面按 IAPP 常见命令书写个别版本对命令命名略有差异以你本地的命令列表为准。// 拼接版本检查地址version_code 换成当前 APK 的版本号 s url http://你的域名/api/check_version.php?app_id1version_code2 // 发起 GET 请求返回内容放入 res http url, get, , res // 调试用先把原始返回弹出来 tip(res)http 命令这四个参数依次是请求地址、请求方式、提交数据、返回变量。GET 请求不需要提交数据传空字符串即可。第一次联调务必把 res 弹出来看很多问题一眼就能发现接口返回了 HTML 错误页、JSON 字段名拼错、或者服务器把请求重定向到了登录页。建议先从 HTTP 明文调试确认全链路通了再上 HTTPS。3.2 PHP 端版本检查接口用数组组装响应服务端接口要返回结构化数据最省事的做法是用 PHP 关联数组组装再交给 json_encode 输出。这里带出一个 IAPP 开发新手容易绕弯的点PHP 接口数组对象经过 json_encode 后变成 JSON 对象字符串IAPP 端再用 JSON 解析还原成可遍历的数组对象两边别试图互传“PHP 对象”统一走字符串才是标准姿势。?php // api/check_version.php header(Content-Type: application/json; charsetutf-8); $appId isset($_GET[app_id]) ? (int)$_GET[app_id] : 0; $currentCode isset($_GET[version_code]) ? (int)$_GET[version_code] : 0; $pdo new PDO(sqlite: . __DIR__ . /../data/app.db); $stmt $pdo-prepare( SELECT id, version_code, version_name, apk_path, md5, update_log FROM app_version WHERE app_id ? ORDER BY version_code DESC LIMIT 1 ); $stmt-execute([$appId]); $latest $stmt-fetch(PDO::FETCH_ASSOC); if (!$latest) { echo json_encode([code 404, msg app not found]); exit; } $hasUpdate $latest[version_code] $currentCode; echo json_encode([ code 200, data [ has_update $hasUpdate, force_update 0, latest_version $latest[version_name], apk_url http://你的域名/download.php?id . $latest[id], update_log $latest[update_log], md5 $latest[md5] ] ], JSON_UNESCAPED_UNICODE);注意 version_code 在两端都必须用整型比较PHP 里用 (int) 强转IAPP 端也要转成数字再加比较。JSON_UNESCAPED_UNICODE 让中文更新日志直接显示不变成 \uXXXX 转义序列调试时肉眼可读。apk_url 不要直接暴露 apk_path 实际路径而是指向 download.php 这种分发入口这样文件层结构调整不影响客户端。3.3 IAPP 端解析数组对象把字段拆到更新弹窗拿到返回字符串后IAPP 端需要把它解析成数组对象再按 key 取字段。常见的坑是直接拿字符串去做相等比较比如判断返回等于“字符串内容”导致永远进不了更新分支。正确流程是先解析 JSON再取 data 里的字段。// res 是接口返回的 JSON 字符串 // 解析成数组对象后按字段名取值示意组件命令请替换 s obj json解析(res) s has 取字段(obj, has_update) s latest 取字段(obj, latest_version) s log 取字段(obj, update_log) if has true dialog(发现新版本 v latest, 更新内容 log) endifhas_update 在 JSON 里是布尔值json 解析后的字符串可能是 true 而不是 true比较前最好先转小写或直接和 true 统一格式。这里有个工程判断如果版本差距过大或涉及紧急修复可以让 PHP 端返回 force_update1IAPP 端弹窗后不给“以后再说”按钮强制去下载页更新。3.4 编码、超时与回调错误处理IAPP 端联调接口时出现最多的是三类问题JSON 解析失败、请求超时、字段类型对不上。前两类往往不在代码逻辑而在环境配置。下面这张表是排错时首先要过一遍的检查项。现象常见原因处理方式IAPP 收到空返回PHP 语法错误或接口直接退出浏览器直接访问接口先看裸输出JSON 解析失败PHP 文件带 BOM或警告信息混入输出文件存成 UTF-8 无 BOM关闭 display_errors请求长时间不返回接口里执行了慢查询SQL 走主键和索引日志表定期清理更新弹窗不出现version_code 比较方向写反把两个版本号都打日志对比后再判PHP 错误处理在这个链路里有一个建议生产环境把 error_log 打开display_errors 关掉避免 PHP Warning 直接混进 JSON 返回内容里。接口地址放在 IAPP 代码里会随 APK 泄露如果要防抓包篡改可以在请求头带一个自定义 token服务端校验不过就拒绝返回数据。4. 把托管服务完整跑通下载直链、下载统计与分发页浏览器适配4.1 download.php不要直接拼用户传的路径下载接口是整个系统里风险最高的文件稍不注意就会演变成任意文件下载。最安全的写法是客户端只传版本记录的 id服务端根据 id 查出 apk_path再用 basename 去掉路径中的目录部分从白名单目录里拼出最终文件地址。?php // download.php?id1 $id isset($_GET[id]) ? (int)$_GET[id] : 0; if ($id 0) { http_response_code(400); exit(bad request); } $pdo new PDO(sqlite: . __DIR__ . /../data/app.db); $stmt $pdo-prepare( SELECT apk_path, version_name, md5 FROM app_version WHERE id ? LIMIT 1 ); $stmt-execute([$id]); $row $stmt-fetch(PDO::FETCH_ASSOC); if (!$row) { http_response_code(404); exit(not found); } $file __DIR__ . /../upload/ . basename($row[apk_path]); if (!is_file($file)) { http_response_code(404); exit(file missing); } header(Content-Type: application/vnd.android.package-archive); header(Content-Length: . filesize($file)); header(Content-Disposition: attachment; filenameapp.apk); readfile($file);代码里的三层防线值得说清楚第一层是 (int) 强转让路径注入无从下手第二层是 basename即使数据库里被写入带目录的相对路径也只会拼出文件名第三层是 is_file 检查文件不存在时返回 404 而不是空白页。Content-Length 尽量保留这样 Android 下载管理器能显示进度条也支持断点续传。生产环境文件名建议固定用 id.apk避免中文 version_name 直接进 header 触发异常。4.2 下载日志与 60 秒防刷统计下载量需要写日志但每次请求都写一条会导致日志表膨胀。轻量环境可以用一个临时文件做时间窗口去重同一 IP 同一小时内只落一条记录既能反映活跃设备量又不会把表冲爆。// 同一 IP 同一小时内只落一条下载日志 $ip $_SERVER[REMOTE_ADDR] ?? 0.0.0.0; $ua $_SERVER[HTTP_USER_AGENT] ?? ; $flag sys_get_temp_dir() . /dl_ . md5($ip . date(YmdH)) . .lock; if (!file_exists($flag)) { file_put_contents($flag, 1); $pdo-prepare( INSERT INTO download_log (app_id, ua, ip, create_time) VALUES (?, ?, ?, ?) )-execute([$id, $ua, $ip, time()]); }这套防刷逻辑不是防御攻击只是把明显的重复计数挡掉。下载量真正上来之后再把日志写入从同步改造成异步常见做法是先用 Redis 列表接收下载事件写个 PHP 队列脚本定时消费落库避免 download.php 在响应下载的同时还要处理磁盘写操作。实时观察日志可以挂在 Nginx 的 access_log 上也可以直接 tail 应用日志文件。4.3 分发页适配二维码生成与 UA 判断APP 托管一般都会有对应的网页分发页用户扫码下载 APK。这个页面的核心逻辑是PC 浏览器显示二维码安卓手机浏览器显示下载按钮。原因是部分移动端内置浏览器对 APK 直链的跳转限制较多常见做法是用白名单页面做中转提示。$ua $_SERVER[HTTP_USER_AGENT] ?? ; $isAndroid stripos($ua, Android) ! false; if ($isAndroid) { echo a classbtn hrefdownload.php?id1下载 APK/a; } else { // PC 端渲染二维码容器 echo div idqrcode/div; }二维码可以用 PHP 端生成图片也可以由前端 JS 绘制。服务端生成的好处是无需加载外部 JS 库加载进来直接就是一张 PNG。常见做法是引入 phpqrcode 单文件类库require_once __DIR__ . /lib/phpqrcode.php; $url http://你的域名/download.php?id1; QRcode::png($url, false, L, 6, 2);QRcode::png 的参数依次是内容、输出文件、容错级别、格子大小、白边宽度。这里容错级别用 L 就够二维码内容只是短链接L 级别生成的图片更小移动网络下加载更快。如果分发页需要异步读取接口数据会遇到浏览器跨域限制这时要么在 PHP 端做代理转发要么用 jsonp 方式返回IAPP 客户端本身没有同源限制这条经验只在网页分发页适用。4.4 上线前必改的配置项清单老源码最容易延续默认配置下面这份清单是我每次部署这类 PHP 工程都会逐项核对的一栏没过就先用内网环境压着别急着对外。配置项建议值/做法说明后台访问路径把 /admin/ 改名减少被批量扫描命中的概率默认口令首次登录强制改掉老源码常见 admin/admin数据库路径放到 web 根目录之外防止 .db 被直接下载API 鉴权增加 token 参数IAPP 端把 token 编译进脚本下载目录防解析Nginx deny 规则或 .htaccess防上传漏洞变成 getshell其中数据库外置和下载目录防解析两条是底线。很多源码建站者喜欢把整个工程放在同一个目录里图省事一旦 upload 目录解析了 PHP后台口令再弱等于把服务器钥匙挂在门口。5. 托管上线后用 curl 验证全链路再决定要不要上增量更新5.1 两条 curl 把接口和文件都验一遍服务端部署完不要急着把 APK 发给测试同事先用两条 curl 命令把接口和下载文件验证到位。第一条看版本接口的 JSON 结构和字段类型第二条直接下载文件并检查 HTTP 状态码与体积。curl -s http://你的域名/api/check_version.php?app_id1version_code1 \ | python3 -m json.tool curl -s -o /dev/null -w HTTP %{http_code}大小 %{size_download} 字节\n \ http://你的域名/download.php?id1第一条命令的返回里应能看到 has_update、latest_version、apk_url 等完整字段。第二条命令返回的 HTTP 200 和 size_download 必须大于 0如果 size_download 等于 0多半是 readfile 之前已经输出了错误内容把 header 输出之前的调试echo全部清掉。建议再把 apk_url 拷贝进浏览器下载一次确认下载下来的文件能正常安装。5.2 MD5 与全量更新优先APK 在传输中可能被截断或损坏服务端需要提前把每个 APK 的 MD5 算好存库IAPP 端下载完成后调用本地的 MD5 计算函数比对不一致就提示重新下载。md5sum upload/your_app.apk把输出值填进 app_version 表的 md5 字段。对个人托管项目来说全量更新永远是最可靠的方案APK 几十 MB 的文件在常规带宽下完全可接受。增量更新的收益随着包体增大才明显小项目上增量反而增加出错面。5.3 增量更新思路从 bsdiff 到 IAPP 合并如果将来包体超过 100MB 且用户量大可以按 bsdiff 的思路做差分下发服务端保留上一版本 APK上传新包时生成差分包客户端下载后与本地旧包合并。bsdiff old.apk new.apk patch.bin # 服务端生成差分包 bspatch old.apk output.apk patch.bin # 客户端合并新包这个方案的工程成本主要在 IAPP 端需要 native 层支持 bspatch 逻辑以及一套版本回滚机制合并失败时不能把用户本地包弄坏。多数托管场景下先保证全量更新 强制更新开关 MD5 校验这三板斧已经能覆盖 95% 的发布需求。先把全量更新脚本跑顺等每天下载量真正上来再设计差分包也不迟。本文还有配套的精品资源点击获取