在ThinkPHP框架(5.0.24)下引入Ueditor并实现向七牛云对象存储上传图片同时将图片信息保存到MySQL数据库,同时实现lazyload懒加载

摘要:
2.图像管理、上传、添加、删除、更改和查询。上传时,将信息保存在数据库中。查询时,从数据库读取数据并从云存储空间获取图像。

这是我花了很多天的时间才得以真正实现的一组需求。

文章后面有完整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

执行后的结果:

微信图片_20190609154005

你的执行过程提示可能与我不同,但是执行结果是一样的。

接下来就可以为应用建立虚拟机,创建过程省略,创建结果是将本地的http://localhost.tp-ue-qn-db/指向到本地的…/tp-ue-qn-db/www/public目录,创建好可以试着运行一下,结果应该如下:

微信图片_20190609154522

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

操作到这里,项目目录结构大概是这样:

image

6. 做与本例无关的必要操作:

主要包括在thinkphp中配置数据库连接、引入需要的前端框架(库或插件)、做一些thinkphp的视力模板等,这些操作必要但与本例无关,而且每个项目都不一样,所以不做讲解。

7. 创建相关文件,编程:

主要目录结构:

image

这里只展示核心代码,完整的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"   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 appcommoncontrollerPictureService;
            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 appcommoncontrollerQiniuService;
            // 调用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 QiniuAuth($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 QiniuAuth($access_key, $secret_key);
        return $auth;
    }

    public function delete_file($bucket, $key)
    {
        $res = new Res;

        try {
            $auth = $this->generate_auth();
            $bucketManager = new QiniuStorageBucketManager($auth);

            $config = new QiniuConfig();
            $bucketManager = new QiniuStorageBucketManager($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 QiniuStorageUploadManager();
            // 调用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 QiniuStorageUploadManager();
            // 调用 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 QiniuStorageBucketManager($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"] . "
";
            }
            $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("//*[sS]+?*//", "", 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

window.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="https://tool.4xseo.com/article/201839.html' + 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' ? '  ' : '') + '><img
class="img-responsive"
 src="https://tool.4xseo.com/article/201839.html' + 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   data-content-id="search"><var id="lang_tab_search"></var></span> -->
        </div>
        <div class="alignBar">
            ……
        </div>
        <div id="tabbody" class="tabbody">

            ……
            <!-- 搜索图片 -->
            
<!-- 修改:关闭图片搜索界面 -->
<!--             <div   class="panel">
                <div class="searchBar">
                    <input     type="text" />
                    <select   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   type="button"  />
                    <input   type="button"  />
                </div>
                <div   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. 效果演示:

image

image

image

image

image

image

image

image

image

image

11. 结束语

文本是我对前段时间所做研究的一个完整的复盘,但是即使是复盘,也并没有一下子就运行成功,而且在复盘时又调试出了新的bug,由此可见,对一些在项目中学习到的新技术进行适当的复盘重现,可以加深自己对技术的掌握,同时也能帮助到其他人,虽然多花了一些时间,但是我认为是值得的。

感谢你花时间读完了文章,如果你对需求有更好的解决方法,或者发现文中的错误和不足,也请你不吝赐教,互相交流以共同进步。

免责声明:文章转载自《在ThinkPHP框架(5.0.24)下引入Ueditor并实现向七牛云对象存储上传图片同时将图片信息保存到MySQL数据库,同时实现lazyload懒加载》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇【json的处理】二、Jackson的处理fatal error C1060: “编译器的堆空间不足“——C1001错误代码下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

浪潮各机型管理芯片BMC IP(智能平台管理接口)设置

NF5240m3/NF5140m3/NF5280m3/SA5212H2/NP5540M3NF5270M3/NF5170M3/NF8420m3IPMI主板集成管理芯片BMCIP设置开机按DEL键进入BIOS设置选择"ServerMgmt"---"BMCNetworkConfiguration"---"lanchannel1/2"---"staticipaddr...

Spring Boot 核心配置文件 bootstrap &amp;amp; application

boostrap由父ApplicationContext加载,比applicaton优先加载boostrap里面的属性不能被覆盖3、bootstrap/application的应用场景application配置文件这个容易理解,主要用于SpringBoot项目的自动化配置。这个父级的SpringApplicationContext是先加载的,在加载appli...

Vue跨层级传递slot的方法

但是我需要通过插槽在父组件中指定一个模板,而B组件引用C组件。组件C的部分模板需要在组件A中配置。模板引用A组件:{{node.text}}&lt;模板引用B组件:spanslot=“nodeMenu”slot scope=“{node}”&gt;node=“node”&gt;/span&gt;/div&gt;2.2如...

nginx 浏览php的时候会变成下载

php的时候会变成下载:这是因为nginx没有设置好碰到php文件时,要传递到后方的php解释器。当然啦,你的php-fpm解析器也需要正常运行,并监听好9000端口,才能最终生效并有效处理php脚本。windows下开启监听的办法,php-cgi.exe-b127.0.0.1:9000-cphpphp.ini待续:。。。。。...

H3C交换机如何配置管理VLAN

1.输入“系统视图”(缩写为“sys”)进入系统配置模式[H3C]...

如何让FireFox/chrome新打开的标签页在后台打开,而不是立即跳转过去

Firefox:在地址栏中输入about:config以查找以下三项,所有这些项都设置为true browser。选项卡。加载后台浏览器。选项卡。加载DivertedInBackgroundbrowser。tabs LoadBookmarksInBackgroundchrome:Mac OS:按住命令键,用鼠标单击,您可以在后台打开一个新选项卡,进入Goog...