ARTICLE DETAIL

资讯详情

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

信创环境CKEditor图片上传失败:PHP与国产数据库兼容性实战改造

信创环境CKEditor图片上传失败:PHP与国产数据库兼容性实战改造 前阵子一个老项目迁移到信创环境OA系统里所有页面的图片突然都传不上去了。用户说得很简单“编辑器里插入图片点了没反应。”我远程上去一看问题远比想象中复杂——CKEditor前端报404PHP日志里全是数据库连接失败的堆栈后台配置的国产数据库驱动根本没加载。这个组合坑表面上是“图片上传失败”实际上是前端编辑器、PHP上传链路、国产数据库兼容性三个环节叠在一起的连锁问题。这篇文章就把整个排查和改造过程完整梳理一遍从CKEditor上传机制讲到底层SQL方言差异再到BLOB字段的读写法全部基于真实环境。做国产化适配的PHP工程师、系统集成商的项目实施人员或者正在把老系统往信创环境搬的朋友可以直接照着改。1. 先拆需求为什么“编辑器传图”在信创环境特别容易翻车1.1 一句话理解图片上传的完整链路任何网页编辑器里插入图片背后都不是一个组件能搞定的。它牵扯到至少四个参与者浏览器里的CKEditor负责发上传请求、接收返回值并把图片渲染到正文PHP接口负责接收文件流、校验格式、保存到服务器磁盘数据库负责记录文件元信息路径、大小、上传人、时间服务器目录权限与Web配置负责让上传好的图片能被浏览器访问到。在普通X86MySQL环境里这套链路闭着眼睛都能跑通。可一旦整体换成信创环境操作系统变了、数据库变了、PHP扩展重新编译了任何一个环节没对齐整条链路就断。最麻烦的是国产数据库对PHP的支持远没有MySQL那么“无脑”——驱动要不要手动装、DSN怎么拼接、函数兼容到什么程度全得自己趟。1.2 CKEditor两种上传模式行为完全不同先搞清楚CKEditor是怎么传图的。CKEditor 4和5的上传模式差异很大信创适配时不少人就是在这一步栽的跟头。CKEditor 4的经典做法是配置filebrowserUploadUrl指向一个PHP上传地址。它有两种提交方式form方式编辑器内部创建一个隐藏的iframe把文件按表单提交过去后端返回一段JSONCKEditor解析JSON里的url字段xhr方式默认走XMLHttpRequest异步上传后端同样返回JSON。CKEditor 5则彻底改版不再提供内置的filebrowserUploadUrl必须自己实现一个UploadAdapter通过loader.file拿到文件然后用fetch或XMLHttpRequest发送到后端。无论哪个版本后端PHP接口返回的JSON格式都有严格约定。CKEditor 4的标准返回是{ uploaded: 1, fileName: example.jpg, url: /uploads/example.jpg, error: { message: 可选错误信息 } }CKEditor 5的标准返回是{ uploaded: true, url: /uploads/example.jpg }这个JSON格式在信创改造中特别容易出问题。很多老项目用的是早期的CKEditor 4上传接口返回的是{success: true, path: ...}这种自定义结构普通环境里配合自定义JS还能跑但一旦换了新版编辑器组件或者前端框架格式对不上图片就永远插不进去。所以第一步不是急着改数据库而是先把前端和上传接口的“接口协议”统一掉。1.3 数据库在最末端却最容易把整个功能拖死很多人有个误区觉得图片上传嘛重点是文件和HTTP数据库只是记个日志而已。但实际排错时发现信创环境里最大的拦路虎恰恰是数据库。原因有三点PHP没有现成的国产数据库扩展得按不同的库手动编译或加载对应驱动不同国产库对PDO支持的成熟度差异很大有的库PDO驱动有bug有的库只能走ODBCSQL方言不兼容老项目里那些MySQL写法在国产库上直接报语法错误。所以下面第二部分先讲架构选型把“连库”这层地基打好再往后做上传和入库。2. 兼容性方案选型先把数据库连接这层地基打牢2.1 驱动选型PDO是唯一不出错的选择信创环境里能遇到的国产数据库常见的有达梦、人大金仓、GBase、OceanBase、GaussDB等。PHP连接这些库归纳下来只有三条路官方原生扩展比如达梦提供的pdo_dm或dm扩展复用它兼容的生态驱动比如金仓是PostgreSQL系直接加载pdo_pgsql就能连走ODBC通用驱动实在没有专用扩展时的兜底方案。我的建议是能用PDO就一律用PDO。原因很实际PDO提供了统一的prepare/execute接口未来就算从达梦换到金仓业务代码不用动只需改DSN和驱动配置。如果项目里还散落着一堆mysqli_query或pg_query信创改造时你会改到怀疑人生。具体到每种库的连接方式整理成表格方便对照数据库类型PHP驱动DSN示意备注达梦 DM8pdo_dm或官方ODBCdm://host:5236?databaseDBNAME官方提供Linux/Windows扩展需手动安装人大金仓pdo_pgsqlpgsql:hostHOST;port5432;dbnameDBNAME金仓兼容PostgreSQL协议可直接复用PG驱动GBase 8spdo_informix或pdo_odbc使用Informix DSN格式部分版本PHP没有官方驱动ODBC兜底OceanBase MySQL 模式pdo_mysqlmysql:hostHOST;port3306;dbnameDBNAME;charsetutf8mb4完全兼容MySQL协议连接层无痛GaussDB取决于内核模式分布式增强版需用openGauss驱动集中式可走ODBC兼容性坑多建议先做冒烟测试这一环节最容易踩的坑是PHP环境里根本没装对应驱动。很多信创服务器出厂时预装的PHP只带了pdo_mysql连达梦的扩展包都没编译进去。第一次连库直接抛could not find driver你会完全摸不着头脑。2.2 写一个DSN工厂把连接配置收敛到一处既然要兼容多种数据库最忌讳的就是在业务代码里到处写死DSN。我习惯的做法是写一个简单的工厂函数根据配置文件里的db_type字段动态拼接DSN。?php /** * 根据数据库类型组装PDO的DSN * 支持 dm / kingbase / gbase / oceanbase / gaussdb / mysql */ function buildDsn(array $dbConfig): string { $type strtolower($dbConfig[db_type] ?? mysql); $host $dbConfig[host] ?? 127.0.0.1; $port $dbConfig[port] ?? ; $dbname $dbConfig[dbname] ?? ; switch ($type) { case dm: // 达梦官方PDO驱动的DSN格式 return dm://{$host}:{$port}?database{$dbname}; case kingbase: // 金仓兼容PostgreSQL return pgsql:host{$host};port{$port};dbname{$dbname};options--client_encodingUTF8; case gbase: // GBase 8s走Informix兼容驱动 return informix:host{$host};service{$port};database{$dbname};serverol_gbase;protocolonsoctcp; case oceanbase: // OceanBase MySQL模式 return mysql:host{$host};port{$port};dbname{$dbname};charsetutf8mb4; case gaussdb: // GaussDB集中式视环境走ODBC或PDO return odbc:Driver{GaussDB};HOSTNAME{$host};PORT{$port};DATABASE{$dbname}; case mysql: default: return mysql:host{$host};port{$port};dbname{$dbname};charsetutf8mb4; } }这个函数的重点在于把“不同库DSN格式不同”这件事隔离在单个函数里。以后新增一种国产库只改这一个函数上传模块、日志模块、用户模块全都不用动。2.3 数据库存储策略路径入库还是BLOB入库图片元数据要不要真正存进数据库这里有两种流派文件系统存储数据库只存路径file_url、file_name这是绝大多数CMS的标准做法图片二进制直接写进BLOB字段数据库和文件一起走备份。信创场景下我强烈建议用“文件系统路径入库”的方案。原因很实在BLOB字段在国产库上的实现差异极大达梦有BLOB金仓用BYTEA或BLOBGBase的BLOB和MySQL又有区别读写代码改起来没完没了图片文件本来就要通过Web访问放磁盘上由Nginx直接服务性能比从数据库读出来再输出强一个数量级信创项目往往涉及等保备案日志审计要求高文件系统上的图片可以用对象存储或独立文件服务器统一管理路径入库反而便于审计。所以下面第三部分的完整实现采用“文件落盘 数据库记路径”的方案。对于确实需要BLOB的场景文章第四部分会单独给兼容写法。3. 核心实现完整上传链路编码实录3.1 前端对接CKEditor 4和5分别怎么配先看CKEditor 4。简单粗暴的方式是在页面初始化时指定上传URL让编辑器内部自己处理表单提交CKEDITOR.replace(editorContent, { filebrowserUploadUrl: /upload/image.php, filebrowserUploadMethod: xhr });如果存在跨域问题比如前台上传域名和接口域名不同CKEditor 4还支持jsonp方式CKEDITOR.replace(editorContent, { filebrowserUploadUrl: /upload/image.php?callbackcallback, filebrowserUploadMethod: jsonp });注意这里后端必须有对应的callback参数处理逻辑我习惯在PHP里统一判断?php $callback isset($_GET[callback]) ? preg_replace(/[^a-zA-Z0-9_]/, , $_GET[callback]) : ; // 正常业务逻辑组装$result数组 if ($callback) { header(Content-Type: application/javascript; charsetutf-8); echo $callback . ( . json_encode($result, JSON_UNESCAPED_UNICODE) . ); exit; } header(Content-Type: application/json; charsetutf-8); echo json_encode($result, JSON_UNESCAPED_UNICODE);CKEditor 5就不一样了必须在ClassicEditor.create时把自定义的UploadAdapter传进去。下面是兼容CKEditor 5的标准实现代码里采用fetch方式发送后端接口返回统一JSON。class ImageUploadAdapter { constructor(loader) { this.loader loader; } upload() { return this.loader.file.then( file new Promise((resolve, reject) { const formData new FormData(); formData.append(file, file); formData.append(action, uploadImage); fetch(/upload/image.php, { method: POST, body: formData, credentials: same-origin }) .then(response response.json()) .then(result { if (result.uploaded) { resolve({ default: result.url }); } else { reject(result.error result.error.message ? result.error.message : 上传失败); } }) .catch(error reject(网络错误: error.message)); }) ); } abort() { // 需要取消上传时可以在这里调用AbortController中断请求 } } const editor await ClassicEditor.create(document.querySelector(#editorContent), { extraPlugins: [function ImageUploadAdapterPlugin(editor) { editor.plugins.get(FileRepository).createUploadAdapter (loader) { return new ImageUploadAdapter(loader); }; }] });这段代码踩过一个比较隐蔽的坑CKEditor 5的resolve({default: result.url})default是关键字ES6的简写属性语法完全合法但如果你用Babel转译且配置不对这里会被转成default: result.url然后依然正常工作问题不大。真正容易出问题的是没判断result.uploaded后端返回错误时前端还把url塞进正文导致编辑器显示一个裂图。3.2 PHP上传接口完整实现后端的/upload/image.php是整个链路的枢纽。它要做的事情包括校验文件、生成安全文件名、保存文件、记录数据库、返回CKEditor规定的JSON。下面是我在信创项目里实际用的版本去掉了业务噪声保留了核心逻辑。?php // 统一返回格式 function uploadResponse($uploaded, $fileName , $url , $errorMsg ) { $result [ uploaded $uploaded, fileName $fileName, url $url ]; if ($errorMsg ! ) { $result[error] [message $errorMsg]; } return $result; } // 1. 检查请求和文件 if ($_SERVER[REQUEST_METHOD] ! POST || !isset($_FILES[file])) { echo json_encode(uploadResponse(0, , , 缺少上传文件), JSON_UNESCAPED_UNICODE); exit; } $file $_FILES[file]; if ($file[error] ! UPLOAD_ERR_OK) { echo json_encode(uploadResponse(0, , , 文件上传过程出错错误码: . $file[error]), JSON_UNESCAPED_UNICODE); exit; } // 2. 校验扩展名和MIME $ext strtolower(pathinfo($file[name], PATHINFO_EXTENSION)); $allowExt [jpg, jpeg, png, gif, webp, bmp]; if (!in_array($ext, $allowExt, true)) { echo json_encode(uploadResponse(0, , , 不支持的图片格式: . $ext), JSON_UNESCAPED_UNICODE); exit; } // 额外校验用getimagesize防止伪造图片 $imageInfo getimagesize($file[tmp_name]); if ($imageInfo false) { echo json_encode(uploadResponse(0, , , 文件内容不是有效图片), JSON_UNESCAPED_UNICODE); exit; } // 3. 生成存储文件名避免原始文件名带来的路径穿越和中文乱码问题 $datePath date(Ymd); $dir __DIR__ . /../uploads/ . $datePath . /; if (!is_dir($dir)) { mkdir($dir, 0755, true); } $newFileName date(YmdHis) . _ . mt_rand(10000, 99999) . . . $ext; $destFile $dir . $newFileName; if (!move_uploaded_file($file[tmp_name], $destFile)) { echo json_encode(uploadResponse(0, , , 文件保存失败请检查目录权限), JSON_UNESCAPED_UNICODE); exit; } $publicUrl /uploads/ . $datePath . / . $newFileName; // 4. 写入数据库记录下面兼容层封装见3.3 $db getPdoInstance(); $sql INSERT INTO t_upload_log (file_name, file_url, file_size, mime_type, create_time) VALUES (?, ?, ?, ?, ?); $stmt $db-prepare($sql); $stmt-execute([ $file[name], $publicUrl, $file[size], $imageInfo[mime], date(Y-m-d H:i:s) ]); // 5. 返回给CKEditor header(Content-Type: application/json; charsetutf-8); echo json_encode(uploadResponse(1, $file[name], $publicUrl), JSON_UNESCAPED_UNICODE);几个容易被忽略的细节文件名我从来不用用户原始文件名而是用“日期时间随机数扩展名”。原因有两个一是原始文件名可能包含中文、空格、特殊字符国产库的字符集设置如果不对写入时就乱码二是原始文件名可能是../../evil.php这种路径穿越格式虽然move_uploaded_file的目标目录已经写死但多一层防御没有坏处。MIME校验不能只看$_FILES[type]这个值客户端可以伪造。getimagesize()是PHP内置函数能真实解析图片文件头伪造文件在这里会被拦住。3.3 数据库访问的兼容封装getPdoInstance()这个函数是兼容层的核心。它读取配置实例化PDO并设置异常模式和字符集。这里有个信创环境的特殊处理达梦和金仓的PDO驱动对ATTR_EMULATE_PREPARES的支持不太一样我统一设置为false让驱动走服务端预处理。?php function getPdoInstance(): PDO { static $pdo null; if ($pdo instanceof PDO) { return $pdo; } $config include __DIR__ . /db_config.php; $dsn buildDsn($config); $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]; try { $pdo new PDO($dsn, $config[username], $config[password], $options); } catch (PDOException $ex) { // 记录错误日志不要直接打印DSN和密码到页面 error_log(DB CONNECT ERROR: . $ex-getMessage() . DSN TYPE: . $config[db_type]); throw new RuntimeException(数据库连接失败请查看服务端日志, 500); } // 特殊兼容处理 $dbType strtolower($config[db_type] ?? ); if ($dbType kingbase || $dbType oceanbase) { $pdo-exec(SET NAMES utf8mb4); } elseif ($dbType dm) { $pdo-exec(SET NAMES UTF8); } return $pdo; }这段代码值得注意的一点是异常处理。生产环境的信创项目数据库连接失败是家常便饭——驱动没装、端口被封、密码策略限制、数据库服务本身没启动。如果在页面直接输出$ex-getMessage()PDO异常会把DSN字符串和用户名带出来这是安全隐患。所以我在捕获异常后只记录日志页面统一返回500。3.4 上传记录的查询与分页兼容上传日志管理界面通常需要分页查询。MySQL写惯了LIMIT换成达梦或金仓就报错。这里给一个兼容的查询写法。?php function getUploadList(PDO $db, int $page, int $pageSize): array { $offset ($page - 1) * $pageSize; $dbType strtolower(getDbType()); switch ($dbType) { case dm: // 达梦支持LIMIT但部分版本要求LIMIT后必须只有一个参数 $sql SELECT id, file_name, file_url, file_size, create_time FROM t_upload_log ORDER BY id DESC LIMIT $pageSize OFFSET $offset; break; case gbase: // GBase 8s的分页语法是SKIP/FIRST $sql SELECT SKIP $offset FIRST $pageSize id, file_name, file_url, file_size, create_time FROM t_upload_log ORDER BY id DESC; break; case kingbase: case oceanbase: case mysql: default: $sql SELECT id, file_name, file_url, file_size, create_time FROM t_upload_log ORDER BY id DESC LIMIT $pageSize OFFSET $offset; break; } $stmt $db-query($sql); return $stmt-fetchAll(); }我刻意在函数内部switch了SQL方言而不是用统一的LIMIT是因为不同数据库对LIMIT的实现细节不一样。达梦的LIMIT语法和MySQL接近但GBase 8s走的是SKIP/FIRST金仓走PostgreSQL路线。如果你硬要写一条SQL兼容所有库大概率两边都跑不了。4. 国产数据库适配的硬核细节与DDL设计4.1 自增主键三种写法差异明显兼容国产库时最无语的就是建表语句。MySQL经典写法是AUTO_INCREMENT金仓和达梦都不认。下面是同一张上传日志表在三种主流国产库下的DDL对照达梦CREATE TABLE t_upload_log ( id INT IDENTITY(1,1) PRIMARY KEY, file_name VARCHAR(255), file_url VARCHAR(255), file_size INT, mime_type VARCHAR(100), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );人大金仓CREATE TABLE t_upload_log ( id SERIAL PRIMARY KEY, file_name VARCHAR(255), file_url VARCHAR(255), file_size INT, mime_type VARCHAR(100), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );GBase 8sCREATE TABLE t_upload_log ( id SERIAL PRIMARY KEY, file_name VARCHAR(255), file_url VARCHAR(255), file_size INT, mime_type VARCHAR(100), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );lastInsertId()的兼容性也要注意。PDO的lastInsertId()在MySQL库上表现正常但达梦驱动有些版本需要传入序列名才能正确返回否则返回0。稳妥的做法是插入后直接查询刚刚写入的记录或者用唯一业务字段回查?php // 如果lastInsertId()返回0使用回查方式 $stmt $db-prepare(INSERT INTO t_upload_log (file_name, file_url, file_size, mime_type, create_time) VALUES (?, ?, ?, ?, ?)); $stmt-execute([...]); $newId $db-lastInsertId(); if ((int)$newId 0) { $query $db-prepare(SELECT id FROM t_upload_log WHERE file_url ? AND create_time ? ORDER BY id DESC LIMIT 1); $query-execute([$publicUrl, date(Y-m-d H:i:s)]); $newId $query-fetchColumn(); }4.2 字段类型映射别拿MySQL思维写DDL国产数据库的字段类型和MySQL不是一一对应的。老项目迁移时最容易踩的坑是TEXT、VARCHAR长度、DATETIME这些类型。用途MySQL习惯达梦金仓GBase 8s短文本VARCHAR(255)VARCHAR(255)VARCHAR(255)VARCHAR(255)长文本TEXTCLOBTEXTTEXT/CLOB二进制BLOBBLOBBYTEABLOB日期时间DATETIMETIMESTAMPTIMESTAMPDATETIME YEAR TO SECOND布尔TINYINT(1)BITBOOLEANBOOLEAN如果你的上传日志还带一个“上传来源IP”的字段IP地址在MySQL里能用VARCHAR(15)存在达梦里VARCHAR长度定义如果超过255需要指定VARCHAR(2048)这种超过这个边界就得用CLOB了但IP根本用不着那么长。所以迁移时不要一股脑照搬重新设计一下字段是正道。4.3 字符串函数与拼接的兼容写法业务逻辑中常见的CONCAT函数MySQL支持达梦和金仓也支持但GBase 8s在某些版本里CONCAT只能接收两个参数导致老代码里一句CONCAT(a, b, c)直接报错。兼容写法有两个方向改成数据库标准的||拼接符所有主流国产库都支持单独封装字符串拼接函数根据库类型走不同SQL。我推荐统一用||这是最接近SQL标准的写法。举个例子?php // 不兼容的写法GBase可能报错 // $sql SELECT id, CONCAT(file_name, |, mime_type) AS info FROM t_upload_log; // 兼容写法 $sql SELECT id, file_name || | || mime_type AS info FROM t_upload_log;4.4 中文乱码与字符集的三层问题图片上传记录里如果包含中文文件名很容易在某一个环节变成乱码。信创环境这个问题被进一步放大因为前端页面编码必须UTF-8TCP传输也是UTF-8PHP连接数据库时如果DSN或连接后没有设置SET NAMES驱动会用数据库默认字符集达梦默认可能不是UTF-8数据库表本身的字符集如果不是UTF-8写入就变问号。三层缺一不可。我自己的项目组里吃过亏达梦数据库建的实例字符集是GBK页面和PHP都是UTF-8导致前端传进来的中文文件名在SELECT出来时全部乱码。后来统一到SQL层面SET NAMES UTF8并且把表字段都改成VARCHAR加CHARACTER SET UTF8才算彻底解决。如果用户上传的图片文件名本身就带着中文我建议不要直接存原始文件名到数据库。稳妥做法是像第三部分那样文件系统上存重命名后的ASCII文件名数据库里另存一个original_name字段记录原始名称这个字段用于展示和下载不乱码也不影响磁盘。5. 常见问题与排查实录5.1 编辑器一直转圈图片就是插不进去症状CKEditor 5点击上传按钮后一直加载动画开发者工具能看到/upload/image.php返回200但前端不渲染图片。排查思路先看返回的JSON是否符合CKEditor 5的规范。很多人后端返回的是{success: 1, data: {url: ...}}这种自定义结构CKEditor 5需要的是{uploaded: true, url: ...}识别不了自然就卡住。解决办法很简单——两种方案任选其一改后端返回结构统一成CKEditor标准在UploadAdapter的upload()里做一次数据转换把自定义结构映射成标准结构。我强烈推荐第一种因为标准结构不仅在CKEditor里能用以后换任何编辑器接口都能复用。5.2 PHP连不上达梦数据库could not find driver症状getPdoInstance()抛出PDOException: could not find driver。原因非常直白PHP环境里没有安装达梦的PDO扩展。很多信创服务器出厂时虽然装了PHP但扩展列表里只有pdo_mysql和pgsql。解决步骤分三步确认达梦数据库安装目录下的samples里有没有配套的pdo_dm.so或者从达梦官网驱动包中获取把扩展文件复制到PHP的extension_dir目录在php.ini里加上extensionpdo_dm.so重启PHP-FPM用php -m验证。第四步才是关键验证扩展加载状态。php -m的输出里如果有pdo_dm字样才算加载成功。这一步千万不能跳过因为PHP-FPM和CLI是两个独立的SAPI命令行下加载了不等于Web进程加载了。5.3 返回给前端的中文文件名乱码症状上传成功但CKEditor正文中出现的图片名称是乱码或者数据库里存的是乱码。这种问题几乎都是字符集串了。按这个顺序排查HTTP响应头是否设置charsetutf-8PHP文件本身保存的编码是不是UTF-8有的老编辑器是GBK编码保存的PHP文件会自带BOM和乱码隐患数据库连接有没有执行SET NAMES UTF8数据库实例和表字段的字符集是不是UTF-8。最诡异的一次排查经历是前三个全对数据库表也是UTF-8但达梦实例的初始化参数CHARACTER_SET是GBK导致JDBC和PHP写入时做了一次隐式转码中文直接变俩问号。这个只能在数据库初始化时改或者用UTF8参数重新建库。5.4 上传大图时PHP直接报500或内存耗尽症状上传2MB以上的图片接口返回500PHP日志显示内存耗尽或者POST Content-Length超出限制。信创环境的PHP配置文件经常被运维初始化为比较保守的值。需要检查并修改php.inifile_uploads On upload_max_filesize 20M post_max_size 25M memory_limit 128M max_file_uploads 20注意post_max_size必须大于upload_max_filesize否则文件本身没超过限制但PHP会认为请求体超限直接拒绝。这是新手最容易忽略的点。另外Nginx层也有client_max_body_size默认为1MB不改的话会先于PHP把请求拦掉。5.5 跨域上传与JSONP的兼容处理如果编辑器页面和上传接口不在同一个域名下CKEditor 4的xhr方式会被浏览器的同源策略拦掉。解决思路有三个后端接口加CORS头最简单CKEditor 4走jsonp方式需要前端配置和后端callback配合通过Nginx反代把上传接口代理到同域名下最稳妥。CORS头的写法就三行header(Access-Control-Allow-Origin: https://editor.example.com); header(Access-Control-Allow-Methods: POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type);但信创内网环境经常有多个安全设备链路加CORS头有时会被WAF策略拦掉预检请求。所以我的优先推荐是第三种Nginx把/upload/前缀代理到后端的PHP接口浏览器看到的是同源从根上规避跨域。5.6 上传接口权限与目录的坑上传后图片无法访问表现为404这个和数据库无关纯粹是目录和Web配置问题。需要检查uploads目录是否存在PHP进程通常是www或nginx用户是否有写权限目录权限不能直接chmod 777生产环境建议chown给PHP运行用户目录设置0755Nginx的root路径配置是否正确/uploads/是否映射到实际磁盘路径。有一次我排查了半小时发现是uploads目录挂载在一个独立的磁盘分区上分区满了文件写入失败但PHP没报错move_uploaded_file返回了false但被日志淹没了。所以建议每次上传失败都记录详细错误日志包括磁盘剩余空间。最后再分享一个排查技巧这套东西做完之后我最大的体会是信创环境下的PHP项目调试日志比什么都重要。普通环境下你还能用Xdebug断点走一遍信创服务器上很多调试工具用不了连扩展都是特殊编译的。所以从一开始就要把错误日志写全error_log里至少要有时间、数据库类型、SQL语句、错误码、当前用户这样出问题才能快速定位。另外一个实用技巧是准备一个独立的“环境自检脚本”放在项目的tools/目录里专门用来验证数据库连接、字符集、上传目录权限和JSON返回格式。每次部署到一个新的信创环境先跑一遍这个脚本能在5分钟内把环境问题全部暴露不用等业务方点出“图片传不了”才发现问题。?php // 自检脚本 check_environment.php echo PHP版本 \n, phpversion(), \n; echo 已加载PDO驱动 \n; print_r(PDO::getAvailableDrivers()); echo 上传限制 \n; echo upload_max_filesize:, ini_get(upload_max_filesize), \n; echo post_max_size:, ini_get(post_max_size), \n; echo 数据库连接 , \n; try { $db getPdoInstance(); echo 连接成功数据库版本: , $db-getAttribute(PDO::ATTR_SERVER_VERSION), \n; $db-exec(SET NAMES UTF8); echo 字符集设置完成\n; } catch (Exception $ex) { echo 数据库连接失败: , $ex-getMessage(), \n; }这个脚本我现在几乎每个项目都会留一份价值不亚于业务代码本身。万一哪天换了新环境、换了新库先跑脚本确认地基稳了再谈业务功能排查效率能翻一倍。
返回列表