Warning: file_put_contents(): open_basedir restriction in effect. File(/var/log/myblog/function_themeUrl.log) is not within the allowed path(s): (/www/wwwroot/myblog/:/tmp/) in /www/wwwroot/myblog/var/Typecho/Common.php on line 934

Warning: file_put_contents(/var/log/myblog/function_themeUrl.log): failed to open stream: Operation not permitted in /www/wwwroot/myblog/var/Typecho/Common.php on line 934

Warning: file_put_contents(): open_basedir restriction in effect. File(/var/log/myblog/function_themeUrl.log) is not within the allowed path(s): (/www/wwwroot/myblog/:/tmp/) in /www/wwwroot/myblog/var/Typecho/Common.php on line 934

Warning: file_put_contents(/var/log/myblog/function_themeUrl.log): failed to open stream: Operation not permitted in /www/wwwroot/myblog/var/Typecho/Common.php on line 934

Warning: file_put_contents(): open_basedir restriction in effect. File(/var/log/myblog/function_themeUrl.log) is not within the allowed path(s): (/www/wwwroot/myblog/:/tmp/) in /www/wwwroot/myblog/var/Typecho/Common.php on line 934

Warning: file_put_contents(/var/log/myblog/function_themeUrl.log): failed to open stream: Operation not permitted in /www/wwwroot/myblog/var/Typecho/Common.php on line 934
命中水

前言

最近在家失业无聊,接了个单,其中有个项目就是关于后台管理系统的,发了几个模板样例给对方,有之前用过的后台框架fastadminlaraveladmin,但是对方看了都觉得怎么跟他之前的后台一个样,要求换一个简洁一点的,于是我在网上搜了一下laravel还有哪些后台框架,于是看到了dcatadmin,把demo网站发给对方看过之后,觉得这个页面还好,比较简单,随交流沟通后,确定用这个后台。

话说dcatadmin

dcatadmin这个框架我还是第一次用到,之前用laravel的后台框架都是用的laravel-admin,相对来说比较熟悉一点,dcatadmin还是第一次听到(因为上份工作都是围绕yii框架来展开的,关于laravel的消息确实查看的不多)。

dcatadmin2.x文档-简介 | 入门 |《Dcat Admin 中文文档 2.x》| Laravel China 社区

看完整体下来,dcatadmin重点需要了解的其实就是3个部分,也是后台系统中的组成:数据表格、表单、详情

但是还是有些地方没怎么看懂,尤其是关于工具表单异步加载这块儿。时间有限,索性就在开发的过程中,再去搞清楚吧

因为现有文档描述不清,在开发过程中自己也是踩了不少坑,以下内容总结了在dcatadmin2.x中常用的几个功能,如有遇到类似疑问的开发者,可以少走一些弯路

数据表单

工具表单

工具表单的文档:工具表单 | 数据表单 |《Dcat Admin 中文文档 2.x》| Laravel China 社区

要实现的业务场景

举个例子:

比如说你想在数据列表中,对一行数据增加某个操作按钮,实现”充值“功能

image-20251120105301499.png

在非dcatadmin2.x框架中一般实现思路是,实现按钮的点击功能,当点击“充值”按钮时,打开一个弹窗,弹窗里是个表单,输入你要充值的金额,然后点提交
image-20251120105432750.png

这个功能在dcatadmin中就可以通过“工具表单”来实现,具体实现方法如下:

首先可以把点击充值这个看成一个动作,在项目中的App\Admin\Actions空间下实现一个充值动作类

class RechargeUserBalance extends RowAction
{
    /**
     * @return string
     */
    protected $title = '';


    public function render()
    {
        // 实例化表单类并传递自定义参数
        $form = RechargeUserBalanceForm::make()->payload(['id' => $this->getKey()]);

        return Modal::make()
            ->lg()
            ->title(admin_trans('admin-user.fields.recharge'))
            ->body($form)
            ->button("<i class='feather'>".admin_trans('admin-user.fields.recharge')."</i>");
    }

}

这个类中其实只需要一个方法就可以了,重点看下

Modal::make()
            ->lg()
            ->title(admin_trans('admin-user.fields.recharge'))
            ->body($form)
            ->button("<i class='feather'>".admin_trans('admin-user.fields.recharge')."</i>");

Modal::make() 表示实例一个弹窗

  • lg 弹窗大小,可选参数有sm、xl

  • title 弹窗标题,左上角

  • body 弹窗内容,这里我是实例了一个表单类,下文再讲

  • button 页面显示的按钮

接下来我们看表单内容的具体实现

表单实现

在命名空间App\Admin\Forms定义一个表单类,我是直接把我的代码贴过来了,可以参考下

class RechargeUserBalanceForm extends Form implements LazyRenderable
{

    use LazyWidget; // 使用异步加载功能

    /**
     * @param array $input
     * @return \Dcat\Admin\Http\JsonResponse
     * @throws \Throwable
     * @author mingzhongshui
     * @date 2025/6/17
     */
    public function handle(array $input)
    {
        $userId = $this->payload['id'] ?? null;
        if (!$userId) {
            throw new \Exception(admin_trans_field('parameter_error'));
        }

        $balance = $input['balance'];
        if (!is_numeric($balance)) {
            throw new \Exception(admin_trans('yk-user.fields.recharge_balance_format_fail'));
        }

        if (floatval($balance) <= 0) {
            throw new \Exception(admin_trans('yk-user.fields.recharge_balance_must_greater_than_0'));
        }

        if (!preg_match('/^\d+(\.\d{1,3})?$/', $balance)) {
            throw new \Exception(admin_trans('yk-user.fields.recharge_balance_point_fail'));
        }

        $user = AdminUser::find($userId);
        if (!$user) {
            throw new \Exception(admin_trans('yk-user.fields.recharge_user_not_exists'));
        }
        DB::beginTransaction();
        DB::connection('mysql_user_backend')->beginTransaction();
        try {

            // 更新原表balance
            $oldBalance = $user['balance'];
            $newBalance = bcadd($oldBalance, $balance, 3);
            $user->balance = $newBalance;
            $user->save();

            // 增加充值日志
            AdminUserBalanceDetailService::createRecord($userId, AdminUserBalanceDetail::TYPE_RECHARGE, AdminUserBalanceDetail::CHANGE_TYPE_ADD, $oldBalance, $balance, $newBalance);

            DB::commit();
            DB::connection('mysql_user_backend')->commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            DB::connection('mysql_user_backend')->rollBack();
            throw new \Exception($e->getMessage());
        }

        return $this->response()->success(admin_trans_field('handle_success'))->refresh();
    }

    /**
     * Build a form here.
     */
    public function form()
    {
        $this->decimal('balance', admin_trans_field('recharge_balance'))->required();
    }

    /**
     * The data of the form.
     *
     * @return array
     */
    public function default()
    {
        return [];
    }
}

handle 就是表单提交之后的操作

form 表单显示内容

定义好动作类和表单类之后,接下来就是在数据列表中引入了

控制器引入动作类

首先在控制器中的列表方法(grid)中增加一个action

protected function grid()
{
    return Grid::make(new AdminUser(), function (Grid $grid) {
        $grid->model()->orderBy('id', 'desc');
        $grid->column('id', admin_trans_field('data_number'))->sortable();
        $grid->column('username');
        $grid->column('balance');
        $grid->column('username');
        $grid->column('status')->radio(YkUserService::getStatusOption());
        $grid->column('created_at')->sortable();

        $grid->filter(function (Grid\Filter $filter) {
            $filter->equal('id');
        });

        $grid->setActionClass(Grid\Displayers\DropdownActions ::class);
        $grid->actions([new RechargeUserBalance()]);
    });
}

$grid->actions([new RechargeUserBalance()]);就是引入咱们上文定义好的动作类。

当然你在增加的时候可以通过某些条件来判断是否要显示,比如

$grid->actions(function ($actions) {
    // 获取当前行数据
    $row = $actions->row;
    // 判断条件
    if ($row->status == \App\Models\YkCollectBloggerTask::STATUS_COMPLETED && $row->collect_data_quantity) {
        // 追加动作(操作按钮)
        $actions->append(new BloggerDataFilterAction());
        $actions->append(new BloggerDataExportAction());
    }
});

又当然,比如你不想使用默认的查看、编辑、删除按钮,要自定义这些,又或者想要按照自己的要求,展示或不展示某些按钮,你还可以这样实现:

public function setTextActions(Grid $grid, array $showActions = ['view' => true, 'edit' => true, 'delete' => true]) 
{
    // 不显示原生查看、编辑、删除按钮
    $grid->disableViewButton();
    $grid->disableEditButton();
    $grid->disableDeleteButton();
        
    // 按需实现要展示那些操作按钮
    $grid->actions(function (Grid\Displayers\Actions $actions) use ($grid, $showActions) {
        $key = $actions->getKey();
        $resource = $grid->resource(); // 获取资源路径
        $viewUrl = admin_url("$resource/$key");
        $editUrl = admin_url("$resource/$key/edit");

        $parts = [];

        if (!empty($showActions['view'])) {
            $parts[] = '<a href="'.$viewUrl.'"><i class="feather">查看</i></a>';
        }

        if (!empty($showActions['edit'])) {
            $parts[] = '<a href="'.$editUrl.'"><i class="feather">编辑</i></a>';
        }

        if (!empty($showActions['delete'])) {
            $parts[] = '<a data-url="'.$viewUrl.'" data-message="ID - '.$key.'" data-action="delete" data-redirect="'.$resource.'" class="feather grid-row-delete" style="cursor:pointer">删除</a>';
        }

        $actions->append(implode('&nbsp;|&nbsp;', $parts));
    });
}

这种的使用场景在一些日志类的列表展示中

image-20251120111956141.png

工具表单不仅可以用在动作上,还可以用在工具上,比如说你想在列表上面工具栏,增加“导入”按钮

image-20251120112636090.png

那么你也可以用action来实现

$grid->tools(function (Grid\Tools $tools) {
    $tools->append(new ImportStaticIp());
});

不过ImportStaticIp的父类要发生变化了,从动作的RowAction变为AbstractTool,因为你要实现的是工具类,而非动作

/**
 * Class ImportAccount
 *
 * @package App\Admin\Actions\YkAccount
 * @author mingzhongshui
 * @date 2025/6/14
 */
class ImportStaticIp extends AbstractTool
{
    /**
     * @return string
     */
    protected $title = '';

    /**
     * @param $title
     */
    public function __construct($title = null)
    {
        $this->title = admin_trans('yk-account.fields.import_account');
    }

    public function render()
    {
        // 实例化表单类并传递自定义参数
        $form = ImportStaticIpForms::make();

        return Modal::make()
            ->lg()
            ->title($this->title)
            ->body($form)
            ->button('<div class="pull-right" data-responsive-table-toolbar="grid-table">

                        <a href="javascript:void(0)" class="btn btn-primary btn-outline">
    <i class="feather icon-plus-circle"></i><span class="d-none d-sm-inline">&nbsp;&nbsp;导入</span>
</a>

                    </div>');
    }
}

title也可以在__construct中实现。

数据表格

数据行日志展示

比如说有这样一个场景,发送任务列表,要显示发送成功的数量以及日志,那应该怎么处理呢?一般情况来说,就是无非在操作页增加一个日志按钮,点击按钮的时候跳转到一个新页面展示发送日志列表,但是在dcatadmin中有着不同的实现方式

可以在数据行,发送数量字段上,给一个弹框,当点击数量的时候,打开一个弹窗,显示发送日志列表。

实现方式也很简单,只要在grid方法,字段末尾追加一个弹框方法就可以

$grid->column('actual_send_quantity', admin_trans_field('send_success_quantity'))->display(function () {
    return YkMassMessageLog::where('mass_id', $this->id)->where('status', YkMassMessageLog::STATUS_SUCCESS)->count();
})->modal(admin_trans('yk-mass-message-setting.fields.send_success_details'), MassMessageSuccessLogTable::make());

modal方法有两个参数

image-20251120132340610.png

参数1:打开弹窗的标题

参数2:回调函数,也就是要显示的内容

MassMessageSuccessLogTable类实现方式如下

// LazyRenderable异步加载类
class MassMessageSuccessLogTable extends Grid\LazyRenderable
{

    public function grid(): Grid
    {
        return Grid::make(new YkMassMessageLog(), function (Grid $grid) {
            // $this->key 就是当前行的id
            $grid->model()->where('mass_id', $this->key)->where('status', \App\Models\YkMassMessageLog::STATUS_SUCCESS);
            $grid->column('id');
            $grid->column('content_type', admin_trans('yk-mass-message-log.fields.content_type'))->display(function ($v) {
                return self::getTypeOptions()[$v] ?? '';
            });
            
            // 分页数量
            $grid->paginate(10);
            // 不显示操作列
            $grid->disableActions();
            // 不显示多选
            $grid->disableRowSelector();
        });
    }

这样就可以实现,在列表中任意字段实现弹窗列表功能了;

总结

以上就是我认为dcatadmin2.x中,比较常用但是文档描述不清楚的功能点了。

一、前言

在工作的这么多年中,其实有很少能接触到挑战自我的项目。在小公司当个小组长,无非就是curd、部署项目、搭建gitlab、review同事代码等繁琐的工作,偶尔写写前端。大公司的话,工作内容其实就没这么繁琐了,部署项目有运维,页面有前端同事,review有部门leader,工作中80%的时间都是curd,当然会有一些小型的基于需求的项目来做,但都是能力范围之内的。

在我这短暂的开发生涯中,还真就遇到过那么一次让我觉得非常有挑战性的事,那就是从php转python,从0到1实现爬虫架构。从结果上来说,虽然达不到100%的爬取成功率,但整个过程也可以说是倾尽了全力,当然这也是我为数不多的我绞尽脑汁想完成的项目(主要是这个项目完成之后的绩效比较诱人,咳咳咳~),而且我对新东西、新事物都有种想挑战一下的心态。

今天就来讲一下我是怎么从0到1实现整个爬虫架构的。

首先声明:我是主导者、也是参与者,当然后期还有更专业的python同事一起加入,帮我调优,给我优化意见,最终才合力完成的这个项目,不是我一个人的功劳。

阅读全文 »

前言

众所周知,SQL文化博大精深,SQL优化那阵算得上百花齐放,啥样的优化方式都有。而我虽然天天干着CURD的工作,和SQL天天打交道,其实说句实话,对SQL优化确实所会不多,只能算的上是皮毛。

这不,什么不会就来什么,刚入职还在试用期的时候,老大派来一个任务

:“小陈,我们有个开户的流程,从其他平台迁移过来,你对接下”

:“好的,老大”

:“对了,还有一个开户的动作,特别慢,要半小时甚至更久才能完成,你顺便优化一下”

:“好的”

什么开户?什么流程?什么优化,刚接到这个问题的我也是一头雾水,后面再深入了解了一下,大概明白了这个问题的来龙去脉

阅读全文 »

一、引言

不管是在电商系统、还是物业系统中都会存在一个账单的东西,账单号是每一个账单的标识,这个标识是唯一的、且不可重复的。

在电商系统中,账单号就等同于订单号;在物业系统中,账单号就是每一次向业主催缴费的账单。

这个账单号一旦重复结果可以说是灾难级的,在物业系统中,收费的时候,明明不是这个业主的钱,怎么多算了;而另一个业主的钱甚至少算,或者该缴的费用被其他业主缴了等等,涉及到钱的问题那就是大问题了。

这篇文章,我们就来具体分析下这个问题,并给出解决方案。

阅读全文 »

一、前言

在我从事的工作经历中,其实接触的电商行业还挺多的,大概占经历的6、70%左右。

而在电商项目中,用到缓存的场景就很多了,像数据缓存、CDN缓存、页面缓存等等,redis是最常用的key-value数据库了,memechache虽然和redis同样都是内存数据库,但是使用的最多的还是redis。

在使用redis之前,先简单了解下redis的特点和数据结构吧;

阅读全文 »

有时候在安装PHP的时候,会漏掉一些平时不太常用的扩展,到以后再要去安装的时候,时常会因为版本不对出现各种问题,本文主要介绍一下通过简单几步安装扩展的方式。

阅读全文 »

2020已经过去快两个月了,才有(想)时(起)间(来)写一下过去一年的总结。其实在年初的时候,各大社区在做有关【2020总结】的时候就想参与一下,但是一直没有提起日程。对于过去一年的回顾以及新的一年的展望就从这篇文章开始吧。

阅读全文 »

前言

为了简化后续爬虫项目上线的步骤和流程,把爬虫API和定时任务的项目整合在一块了,由于涉及到相关的服务较多,也需要遵循修改服务、启动的先后顺序(不然该启动的服务没开,后续相关的服务受影响就尴尬了),所以在上线前,用了将近一个上午的时间,整理了一个上线流程。在上线时能够提供一些清晰的帮助。

上线步骤

包安装

安装grequests、itemadapter包

阅读全文 »

在工作中,已经陆陆续续使用爬虫做需求将近半年时间了,在这半年时间里,从一个python小白到爬虫入门,再至功能实现。从上午PHP到下午Python忙的焦头烂额到现在的PHP/Python随心切换,其中的曲折不言而喻,也着实走了不少弯路。但好在功夫不负有心人,在半年的时光里,使用Python的同时也和它一起成长。如今总结一下,希望可以帮助到有需要的同学。

学习篇

python

从最开始接触爬虫,首先最需要了解的就是python的环境搭建、语法及特性,网络上有很多相关的教程,以下列举几个我在学习过程中使用到的教程,对于Python的快速入门都能起到很大的作用

  • 廖雪峰老师的Python教程,最先就是在这里开启Python之旅的。一边看文章学习一边跟着写写demo

  • 菜鸟教程里的Python基础教程也可以作为快速学习使用

  • Python3文档,在忘记一些函数的时候可以很快速的找到

阅读全文 »

python 爬虫

前言

当我们在爬取网页的时候,有部分是静态的,这种类型的网页,我们采用一般的方法就能很容易爬取到数据。但有些网页爬取的门槛还是有的,是动态的,是通过js渲染(包括ajax)出来的,这类型的网页采取一般的爬取方式就不行了,会出现爬取不到指定的数据。这时候,就要换种思路来解决了。所谓道高一尺,魔高一丈。本篇文章来介绍一下采用Splash和selenium来爬取动态网页,并对比一下两者的区别。

阅读全文 »