这是我花了很多天的时间才得以真正实现的一组需求.
文章后面有完整 Demo 的 GitHub 链接.
一, 需求描述
1. 应用是基于 ThinkPHP5 开发的;
2. 服务器环境是 LNMP,PHP 版本是 7.2, 数据库是 MySQL5.6;
3. 由用户 (包括管理员) 上传的图片一类的媒体文件不能直接上传到应用目录中, 而要上传到单独的对象存储服务器上;
4. 需要使用富文本编辑器, 编辑器中需要上传的图片也都要保存到对象存储服务器;
5. 可以对已上传的图片进行删改查操作.
二, 方案选型
1. 框架: ThinkPHP 5.0.24(比较常用)
2. 编辑器: ueditor1_4_3_3-utf8-PHP(停更前的最新版本)
3. 对象存储: 七牛云(免费 10G 空间, 官方 SDK 齐全)
4. 开发环境: Windows+WAMPServer
三, 产品设计
本文要做的只是一个 demo, 其中只包含需求中说明的功能, 而其他相关的功能比如登录, 权限之类的就不在本文的研究范围内.
对以上需求和方案进行分析, 可以总结出本文 demo 需要实现的具体功能如下:
1. bucket 管理(七牛云的存储空间), 增删改查.
2. 图片管理, 上传, 增删改查, 上传时将信息保存到数据库中, 查询时从数据库读取数据并向云存储空间获取图片.
3. ueditor 演示, 插入本地图片时将图片上传到云存储中并向数据库插入记录, 插入远程图片时从数据库读取数据并向云存储获取图片, 查看远程列表时获取缩略图, 插入时需要获取大图.
四, 实现过程
说明: 本文将这个需求当作一个单独的项目来重现实现过程, 而且这个实现过程中会对与本主题无关的但在开发过程中需要用到的内容做忽略处理(比如 Composer, 比如其他前端框架), 只关注问题本身.
这个 demo 使用的前端框架 (库或插件) 主要包括 Bootstrap,jQuery,adminLte,jQuery-lazyload 等.
1. 在七牛云上准备开发者账号, 存储空间和图片样式:
这一过程在本文中大致省略, 在七牛官网上一步一步的操作即可, 操作完成后需要记下几个参数:
access_key 和 secret_key(这里暂时只记录主账号的 key, 更复杂权限操作本文不深入研究);
存储空间的名称, 本例创建两个空间分别名为 wandoubaba 和 wandoubaba_user;
每个空间分别设置各自的图片样式, 本例都用同样的样式策略(具体样式根据你的实际情况设置):
缩略图: w150.h150.cut
原图: original
原图水印图: original.water
限制宽度等比缩放: w800.water
限制高度等比缩放: h480.water
此外还要对每个存储空间分别绑定域名, 七牛云虽然会默认提供一个域名, 但是这个默认的域名只能使用 1 个月, 所以还是自己去绑定一个, 需要把每个空间对应的域名单独记录下来.
2. 创建并设计数据库:
MySQL 中创建数据库, 名为 tp-ue-qn-db:
CREATE DATABASE `tp-ue-qn-db` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
创建表 db_bucket 和 db_picture:
- SET NAMES utf8mb4;
- SET FOREIGN_KEY_CHECKS = 0;
- -- ----------------------------
- -- Table structure for db_bucket
- -- ----------------------------
- DROP TABLE IF EXISTS `db_bucket`;
- CREATE TABLE `db_bucket` (
- `bucket_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bucket 名称',
- `bucket_domain` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'bucket 对应的 domain',
- `bucket_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ''COMMENT'文字描述',
- `bucket_default` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '默认, 0 为否, 1 为是',
- `bucket_style_thumb` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '缩略图样式名',
- `bucket_style_original` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原图样式名',
- `bucket_style_water` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原图打水印样式名',
- `bucket_style_fixwidth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制宽度样式名',
- `bucket_style_fixheight` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制高度样式名',
- PRIMARY KEY (`bucket_name`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
- -- ----------------------------
- -- Table structure for db_picture
- -- ----------------------------
- DROP TABLE IF EXISTS `db_picture`;
- CREATE TABLE `db_picture` (
- `picture_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '图片唯一 ID',
- `picture_key` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '云存储文件名',
- `bucket_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '存储仓库',
- `picture_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ''COMMENT'本机文件描述名',
- `picture_description` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ''COMMENT'图片描述',
- `picture_protected` tinyint(4) NULL DEFAULT NULL COMMENT '是否保护, 0 为不保护, 1 为保护',
- `admin_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ''COMMENT'上传者管理员 ID, 后台上传时保存',
- `user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT ''COMMENT'上传者用户 ID, 用户上传时保存',
- `create_time` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间',
- `update_time` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '编辑时间',
- PRIMARY KEY (`picture_id`) USING BTREE,
- INDEX `bucket_name`(`bucket_name`) USING BTREE,
- CONSTRAINT `db_picture_ibfk_1` FOREIGN KEY (`bucket_name`) REFERENCES `db_bucket` (`bucket_name`) ON DELETE CASCADE ON UPDATE CASCADE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
- SET FOREIGN_KEY_CHECKS = 1;
其中, 在 bucket 表中直接将 bucket_name 设置为主键, 同时还设置了 thumb,original,water,fixwidth,fixheight 这五个图片样式名, 这是结合七牛云的图片规则而设置的.
3. 本地创建项目目录, 配置虚拟主机:
我在本地为项目创建目录.../tp-ue-qn-db/, 然后在命令行中进入这个目录, 执行 Composer 命令安装 thinkphp5.0.24 框架到 www 目录:
Composer create-project topthink/think=5.0.* www --prefer-dist
执行后的结果:
你的执行过程提示可能与我不同, 但是执行结果是一样的.
接下来就可以为应用建立虚拟机, 创建过程省略, 创建结果是将本地的 http://localhost.tp-ue-qn-db / 指向到本地的.../tp-ue-qn-db/www/public 目录, 创建好可以试着运行一下, 结果应该如下:
4. 引入第三方开发包:
接下来命令行进入 www 目录, 用 Composer 引入七牛云官方提供的 PHP-sdk:
Composer require qiniu/PHP-sdk
5. 引入 ueditor 插件
官网下载地址: https://ueditor.baidu.com/website/download.html
我下载的是 1.4.3.3 PHP 版本中的 UTF-8 版本, 但是我遇到了下载不成功的问题, 最后是用我的 Amazon 测试主机中使用 wget 下载成功, 然后再用 ftp 下载到我本地.
wget https://github.com/fex-team/ueditor/releases/download/v1.4.3.3/ueditor1_4_3_3-utf8-php.zip
下载后把压缩包内容解压到应该目录下面, 我的解压路径是:
.../tp-ue-qn-db/www/public/static/lib/ueditor
操作到这里, 项目目录结构大概是这样:
6. 做与本例无关的必要操作:
主要包括在 ThinkPHP 中配置数据库连接, 引入需要的前端框架(库或插件), 做一些 ThinkPHP 的视力模板等, 这些操作必要但与本例无关, 而且每个项目都不一样, 所以不做讲解.
7. 创建相关文件, 编程:
主要目录结构:
这里只展示核心代码, 完整的 demo 可以到 GitHub 中去获取.
- (1) bucket.HTML
- <div class="box">
- <div class="box-header">
- <span>
- <a href="javascript:;" onclick="modal_show_iframe('添加存储空间','{:url("index/picture/bucket_add")}',90)" class="btn btn-primary"><i class="fa fa-fw fa-plus-square"></i> 新增数据</a>
- </span>
- <span class="pull-right">共有数据:<strong>{$count}</strong> 条</span>
- </div>
- <div class="box-body" style="overflow-y: hidden;overflow-x: scroll;">
- <table class="table table-bordered table-strited table-hover text-nowrap">
- <thead>
- <tr>
- <th scope="col" colspan="8">存储空间</th>
- </tr>
- <tr>
- <th > 操作</th>
- <th > 名称</th>
- <th > 域名</th>
- <th > 描述</th>
- <th > 默认</th>
- <th > 缩略图样式</th>
- <th > 原图样式</th>
- <th > 原图水印样式</th>
- <th > 适应宽度样式</th>
- <th > 适应高度样式</th>
- </tr>
- </thead>
- <tbody>
- {volist name='list' id='vo'}
- <tr title="{$vo.bucket_description}">
- <td class="td-manage">
- <a title="编辑" href="javascript:;" onclick="modal_show_iframe('编辑存储空间','{:url("index/picture/bucket_edit",["name"=>$vo.bucket_name])}','')"><i class="fa fa-fw fa-pencil-square-o"></i></a>
- <a title="删除" href="javascript:;" onclick="ajax_post_confirm('{:url("index/picture/do_bucket_delete")}',{name:'{$vo.bucket_name}'},'{$vo.bucket_name}','删除');"><i class="fa fa-fw fa-trash-o"></i></a>
- </td>
- <td><span class="name">{$vo.bucket_name}</span></td>
- <td>{$vo.bucket_domain}</td>
- <td>{$vo.bucket_description}</td>
- <td>{$vo.bucket_default}</td>
- <td>{$vo.bucket_style_thumb}</td>
- <td>{$vo.bucket_style_original}</td>
- <td>{$vo.bucket_style_water}</td>
- <td>{$vo.bucket_style_fixwidth}</td>
- <td>{$vo.bucket_style_fixheight}</td>
- </tr>
- {/volist}
- </tbody>
- </table>
- </div>
- <div class="box-footer">
- <div class="text-warning text-center">在电脑上操作会更舒服一些.</div>
- </div>
- </div>
- (2) bucket_add.HTML
- <form method="post" class="form-horizontal">
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 空间名称:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="空间名称, 与云上的 bucket 一致" id="bucket_name" name="bucket_name" rangelength="[1,50]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 空间域名:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="空间域名, http://.../ 形式, 以 / 结尾" id="bucket_domain" name="bucket_domain" rangelength="[4,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3">描述:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="文字描述" id="bucket_description" name="bucket_description" maxlength="100">
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3">默认空间:</label>
- <div class="col-sm-9">
- <input type="checkbox" id="bucket_default" name="bucket_default" />
勾选为默认, 只可以有 1 个默认空间
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 缩略图样式:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="缩略图样式名" id="bucket_style_thumb" name="bucket_style_thumb" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 原图样式:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="原图样式名" id="bucket_style_original" name="bucket_style_original" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 原图水印样式:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="原图加水印样式名" id="bucket_style_water" name="bucket_style_water" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 适应宽度样式:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="适应宽度样式名" id="bucket_style_fixwidth" name="bucket_style_fixwidth" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 适应高度样式:</label>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="适应高度样式名" id="bucket_style_fixheight" name="bucket_style_fixheight" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <div class="col-sm-9 col-sm-offset-3">
- <button type="submit" class="btn btn-success disabled" disabled="true">提交数据</button>
- </div>
- </div>
- </form>
对表单进行前端验证时不要忘了引入 jQuery-validation 插件.
- $(function() {
- // 初始化 checkbox 的 icheck 样式
- $('input[type="checkbox"],input[type="radio"]').iCheck({
- checkboxClass: 'icheckbox_minimal-blue',
- radioClass : 'iradio_minimal-blue'
- })
- // 只有当表单中有数据变化时, 提交按钮才可用
- $("form").children().bind('input propertychange ifChecked ifUnchecked',function() {
- $(":submit").removeClass('disabled').removeAttr('disabled');
- });
- $("form").validate({
- rules: {
- bucket_domain: {
- url: true
- }
- },
- submitHandler: function(form) {
- // 当验证通过时执行 Ajax 提交
- ajax_post("{:url('index/picture/do_bucket_add')}",$("form").serialize());
- }
- });
- });
- (3) bucket_edit.HTML
- <form method="post" class="form-horizontal">
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 空间名称(只读):</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_name}" type="text" class="form-control" id="bucket_name" name="bucket_name" readonly="true">
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 空间域名:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_domain}" type="text" class="form-control" placeholder="空间域名, http://.../ 形式, 以 / 结尾" id="bucket_domain" name="bucket_domain" rangelength="[4,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3">描述:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_description}" type="text" class="form-control" placeholder="文字描述" id="bucket_description" name="bucket_description" maxlength="100">
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3">默认空间:</label>
- <div class="col-sm-9">
- <input {eq name="bucket.bucket_default" value="1"} checked="true" {/eq} type="checkbox" id="bucket_default" name="bucket_default" />
勾选为默认, 只可以有 1 个默认空间
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 缩略图样式:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_style_thumb}" type="text" class="form-control" placeholder="缩略图样式名" id="bucket_style_thumb" name="bucket_style_thumb" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 原图样式:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_style_original}" type="text" class="form-control" placeholder="原图样式名" id="bucket_style_original" name="bucket_style_original" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 原图水印样式:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_style_water}" type="text" class="form-control" placeholder="原图加水印样式名" id="bucket_style_water" name="bucket_style_water" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 适应宽度样式:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_style_fixwidth}" type="text" class="form-control" placeholder="适应宽度样式名" id="bucket_style_fixwidth" name="bucket_style_fixwidth" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 适应高度样式:</label>
- <div class="col-sm-9">
- <input value="{$bucket.bucket_style_fixheight}" type="text" class="form-control" placeholder="适应高度样式名" id="bucket_style_fixheight" name="bucket_style_fixheight" rangelength="[3,100]" required>
- </div>
- </div>
- <div class="form-group">
- <div class="col-sm-9 col-sm-offset-3">
- <button type="submit" class="btn btn-success disabled" disabled="true">提交数据</button>
- </div>
- </div>
- </form>
- $(function() {
- // 初始化 checkbox 的 icheck 样式
- $('input[type="checkbox"],input[type="radio"]').iCheck({
- checkboxClass: 'icheckbox_minimal-blue',
- radioClass : 'iradio_minimal-blue'
- })
- // 只有当表单中有数据变化时, 提交按钮才可用
- $("form").children().bind('input propertychange ifChecked ifUnchecked',function() {
- $(":submit").removeClass('disabled').removeAttr('disabled');
- });
- $("form").validate({
- rules: {
- bucket_domain: {
- url: true
- }
- },
- submitHandler: function(form) {
- // 当验证通过时执行 Ajax 提交
- ajax_post("{:url('index/picture/do_bucket_edit')}",$("form").serialize());
- }
- });
- });
- (4) picture.HTML
- <div class="nav-tabs-custom">
- <ul id="main-nav" class="nav nav-tabs">
- <li class="header">空间 <i class="fa fa-arrow-right"></i> </li>
- {volist name="bucketlist" id="vo"}
- <li class="{if condition='$vo.bucket_default eq 1'}active{/if}">
- <a href="#{$vo.bucket_name}" data-toggle="tab">{$vo.bucket_name}</a>
- </li>
- {/volist}
- </ul>
- <div id="main-nav-tabs" class="tab-content">
- {volist name="bucketlist" id="vo"}
- <div class="tab-pane {eq name='vo.bucket_default'value='1'}active{/eq}" id="{$vo.bucket_name}">
- <div class="row">
- <div class="col-xs-3">
- <a href="javascript:;" onclick="modal_show_iframe('上传图片','{:url("index/picture/add",["bucket"=>$vo.bucket_name])}','')"class="btn btn-primary"><i class="fa fa-fw fa-plus-square"></i> 上传图片</a>
- </div>
- </div>
- <div class="row mt-3">
- {volist name="vo.child" id="vo_c" mod="6" empty="没有图片"}
- <div class="col-xs-6 col-md-4 col-lg-2">
- <div class="panel {eq name='vo_c.picture_protected'value='1'}panel-danger{else/}panel-info{/eq}">
- <div class="panel-heading ellipsis">
- <span title="{$vo_c.picture_name}">
- {$vo_c.picture_name}
- </span>
- </div>
- <div class="panel-body">
- <a href="{$vo.bucket_domain}{$vo_c.picture_key}-{$vo.bucket_style_water}" data-lightbox="qiniu-image">
- <img class="lazy img-responsive"
- src="__STATIC__/img/loading-0.gif"
- data-src="{$vo.bucket_domain}{$vo_c.picture_key}-{$vo.bucket_style_thumb}"
- data-original="{$vo.bucket_domain}{$vo_c.picture_key}-{$vo.bucket_style_thumb}"
- alt="">
- </a>
- </div>
- <div class="panel-footer ellipsis">
- <span title="{$vo_c.picture_description}">
- {$vo_c.picture_description}
- </span><br/>
- <span title="{$vo_c.create_time}">
- {$vo_c.create_time}
- </span><br/>
- <span title="{$vo_c.update_time}">
- {$vo_c.update_time}
- </span><br/>
- <span class="pull-right">
- <a href="javascript:;" onclick="modal_show_iframe('编辑图片','{:url("index/picture/edit",["id"=>$vo_c.picture_id])}','')"title=" 编辑 "><i class="fa fa-edit fa-fw"></i></a>
- <a href="javascript:;" onclick="ajax_post_confirm('{:url("index/picture/do_picture_delete")}',{id:'{$vo_c.picture_id}'},'{$vo_c.picture_name}','删除');" title="删除"><i class="fa fa-trash fa-fw"></i></a>
- </span>
- </div>
- </div>
- </div>
- {eq name="mod" value="5"}
- </div><div class="row">
- {/eq}
- {/volist}
- </div>
- </div>
- {/volist}
- <!-- /.tab-pane -->
- </div>
- <!-- /.tab-content -->
- </div>
- $(function() {
- // 图片 lazyload 懒加载
- $("img.lazy").lazyload();
- // 如果没有默认空间, 则默认激活第 1 个空间
- if(!$("#main-nav-tabs .tab-pane.active")==false) {
- $("#main-nav a:first").tab("show");
- }
- });
引入 lazyload 组件以实现图片的懒加载, 详细信息详见网址:
https://appelsiini.net/projects/lazyload
引入 lightbox2 组件以实现图片预览, 具体信息详见网址:
- https://lokeshdhakar.com/projects/lightbox2/
- (5) picture_add.HTML
- <form method="post" enctype="multipart/form-data" class="form-horizontal">
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 选择空间</label>
- <div class="col-sm-9">
- <select name="bucket_name" class="form-control">
- {volist name="bucketlist" id="vo"}
- <option value="{$vo.bucket_name}"
- {empty name="bucket"}
- {eq name="vo.bucket_default" value="1"} selected="true" {/eq}
- {else/}
- {eq name="vo.bucket_name" value="$bucket"} selected="true" {/eq}
- {/empty}
- >{$vo.bucket_name}</option>
- {/volist}
- </select>
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-sm-3"><span class="text-red">*</span > 图片文件</label>
- <div class="col-sm-9">
- <input type="file" class="form-control" placeholder="请选择图片文件" id="picture_file" name="picture_file" accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" required />
- </div>
- </div>
- <div class="form-group">
- <lable class="control-label col-sm-3">图片标题</lable>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="给图片设定一个标题, 空白默认文件名" id="picture_name" name="picture_name" />
- </div>
- </div>
- <div class="form-group">
- <lable class="control-label col-sm-3">图片描述</lable>
- <div class="col-sm-9">
- <input type="text" class="form-control" placeholder="给图片编辑一段描述" id="picture_description" name="picture_description" />
- </div>
- </div>
- <div class="form-group">
- <lable class="control-label col-sm-3">权限保护</lable>
- <div class="col-sm-9">
- <label>
- <input type="checkbox" id="picture_protected" name="picture_protected" />
勾选表示设置权限保护(轻易不要勾选)
- </label>
- </div>
- </div>
- <div class="form-group">
- <div class="col-sm-9 col-sm-offset-3">
- <button type="submit" class="btn btn-success disabled" disabled="true">提交数据</button>
- </div>
- </div>
- </form>
- $(function() {
- // 初始化 checkbox 的 icheck 样式
- $('input[type="checkbox"]').iCheck({
- checkboxClass: 'icheckbox_minimal-blue',
- radioClass : 'iradio_minimal-blue'
- })
- // 只有当表单中有数据变化时, 提交按钮才可用
- $("form").children().bind('input propertychange ifChecked ifUnchecked',function() {
- $(":submit").removeClass('disabled').removeAttr('disabled');
- });
- $("form").validate({
- rules: {
- },
- submitHandler: function(form) {
- // 当验证通过时执行 Ajax 提交
- upload();
- }
- });
- });
- function upload() {
- var formData = new FormData();
- var file = $("[name='picture_file']")[0].files[0];
- formData.append("picture_file", file);
- formData.append("bucket_name", $("[name='bucket_name']").val());
- formData.append("picture_name", $("[name='picture_name']").val());
- formData.append("picture_description", $("[name='picture_description']").val());
- formData.append("picture_protected", $("[name='picture_protected']").is(':checked') ? 1 : 0);
- $.Ajax({
- url: "{:url('index/picture/do_picture_add')}",
- type: 'POST',
- data: formData,
- // 告诉 jQuery 不要去处理发送的数据
- processData: false,
- // 告诉 jQuery 不要去设置 Content-Type 请求头
- contentType: false,
- beforeSend: function () {
- var loading = layer.load(1, {
- shade: [0.1,'#fff'] //0.1 透明度的白色背景
- });
- },
- success: function (data) {
- console.log(data);
- layer.closeAll();
- // 当 Ajax 请求执行成功时执行
- if (data.status == true) {
- // 返回 result 对象中的 status 元素值为 1 表示数据插入成功
- layer.msg(data.message, {icon: 6, time: 2000}); // 使用 H-ui 的浮动提示框, 2 秒后自动消失
- setTimeout(function() {
- parent.location.reload();
- }, 2000); //2 秒后对父页面执行刷新(相当于关闭了弹层同时更新了数据)
- } else {
- // 返回 result 对象的 status 值不为 1, 表示数据插入失败
- layer.alert(data.message+"<p > 请自行刷新页面</p>", {icon: 5});
- // 页面停留在这里, 不再执行任何动作
- }
- },
- error: function (data) {
- console.log(data);
- }
- });
- }
- (6) picture_edit.HTML
- <form method="post" enctype="multipart/form-data" class="form-horizontal">
- <div class="form-group hide">
- <lable class="control-label col-sm-3">图片 ID<span class="text-red">*</span></lable>
- <div class="col-sm-9">
- <input value="{$picture.picture_id}" type="text" class="form-control" id="picture_id" name="picture_id" readonly="true" required />
- </div>
- </div>
- <div class="form-group">
- <lable class="control-label col-sm-3">图片标题 < span class="text-red">*</span></lable>
- <div class="col-sm-9">
- <input value="{$picture.picture_name}" type="text" class="form-control" placeholder="给图片设定一个标题" id="picture_name" name="picture_name" required />
- </div>
- </div>
- <div class="form-group">
- <lable class="control-label col-sm-3">图片描述</lable>
- <div class="col-sm-9">
- <input value="{$picture.picture_description}" type="text" class="form-control" placeholder="给图片编辑一段描述" id="picture_description" name="picture_description" />
- </div>
- </div>
- <div class="form-group">
- <lable class="control-label col-sm-3">权限保护</lable>
- <div class="col-sm-9">
- <label>
- <input {eq name="picture.picture_protected" value="1"} checked="true" {/eq} type="checkbox" id="picture_protected" name="picture_protected" />
勾选表示设置权限保护(轻易不要勾选)
- </label>
- </div>
- </div>
- <div class="form-group">
- <div class="col-sm-9 col-sm-offset-3">
- <button type="submit" class="btn btn-success disabled" disabled="true">提交数据</button>
- </div>
- </div>
- </form>
JS 部分省略, 详见 GitHub.
(7) index/controller/Index.PHP
无逻辑处理, 省略, 详见 GitHub.
- (8) index/controller/Picture.PHP
- class Picture extends Base
- {
- public function index()
- {
- $this->view->assign('pagetitle', '图片管理');
- // 加载 bucket 列表
- $bucketlist = BucketModel::all(function($query) {
- $query->order(['bucket_default'=>'desc', 'bucket_name'=>'asc']);
- });
- // 加载 bucket 里的图片
- $picturelist;
- // 遍历 bucket
- foreach($bucketlist as $n=>$bucket) {
- $picture = new PictureModel;
- $picturelist = $picture
- ->where(['bucket_name'=>$bucket->bucket_name])
- ->order(['create_time'=>'desc'])
- ->select();
- $bucketlist[$n]['child'] = $picturelist;
- }
- $this->view->assign('bucketlist', $bucketlist);
- return $this->view->fetch('picture/picture');
- }
- /**
- * 加载添加图片页面
- */
- public function add()
- {
- $this->view->assign('pagetitle', '上传图片');
- $bucket = input('?bucket') ? input('bucket') : '';
- $this->view->assign('bucket', $bucket);
- $bucketlist = BucketModel::all(function($query) {
- $query->order(['bucket_default'=>'desc', 'bucket_name'=>'asc']);
- });
- $this->view->assign('bucketlist', $bucketlist);
- return $this->view->fetch('picture/picture_add');
- }
- /**
- * 加载编辑图片页面
- * @return [type] [description]
- */
- public function edit()
- {
- $this->view->assign('pagetitle', '编辑图片信息');
- if(!input('?id')) {
- $this->error('参数错误');
- return;
- }
- $id = input('id');
- $picture = PictureModel::get($id);
- if(!$picture) {
- $this->error('参数错误');
- return;
- }
- $this->view->assign('picture', $picture);
- return $this->view->fetch('picture/picture_edit');
- }
- /**
- * 执行编辑图片操作
- * @return [type] [description]
- */
- public function do_picture_edit()
- {
- $res = new Res;
- $res->data = input();
- $res->data['picture_protected'] = input('?picture_protected') ? 1 : 0;
- try {
- $picture = new PictureModel;
- $res->data_row_count = $picture->isUpdate(true)->allowField(true)->save([
- 'picture_name'=>$res->data['picture_name'],
- 'picture_description'=>$res->data['picture_description'],
- 'picture_protected'=>$res->data['picture_protected']
- ],['picture_id'=>$res->data['picture_id']]);
- if($res->data_row_count) {
- $res->success();
- }
- } catch (\Exception $e) {
- $res->faild($e->getMessage());
- }
- return $res;
- }
- /**
- * 执行添加图片操作
- * @return [type] [description]
- */
- public function do_picture_add()
- {
- $res = new Res;
- $picture_file = request()->file('picture_file');
- $picture = new PictureModel;
- $picture->bucket_name = input('bucket_name');
- $picture->picture_name = input('picture_name')?:$picture_file->getInfo('name');
- $picture->picture_description = input('picture_description')?:$picture->picture_name;
- $picture->picture_protected = input('picture_protected');
- // 由于 demo 中没做登录部分, 所以这里获取不到值
- // $picture->admin_id = Session::has('admin_infor')?Session::get('admin_infor')->admin_id:'';
- if($picture_file) {
- // 创建 PictureService 对象实例
- $pservice = new \App\common\controller\PictureService;
- try {
- // 调用 up_file 方法向指定空间上传图片
- $res = $pservice->up_picture($picture_file, $picture);
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- }
- }
- return $res;
- }
- /**
- * 执行删除图片的操作
- * @return [type] [description]
- */
- public function do_picture_delete()
- {
- $res = new Res;
- if(!input('?id')) {
- // 未取到 id 参数
- $res->failed('参数错误');
- return $res;
- }
- $id = input('id');
- try {
- $res->data = PictureModel::get($id);
- if(!$res->data) {
- // 取到的 id 参数没有对应的记录
- $res->failed('参错错误');
- return $res;
- }
- if($res->data['picture_protected']) {
- $res->failed('不能删除受保护的图片');
- return $res;
- }
- // 创建 QiniuService 对象实例
- $qservice = new \App\common\controller\QiniuService;
- // 调用 delete_file 方法删除指定 bucket 和指定 key 的文件
- $res = $qservice->delete_file($res->data['bucket_name'], $res->data['picture_key']);
- if($res->status) {
- // 文件删除成功, 开始删除数据
- PictureModel::where(['picture_id'=>$id])->delete();
- $res->append_message('<li > 数据库记录删除成功</li>');
- }
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- }
- return $res;
- }
- /**
- * 加载空间管理页面
- * @return [type] [description]
- */
- public function bucket()
- {
- $this->view->assign('pagetitle','存储空间');
- $bucketlist = BucketModel::all(function($query) {
- $query->order(['bucket_default'=>'desc', 'bucket_name'=>'asc']);
- });
- $this->view->assign('list', $bucketlist);
- $this->view->assign('count', count($bucketlist));
- return $this->view->fetch('picture/bucket');
- }
- /**
- * 加载添加空间页面
- * @return [type] [description]
- */
- public function bucket_add()
- {
- $this->view->assign('pagetitle', '添加存储空间');
- return $this->view->fetch('picture/bucket_add');
- }
- /**
- * 执行添加空间操作
- * @return [type] [description]
- */
- public function do_bucket_add()
- {
- $res = new Res;
- $res->data = input();
- $res->data['bucket_default'] = input('?bucket_default') ? 1 : 0;
- $bucket = new BucketModel;
- $validate = Loader::validate('Bucket');
- if(!$validate->check($res->data)) {
- $res->failed($validate->getError());
- return $res;
- }
- if($res->data['bucket_default']) {
- $default = BucketModel::get(['bucket_default'=>1]);
- // 单独验证只可以有一条默认空间
- if($default) {
- $res->failed('只能有 1 个默认空间: 已经存在默认空间'.$default->bucket_name);
- return $res;
- }
- }
- try {
- $res->data_row_count = $bucket->isUpdate(false)->allowField(true)->save([
- 'bucket_name' => $res->data['bucket_name'],
- 'bucket_domain' => $res->data['bucket_domain'],
- 'bucket_description'=> $res->data['bucket_description'],
- 'bucket_default'=> $res->data['bucket_default'],
- 'bucket_style_thumb'=> $res->data['bucket_style_thumb'],
- 'bucket_style_original'=> $res->data['bucket_style_original'],
- 'bucket_style_water'=> $res->data['bucket_style_water'],
- 'bucket_style_fixwidth'=> $res->data['bucket_style_fixwidth'],
- 'bucket_style_fixheight'=> $res->data['bucket_style_fixheight'],
- ]);
- if($res->data_row_count) {
- $res->success();
- }
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- }
- return $res;
- }
- /**
- * 加载编辑空间页面
- * @return [type] [description]
- */
- public function bucket_edit()
- {
- $this->view->assign('pagetitle', '编辑存储空间');
- if(!input('?name')) {
- $this->error('参数错误');
- return;
- }
- $name = input('name');
- $bucket = BucketModel::get(['bucket_name'=>$name]);
- if(!$bucket) {
- $this->error('参数错误');
- return;
- }
- $this->view->assign('bucket', $bucket);
- return $this->view->fetch('picture/bucket_edit');
- }
- /**
- * 执行修改空间 (描述) 操作
- * @return [type] [description]
- */
- public function do_bucket_edit()
- {
- $res = new Res;
- $res->data = input();
- $res->data['bucket_default'] = input('?bucket_default') ? 1 : 0;
- $validate = Loader::validate('Bucket');
- if(!$validate->scene('edit')->check($res->data)) {
- $res->failed($validate->getError());
- return $res;
- }
- $bucket = new BucketModel;
- if($res->data['bucket_default']) {
- $default = $bucket->where('bucket_default', 'eq', 1)->where('bucket_name','neq',$res->data['bucket_name'])->find();
- if($default) {
- $res->failed('只能有 1 个默认空间: 已经存在默认空间'.$default->bucket_name);
- return $res;
- }
- }
- try {
- $res->data_row_count = $bucket->isUpdate(true)->allowField(true)->save([
- 'bucket_domain'=>$res->data['bucket_domain'],
- 'bucket_description'=>$res->data['bucket_description'],
- 'bucket_default'=>$res->data['bucket_default'],
- 'bucket_style_thumb'=>$res->data['bucket_style_thumb'],
- 'bucket_style_original'=>$res->data['bucket_style_original'],
- 'bucket_style_water'=>$res->data['bucket_style_water'],
- 'bucket_style_fixwidth'=>$res->data['bucket_style_fixwidth'],
- 'bucket_style_fixheight'=>$res->data['bucket_style_fixheight'],
- ], ['bucket_name'=>$res->data['bucket_name']]);
- if($res->data_row_count) {
- $res->success();
- } else {
- $res->failed('未更改任何数据');
- }
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- }
- return $res;
- }
- /**
- * 执行删除空间 (非默认) 操作
- * @return [type] [description]
- */
- public function do_bucket_delete()
- {
- $res = new Res;
- $name = input('?name') ? input('name') : '';
- $bucket = BucketModel::get(['bucket_name'=>$name]);
- $res->data = $bucket;
- if(empty($bucket)) {
- $res->failed("参数错误");
- return $res;
- }
- if($bucket->bucket_default==1) {
- $res->failed("默认空间不允许删除");
- return $res;
- }
- try {
- $res->data_row_count = BucketModel::where(['bucket_name'=>$name])->delete(); // 执行真删除
- $res->success();
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- }
- return $res;
- }
- }
- (9) common/controller/QiniuService.PHP
QiniuService 并没有继承 common/controller/base, 因为它不需要使用 ThinkPHP 的 controller 特性.
- class QiniuService
- {
- /**
- * 向七牛云存储获取指定 bucket 的 token
- * @param string $bucket [指定 bucket 名称]
- * @return [type] [description]
- */
- private function get_token($bucket)
- {
- $access_key = Env::get('qiniu.access_key');
- $secret_key = Env::get('qiniu.secret_key');
- $auth = new \Qiniu\Auth($access_key, $secret_key);
- $upload_token = $auth->uploadToken($bucket);
- return $upload_token;
- }
- private function generate_auth()
- {
- $access_key = Env::get('qiniu.access_key');
- $secret_key = Env::get('qiniu.secret_key');
- $auth = new \Qiniu\Auth($access_key, $secret_key);
- return $auth;
- }
- public function delete_file($bucket, $key)
- {
- $res = new Res;
- try {
- $auth = $this->generate_auth();
- $bucketManager = new \Qiniu\Storage\BucketManager($auth);
- $config = new \Qiniu\Config();
- $bucketManager = new \Qiniu\Storage\BucketManager($auth, $config);
- $err = $bucketManager->delete($bucket, $key);
- // dump($err->getResponse('statusCode')->statusCode);
- /*
- HTTP 状态码 说明
- 298 部分操作执行成功
- 400 请求报文格式错误
- 包括上传时, 上传表单格式错误. 例如 incorrect region 表示上传域名与上传空间的区域不符, 此时需要升级 SDK 版本.
- 401 认证授权失败
- 错误信息包括密钥信息不正确; 数字签名错误; 授权已超时, 例如 token not specified 表示上传请求中没有带 token , 可以抓包验证后排查代码逻辑; token out of date 表示 token 过期, 推荐 token 过期时间设置为 3600 秒(1 小时), 如果是客户端上传, 建议每次上传从服务端获取新的 token;bad token 表示 token 错误, 说明生成 token 的算法有问题, 建议直接使用七牛服务端 SDK 生成 token.
- 403 权限不足, 拒绝访问.
- 例如 key doesn't match scope 表示上传文件指定的 key 和上传 token 中, putPolicy 的 scope 字段不符. 上传指定的 key 必须跟 scope 里的 key 完全匹配或者前缀匹配; ExpUser can only upload image/audio/video/plaintext 表示账号是体验用户, 体验用户只能上传文本, 图片, 音频, 视频类型的文件, 完成实名认证即可解决; not allowed 表示您是体验用户, 若想继续操作, 请先前往实名认证.
- 404 资源不存在
- 包括空间资源不存在; 镜像源资源不存在.
- 405 请求方式错误
- 主要指非预期的请求方式.
- 406 上传的数据 CRC32 校验错误
- 413 请求资源大小大于指定的最大值
- 419 用户账号被冻结
- 478 镜像回源失败
- 主要指镜像源服务器出现异常.
- 502 错误网关
- 503 服务端不可用
- 504 服务端操作超时
- 573 单个资源访问频率过高
- 579 上传成功但是回调失败
- 包括业务服务器异常; 七牛服务器异常; 服务器间网络异常. 需要确认回调服务器接受 POST 请求, 并可以给出 200 的响应.
- 599 服务端操作失败
- 608 资源内容被修改
- 612 指定资源不存在或已被删除
- 614 目标资源已存在
- 630 已创建的空间数量达到上限, 无法创建新空间.
- 631 指定空间不存在
- 640 调用列举资源 (list) 接口时, 指定非法的 marker 参数.
- 701 在断点续上传过程中, 后续上传接收地址不正确或 ctx 信息已过期.
- */
- if($err) {
- if($err->getResponse('statusCode')->statusCode==612) {
- // 指定资源不存在或已被删除
- $res->success('目标文件已不存在');
- } else {
- $res->failed($err->message());
- }
- } else {
- $res->success();
- }
- } catch (\Exception $e) {
- $res->failed($e->getMessage());
- }
- return $res;
- }
- /**
- * 向指定七牛云存储空间上传文件
- * @param [type] $bucket [指定存储空间 bucket 名称]
- * @param [type] $file [需上传的文件]
- * @return [type] [Res 对象实例]
- */
- public function up_file($bucket, $file = null)
- {
- $token = $this->get_token($bucket);
- $res = new Res;
- $res->data = '';
- $res->result = ['token'=>$token];
- if($file) {
- // 要上传图片的本地路径
- $file_path = $file->getRealPath();
- // 文件名后缀
- $ext = pathinfo($file->getInfo('name'), PATHINFO_EXTENSION);
- // 文件前缀(类似文件夹)
- $prefix = str_replace("-","",date('Y-m-d/'));
- // 上传后保存的文件名(无后缀)
- $file_name = uniqid();
- // 上传后的完整文件名(含前缀后缀)
- $key = $prefix.$file_name.'.'.$ext;
- // 域名
- $domain = Bucket::get(['bucket_name'=>$bucket])->bucket_domain;
- // 初始化 UploadManager 对象并进行文件上传
- $upload_manager = new \Qiniu\Storage\UploadManager();
- // 调用 UploadManager 的 putFile 方法进行文件上传
- list($ret, $err) = $upload_manager->putFile($token, $key, $file_path);
- if($err!==null) {
- $res->failed($err);
- } else {
- $res->success();
- $res->result['domain'] = $domain;
- $res->result['key'] = $ret['key'];
- $res->result['hash'] = $ret['hash'];
- $res->result['bucket'] = $bucket;
- }
- } else {
- $res->failed('未接收到文件');
- }
- return $res;
- }
- /**
- * 从服务器传输文件到七牛云
- * @param [type] $bucket 目标 bucket
- * @param [type] $file_path 要传输文件的服务器路径
- * @return [type] res
- */
- public function transfer_file($bucket, $file_path)
- {
- // 构建鉴权对象
- $auth = $this->generate_auth();
- // 生成上传 Token
- $token = $auth->uploadToken($bucket);
- // 文件后缀
- $ext = pathinfo($file_path, PATHINFO_EXTENSION);
- // 文件前缀(类似文件夹)
- $prefix = str_replace("-","",date('Y-m-d/'));
- // 上传到七牛后保存的文件名(不带后缀)
- $file_name = uniqid();
- // 上传后的完整文件名(含前缀后缀)
- $key = $prefix.$file_name.'.'.$ext;
- // 域名
- $domain = Bucket::get(['bucket_name'=>$bucket])->bucket_domain;
- $res = new Res;
- try {
- // 初始化 UploadManager 对象并进行文件的上传.
- $uploadMgr = new \Qiniu\Storage\UploadManager();
- // 调用 UploadManager 的 putFile 方法进行文件的上传.
- list($ret, $err) = $uploadMgr->putFile($token, $key, '.'.$file_path);
- if ($err !== null) {
- $res->failed();
- $res->result['obj'] = $err;
- } else {
- $res->success();
- $res->result['obj'] = $ret;
- $res->result['domain'] = $domain;
- $res->result['key'] = $ret['key'];
- $res->result['hash'] = $ret['hash'];
- $res->result['bucket'] = $bucket;
- }
- } catch (\Exception $e) {
- $res->failed($e->getMessage());
- }
- return $res;
- }
- /**
- * 获取七牛云指定 bucket 存储空间的文件列表
- * @param [type] $bucket [指定存储空间名称]
- * @param string $marker [上次列举返回的位置标记, 作为本次列举的起点信息]
- * @param string $prefix [要列取文件的公共前缀]
- * @param integer $limit [本次列举的条目数]
- * @return [type] [description]
- */
- public function list_file($bucket, $marker='', $prefix='', $limit=100)
- {
- $auth = $this->generate_auth();
- $bucketManager = new \Qiniu\Storage\BucketManager($auth);
- $delimiter = '';
- // 列举文件
- list($ret, $err) = $bucketManager->listFiles($bucket, $prefix, $marker, $limit, $delimiter);
- if ($err !== null) {
- $result = $err;
- } else {
- if (array_key_exists('marker', $ret)) {
- echo "Marker:" . $ret["marker"] . "\n";
- }
- $result = $ret;
- }
- return $result;
- }
- }
- (10) common/controller/PictureService.PHP
- class PictureService extends CommonBase
- {
- /**
- * 从数据库中找到第 1 个默认 bucket
- * @return [type] [description]
- */
- private function default_bucket()
- {
- $bucket = new Bucket;
- // 向数据库查询 bucket_default 为 1 的记录
- $default_bucket = $bucket->where(['bucket_default'=>1])->find();
- // 如果没有 bucket_default 为 1 的记录, 再尝试取第 1 条 bucket 记录
- if(!$default_bucket) {
- $default_bucket = $bucket->where('1=1')->find();
- }
- // 如果实在取不到, 这里就算了, 返回吧
- return $default_bucket;
- }
- public function up_picture($file, $picture)
- {
- $res = new Res;
- if(empty($picture->toArray()['bucket_name'])) {
- $bucket = $this->default_bucket();
- if($bucket) {
- $picture->bucket_name = $this->default_bucket()->bucket_name;
- } else {
- $res->failed('无法获取 bucket 信息');
- return $res;
- }
- }
- if(empty($picture->toArray()['picture_name'])) {
- $picture->picture_name = $file->getInfo('name');
- }
- if(empty($picture->toArray()['picture_description'])) {
- $picture->picture_description = $picture->picture_name;
- }
- if($file) {
- // 创建 QiniuService 对象实例
- $qservice = new QiniuService;
- try {
- // 调用 up_file 方法向指定空间上传图片
- $res = $qservice->up_file($picture->bucket_name, $file);
- if($res->status) {
- // 上传成功, 写入数据库
- $picture->picture_key = $res->result['key'];
- // 在我的项目中有一个自动生成全局唯一且递增 ID 的方法, 但是 demo 中没做相关配置部分
- //demo 中将 picture_id 直接设置成自增 ID 了
- //$picture->picture_id = $this->apply_full_global_id_str();
- $res_db = new Res;
- $res_db->data_row_count = $picture->isUpdate(false)->allowField(true)->save();
- if($res_db->data_row_count) {
- // 写入数据库成功
- $res_db->success();
- $res_db->data = $picture;
- }
- // 将写入数据库的结果作为返回结果的一个属性
- $res->result["db"] = $res_db;
- }
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- }
- }
- return $res;
- }
- public function up_scrawl($ext = null, $content = null, $path = null)
- {
- // 保存图片到服务器, 取得服务器路径
- $file_path = $this->save_picture($ext, $content, $path);
- // 传输服务器图片到七牛云, 取得返回的 url
- $url = $file_path;
- $res = new Res;
- $picture = new Picture;
- $picture->bucket_name = $this->default_bucket()->bucket_name;
- $picture->picture_name = pathinfo($file_path, PATHINFO_BASENAME);
- $picture->picture_description = $picture->picture_name;
- try {
- $qservice = new QiniuService;
- $res = $qservice->transfer_file($picture->bucket_name, $file_path);
- if($res->status) {
- // 保存数据库信息
- $picture->picture_key = $res->result['key'];
- // 在我的项目中有一个自动生成全局唯一且递增 ID 的方法, 但是 demo 中没做相关配置部分
- //demo 中将 picture_id 直接设置成自增 ID 了
- // $picture->picture_id = $this->apply_full_global_id_str();
- $res_db = new Res;
- $res_db->data_row_count = $picture->isUpdate(false)->allowField(true)->save();
- if($res_db->data_row_count) {
- // 写入数据库成功
- $res_db->success();
- $res_db->data = $picture;
- }
- // 将写入数据库的结果作为返回结果的一个属性
- $res->result["db"] = $res_db;
- // 准备 url
- // bucket 对应的域名
- $url = $res->result['domain'];
- // 图片在 bucket 中的 key
- $url .= $res->result['key'];
- // 默认插入水印板式
- $url .= '-'.Bucket::get(['bucket_name'=>$res->result['bucket']])->bucket_style_water;
- }
- } catch(\Exception $e) {
- $res->failed($e->getMessage());
- $url = '';
- }
- // 删除服务器图片
- unlink('.'.$file_path);
- // 返回的是七牛云上的 url
- return $url;
- }
- /**
- * 在服务器保存图片文件
- * @param [type] $ext [description]
- * @param [type] $content [description]
- * @param [type] $path [description]
- * @return [type] [description]
- */
- private function save_picture($ext = null, $content = null, $path = null)
- {
- $full_path = '';
- if ($ext && $content) {
- do {
- $full_path = $path . uniqid() . '.' . $ext;
- } while (file_exists($full_path));
- $dir = dirname($full_path);
- if (!is_dir($_SERVER['DOCUMENT_ROOT'].$dir)) {
- mkdir($_SERVER['DOCUMENT_ROOT'].$dir, 0777, true);
- }
- file_put_contents($_SERVER['DOCUMENT_ROOT'].$full_path, $content);
- }
- return $full_path;
- }
- }
- (11) API/controller/Ueditor.PHP
- class Ueditor extends ApiBase
- {
- private $uploadfolder='/upload/'; // 上传地址
- private $scrawlfolder='/upload/_scrawl/'; // 涂鸦保存地址
- private $catchfolder='/upload/_catch/'; // 远程抓取地址
- private $configpath='/static/lib/ueditor/utf8-php/php/config.json'; // 前后端通信相关的配置
- private $config;
- public function index(){
- $this->type=input('edit_type','');
- date_default_timezone_set("Asia/chongqing");
- error_reporting(E_ERROR);
- header("Content-Type: text/html; charset=utf-8");
- $CONFIG = json_decode(preg_replace("/\/\*[\s\S]+?\*\//", "", file_get_contents($_SERVER['DOCUMENT_ROOT'].$this->configpath)), true);
- $this->config=$CONFIG;
- $action = input('action');
- switch ($action) {
- case 'config':
- $result = json_encode($CONFIG);
- break;
- /* 上传图片 */
- case 'uploadimage':
- $result = $this->_qiniu_upload();
- break;
- /* 上传涂鸦 */
- case 'uploadscrawl':
- $result = $this->_upload_scrawl();
- break;
- /* 上传视频, demo 暂时没有实现, 可以查看其他文章 */
- case 'uploadvideo':
- $result = $this->_upload(array('maxSize' => 1073741824,/*1G*/'exts'=>array('mp4', 'avi', 'wmv','rm','rmvb','mkv')));
- break;
- /* 上传文件, demo 暂时没有实现, 可以查看其他文章 */
- case 'uploadfile':
- $result = $this->_upload(array('exts'=>array('jpg', 'gif', 'png', 'jpeg','txt','pdf','doc','docx','xls','xlsx','zip','rar','ppt','pptx',)));
- break;
- /* 列出图片 */
- case 'listimage':
- $result = $this->_qiniu_list($action);
- break;
- /* 列出文件, demo 暂时没有实现, 可以查看其他文章 */
- case 'listfile':
- $result = $this->_list($action);
- break;
- /* 抓取远程文件, demo 暂时没有实现, 可以查看其他文章 */
- case 'catchimage':
- $result = $this->_upload_catch();
- break;
- default:
- $result = json_encode(array('state'=> '请求地址出错'));
- break;
- }
- /* 输出结果 */
- if (isset($_GET["callback"]) && false ) {
- if (preg_match("/^[\w_]+$/", $_GET["callback"])) {
- echo htmlspecialchars($_GET["callback"]) . '(' . $result . ')';
- } else {
- echo json_encode(array(
- 'state'=> 'callback 参数不合法'
- ));
- }
- } else {
- exit($result) ;
- }
- }
- private function _qiniu_upload($config=array())
- {
- $title = '';
- $url='';
- if(!empty($config)){
- $this->config=array_merge($this->config,$config);;
- }
- $file = request()->file('upfile');
- if($file){
- $picture = new Picture;
- // demo 中暂时关闭关于 admin 的处理
- // $picture->admin_id = Session::has('admin_infor')?Session::get('admin_infor')->admin_id:'';
- $pservice = new PictureService;
- $res = $pservice->up_picture($file, $picture);
- if($res->status) {
- // bucket 对应的域名
- $url = $res->result['domain'];
- // 图片在 bucket 中的 key
- $url .= $res->result['key'];
- // 默认插入水印板式
- $url .= '-'.Bucket::get(['bucket_name'=>$res->result['bucket']])->bucket_style_water;
- $title = $res->result['key'];
- $state = 'SUCCESS';
- }else{
- $state = $res->message();
- }
- }else{
- $state = '未接收到文件';
- }
- $response=array(
- "state" => $state,
- "url" => $url,
- "title" => $title,
- "original" =>$title,
- );
- return json_encode($response);
- }
- private function _upload_scrawl()
- {
- $data = input('post.' . $this->config ['scrawlFieldName']);
- $url='';
- $title = '';
- $oriName = '';
- if (empty ($data)) {
- $state= 'Scrawl Data Empty!';
- } else {
- $pservice = new PictureService;
- // 在服务器保存图片文件
- $url = $pservice->up_scrawl('png', base64_decode($data), $this->scrawlfolder);
- if ($url) {
- $state = 'SUCCESS';
- } else {
- $state = 'Save scrawl file error!';
- }
- }
- $response=array(
- "state" => $state,
- "url" => $url,
- "title" => $title,
- "original" =>$oriName ,
- );
- return json_encode($response);
- }
- private function _qiniu_list($action)
- {
- /* 判断类型 */
- switch ($action) {
- /* 列出文件 */
- case 'listfile':
- $allowFiles = $this->config['fileManagerAllowFiles'];
- $listSize = $this->config['fileManagerListSize'];
- $prefix='/';
- break;
- /* 列出图片 */
- case 'listimage':
- default:
- $allowFiles = $this->config['imageManagerAllowFiles'];
- $listSize = $this->config['imageManagerListSize'];
- $prefix='/';
- }
- // 这里暂时没有用 20190606
- $start = 0;
- // 准备文件列表
- $list = [];
- $picture = Picture::all();
- foreach($picture as $n=>$p) {
- $list[] = array(
- 'url'=>$p->bucket->bucket_domain.$p->picture_key.'-'.$p->bucket->bucket_style_thumb,
- 'title'=>$p->picture_name,
- 'url_original'=>$p->bucket->bucket_domain.$p->picture_key.'-'.$p->bucket->bucket_style_water,
- );
- }
- /* 返回数据 */
- $result = json_encode(array(
- "state" => "SUCCESS",
- "list" => $list,
- "start" => $start,
- "total" => count($list)
- ));
- return $result;
- }
- /**
- * 遍历获取目录下的指定类型的文件
- * @param string $path
- * @param string $allowFiles
- * @param array $files
- * @return array
- */
- function getfiles($path, $allowFiles, &$files = array())
- {
- if (!is_dir($path)) return null;
- if(substr($path, strlen($path) - 1) != '/') $path .= '/';
- $handle = opendir($path);
- while (false !== ($file = readdir($handle))) {
- if ($file != '.' && $file != '..') {
- $path2 = $path . $file;
- if (is_dir($path2)) {
- $this->getfiles($path2, $allowFiles, $files);
- } else {
- if (preg_match("/\.(".$allowFiles.")$/i", $file)) {
- $files[] = array(
- 'url'=> substr($path2, strlen($_SERVER['DOCUMENT_ROOT'])),
- // 'document_root'=> $_SERVER['DOCUMENT_ROOT'],
- // 'root_path'=> ROOT_PATH,
- // 'path2'=> $path2,
- // 'path'=> $path,
- // 'mtime'=> filemtime($path2)
- );
- }
- }
- }
- }
- return $files;
- }
- }
(12) 修改 ueditor 中的代码:
- path-to-ueditor/ueditor.config.JS
- Windows.UEDITOR_CONFIG = {
- // 为编辑器实例添加一个路径, 这个不能被注释
- UEDITOR_HOME_URL: URL
- // 服务器统一请求接口路径
- // 修改为自定义的 serverUrl,demo 中就是 / API/ueditor/index , serverUrl: "/api/ueditor/index"
- // 工具栏上的所有的功能按钮和下拉框, 可以在 new 编辑器的实例时选择自己需要的重新定义
- // , toolbars: [[
- // 'fullscreen', 'source', '|',
- // 'undo', 'redo', '|',
- // 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|',
- // 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
- // 'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
- // 'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
- // 'directionalityltr', 'directionalityrtl', 'indent', '|',
- // 'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|',
- // 'touppercase', 'tolowercase', '|',
- // 'link', 'unlink', 'anchor', '|',
- // 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
- // 'simpleupload', 'insertimage', 'emotion', 'scrawl', 'insertvideo', 'music', 'attachment', 'map', 'gmap', 'insertframe', 'insertcode', 'webapp', 'pagebreak', 'template', 'background', '|',
- // 'horizontal', 'date', 'time', 'spechars', 'snapscreen', 'wordimage', '|',
- // 'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|',
- // 'print', 'preview', 'searchreplace', 'drafts', 'help'
- // ]]
- // 修改: 关闭不需要的按钮
- , toolbars: [[
- 'fullscreen', 'source', '|',
- 'undo', 'redo', '|',
- 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|',
- 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
- 'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
- 'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
- 'directionalityltr', 'directionalityrtl', 'indent', '|',
- 'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|',
- 'touppercase', 'tolowercase', '|',
- 'link', 'unlink', 'anchor', '|',
- 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
- 'simpleupload', 'insertimage', 'emotion', 'scrawl', 'map', 'insertframe', 'insertcode', 'pagebreak', 'template', '|',
- 'horizontal', 'date', 'time', 'spechars', '|',
- 'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|',
- 'print', 'preview', 'searchreplace', 'drafts', 'help'
- ]]
- path-to-ueditor/ueditor.all.JS
- UE.commands['insertimage'] = {
- execCommand:function (cmd, opt) {
- ......if (img && /img/i.test(img.tagName) && (img.className != "edui-faked-video" || img.className.indexOf("edui-upload-video")!=-1) && !img.getAttribute("word_img")) {
- ......var floatStyle = first['floatStyle'];
- } else {
- var HTML = [], str = '', ci;
- ci = opt[0];
- if (opt.length == 1) {
- unhtmlData(ci);
- // 修改: 添加 Bootstrap 的 img-responsive 样式以支持响应式图片 str = '<img class="img-responsive"src="' + ci.src + '"' + (ci._src ?' _src="'+ ci._src +'" ':'') +
- (ci.width ? 'width="' + ci.width + '"' :'') +
- (ci.height ? 'height="' + ci.height + '"' :'') +
- (ci['floatStyle'] == 'left' || ci['floatStyle'] == 'right' ? 'style="float:'+ ci['floatStyle'] +';"':'') +
- (ci.title && ci.title != ""?' title="'+ ci.title +'"' : '') +
- (ci.border && ci.border != "0" ? 'border="' + ci.border + '"':'') +
- (ci.alt && ci.alt != ""?' alt="'+ ci.alt +'"' : '') +
- (ci.hspace && ci.hspace != "0" ? 'hspace ="' + ci.hspace + '"':'') +
- (ci.vspace && ci.vspace != "0" ? 'vspace ="' + ci.vspace + '"':'') + '/>';
- if (ci['floatStyle'] == 'center') {
- str = '<p style="text-align: center">' + str + '</p>';
- }
- HTML.push(str);
- } else {
- for (var i = 0; ci = opt[i++];) {
- unhtmlData(ci);
- // 修改: 添加 Bootstrap 的 img-responsive 样式以支持响应式图片
- str = '<p' + (ci['floatStyle'] == 'center' ? 'style="text-align: center"' :'') + '><img
- class="img-responsive"
- src="'+ ci.src +'" ' +
- (ci.width ? 'width="' + ci.width + '"' :'') + (ci._src ? '_src="' + ci._src + '"' :'') +
- (ci.height ? 'height="' + ci.height + '"' :'') +
- 'style="' + (ci['floatStyle'] && ci['floatStyle'] != 'center' ? 'float:' + ci['floatStyle'] + ';' : '') +
- (ci.border || '') +'" ' +
- (ci.title ? 'title="' + ci.title + '"':'') + '/></p>';
- HTML.push(str);
- }
- }
- ......
- }
- ......
- }
- };
- UE.plugin.register('simpleupload', function (){
- ......function callback(){
- try{
- var link, JSON, loader,
- body = (iframe.contentDocument || iframe.contentWindow.document).body,
- result = body.innerText || body.textContent || '';
- JSON = (new Function("return" + result))();
- link = me.options.imageUrlPrefix + JSON.url;
- if(JSON.state == 'SUCCESS' && JSON.url) {
- loader = me.document.getElementById(loadingId);
- loader.setAttribute('src', link);
- loader.setAttribute('_src', link);
- loader.setAttribute('title', JSON.title || '');
- loader.setAttribute('alt', JSON.original || '');
- loader.removeAttribute('id');
- domUtils.removeClasses(loader, 'loadingclass');
- // 修改: 添加 Bootstrap 的 img-responsive 样式以支持响应式图片 domUtils.addClass(loader, 'img-responsive');
- } else {
- showErrorLoader && showErrorLoader(JSON.state);
- }
- }catch(er){
- showErrorLoader && showErrorLoader(me.getLang('simpleupload.loadError'));
- }
- form.reset();
- domUtils.un(iframe, 'load', callback);
- }
- ...... });
- path-to-ueditor/dialogs/image/image.JS
- /* 添加图片到列表界面上 */
- pushData: function (list) {
- ...... domUtils.on(img, 'load', (function(image){
- return function(){
- _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight);
- }
- })(img));
- img.width = 113;
- img.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) );
- // 修改: 设置插入图片时引用七牛的原图 (水印) 样式
- img.setAttribute('_src', urlPrefix + list[i].url_original);
- // 修改: 给图片添加 titleicon.setAttribute('title', list[i].title);
- domUtils.addClass(icon, 'icon');
- item.appendChild(img);
- item.appendChild(icon);
- this.list.insertBefore(item, this.clearFloat);
- }
- }
- },
- path-to-editor/dialogs/image/image.HTML
- <div id="tabhead" class="tabhead">
- <span class="tab" data-content-id="remote"><var id="lang_tab_remote"></var></span>
- <span class="tab focus" data-content-id="upload"><var id="lang_tab_upload"></var></span>
- <span class="tab" data-content-id="online"><var id="lang_tab_online"></var></span>
- <!-- 修改, 关闭图片搜索界面 -->
- <!-- <span class="tab" data-content-id="search"><var id="lang_tab_search"></var></span> -->
- </div>
- <div class="alignBar">
- ......
- </div>
- <div id="tabbody" class="tabbody">
- ......
- <!-- 搜索图片 -->
- <!-- 修改: 关闭图片搜索界面 -->
- <!-- <div id="search" class="panel">
- <div class="searchBar">
- <input id="searchTxt" class="searchTxt text" type="text" />
- <select id="searchType" class="searchType">
- <option value="&s=4&z=0"></option>
- <option value="&s=1&z=19"></option>
- <option value="&s=2&z=0"></option>
- <option value="&s=3&z=0"></option>
- </select>
- <input id="searchReset" type="button" />
- <input id="searchBtn" type="button" />
- </div>
- <div id="searchList" class="searchList"><ul id="searchListUl"></ul></div>
- </div>
- -->
- </div>
- path-to-ueditor/third-part/webuploader/webuploader*.JS
由于七牛云在多线程上传时会时常报错, 所以我们需要按照队列一个一个去上传就好了, 上传调用的是百度自家的 webuploader 组件. 我没有仔细研究 ueditor 到底调用的是哪一个文件, 干脆就把所有文件中的
threads:3 改成 threads:1 .
8. 调试改错和已知 bug:
调试改错这个过程是必须要经历的, 有时候还是非常痛苦的, 很多细小的忽视都会导致程序运行失败, 认真并耐心就好了.
已知 bug:
程序里使用 unlink('.'.$file_path); 这一句用来删除涂鸦临时保存在应用服务器上的文件, 但是有时候会出现删不掉的情况.
9. GitHub:
我把完整的 Demo 上传到的我的 GitHub 仓库中, 如需要完整源码可自行下载:
https://github.com/wandoubaba/tp-ue-qn-db
10. 效果演示:
11. 结束语
文本是我对前段时间所做研究的一个完整的复盘, 但是即使是复盘, 也并没有一下子就运行成功, 而且在复盘时又调试出了新的 bug, 由此可见, 对一些在项目中学习到的新技术进行适当的复盘重现, 可以加深自己对技术的掌握, 同时也能帮助到其他人, 虽然多花了一些时间, 但是我认为是值得的.
感谢你花时间读完了文章, 如果你对需求有更好的解决方法, 或者发现文中的错误和不足, 也请你不吝赐教, 互相交流以共同进步.
来源: https://www.cnblogs.com/chenqiang001/p/10998776.html