1 Star 0 Fork 15

yanghxc / 禾匠2019

forked from secondyang / 禾匠2019 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
yii
Loading...
README

禾匠商城 - 核心

目录:

规范

数据库

以下是对 MySQL Internals Manual :: 26.1.1 Coding Style 的强调与补充。

  • 禁止设置字段允许 NULL,使用默认值代替。

  • 能用 Unique 索引限制唯一,则不要用 Index 索引。

  • 表、字段 Charset 统一 utf8,Collation 统一 utf8_general_ci,存储引擎统一 InnoDB

  • 类似 is_delete 的字段,统一使用 TINYINT(1) 类型,且务必建 Index 索引。

  • 除非情况特殊,严禁使用 TEXT / LONGTEXT / BLOB / LONGBLOB 等类型。

  • 图标、超过255字节的文本,尽量不要存进数据库,会引起 行存储溢出

  • 对于能够使用 INNER JOIN 的场景,尽可能少用 LEFT JOIN,且关联的字段务必建索引。

  • 数据库升级脚本内,对于 CREATE TABLE 等语句,务必加入 IF NOT EXISTS,尽可能避免引起异常。

  • 对于存储 URL 的字段,必须采用 VARCHAR 类型,建议长度:2048 - 8192,参见:https://stackoverflow.com/questions/2659952/maximum-length-of-http-get-request

  • JOIN ON 后面只带关联条件,将固定条件移动至 WHERE 后。

代码

以下是对 PSR-1PSR-2 的强调与补充;有关 PSR 公共规范,请参见:https://github.com/summerblue/psr.phphub.org/tree/master/psrs

  • 局部变量统一使用小驼峰,例如:$goodsList

  • foreach 修改数组元素的值,可以使用引用赋值的方式,例如:

    foreach($list as $k => $v){
        $list[$k]['foo'] = 'bar';
    }

    可以优化为:

    foreach($list as $k => &$v){
        $v['foo'] = 'bar';
    }
    unset($v); // 建议随手 unset,否则修改 $v 会改变数组末尾元素的值
  • 对于外部不需要访问的属性或方法,尽可能写成 protected / private,以增强健壮性。

  • 对于运行时可能出现的问题(例如类型错误等),尽可能采取「零容忍」态度,且报错越早越好;避免 return false,使用 throw new Exception

  • 尽可能减少代码冗余,将重复的部分提取凝练到一起,例如:

    public function couponInfo()
    {
        $coupon = Coupon::find()->where(['id'=>$this->id])->asArray()->one();
        if($coupon['appoint_type'] == 1){
            $info = Cat::find()->select('id,name')->where(['id'=>json_decode($coupon['cat_id_list'])])->andWhere(['is_delete'=>0,'store_id'=>$this->store_id])->asArray()->all();
        }else if($coupon['appoint_type'] == 2){
            $info = Goods::find()->select('id,name')->where(['id'=>json_decode($coupon['goods_id_list'])])->andWhere(['is_delete'=>0,'store_id'=>$this->store_id])->asArray()->all();
        }else{
            $info = [];
        }
        return [
            'code' => 0,
            'data' => [
                'coupon'=>$coupon,
                'info'=>$info,
            ],
        ];
    }

    可以优化为:

    public function couponInfo()
    {
        $coupon = Coupon::find()->where(['id'=>$this->id])->asArray()->one();
        switch($coupon['appoint_type'])
        {
            case 1:
            $info = Cat::find()->where(['id' => json_decode($coupon['cat_id_list'])]);
            case 2:
            $info = Goods::find()->where(['id' => json_decode($coupon['goods_id_list'])]);
            default:
            $info = null;
        }
        if($info){
            $info = $info->select('id,name')->andWhere(['is_delete' => 0, 'store_id' => $this->store_id])->asArray()->all();
        }
        return [
            'code' => 0,
            'data' => [
                'coupon'=>$coupon,
                'info'=>$info,
            ],
        ];
    }

    增强代码可读性,降低维护难度。

  • 数据库查询出来的原始数据,将格式转换封装进 模型获取器 进行处理。

  • switch 语句 return 后无需使用 break

  • switch 语句必须带 default 子句,如遇不可能值,则抛出异常。

  • 对于废弃的函数、语句、变量、类,严禁注释、抛出异常或 return,必须删除代码。

  • 禁止使用 rand 函数,请用 mt_rand 代替。

  • 禁止使用 md5 函数,请用 sha1 代替。

  • 提取二维数组内某元素的值作为一个单独的数组,建议使用 array_map

结构

Controllers

  • 控制器基础的方法名称统一,特殊方法可自定义。

  • 控制器中不写逻辑代码,所逻辑代码处理放在 ModelForm 中。

  • Admin 模块

    • actionIndex (列表显示页面)
    • actionCreate (添加数据页面)
    • actionStore (添加数据)
    • actionEdit (编辑数据页面)
    • actionUpdate (更新数据)
    • actionDestroy (删除数据)
    • 其它 (自定义名称)

    例:

    public function actionIdnex()
    {
        return 'index'
    }
    
    ...
    
    public function actionOther()
    {
        return 'other'
    }
  • Api 模块

    • actionIndex (列表数据)
    • actionStore (添加数据)
    • actionEdit (单条数据)
    • actionUpdate (更新数据)
    • actionDestroy (删除数据)
    • 其它 (自定义名称)

    例:

    public function actionIdnex()
    {
        return 'index';
    }
    
    ...
    
    public function actionOther()
    {
        return 'other';
    }

ModelForms

  • 每一个控制器对应一个 ModelForm 目录,目录名称和控制器名称相同。

    User +
         |- GetUserForm.php
         |- GetUserTypeForm.php
         ...
  • 单一原则,一个方法中只做一件事。

    错误:

    public function getUsers()
    {
        $users = User::find()->all();
        
        $data = [];
        foreach($users as $item) {
            $data[] = $item['name'];
            ...
        }
    
        return $data;
    }

    正确:

    public function getUsers()
    {
        $users = User::find()->all();
    
        return $data;
    }
    
    public function getResetUsers($users)
    {
        $data = [];
        foreach($users as $item) {
            $data[] = $item['name']
            ...
        }
        
        return $data;
    }
  • 需要获取关联数据时,尽可能采用 hasOne / hasMany 定义模型关系,替代原生的 leftJoin 查询;参见:https://www.yiiframework.com/doc/guide/2.0/zh-cn/db-active-record#relational-data

    错误:

    User:find()->alias('u')->leftJoin(['g' => Goods::tableName()], 'o.goods_id=u.id')->asArray()->all();

    正确:

    class User extends ActiveRecord 
    {
        public function getGoods()
        {
            return $this->hasMany(Good::className(), ['user_id' => 'id']);
        }
    }
    
    class UserFormModel extends Model
    {
        public function getUsers()
        {
            $users = User::find()->with('goods')->all();
            
            return $users;
        }
    }
  • FormModel 中进行数据查询时,尽量不要使用 asArray() 方法。否则导致模型特性消失,无法访问关联模型的数据。

    错误:

    User::find()->asArray()->all();

    正确:

    User::find()->all();
  • 在条件查询时,不要出现 type=1is_delete=0, status=2 等情况。

    错误:

    User::find()->where(['type' => 1, 'is_delete' = 0, 'status' => 2])->all();

    正确:

    class User extends ActiveRecord 
    {
        /**
        * 用户类型:管理员
        */
        const USER_TYPE_ADMIN = 1
        
        /**
        * 用户类型:会员
        */
        const USER_TYPE_MEMBER = 2
        
        /**
        * 用户状态:启用
        */
        const USER_STATUS_TRUE = 1
        
        ...
    }
    
    class UserFormModel extends Model
    {
        public function getUsers()
        {
            $users = User::find()->andWhere(['type' => User::USER_TYPE_ADMIN, 'status' => User::USER_STATUS_TRUE])->all();
            
            return $users;
        }
    }

Models

  • 所以关联关系写在对应的模型中(例子在 ModelForm 小节)。
  • 模型中一个字段有着多种情况,应在模型中定义成常量并标识该字段含义(例子在 ModelForm 小节)。

Common

  • 公共逻辑目录:core/models/common
  • 公共逻辑目录分为 Admin(后台管理)Api(小程序接口)
  • 在编写代码的过程中,如果有部分逻辑代码是通用则可以写在 Common 中。例如:创建订单、处理订单、支付等。

响应

HTTP API

响应格式:

{
  "code": <int>,
  "msg": <string>,
  "data": <array> | <object>
}
  • 在 Controller 中,可直接返回数组,以输出 JSON 数据:

    return ['code' => $code, ...];
  • 在 Filter 中,可直接使用如下方式设置响应数据,并截断执行:

    \Yii::$app->response->data = $some_response_data;
    return false; // 参见:<https://www.yiiframework.com/doc/guide/2.0/zh-cn/structure-filters#creating-filters>
  • api module 的 Controller 中,应使用如下方式输出结构化响应数据:

    return new \app\hejiang\ApiResponse($code, $msg, $data);

    或:

    return new \app\hejiang\BaseApiResponse($array);

    切勿在输出响应时使用废弃的 json_encoderenderJson 方法,将会引发 app\hejiang\exceptions\InvaildResponseException

模型验证错误

  • 若模型继承 app\models\Model,则可以直接使用 errorResponse 属性:
if(!$this->validate()){
    return $this->errorResponse;
}
  • 其它情况,可使用如下形式输出模型验证错误:
return new \app\hejiang\ValidationErrorResponse($model->errors);

切勿在输出响应时使用废弃的 Model::getModelError 方法。

注释

  • IDE 生成的注释记得改名字,例如:
/**
 * Created by PhpStorm.
 * User: <YOUR NAME>
 * Date: 2017/8/14
 * Time: 17:46
 */
  • 行内注释尽量首部带空格,例如:
// This is a function.
function foo(){
    // do nothing.
}
  • 对于注释无用代码,尽可能不要将 // 打在行首,会引起代码格式化时编辑器误判,例如:

    • 错误:
    function foo(){
        $bar = 1000;
    //    $bar = -1000;
        return $bar;
    }
    • 正确:
    function foo(){
        $bar = 1000;
        // $bar = -1000;
        return $bar;
    }

发行版

我们采用 git 的 tag 功能来实现对版本的精准控制。Gitee 自带了发行版(release)控制功能(基于 git tag),所以我们直接在 gitee 上使用图形化操作的方式创建即可。

创建发行版链接:https://gitee.com/zjhj/zjhj_mall/releases

  • 版本命名格式:v*.*.*,详细参见:https://semver.org/lang/zh-CN/
  • 标题命名格式:v*.*.* - 20**年**周
  • 描述采用多数 git 平台通用的 Markdown 文本格式,原生兼容 HTML;通常只需掌握基础用法即可,参见:Markdown-语法简明介绍

小程序前端图片

  • 图片目录:/web/statics/wxapp/images/
  • 代码添加:/modules/api/models/StoreForm.php
  • 前端应用:小程序图片数组已存入缓存,Key 值:wxapp_img;页面调用变量:__wxapp_img

异常

抛出与处理

多数情况,异常(exception)对我们可能比较陌生,多数都是框架或系统抛出异常,我们接收并处理。但实际上,如果能够在我们编写的实际业务代码中恰当地抛出异常,将会有意想不到的效果。下面我用几个符合我们实际场景的简明实例来说明。

优化前:

function actionIndex(){
    $result = '';
    $foo = Yii::$app->request->post('foo');
    if(empty($foo)) {
        $result = 'foo error';
    }
    else {
        $bar = Yii::$app->request->post('bar');
        if(empty($bar)) {
            $result = 'bar error';
        }
        $result = 'ok';
    }
    return $result;
}

如上可以发现,代码嵌套层数非常多,当然你可以优化成直接 return 的方式,但下面这个呢:

function actionIndex(){
    $result = '';
    $foo = getValueByName('foo');
    if($foo === null) {
        return 'foo error';
    }
    $bar = getValueByName('bar');
    if($bar === null) {
        return 'bar error';
    }
    return 'ok';
}

function getValueByName($name){
    $value = Yii::$app->request->post($name);
    if($value === null) {
        return null;
    }
    else if(empty($value)){
        return null;
    }
    return $value;
}

如上代码,存在的问题:

  • 错误丢失:在 actionIndex 内我们只能得到的值是 null,无法判断 getValue 内部具体错误是什么;虽然可以给 getValue 方法多增加一个 $error 参数返回具体错误,但在外层也需要多做判断,无疑大大增加代码复杂度,这不是我们想要的。
  • 依赖外层判断:在 getValue 方法内如果出现错误,例如某个值不存在,而这个值可能导致后续系统运行出现 BUG;如果我们直接返回 null,那就无法保证这个错误会在外部被处理,可能在外层代码压根不会有人判断,由此引发更大的隐患。

因此我们将代码进行优化,优化后:

function actionIndex(){
    $result = 'ok';
    try{
        $foo = getValueByName('foo');
        $bar = getValueByName('bar');
    }
    catch(\Exception $e){
        $result = $e->message; // 读取异常的 message 属性
    }
    return $result;
}

function getValueByName($name){
    $value = Yii::$app->request->post($name);
    if($value === null) {
        throw new \Exception('value is null'); // 参数为 message 属性
    }
    else if(empty($value)) {
        throw new \Exception('value is empty'); // 参数为 message 属性
    }
    return $value;
}

如上,代码清爽了不少。而且既能在不改变原有函数结构的基础上,将错误信息完整地传递到外层;又可以保证异常一定会被处理,否则就会报错(例如显示 Yii 框架的错误页面)。

其实,异常应该按照类型进行区分,根据不同类型的异常做不同的处理,比较规范的做法应该如下:

function actionIndex(){
    $result = 'ok';
    try{
        $foo = getValueByName('foo');
        $bar = getValueByName('bar');
    }
    catch(\yii\base\UnknownPropertyException $e){
        $result = $e->message . 'is null';
    }
    catch(\yii\base\InvalidValueException $e){
        $result = $e->message . 'is empty';
    }
    return $result;
}

function getValueByName($name){
    $value = Yii::$app->request->post($name);
    if($value === null) {
        throw new \yii\base\UnknownPropertyException($name);
    }
    else if(empty($value)) {
        throw new \yii\base\InvalidValueException($name);
    }
    return $value;
}

我们也可以自定义异常类,不过在大多数中小型系统的实际业务场景下,略显麻烦。

更多关于 PHP 异常处理的详细介绍,参见:http://www.w3school.com.cn/php/php_exception.asp

Sentry

目前我已经在框架层面完全集成 Sentry SDK,配置文件位于 core/config/web.php。在非 debug 模式下,只要产生未被处理的异常,就会被 Sentry 捕捉并记录到服务器,同时产生一条事件记录,对应此记录的 Event ID 将会被显示在页面上:

注:此页面的 view 位于 @app/views/error/ 目录下。

同时,你可以主动提交异常(exception)或消息(message)到 Sentry:

// 提交异常
\Yii::$app->sentry->captureException($ex);

// 高级用法,附带其他数据
\Yii::$app->sentry->captureException($ex, [
    'extra' => [ // extra 必须为数组
        'foo' => 'bar',
        '...' => '...'
    ],
]);

// 提交消息
\Yii::$app->sentry->captureMessage('my log message');

// 高级用法,格式化消息并附带其他数据
// 注意,格式化消息是只有 captureMessage 方法才具备的特性
\Yii::$app->sentry->captureMessage('my %s message', ['log'], [
    'extra' => [ // extra 必须为数组
        'foo' => 'bar',
        '...' => '...'
    ],
    'level' => 'warning' // 默认 error
]);

另外,你还可以在某些关键点记录「日常运行产生的数据(breadcrumbs)」。这些数据将会在发生异常或消息时,作为附属数据一并被提交到 Sentry;而在运行正常时,是不会提交到 Sentry 的。

\Yii::$app->sentry->breadcrumbs->record([
    'foo' => 'bar',
    '...' => '...',
]);

特性

调试模式

为了区别生产和调试环境,不同环境采用不同的日志级别、异常处理方式。目前,index.php 默认被定义为生产环境入口,原有 debug 特性迁移至 debug.php 以及 .env 文件。

简单来说,需要开启 debug 特性,有两种方法:

  1. 在远程调试客户错误时,将 URL 中 index.php 修改为 debug.php 即可。
  2. 开发过程中,本地在 core 文件夹下新建名为 .env 的文件,并配置环境变量即可(见下方说明)。

环境配置

环境配置(env)文件位于 core/.env;其文件模板位于 core/.env.example,根据情况取消注释即可。

业务代码中,可使用 env($name, $defaultValue) 函数读取环境变量的值。

Git 分支

关于新增 dev 分支后,有两种方式将本地改动提交到远程 dev 分支。

  • (1)

    git fetch origin dev ## fetch dev分支到本地
    git checkout dev ## 创建并切换到 dev 分支,且关联到远程的 dev 分支
  • (2)

    git fetch origin dev ## fetch dev分支到本地
    git branch --set-upstream-to=origin/dev master ## 将本地 master 分支关联到远程 dev 分支

辅助函数

请查看:core/helpers.php

缩略图

将图片地址修改如下即可:

https://<DOMAIN>/<PATH_TO_CORE>/web/thumb.php?src=<IMG_PATH>&size=<WIDTH>x<HEIGHT>&zoom=1

  • DOMAIN:域名
  • PATH_TO_CORE:指向 core 目录的 URL 地址
  • IMG_PATH:服务器上的图片绝对路径(务必确保是路径而非 URL)
  • WIDTH:预期宽度
  • HEIGHT:预期高度

更多详细说明,请参见:https://github.com/wi1dcard/thumb-php

数据序列化

目前我已经将数据序列化写成 Yii Component,请使用:

\Yii::$app->serializer->encode($data);
\Yii::$app->serializer->decode($data);

替代原有:

serialize();
unserialize();

运行环境

运行 core/web/tester.php 即可,将会检查所需扩展是否安装、配置是否正常。

例如(点击链接查看示例图片):

文件存储

用法参见:https://github.com/wi1dcard/yii2-hejiang-storage

在本项目内,可使用如下方式获取 StorageComponent 实例;其余操作与扩展包文档一致。

$storage = \Yii::$app->storage; // 获取 StorageComponent
// 或
$storage = \Yii::$app->storageTemp; // 获取用于存储临时文件的 StorageComponent

物流

用法参见:https://github.com/wi1dcard/yii2-hejiang-express

短信

用法参见:https://github.com/wi1dcard/yii2-hejiang-sms

事件

用法参见:https://github.com/wi1dcard/yii2-hejiang-event

在本项目内,可使用如下方式获取 EventDispatcher 实例;其余操作与扩展包文档一致。

$ed = \Yii::$app->eventDispatcher;

动作重定向

在 Controller 内,将某 Action 改为内部调用其它 Action,此种行为被称作 动作重定向

在本项目内,可使用如下方式实现:

class Controller
{
    public function actions()
    {
        return [
            'foo' => new \app\utils\RedirectAction('bar'), // 访问 `module/controller/foo` 将会被重定向至 `actionBar`
            // ...
        ];
    }

    public function actionBar()
    {
        return 'bar';
    }
}

控制器重定向

参见:https://wi1dcard.github.io/tutorials/yii2-redirect-controller-in-module/

本项目已集成至 app\modules\mch\Module,在此类的实例内,可使用如下方式实现:

$this->redirectController('bar', 'foo'); // 将 foo(虚拟控制器)重定向至 bar(实际控制器)

// 或者批量方式...

$map = [
    'foo' => 'bar',
    'foo/bar' => 'bar/foo'
];
array_walk($map, [$this, 'redirectController']);

响应下载

参见:https://wi1dcard.github.io/snippets/yii2-response-send-file/

工具集

如无特别说明,以下列出均为 Linux 命令;Windows 下可以使用 Git Bash 运行。

Git 仓库总文件数

git ls-tree -r --name-only HEAD | wc -l

批量替换数据库字段

参见:https://wi1dcard.github.io/snippets/mysql-replace-text-in-all-fields/

检查 Code-Standard

执行检查:

composer check-cs <DIR_OR_FILE>

尝试自动修复:

composer fix-cs <DIR_OR_FILE>

已知 Issues

  • WDCP 面板 Nginx + Apache 无法检测 HTTPS

    修改 /www/wdlinux/nginx/conf/naproxy.conf,新增一行 proxy_set_header X-Forwarded-Proto $scheme; 即可。

  • 宝塔面板上传文件 sha1_file 失败

    sha1_file(): open_basedir restriction in effect. File(/www/wwwroot/tmp/***) is not within the allowed path(s): (***)

    宝塔面板创建站点时默认添加 .user.ini,文件内包含 open_basedir 环境变量,参见https://blog.csdn.net/fdipzone/article/details/54562656。解决方案有二:

    1. 删除 .user.ini 文件。
    2. open_basedir=*** 后追加 : + 报错信息内文件所在目录,例如 /www/wwwroot/tmp/***
  • AMH 面板重复加载 MySQL 扩展

    PHP Warning: Module 'mysql' already loaded in Unknown on line 0

    解决方案:

    打开对应环境的 php.ini 配置文件(通常位于 /home/wwwroot/<ENV>/etc/amh-php.ini),使用 #; 注释 extension=mysql.so 此行即可。

空文件

简介

禾匠商城2019版本 展开 收起
PHP
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
PHP
1
https://gitee.com/yanghxc/hejiang-2019.git
git@gitee.com:yanghxc/hejiang-2019.git
yanghxc
hejiang-2019
禾匠2019
master

搜索帮助