ARTICLE DETAIL

资讯详情

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

CKEditor图片上传在国产数据库下的适配与PHP改造实战

CKEditor图片上传在国产数据库下的适配与PHP改造实战 在信创环境里做系统适配最容易被低估的从来不是核心业务功能而是你下意识觉得“和数据没关系”的边缘功能。我这次碰上的就是客户的富文本编辑器是经典款 CKEditor 4后端 PHP 7.4 Nginx数据库从 MySQL 换成了国产数据库。业务系统里各种增删改查都调得差不多了最后卡住验收的居然是编辑器里传图片传不上去。更魔幻的是文件本身已经挪进上传目录了问题出在“上传成功后往附件表插一条记录”这一步。很多朋友做信创适配时习惯性只盯着复杂 SQL 有没有兼容却容易忽略图片上传这类带有完整链路的场景。今天就拿“CKEditor 图片上传 PHP 国产数据库”这个组合把兼容改造的思路和踩过的坑一起捋一遍。如果你的项目也在做国产数据库适配这篇内容应该能帮你少走弯路。下面我都假设你用的是 CKEditor 44.17 左右的常用版本同时会带一下 CKEditor 5 的差异后端 PHP 版本以 7.x/8.x 为主。1. 一张图片从点击上传到显示出来数据库在哪个环节出场一次看起来平平无奇的编辑器传图实际上是一条五段式链路。我先把这五个环节摆出来因为只有清楚链路你才知道兼容问题会出在哪个环节。用户在编辑器里选中图片前端把文件以字段名uploadPOST 到配置的上传处理地址。PHP 脚本在$_FILES[upload]里拿到文件校验类型、大小、扩展名。脚本调用move_uploaded_file()把临时文件挪到 uploads 目录生成访问 URL。脚本把文件名、路径、大小、类型、关联文章 ID 等元数据INSERT 进附件表。接口返回固定格式的 JSON前端拿到 URL 后把图片回显到编辑区域。数据库只出现在第 4 步但恰恰是这个环节断了整条链路。后面你会看到文件系统一切正常、上传目录权限也没问题报错却直接从数据库层冒出来。这是整个适配过程里最反直觉的地方也是我建议你重点盯的位置。1.1 CKEditor 4 / 5 对上传接口的硬性要求CKEditor 4 最常见的是config.uploadUrl /upload.php前端会把文件用upload字段名提交过来。上传成功后接口需要返回这样的 JSON{ uploaded: 1, url: http://domain/uploads/2024-05-20/abc.jpg, fileName: abc.jpg }uploaded这个键必须是个数字 1不少接口开发在这里栽过跟头写成了{success: true}或者{uploaded: true}前端一概不认。如果项目还在用更老的filebrowserUploadUrl配置返回格式会要求走window.parent.CKEDITOR.tools.callFunction(funcNum, url, message)那套回调。CKEditor 5 换成 UploadAdapter 后通常也是 POST 文件后收{ url: ... }这类 JSON。所以无论哪一代接口第一职责都是回标准 JSON这一步和数据库无关但一定要先单独调通。1.2 附件表里究竟存了什么东西如果“传图回显”不需要历史记录确实可以不写库。但只要系统稍微正经一点素材管理、文章配图统计、后台搜索、审计追踪几乎都要求把附件元数据落库。我做过的项目里附件表大致是这样字段说明id附件主键article_id关联的文章或业务单据 IDfile_name原始文件名素材列表展示用file_path相对网站根目录的访问路径file_size文件字节数统计清理用file_ext扩展名分类筛选用create_time上传时间这张表看起来人畜无害可它恰恰是“最 MySQL 化”的孵化器自增列、反引号、NOW()、注释写在列定义里各种 MySQL 习惯都可能在它身上出现。迁移到国产库时这些细节会成为第一批引爆点。1.3 为什么这条链路最容易在迁移时坏掉我见过太多团队迁移时只盯着复杂的业务查询按重要度排优先级一张“传个图用的表”根本不值得排期。等到真机测试时一张图丢进去才发现附件表的 DDL 在国产库上建都建不出来后续 INSERT 更是全线报错。图片上传功能在迁移清单里被划进“简单功能”区但它背后拖着文件系统、SQL 方言、字符集、JSON 格式、跨域五件事属于麻雀虽小五脏俱全的典型场景。把它单拎出来做专项测试是这次适配里回报最高的决定。2. MySQL 生态长大的 PHP 代码换到国产库为什么这么痛很多人第一反应是“改一下数据库连接不就行了”实际远没这么简单。这里必须先泼一盆冷水国产数据库不是“一个”产品达梦 DM8 走 Oracle 风格语法人大金仓 KingbaseES 核心是 PostgreSQL神通数据库接近 OracleOpenGauss 系又是另一种脾气。你在心里默认“国产库MySQL”从驱动开始就会处处吃瘪。2.1 驱动层就先死一遍传统 PHP 项目大量使用 mysqli甚至老代码里还有mysql_xxx函数。mysqli 只认 MySQL 协议达梦、金仓在协议层都不认想继续用 mysqli 连国产库基本没戏。业界统一的换法是收敛到 PDO它是一层数据访问抽象同一套 API 换底层驱动即可。MySQLmysql:host127.0.0.1;dbnameblog;charsetutf8mb4达梦装好官方 pdo_dm / php_dm 扩展后DSN 形如dm://用户名:密码127.0.0.1:5236?charsetUTF-8金仓兼容 PG 协议装 pdo_pgsqlDSN 形如pgsql:host...;port54321;dbname...如果你在信创服务器上还习惯用宝塔面板或小皮面板这类一键环境要多留个心眼。它们对 MySQL 支持很好但达梦扩展不一定有现成的安装按钮。尤其麒麟 V10、统信 UOS 这类系统常常不能直接访问公网软件源离线编译 pdo_dm 需要的依赖得提前备好。这一步我强烈建议在项目排期里单独列一项别看它不起眼卡起人来一卡就是两三天。2.2 方言雷区清单我把迁移中实际会踩的 SQL 差异整理成一张速查表适合直接贴进项目文档。功能点MySQL 习惯写法达梦 DM8人大金仓 / PG统一建议自增主键AUTO_INCREMENTIDENTITY(1,1) 或序列SERIAL / IDENTITY建表脚本按库分开写当前时间NOW()SYSDATENOW()统一用 CURRENT_TIMESTAMP字段注释COMMENT xxCOMMENT ON COLUMNCOMMENT ON COLUMN不在建表 SQL 里强行统一标识符引用反引号双引号双引号一律不加引号分页LIMIT a, bLIMIT b OFFSET aLIMIT b OFFSET a统一 LIMIT n OFFSET m插入冲突更新ON DUPLICATE KEY UPDATEMERGE INTOON CONFLICT ... DO UPDATE先查后插或用 MERGE这里最容易被忽视的是自增主键。MySQL 的AUTO_INCREMENT写起来最舒服到达梦直接建表报错得换成IDENTITY(1,1)或序列到金仓可以用SERIAL。想同一套脚本通吃多个国产库基本不可能。所以我一直建议生产项目放弃“一套脚本跑所有库”的执念改成数据访问层统一用 PDO 和标准 SQL建表脚本按目标库出两版。这比在 SQL 里绕来绕去省事得多。2.3 字符集和客户端连接也是个暗坑附件表里几乎必有文件名和路径字段一旦中文文件名出现字符集问题就来了。MySQL 里你习惯 utf8mb4但国产库这边达梦实例初始化时就要选字符集常见是 UTF-8 或 GB18030。如果连接串没带字符集参数服务端可能把客户端传进来的 UTF-8 字符串按 GBK 解读入库后就是乱码回显出来 URL 直接 404。治本的办法有两层。第一层连接串和数据库两侧都显式指定 UTF-8第二层上传后的存储路径在服务端用uniqid()或时间戳重命名确保 file_path 里只出现 ASCII 字符原始中文名单独存在 file_name 字段里。我在实际项目里始终坚持“文件路径只允许 ASCII”这条纪律这让跨库字符集问题直接少了一大半。顺带说一句也别把附件信息序列化拼成一个字段塞进库里序列化数据一旦撞上字符集转换排查起来比乱码还痛苦。3. 让图片上传在国产库上丝滑落地一次到位的 PHP 改造方案不复杂但每一层都要做对。下面这套东西我在达梦和金仓上都跑通过同一个上传接口核心套路就是“PDO 做连接层、薄适配做方言层、标准 SQL 做数据层”。3.1 第一步连接层全部切 PDO无论原来用 mysqli 还是手写mysql_query建议全部收敛到 PDO好处至少三个。第一驱动可替换换库不换 API第二预处理默认处理了参数绑定顺便挡掉注入风险第三PDOException的错误信息比 mysqli 的报错直观得多排障能省很多时间。?php // db-config.php $dsnMap [ mysql mysql:host127.0.0.1;dbnameblog;charsetutf8mb4, dm dm://app_user:your_pwd127.0.0.1:5236?charsetUTF-8, pgsql pgsql:host127.0.0.1;port54321;dbnameblog;client_encodingUTF8, ]; $dbType getenv(DB_TYPE) ?: dm; $pdo new PDO($dsnMap[$dbType], app_user, your_pwd, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, ]);注意我在连接串里把编码写死了这就是上一节说的“显式指定字符集”。达梦官方扩展的 DSN 具体写法在不同版本会有差异以厂商文档为准思路是一致的。3.2 第二步一个很薄的方言适配层业务 SQL 大部分都是标准 SQL真正需要方言处理的就那几样取自增 ID、分页写法、个别函数。做一个很薄的适配类就够了不要引入太重的 ORM 重写整站。?php class DbHelper { private PDO $pdo; private string $driver; public function __construct(PDO $pdo) { $this-pdo $pdo; $this-driver $pdo-getAttribute(PDO::ATTR_DRIVER_NAME); } public function lastInsertId(string $sequence ): string { if ($this-driver mysql || $this-driver dm) { return $this-pdo-lastInsertId(); } if ($this-driver pgsql) { return $this-pdo-lastInsertId($sequence); } return $this-pdo-lastInsertId(); } public function limit(int $limit, int $offset): string { if ($this-driver mysql) { return LIMIT {$offset},{$limit}; } return LIMIT {$limit} OFFSET {$offset}; } }这里有个细节要特别提醒达梦如果用序列做主键lastInsertId在某些版本下的行为并不稳定保险方案是插入后查序列的CURRVAL或建表时直接用 IDENTITY 列再验证。金仓走 PG 内核lastInsertId(序列名)一般可用。在项目交付前把“上传一张图回读自增 ID”单独写进测试用例别到真机联调时才暴露。3.3 第三步完整的上传接口代码下面是兼容改造后最核心的接口代码。注意看 SQL 部分没有反引号、没有 MySQL 专属函数、表名字段名保持统一风格、插入用预处理绑定参数。?php // upload.php $file $_FILES[upload] ?? null; if (!$file || $file[error] ! UPLOAD_ERR_OK) { echo json_encode([uploaded 0, error [message 上传参数错误]]); exit; } $allowed [jpg, jpeg, png, gif, webp]; $ext strtolower(pathinfo($file[name], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed)) { echo json_encode([uploaded 0, error [message 仅支持图片格式]]); exit; } $dayDir date(Ymd); $dir __DIR__ . /uploads/ . $dayDir; if (!is_dir($dir)) { mkdir($dir, 0755, true); } $fileName uniqid(pic_, true) . . . $ext; $targetPath /uploads/ . $dayDir . / . $fileName; if (!move_uploaded_file($file[tmp_name], __DIR__ . $targetPath)) { echo json_encode([uploaded 0, error [message 文件保存失败]]); exit; } $insertSql INSERT INTO ATTACHMENT (ARTICLE_ID, FILE_NAME, FILE_PATH, FILE_SIZE, FILE_EXT, CREATE_TIME) VALUES (:article_id, :file_name, :file_path, :file_size, :file_ext, CURRENT_TIMESTAMP); $stmt $pdo-prepare($insertSql); $stmt-execute([ :article_id $_POST[article_id] ?? 0, :file_name $file[name], :file_path $targetPath, :file_size $file[size], :file_ext $ext, ]); $attachId $dbHelper-lastInsertId(); echo json_encode([ uploaded 1, url $targetPath, fileName $file[name], attachId $attachId, ]);如果项目还在用filebrowserUploadUrl配置这条 JSON 会被前端拒绝需要按老回调格式返回一个scriptwindow.parent.CKEDITOR.tools.callFunction(funcNum, url);/script。我建议直接把两种返回格式做成参数切换接口按请求参数自动选择省得后面换主题时再改一遍。如果你用的是 ThinkPHP 或 Laravel迁移思路完全一样只是把 PDO 替成框架的 Db Facade 或 Eloquent。重点检查框架生成的 SQL 是否含有 MySQL 专属写法比如 ThinkPHP 提供的fetchSql()可以打印 SQLLaravel 可以开 query log把这些日志在国产库上跑一遍比对着文档逐个函数找高效多了。框架的迁移插件在国产库支持上一般不如 MySQL 完善建表仍建议手工维护 SQL 脚本。3.4 第四步CKEditor 配置和跨域处理PHP 端改好后前端配置也要对得上。CKEditor 4 这样配CKEDITOR.replace(editor1, { uploadUrl: /upload.php });如果页面和上传接口不同源CKEditor 的 XHR 会先发送 OPTIONS 预检请求PHP 接口必须处理跨域头header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: POST, GET, OPTIONS); header(Access-Control-Allow-Headers: Content-Type); if ($_SERVER[REQUEST_METHOD] OPTIONS) { exit; }很多朋友调了半天文件其实已经传到服务器了编辑器里就是不显示图十有八九是跨域预检没过。浏览器控制台会直接给出 CORS 报错按提示加头就行这算是最容易自查的一类问题了。4. 达梦 DM8 实战三个撞出来的报错与排查过程纸上谈兵没用我在达梦 8 上实操时真实撞过三个报错每个都很有代表性。下面按当时的时间线讲金仓和 OpenGauss 系可以举一反三。4.1 建表时 AUTO_INCREMENT 直接把 SQL 打回最初建附件表的脚本是从 MySQL 老项目里拷来的执行时在达梦上报错提示不支持AUTO_INCREMENT关键字。解决办法就是改用 IDENTITY 列CREATE TABLE ATTACHMENT ( ID BIGINT IDENTITY(1,1) PRIMARY KEY, ARTICLE_ID BIGINT, FILE_NAME VARCHAR(255), FILE_PATH VARCHAR(500), FILE_SIZE BIGINT, FILE_EXT VARCHAR(10), CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP );如果项目里习惯用序列也可以建一个序列再手工取 NEXTVAL。两者都能跑但我个人更倾向 IDENTITY因为少维护一个序列对象PHP 代码里也不用显式管理序列值。4.2 NOW() 找不到字段列表里冒出来的“函数”插入记录时习惯性写了NOW()达梦报错说找不到这个函数。这其实反映了两个数据库对内置函数映射的差异。达梦对 MySQL 函数的兼容不是默认全开的有些兼容开关没打开或者干脆没有这个函数映射。统一改成CURRENT_TIMESTAMP后同一句 SQL 在达梦和金仓都能跑。以后写 SQL 时时间相关优先选标准 SQL 里的CURRENT_TIMESTAMP、CURRENT_DATE少碰NOW()、DATE_FORMAT()这类 MySQL 专属函数。这类函数缺失问题除了改 SQL还可以查数据库有没有提供 MySQL 兼容开关。达梦有些版本提供COMPATIBLE_MODE等参数打开后能支持一部分 MySQL 函数和语法但我不建议依赖它。一是开关常常是全局的会影响其他模块的查询计划二是同一套代码将来如果还要跑到声明不兼容的库上就还得再改一遍。最稳的做法仍然是把 SQL 本身按标准写法改干净。4.3 大小写把“字段找不到”演绎得淋漓尽致有次报错是“列名无效”。表在达梦里叫BLOG_ATTACHMENT字段叫IDSQL 写的也是ID按说没问题但它就是报错。查下来发现建表时我给字段名加了双引号比如id达梦建出来的实际列名是小写id。查询不加引号写ID时达梦会把标识符转成大写去库里找找到的是大写字段集合里的ID和实际存的小写id对不上。这个教训总结成三条纪律写进任何信创项目都适用建表脚本里表名、字段名一律不加引号让数据库用默认大小写风格。应用层 SQL 里的表名、字段名和建表脚本保持同一大小写风格。规划名时尽量统一用小写PG 系库天然收纳小写达梦不加引号时会转大写但只要“建表和查询都不加引号且同风格”大小写转换仍然能正确匹配。最怕的就是一半加引号一半不加。4.4 排查这类问题的最快路径遇到和数据库相关的上传失败别在 PHP 里瞎猜先把 PDO 的 ERRMODE 设为 EXCEPTION在 catch 里把当前 SQL 原文和参数数组都打出来try { $stmt-execute($params); } catch (PDOException $e) { error_log($e-getMessage()); error_log($insertSql); error_log(json_encode($params, JSON_UNESCAPED_UNICODE)); echo json_encode([uploaded 0, error [message 保存失败]]); exit; }然后去达梦的数据目录通常在 $DM_HOME/log 下拉出最近一条报错的完整 SQL对照上面的方言速查表看一眼10 分钟内基本能定位。金仓同理看数据库日志的 SQLSTATE 状态码。这套“看报错码、拉原文、对函数、查大小写、验编码”的五段式排查是我每次做数据库适配时的固定动作。5. 上线前照着过一遍信创环境图片上传自检清单最后给一份我在信创项目上用的自检清单覆盖从驱动到回显的全链路。5.1 全链路检查项一览检查项具体操作常见问题PHP 扩展php -m 看 pdo_dm / pdo_pgsql 是否加载离线环境漏装依赖连接串显式指定字符集和端口默认端口冲突连错实例建表脚本按目标库用 IDENTITY / SERIALAUTO_INCREMENT 报错时间函数全局搜索 NOW()、DATE_FORMAT函数不存在标识符大小写建表和查询保持同一风格双引号列名导致列名无效返回 JSONuploaded 必须是数字 1前端不识别跨域头前后端不同源时处理 OPTIONS浏览器 CORS 报错中文文件名存储路径用 uniqid 重命名路径乱码、404自增 ID 回读上传后打印 attachId 验证后续关联失败这张表不是形式主义每一项都对应我踩过的真实问题。不建议上线前一天才做最好在功能开发阶段就按它过一轮。5.2 部署层沉淀下来几个习惯第一建表脚本、初始化数据脚本一定要和代码分开不要靠程序启动时自动建表。信创环境数据库账号权限往往收得紧自动建表一遇到权限不足整个项目都起不来。我一般用 migrations 目录放明文 SQL部署时按目标库执行对应文件。第二数据库连接和上传目录都走配置文件或环境变量。DB_TYPEdm、UPLOAD_DIR/data/www/static/uploads这种宁可写得冗余也不要写在代码里让人猜。信创项目交付后换库、换目录的情况太常见了。第三接口联调必须用真实浏览器操作编辑器传图不要用 Postman 对接口假装调通。CKEditor 对返回 JSON 有严格约定Postman 看不到编辑器侧的真实解析行为很多“看起来通了”的隐患就藏在这一步。另外一点容易被忽略很多信创项目对数据库账号权限有明确限制业务账号可能连建序列、建表权限都没有。DDL 要交给 DBA 统一执行程序只拿 INSERT、SELECT、UPDATE 这类权限。如果你的上传接口还需要在文章删除时同步清理附件记录记得在检查清单里加上一条——确认业务账号对附件表有 DELETE 权限否则运行时才发现交付名单上又要多一个延期理由。5.3 PHP 8 时代的额外注意如果系统已经上了 PHP 8.x再补一句。PHP 8 里 PDO 行为更严格很多隐式类型转换会变成 TypeError旧代码容易在execute传参数时翻车尤其是把整数当成字符串、或反过来。用 PHPStorm 之类的 IDE 带静态分析跑一遍能提前揪出不少类型问题。另一个容易被忽略的点是PHP 8 对老版达梦驱动可能不友好安装扩展时务必确认用的是支持 PHP 8 的编译版本否则php -m里看起来加载了一调 PDO 就段错误这种问题最熬人。最后说点个人体会。在信创环境里做 PHP 项目适配图片上传是最被低估的“兼容性试金石”。它看着小却把文件系统、POST 参数、SQL 方言、字符集、JSON 格式、跨域全串了一遍。我见过不少团队花大功夫调通核心业务报表最后被一个编辑器传图卡住验收。建议你提前把这个功能当成独立测试项在真实国产库上用真实浏览器走一遍完整链路从点选图片到回显成功的每一跳都确认是通的再往上报进度。这个土办法比任何检查清单都好使。
返回列表