ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DayinCMS 3D模型管理系统:PHP上传、部署与Three.js预览实战

DayinCMS 3D模型管理系统:PHP上传、部署与Three.js预览实战 简介DayinCMS是一套基于PHP构建的3D模型管理系统完整源码面向PHP中高级开发者、CMS二次开发人员以及设计、建筑、游戏开发等行业中需要集中管理3D模型资源的Web技术人员。系统从模型上传、分类存储到在线检索与展示覆盖日常管理的关键环节可直接作为二次开发起点或实战学习范例。压缩包共1743个文件大小13.63MB含有371个PHP脚本、332个PNG与334个GIF图片、212个CSS样式、150个HTML页面、139个JavaScript脚本另提供SQL数据库文件、使用说明及若干配置文件目录划分清晰便于按模块阅读和部署。目前已有83人参与学习下载。分析这套源码可以帮助开发者系统掌握MVC分层架构、PDO方式操作MySQL、文件上传安全校验、模型文件格式解析、用户注册登录与权限控制、模板引擎应用等多项PHP开发技能同时也能借鉴其前后端协作方式为自建模型资源管理平台提供直接参考。1. DayinCMS 3D模型管理系统一份 PHP 源码包背后的真实需求假设今天的任务是把一仓库的 STL/OBJ/GLB 模型变成可在管理系统里筛选、预览、下载的资源库。如果仍然用「FTP 上传 群里发链接」的方式不出一个月就会出现模型找不到版本、贴图和模型分离、同事打开压缩包不知道看哪个文件等一堆问题。DayinCMS 3D模型管理系统 php版 正是这类场景的典型 PHP 实例开发源码它把模型管理切成了三个固定环节——上传入库、元数据维护、页面预览。拿到这份 zip第一步先看目录结构和数据库设计第二步是部署和 Nginx 伪静态规则第三步是前端的 3D 预览接入。这篇文章就按这三段走全程使用 PHP 7.x 兼容的写法代码可以直接抄回项目里。如果你正在评估「源码建站」或者想快速搭一个模型管理系统下面的内容比看截图更有用。2. 从 zip 拆解 DayinCMS目录结构、数据表与模型上传的最小闭环拿到DayinCMS_3D_v1.zip这类包先不要急着上传到服务器。解开压缩包第一件事是确认目录形态和入口文件这决定了你后续改代码和配伪静态的方向。2.1 源码包的目录形态与路由入口unzip -q DayinCMS-model-system.zip -d dayincms cd dayincms tree -L 2常见的目录输出是这样的dayincms ├── app │ ├── Controller │ ├── Model │ └── Service ├── config │ └── database.php ├── public │ ├── index.php │ └── upload ├── routes └── viewDayinCMS 这类 PHP 源码大多走「单一入口 路由转发」的模式Web 服务器把请求交给public/index.php再由路由表分发给app/Controller下的 PHP 类。如果你拿到包的根目录没有vendor说明框架依赖没装全先执行composer install把依赖拉下来再谈部署。在这个结构里Controller负责接收参数和返回 JSON 或模板Model负责和数据库交互Service放业务逻辑比如模型文件搬移、贴图关联检查。这种分层的好处是后面你要加一个「模型审核」状态只需要在Service层加逻辑而不需要动前端模板。2.2 模型元数据与分类表的建表 SQL模型文件本身是二进制的数据库只需要记录它的元数据。DayinCMS 的核心表格可以精简为「分类表 模型表」两张模型表设计如下CREATE TABLE dayin_model ( id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT(11) UNSIGNED NOT NULL DEFAULT 0, title VARCHAR(255) NOT NULL, file_path VARCHAR(255) NOT NULL, file_type VARCHAR(16) NOT NULL DEFAULT glb, file_size BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, preview_img VARCHAR(255) DEFAULT , material_path VARCHAR(255) DEFAULT , tags VARCHAR(255) DEFAULT , status TINYINT(1) NOT NULL DEFAULT 1, created_at INT(10) UNSIGNED NOT NULL, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段对应的逻辑说明如下file_size用BIGINT而不是INT因为单个 GLB 或带贴图的 OBJ 压缩包很容易超过 2GB 的 INT 上限。material_path单独存贴图或材质目录不要拼在file_path里。很多模型文件和贴图是分开放置的合在一起会导致编辑器加载时 404。status控制上架/下架前端列表只读status 1的记录。分类表更简单id、parent_id、name、sort_order四列就够不建议做成无限级树表因为模型管理的分类层级一般不会超过三层。2.3 PHP 模型上传的控制器代码上传是模型管理里最容易被写出问题的一环。等会讲解如何通过 DayinCMS 上传跨域 JSONP但不推荐通过 JSONP 来加速上传因为浏览器对文件上传的支持问题是存在的。文件入库先将文件移到独立目录再写数据库?php // app/Controller/ModelController.php public function upload(): void { // 有文件上传漏洞必须做白名单校验 $allowed [obj, fbx, stl, glb, gltf]; $ext strtolower(pathinfo($_FILES[file][name], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed, true)) { $this-error(模型格式不支持: . $ext); } $dateDir date(Y/m/d); $saveDir dirname(__DIR__, 2) . /public/upload/model/ . $dateDir; if (!is_dir($saveDir) !mkdir($saveDir, 0755, true)) { $this-error(目录创建失败请检查 upload 目录权限); } // 用 uniqid 重新命名避免中文名和空格带来的兼容问题 $newName date(His) . _ . substr(md5(uniqid(, true)), 0, 8) . . . $ext; if (!move_uploaded_file($_FILES[file][tmp_name], $saveDir . / . $newName)) { $this-error(文件保存失败); } $modelService new \ReflectionClass(\App\Service\ModelService); $service $modelService-newInstance(); // 写入数据库库结构对应前文的 dayin_model 表 $service-create([ title $_POST[title] ?? $newName, category_id (int)($_POST[category_id] ?? 1), file_path /upload/model/ . $dateDir . / . $newName, file_type $ext, file_size $_FILES[file][size], ]); }这里的move_uploaded_file必须配合绝对路径使用推荐把模型文件放在 Web 根目录之外例如/data/models/然后通过 Nginx alias 或控制器读文件流输出避免模型被浏览器直接下载无法控制权限。存储目录建议用日期分片而不是全部堆在一个目录。下面是一张对比表策略单目录数量备份/迁移推荐场景全部直接放 upload 下上万列表卡顿简单内部测试按日期分目录每天 500需要 rsync 增量正式环境按模型 ID 分片model/1/单目录少改动大配合 CDN内容寻址存储哈希名前缀稳定较复杂大规模如果没有明确需求按日期分目录是维护成本最低的方案。3. 用 Windows 10 的 Nginx PHP 环境或服务器部署 DayinCMS伪静态与上传参数配置模型管理系统的部署和普通 PHP 站点部署没什么两样但要注意三个点伪静态规则直接影响详情页 URLphp.ini上传参数决定能不能接收大模型上传目录的权限决定安全下限。3.1 Nginx 伪静态规则与 PHP 8 版本兼容在 Windows 10 本地调试时常见组合是Windows 10 Nginx PHPNginx 只负责转发。在正式服务器上也用同一套规则。如果路由是 ShortUrl 风格Nginx 就需要把所有非文件、非目录请求交给index.phpserver { listen 80; server_name model.example.com; root /var/www/dayincms/public; index index.php; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 模型文件缓存放这里减少 PHP 输出压力 location /upload/ { expires 7d; add_header Cache-Control public; } }要点拆开讲try_files让前端路由在找不到静态文件时自动落到 PHPSCRIPT_FILENAME必须写$document_root$fastcgi_script_name否则会出现「找不到文件」的空白页。如果 PHP 版本是 7.4把php8.1-fpm.sock换成对应版本即可。3.2 php.ini 上传参数调整表模型文件动辄几十兆上传出错时先看错误类型。如果报UPLOAD_ERR_INI_SIZE就是 php.ini 限制住了如果报413 Request Entity Too Large是 Nginx 的client_max_body_size不够。参数默认值模型站点建议说明file_uploadsOnOn保持开启upload_max_filesize2M512M允许单个文件的最大值post_max_size8M600M必须比upload_max_filesize大因为文件上传走 POST bodymax_execution_time30300大文件上传时间可能超过 30 秒memory_limit128M256M如果只是保存文件不必和上传大小保持一致post_max_size和upload_max_filesize的关系经常有人搞反post_max_size限制的是整个 POST 内容的大小它还包含表单普通字段。如果你只调大了upload_max_filesize而post_max_size不变模型上传还是会被截断。改完 php.ini 记得用以下命令验证php -i | grep upload_max_filesize3.3 上传目录要禁止解析 PHP防止 php 上传漏洞扩大化模型上传目录的主要风险是「上传一个带有 PHP 代码的文件并被 Nginx 当作 PHP 执行」。虽然前文代码已经做了扩展名白名单但 Nginx 建议再补一层保险禁止解析 upload 目录的所有 PHP 文件location ~* ^/upload/.*\.(php|php5|phtml)$ { deny all; }同时给目录一个合理的 Shell 权限校验逻辑目录 755、文件 644 就够不要使用 777也不要让 PHP-FPM 用户和 Nginx 用户混用同一个权限组。如果模型文件涉及商业付费下载建议把文件挪到 web 根目录之外由 PHP 控制器校验登录态后通过readfile()输出此时 Nginx 只暴露一个download网关地址。4. 给 DayinCMS 加 3D 预览用 Three.js 加载 GLB 模型的最小前端模型管理系统的核心价值在于「预览」。在没预览之前用户必须把模型下载到本地再用 Blender 打开DayinCMS 接入 Three.js 之后网页就能直接看模型。这里给出一个能直接嵌入后台view/model/detail.php的最小方案适合加载 GLB 或 GLTF 格式的模型。4.1 后端接口返回模型数据模型详情接口/api/model/detail?id1应该返回这样一份 JSON{ code: 0, data: { id: 12, title: 机械臂模组, glbUrl: /upload/model/2024/05/12/151230_4f2ad9c8.glb, previewImg: /upload/model/2024/05/12/preview.jpg, fileSize: 204800 } }在 DayinCMS 的 Controller 里输出 JSON 的代码可以这样写public function detail(int $id): void { $model (new \App\Model\Model())-find($id); if (!$model || $model-status ! 1) { echo json_encode([code 404, data null]); return; } // 统一走 https 相对路径避免前端页面被 http/https 混用阻断 echo json_encode([ code 0, data [ id $model-id, title $model-title, glbUrl $model-file_path, previewImg $model-preview_img, fileSize $model-file_size, ], ]); }4.2 Three.js 加载 GLB 并自动旋转下面这一段是前端模板的核心代码。使用三渲染器来载入 GLB同时加载OrbitControls实现旋转缩放div idcontainer stylewidth:100%;height:600px;/div script typeimportmap { imports: { three: https://unpkg.com/three0.160.0/build/three.module.js, three/addons/: https://unpkg.com/three0.160.0/examples/jsm/ } } /script script typemodule import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; import { GLTFLoader } from three/addons/loaders/GLTFLoader.js; const container document.getElementById(container); const scene new THREE.Scene(); scene.background new THREE.Color(0x222222); const camera new THREE.PerspectiveCamera(45, 600/600, 0.1, 1000); camera.position.set(3, 3, 5); camera.lookAt(0, 0, 0); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.clientWidth, container.clientHeight); container.appendChild(renderer.domElement); // 轨道控制支持旋转、缩放关闭平移以避免模型被拖出视野 const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.enablePan false; controls.autoRotate true; controls.autoRotateSpeed 2.0; // 基础照明 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const dirLight new THREE.DirectionalLight(0xffffff, 1.2); dirLight.position.set(5, 10, 7); scene.add(dirLight); // 加载 GLBglbUrl 来自后端接口 const loader new GLTFLoader(); loader.load(__GLB_URL__, (gltf) { scene.add(gltf.scene); const box new THREE.Box3().setFromObject(gltf.scene); const size box.getSize(new THREE.Vector3()); const center box.getCenter(new THREE.Vector3()); camera.position.set(center.x size.length() * 0.6, center.y size.length() * 0.4, center.z size.length() * 0.6); camera.lookAt(center.x, center.y, center.z); }); function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); /script代码说明要点如下OrbitControls的enableDamping让旋转带缓动效果如果不用拖拽停止时画面直接就硬停观感很差。autoRotate用于模型首次载入后自动展示适合后台预览列表减少人工拖拽。模型尺寸存在差异加载后用Box3计算包围盒动态调整相机位置而不是用固定的camera.position否则小的模型会显得特别远。4.3 预览加载失败时按顺序排查GLB 预览最常见的问题不是代码逻辑而是三个外部问题按顺序排查现象直接怀疑修复办法黑屏但有 UI 文字WebGLRenderer创建失败换显卡驱动或在浏览器chrome://gpu查看 WebGL 是否被禁用加载耗时较长后无反应文件太大且没有加载进度条为loader.load加上onProgress展示进度提示跨域或 JSON 解析失败Nginx 未开放对应目录的 CORS 头在 Nginxlocation /upload/里加add_header Access-Control-Allow-Origin *跨域这一条要特别说Three.js 加载模型走的是 XHR如果 DayinCMS 部署在www.a.com模型文件在static.b.com就必须在 CDN 上配置Access-Control-Allow-Origin否则预览永远白屏而你打开 DevTools 才能看到报错。5. DayinCMS 批量导入技巧CLI 扫描、列表缓存与 curl 冒烟验证当现有模型文件已经存放在服务器目录里再通过网页一个个上传会非常痛苦。用 PHP CLI 脚本配合定时任务做批量导入是这一类型模型管理系统最高效的落地方式。5.1 用 PHP CLI 扫描目录并批量入库?php // scripts/sync_models.php // 用法: php sync_models.php --src/data/models --batch200 $srcDir getenv(SRC_DIR) ?: ($argv[1] ?? /data/models); $batch (int)($argv[2] ?? 200); $iterator new RecursiveIteratorIterator( new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS) ); $count 0; $db new PDO(mysql:host127.0.0.1;dbnamedayincms;charsetutf8mb4, user, pass); $stmt $db-prepare(INSERT INTO dayin_model (title, file_path, file_size, file_type, status, created_at) VALUES (?, ?, ?, ?, 1, ?)); foreach ($iterator as $file) { if (!in_array($file-getExtension(), [glb, obj, fbx], true)) { continue; } $relativePath substr($file-getPathname(), strlen($srcDir)); $stmt-execute([ $file-getBasename(), /models . $relativePath, $file-getSize(), $file-getExtension(), time(), ]); $count; if ($count % $batch 0) { echo 已入库 {$count} 条\n; } } echo 扫描完成共入库 {$count} 个模型\n;脚本存在的价值是避免重复写多次LOAD DATA INFILE每次执行会扫描整个目录并把新文件追加为status1。如果目录里已经存在相同路径的文件再跑一次就会产生重复数据所以实际环境里建议加上file_path唯一索引并用INSERT ... ON DUPLICATE KEY UPDATE。5.2 列表缓存用 Redis 解决分类页大流量问题模型列表页是高频查询分类和筛选条件组合多。不建议在 SQL 层面反复聚合直接把渲染后的 HTML 片段缓存到 Redis。DayinCMS 里可以封装一个缓存方法public function categoryPage(int $catId, int $page): string { $redis new \Predis\Client(); $key sprintf(dayin:cat:%d:page:%d, $catId, $page); if ($html $redis-get($key)) { return $html; } // 渲染列表模板这里省略具体查询逻辑 $html $this-renderModelList($catId, $page); // 缓存 5 分钟模型数据短时间变化不大 $redis-setex($key, 300, $html); return $html; }这个方案适合后台列表和门户页。注意不要让缓存键无尽膨胀分类数量后运行定时任务按dayin:cat:*前缀清理。5.3 部署完成后用 curl 冒烟测试上传和详情页改完不要只依赖浏览器手动点可以直接用 curl 模拟一次上传和一次读详情curl -X POST http://model.example.com/api/model/upload \ -F title测试机械臂 \ -F category_id3 \ -F file/tmp/test_model.glb \ -w HTTP %{http_code}\n curl http://model.example.com/api/model/detail?id1 \ -H Content-Type: application/json第一条如果返回HTTP 200而不是413说明 Nginx 和 PHP 的上传限制都调对了第二条返回有效 JSON 则路由解析无问题。接下来你还可以继续测一条curl -I http://model.example.com/upload/model/2024/05/12/test.glb看看静态模型文件的过期时间是不是能命中 7 天缓存命中后状态码会显示200且带Cache-Control: public。到这里DayinCMS 从解压、建库、上传、部署、三维预览到批量导入的主干流程已经全部打通后续再加模型审核、素材标签、下载权限校验时按前面几个控制器的分层位置往里补即可。本文还有配套的精品资源点击获取
返回列表