ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ThinkPHP小额贷系统APP封装实战:PHP 8兼容与WebView落地

ThinkPHP小额贷系统APP封装实战:PHP 8兼容与WebView落地 简介ThinkPHP小额贷系统源码是一套可直接部署的借贷业务解决方案面向需要快速搭建小额分期贷款平台的开发者、创业者及金融科技学习者。系统内置大数据风控模块支持分期还款、会员体系及前期审核费用管理后台提供基础管理界面适合用于产品原型验证、业务流程演示或二次开发。压缩包共2000个文件约118.84MB其中1177个PHP业务逻辑文件承担核心功能132个JS与95个CSS构成前端交互样式另有HTML页面、PNG/JPG图片素材、SQL数据库脚本及APK封装文件便于直接安装测试或打包安卓应用。内附简单安装说明部署时导入sjk.sql数据库并修改config.php配置后台默认账号admin/admin。目前已有242人学习/下载。借助压缩包内完整目录结构和源码注释可较快梳理借贷流程、风控规则和会员权限设计思路也可作为学习ThinkPHP开发企业级应用的参考范例。1. 从“能跑”到“能上架”的 ThinkPHP 小额贷封装路径网上挂着“Thinkphp小额贷系统源码 可封装APP亲测”标签的源码包不少下载下来解压后却常常卡在同一个地方代码在作者电脑上是好的换到你服务器上就白屏页面在浏览器里能点套进 APP 里却连登录都过不去。大多数积累在“ThinkPHP 旧版本兼容 PHP 8”“接口封装不彻底”“APP 抓包失败”这三类问题上。这条标题里真正值钱的信息其实只有两件事一是这套业务本身完整借款、审核、放款、还款计划二是有人把 Web 端和 APP 端跑通过一次。你要复现的不是“某个神秘源码包”而是一条从部署、API 化改造到 WebView 壳落地的完整链路。适合正在接手类似项目的人也适合想用最低成本把 PHP 借贷系统快速做成 App 的团队。下面按我自己会做事的顺序来写。2. 定运行时ThinkPHP 3.2 在 PHP 8 下的兼容处理与部署结构2.1 先看框架版本TP3.2 的代码为什么一换 PHP 8 就崩小额贷源码因为业务逻辑密集、表结构复杂很大一部分还趴在 ThinkPHP 3.2 上。这个版本当年跑在 PHP 5.6 / 7.0 上没有任何问题但新服务器默认装的是 PHP 8.x。PHP 8 移除了一批旧函数和语法比如each()、create_function()、mysql_*函数族、花括号访问字符串下标TP3.2 的核心和业务代码里恰好到处都是这些东西。我的建议是分两步走先确认源码里的ThinkPHP目录版本再决定是给老代码打兼容补丁还是直接重构到 TP6。如果业务表超过 30 张、还涉及到队列和短信服务重构周期太长补丁更现实如果代码本来就很薄只有几张表几个页面那直接换 TP6 重写接口后面 APP 封装反而省心。2.1.1 PHP 8 兼容补丁的典型写法最常见的崩法在数据库连接和each遍历上。TP3.2 的ThinkPHP/Library/Think/Db/Driver/Mysql.class.php里用的是mysql_connectPHP 8 里这个函数已不存在。我一般加一个兼容文件在入口index.php里提前引入// compat.php - PHP 8 下为 ThinkPHP 3.2 打的兼容层 if (!function_exists(mysql_connect)) { function mysql_connect($host, $user, $pwd) { return mysqli_connect( str_replace(:port, , $host), $user, $pwd ); } } if (!function_exists(mysql_select_db)) { function mysql_select_db($dbname, $link null) { $link $link ?: mysqli_connect(...); return mysqli_select_db($link, $dbname); } } if (!function_exists(mysql_query)) { function mysql_query($sql, $link null) { return mysqli_query($link, $sql); } }这里只是把函数名映射到mysqli同名方法要注意的是mysql_fetch_array、mysql_num_rows这些结果集函数也必须一起补否则前面连接不报错、取数据时照样白屏。补丁只能解决函数缺失解决不了 SQL 写法本身在 MySQL 8 下的兼容问题比如GROUP BY严格模式、ONLY_FULL_GROUP_BY报错那是另一个排障点。参数说明$host里如果带端口mysqli_connect不认冒号写法要拆开解析上面代码里str_replace只是示意实际要按:port拆分传参。建议直接改成在数据库配置里单独写主机名和端口别偷懒。2.2 小额贷业务模块在 ThinkPHP 目录里的落位与建表约定拿到的源码解压后先不要急着配数据库而是确认业务模块。小额贷系统一般有四个端用户端 H5、管理后台、API 接口目录、定时任务目录。在 ThinkPHP 里对应的是Application/ ├── Admin/ # 管理后台审核、放款、设置 ├── Api/ # APP 接口登录、借款、还款 ├── Home/ # H5 用户端页面 ├── Cron/ # 定时任务还款提醒、逾期自动罚息 └── Common/Common/ # 公共函数与配置核心业务表一般包括借款申请表loan_apply、还款计划表repayment_plan、资金流水表fund_log、用户表member。这些表名不同项目差别不大你可以直接看Application/Api/Controller里的模型引用反推哪张表是主表。推荐先用SHOW CREATE TABLE loan_apply看一遍字段确认收款银行卡、借款期限、利率、状态这几个关键字段是否齐全。2.2.1 Nginx 伪静态与目录权限TP3.2 的 URL 模式下index.php后面要跟模块名。Nginx 下配成 PATHINFO 模式用户在 APP 里拿到的接口地址才是干净的server { listen 80; server_name loan.example.com; root /var/www/loan; index index.php; location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php/$1 last; } } location ~ \.php(/|$) { include fastcgi_params; fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }Runtime目录必须给到写权限否则 TP 的模板编译和日志会直接报错。用chmod -R 775 Runtime就能解决别再图省事chmod 777后面讲安全时会提到原因。2.3 部署自检用 curl 跑通一条完整借款流程代码部署完先不看页面用命令行验证接口链路。以下是一组常见 API 的最小验证顺序# 1. 注册 curl -X POST http://loan.example.com/api/member/register \ -d mobile13800138000password123456code1234 # 2. 登录拿 token curl -X POST http://loan.example.com/api/member/login \ -d mobile13800138000password123456 # 3. 发起借款申请 curl -X POST http://loan.example.com/api/loan/apply \ -H token: xxxxx \ -d amount5000term7bank_card_id12 # 4. 查询还款计划 curl -X GET http://loan.example.com/api/repayment/plan \ -H token: xxxxxloan_id88参数说明token 的传递方式每个源码不一样有的放在 Header 里-H有的放在参数里。你先看loan/apply控制器的入口获取了哪个变量curl 里跟着改。这一步跑通了说明数据库连接、路由、鉴权三个最基础的问题都解决了才有资格谈 APP 封装。3. 接口封装小额贷系统为 APP 改造 JSON API 的关键动作3.1 统一返回格式与公共控制器网页端能用 TP 的模板渲染直接输出 HTMLAPP 不行APP 只认 JSON。小额贷系统 APP 封装的第一件事不是写壳而是把后端所有控制器返回值改成统一格式。常见做法是加一个BaseController提供success和error两个方法。// Application/Api/Controller/BaseController.class.php namespace Api\Controller; use Think\Controller; class BaseController extends Controller { protected function success($data [], $msg ok) { $result [ code 0, msg $msg, data $data, timestamp time() ]; echo json_encode($result, JSON_UNESCAPED_UNICODE); exit; } protected function error($msg error, $code 1) { $result [ code $code, msg $msg, data null, timestamp time() ]; echo json_encode($result, JSON_UNESCAPED_UNICODE); exit; } }参数说明code为 0 代表成功非 0 是业务错误码比如 10001 参数缺失、10002 token 过期。timestamp字段看着多余但它能帮你在 APP 端排查请求是否走了缓存也能和时间戳校验的防重放逻辑配合。注意 TP3.2 里exit是必要的否则 TP 底层可能继续输出调试信息造成 JSON 后跟 HTMLAPP 解析失败。这种方法在 TP3.2 下最省事但在 PHP 8 的 TP6 里更推荐用return json($data)配合中间件。不要直接 echo框架的异常处理器会把exit前的东西吞掉。3.2 跨域与调试环境给 Api 模块放开 CORSWebView 加载 H5 页面时其实没有浏览器跨域问题——页面和后端接口都是 http 协议WebView 不做同源限制。但你开发调试时要在电脑浏览器打开 H5这时接口跨域就会被拦截。所以仍然建议在 API 入口统一放行// Application/Api/Controller/BaseController 的 _initialize 里 public function _initialize() { header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, token); if (strtoupper($_SERVER[REQUEST_METHOD]) OPTIONS) { exit; } }参数说明Access-Control-Allow-Origin生产环境别用*改成 APP 包名对应的域名列表或直接在 Nginx 层配白名单。OPTIONS预检请求必须立即返回否则前端 axios 或 fetch 会一直卡在等待响应。这里有个很容易踩的坑你的 H5 页面和生产 API 域名不一致时请求带了自定义tokenHeader被浏览器判定为复杂请求如果后端没处理 OPTIONS报错现象是接口正常但 APP 无响应。3.3 防重复放款事务、状态机与 Redis 锁小额贷系统封装成 APP 后接口被用户高频调用最容易出生产事故的不是代码逻辑而是并发操作同一笔借款单。比如用户同时点了两次“确认借款”两笔放款请求同时到达就可能给同一张银行卡放了两遍钱。我的做法是三个层面同时兜底。第一层数据库字段状态约束。放款操作写成条件更新// 只更新状态为“待放款”的记录 $updated M(loan_apply)-where([ id $loan_id, status 1 // 1待放款 ])-save([status 2, loan_at time()]); if (!$updated) { $this-error(借款单状态已变更请勿重复操作); }第二层事务包裹。生成还款计划和扣减余额必须在放款更新成功后放入同一个事务代码里用M()-startTrans()包住任一环节失败rollback。第三层Redis 锁处理接口级重复提交$lockKey loan_apply_lock_ . $member_id . _ . $loan_id; $lock S($lockKey, 1, [type redis, expire 5]); if (!$lock) { $this-error(操作太频繁请稍后再试); }参数说明锁的 key 要同时包含用户 ID 和借款单 ID只放用户 ID 会把该用户的所有借款操作串行化影响其他正常订单。expire设 5 秒是经验值短于前端页面的轮询间隔会导致正常用户的第二次请求被误杀长于 10 秒会拖慢重试。建议做成配置不同业务压测后调整。这三层顺序有讲究先 Redis 锁挡并发请求再事务保证数据一致性最后状态字段做最后一道保险。少了任何一层在极端流量下都会出数据不一致的故障。4. APP 封装实践用 WebView 壳跑通 ThinkPHP 借贷系统4.1 选壳为什么优先 WebView 而不是原生重写拿到这套 PHP 系统后封装 APP 有几种路线原生 Android 套 WebView、Cordova、Capacitor、Flutter 嵌套 WebView甚至直接用 PWA 糊一个图标快捷方式。对熟悉 PHP 的团队而言我推荐先做纯 WebView 壳。方案开发成本原生能力调用适用场景原生 WebView低通过 JS Bridge 逐个扩展纯展示型业务最快上线Cordova中插件生态丰富需要相机、推送等更多系统能力Capacitor中接近 Cordova面向新项目Web 与原生代码混编Flutter 套 WebView较高强团队已投入 FlutterPWA 添加到桌面最低几乎无法调用原生功能只想先测试效果小额贷系统的核心交互是表单填写、列表刷新、支付和拍照传身份证。前两者在 H5 里已经能做得很好拍照传证件是关键 —— 如果你不想在 H5 里用input typefile绕一圈那就需要走原生。我建议第一版直接 WebView 壳支付走 H5 跳转支付宝/微信 SDK拍照用 JS Bridge 调原生相机完全够用。4.2 最小可跑的 Android WebView 壳下面的 MainActivity 是去掉所有依赖库的最小实现新建 Android 工程后直接替换// MainActivity.java package com.example.loanshell; import android.annotation.SuppressLint; import android.app.Activity; import android.os.Bundle; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; public class MainActivity extends Activity { private WebView webView; SuppressLint(SetJavaScriptEnabled) Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); webView findViewById(R.id.web_view); WebSettings settings webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setUserAgentString(settings.getUserAgentString() LoanApp/1.0); webView.setWebViewClient(new WebViewClient() { Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(http://) || url.startsWith(https://)) { view.loadUrl(url); } return true; // 拦截非 http 协议的跳转 } }); // 接口地址从 BuildConfig 读取 webView.loadUrl(BuildConfig.API_BASE_URL); } Override public void onBackPressed() { if (webView.canGoBack()) { webView.goBack(); } else { super.onBackPressed(); } } }这里几个参数值得说明。setJavaScriptEnabled(true)是 H5 页面能跑起来的底线但同时会把 JS 注入漏洞面打开所以下文的 JS Bridge 必须在addJavascriptInterface时做白名单校验。setDomStorageEnabled(true)对应 H5 里的 localStorageThinkPHP 的 H5 模板如果用了本地缓存没这项配置会白屏但不报错特征是 Chrome 调试正常、APP 里空白。UserAgent加后缀是为了让后端接口能识别请求来自 APP方便后续做接口权限控制。关键点API_BASE_URL放在build.gradle里通过buildConfigField注入不要写死在 Java 文件里否则打测试包和正式包要改代码。常见做法是buildTypes { debug { buildConfigField String, API_BASE_URL, \http://192.168.1.8:8080\ } release { buildConfigField String, API_BASE_URL, \https://loan.example.com\ } }4.3 JS Bridge让 H5 调用原生相机处理身份证识别小额贷业务绕不开的环节是用户上传身份证和银行卡照片。如果你让 H5 自己用input file在 Android WebView 里经常触发不了系统相机。用 JS Bridge 是稳定做法给 WebView 加一个接口对象// DroidBridge.java public class DroidBridge { private Activity activity; private ValueCallbackUri filePathCallback; public DroidBridge(Activity activity) { this.activity activity; } JavascriptInterface public void takePicture(String type) { Intent intent new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 保存到临时文件拿到返回的 Uri activity.startActivityForResult(intent, REQUEST_CAMERA); } JavascriptInterface public void openAlbum(String type) { Intent intent new Intent(Intent.ACTION_PICK); intent.setType(image/*); activity.startActivityForResult(intent, REQUEST_ALBUM); } }Java 里注册webView.addJavascriptInterface(new DroidBridge(this), AndroidBridge)。H5 页面里的调用方式是// 在 H5 贷款申请页里调用原生相册 if (window.AndroidBridge) { window.AndroidBridge.openAlbum(id_card_front); } else { // 降级到 WebView 自带的 input file document.getElementById(file_input).click(); }这里有一个安全坑必须提addJavascriptInterface在 Android 4.2 以下有任意代码执行漏洞现在主流机型早就超碎了但如果你要兼容老设备需要在注入前判断版本。另一个细节是JavascriptInterface注解一定不能漏漏了会导致 JS 调用一直报 undefined。H5 拿到的返回值建议封装成 JSON 字符串不要返回原生Uri对象这种无法序列化的数据类型。5. 上线前自查小额贷系统的漏洞盘点与 APP 抓包验收5.1 按威胁列表过滤一遍 ThinkPHP 借贷系统小额贷系统业务敏感一旦出现漏洞损失不只是数据而是信任。除了框架本身的历史漏洞业务漏洞更常见。检查项验证方法通过标准越权访问他人借款单登录A账号直接改 URL 里的订单ID访问B账号数据返回无权访问或404而不是返回订单详情SQL 注入sqlmap 对loan/apply、repayment/plan接口做一轮注入测试无高危告警调试信息泄露访问/index.php?mHomecIndexaindex看报错页页面不显示 SQL、文件路径重放攻击抓包后重复提交放款接口第二次请求被拦截或业务状态不变积分配额用一个账号并发调用借款申请 10 次只生成 1 笔有效申请越权问题在老旧 TP 代码里最常见根因是控制器方法里直接用I(get.id)查询然后返回数据没校验member_id。我通常会在基类的初始化钩子里统一做一次身份解析并提供一个checkOwner方法protected function checkOwner($loanId, $memberId) { $count M(loan_apply)-where([id $loanId, member_id $memberId])-count(); if ($count 0) { $this-error(无权访问该借款单); } }参数说明这里的关键是查询条件里同时带上member_id而不是先查订单再在 PHP 里比较。数据库层的条件过滤能有效防止条件竞争。count用主键和会员 ID 查询会走索引性能不是问题。5.2 APP 抓包失败时的排查顺序APP 封装完后第一步就是抓包验证请求是否正常。你很可能遇到“APP 抓包失败”Android 7.0 及以上版本默认不信任用户安装的 CA 证书这是最常见的原因。解决方法是给 debug 包单独配置网络安全策略在res/xml/network_security_config.xml里?xml version1.0 encodingutf-8? network-security-config base-config cleartextTrafficPermittedtrue / debug-overrides trust-anchors certificates srcuser / /trust-anchors /debug-overrides /network-security-config然后在AndroidManifest.xml的 application 标签上引用这个文件并且只在 debug 构建里启用。release 包如果也放行用户证书就相当于允许用户用抓包工具看全部明文流量。抓包工具方面Windows 上用 Charles、macOS 用 Whistle 都行。连接时注意手机代理设置要和电脑在同一局域网代理端口默认 8888。如果手机走 HTTPS 代理后页面加载不出先关掉代理验证是否是证书问题再用浏览器访问http://charlesproxy.com/getssl安装证书最后才轮到查代码。顺序反了会让你在没问题的代码上浪费很多时间。最后的验收标准是把抓包工具关掉手机切到 4G 网络重新走一遍注册、登录、借款、还款计划查询全流程确认每个请求都有响应、每个界面没有白屏和设备内存溢出。做到这一步这套 ThinkPHP 小额贷系统的 Web 端和 APP 端才算真正贯通。本文还有配套的精品资源点击获取
返回列表