# phpGcoderTemplate **Repository Path**: qrqy/gcoder ## Basic Information - **Project Name**: phpGcoderTemplate - **Description**: gcoder是团队整理的PHP项目开发框架,其中集成了: - 快速生成代码,Model->Manager->(Service)->Controller,以及api和web的路由 - 鉴权机制,token放置在header中 - 缓存设置,通过env的配置设置 - 加密选项,进行接口加密 - 统一异常管理 - RBAC的权限控制 - 接口格式封装,ApiResponse - 一些常用的SDK - 一些标准模块的样例,例如用户注册、登录、下发验证码等 - 其他 - **Primary Language**: PHP - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2020-08-06 - **Last Updated**: 2025-06-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # gcoder概述 gcoder是团队整理的PHP Laravel项目开发框架,其中集成了: - 快速生成代码,Model->Manager->(Service & Trait)->Controller,以及api和web的路由 - 关于V2版本 By 海波 - 关于Schedule的写法 By 海波 - 鉴权机制,token放置在header中 - 缓存设置,通过env的配置设置 - 加密选项,进行接口加密 - 统一异常管理 - RBAC的权限控制 - 接口格式封装,ApiResponse - 一些常用的SDK - 一些标准模块的样例,例如用户注册、登录、下发验证码等 - 其他 gcoder可以说是团队智慧的结晶,可以较好地提升项目开发效率和规范性,每一个初入团队的小伙伴,都应该学习gcoder的使用,以便快速进入状态,参与项目开发协作。 ## gcoder版本 原gcoder版本均使用静态方法,没有使用面向对象的编程,但比较适合于当前团队的项目执行情况。 新版的gcoder仍旧没有使用类和对象的方法,究其原因,一切架构都服务于业务,如果我们的业务庞大到要有较好的架构来支持,那么建议使用SpringBoot/SpringCloud的框架,200万用户以上,PHP适合于中小型项目(通过模块化的管理也可以支持适当的中大型项目)的开发,随着项目的发展,代码重构是必不可少的工作。 ## gcoder的初衷 gcoder的初衷是提升效率、提升规范性、减低开发难度,开发人员只要有基础的开发能力就可以有效地参与项目开发工作。 TIPS:多年的项目实施经验告诉我们,代码只是工具,重要的是思维,那么要求后端人员具备的基本思维能力是抽象思考能力(数据模型)、流程化的思维能力(业务流程实现),再有就是考虑问题的全面性(接口的健壮性、规范性、安全性)等等 ## gcoder项目信息 请注意,本次gcoder使用laravel 7的框架,要求PHP 版本为7.4版本以上,在实际项目中,并不建议使用laravel7的版本,主要原因是: - 宝塔不支持PHP 7.4的扩展管理,自己安装扩展太麻烦,建议还是使用7.2的版本 - 一些扩展包是基于laravel 5.8框架的,所以从生态角度来讲,目前还是laravel 5.8更适合项目的开发工作 # gcoder的使用方法 gcoder的使用同gcode一样,具体为: - 修改.env文件,连接至项目数据库 ``` #数据库连接 mysql-测试 DB_CONNECTION=mysql DB_HOST=152.136.115.180 DB_PORT=3306 DB_DATABASE= DB_USERNAME= DB_PASSWORD= ``` - 运行php artisan auto:createFiles命令,将在storage的app/code目录下生成相关代码 请注意,不要在gcoder里面进行项目开发,请将生成的代码copy到目标项目中,然后全文替换一些路径 ## 关于生成的文件路径 生成的代码放置在storage/app/Code下,其中要求所有的Laravel项目必须使用分模块的方案实现,模块的命名规则为: - xxxPortal:UserPortal、AdminPortal为Api接口的模块,即其中暴露的都是Api,例如未来有门店Api,则命名为ShopPortal - xxxConsole:AdminConsole为blade模板,即blade模板 具体文件夹为: - Controllers:AdminConsole、AdminPortal、UserPortal,主要职责为接收参数、参数校验/转换、返回结果 - DB:数据库建立默认索引,主要是status和sort - Managers:Managers层数据库CURD的封装 - Models:模型文件 - Route:路由文件,分别映射Controllers的文件夹 - Traits:V2版本添加,主要用于Managers的扩展(非必要) ## 关于V2版本的修改 - V2版本与原版本并不冲突,原版本保持不变 - 新版的代码生成命令:php artisan auto:createFilesV2 - 代码生成在之前版本目录的v2文件夹下,Traits除外 ``` |--storage |--app |--code |--Controllers |--AdminConsole |--V2 |--AdminPortal |--V2 |--... |--Managers |--MySQL |--V2 |--... |--Traits ``` - 传递数组的优化,再往版本中前端传递的数组类型都是都是','分割的字符串,后端处理成数组。以后前端传递的就是数组类型的参数。 - 数据库枚举型字段的映射xxx_type、xxx_status等以后直接写在Model中,以往的Project随着项目的增长,将变得难以维护。但是公共status字段无需改动。 - 优化了Manager层getListByCon方法、setInfo方法; 以往我们在对数据库字段做增、删、改时需要同时对这两个方法一并进行修改。 现在manager添加了静态属性,专门维护数据库字段。无需改动这个两个方法,同时也减少了代码行可读性更高 ``` // 数据表全列数组,数据库增删字段时需要手动操作这里 protected static $table_columns = [ 'id', 'f_table', // ... ]; // ********* getListByCon start ********* // 根据传入的字段条件,拼装where条件(必须数据库中有的字段),对每次数据库字段的增删不需要调整这里 foreach ($con_arr as $column => $values) { if (in_array($column, self::$table_columns)) { // 如果values不是数组,认定等号操作 if (!is_array($values)) { $infos = $infos->where($column, $values); } else { // 非等于的查询 if (isset($values['operator']) && isset($values['value'])) { switch ($values['operator']) { // 可自行补充null、not_null、exists、not_exists等,默认=、<=、>=、<> default : $infos = $infos->where($column, $values['operator'], $values['value']); break; case 'in' : $infos = $infos->whereIn($column, $values['value']); break; } } } } } // 特殊的,例如需要调用闭包函数的需单独写,con_arr 中的字段名不能与数据库字段名重复 if (array_key_exists('search_word', $con_arr) && !Utils::isObjNull($con_arr['search_word'])) { $keyword = $con_arr['search_word']; $infos = $infos->where(function ($query) use ($keyword) { $query->where('name', 'like', "%{$keyword}%") ->orwhere('id', 'like', "%{$keyword}%"); }); } // 特殊的,例如年月日等日期的转换需单独写,year、month、day, con_arr 中的字段名不能与数据库字段名重复 if (array_key_exists('gte_created_at_date', $con_arr) && !Utils::isObjNull($con_arr['gte_created_at_date'])) { $infos = $infos->whereDate('created_at', '>=', $con_arr['gte_created_at_date']); } // ********* getListByCon ********* // ********* setInfo start ********* // 对每次数据库字段的增删不需要调整这里 foreach ($data as $column => $value) { if (in_array($column, self::$table_columns) && !Utils::isObjNull($value)) { $info->{$column} = $value; } } // ********* setInfo end ********* ``` - 新增了Traits层,它是Manager层的扩展;在实际业务中manager的提供的方法还远远不够,我们会在manager添加很多方法, 虽然奇哥新增了service层,但是manager层还是非常臃肿,为了减少manager层的代码行数,利用了trait特性来扩展manager, 所有的衍生方法写在Traits中,manager中use该trait,在控制器或Service中还可利用manager静态调用Traits的方法,非必要可不用。 - 对删除数据做了调整,在之前的版本中删除数据,有单个删除deleteById、批量删除bathDelete。现在统一改成了destroy,支持int型和array型的参数。 - 新增根据条件删除数据方法deleteByCon。涉及到缓存,建议使用 - 新增根据条件更新数据方法updateByCon。涉及到缓存,建议使用 - 新增根据不同id批量修改同一字段的不同值方法batchUpdateByColumn。以往遇到这种情况大家都是利用循环去更新数据库,这么做增加了数据库的连接次数浪费服务器资源。 现在,利用循环处理数据,直接调用此方法 ``` $update_data = []; foreach ($models as $k => $model) { $k ++; $arr = [ 'id' => $model->id, 'sort' => $k, ]; array_push($update_data, $arr); } XXXManager::batchUpdateByColumn($update_data); ``` 这么做的好处是,虽然利用了循环但是只做了一次数据库操作 - 关于图片存储,在以后的项目中建议单张图片存储字段用img,多张图片字段用images_json,前端处理完数据后直接插入数据库无需额外的处理 - 优化setNum方法。 ``` /** * 统一封装数量操作,部分对象涉及数量操作,例如产品销售,剩余数等,统一通过该方法封装 * * @param object $info model对象 * 一般都是在controller查询后进行计算,此处直接传递对象,无需重复查询 * @param string $column 操作字段 * @param int num 加减数值,有符号,加正号、减负号 * @return object|bool */ public static function setNum($info, $column, $num) { // 必须是数据库中的字段,info必须是model对象 if (!in_array($column, self::$table_columns) || $info == null) { return false; } // 如是负数此方法可正常加减 $info->increment($column, $num); // 如果开启缓存,则应该把值存入缓存 if (env('ENABLE_MODEL_CACHE', false)) { $key = "{{$var_name}}:" . $info->id; GLogger::processLog(__METHOD__, "开启Model缓存,删除数据需要同步删除缓存中的数据,缓存的key:" . $key, GLogger::LOG_DEBUG); CacheManager::forget($key); } return $info; } ``` ## 关于schedule的写法 Laravel的schedule以往我们的写法是直接写到Console/Kernel中,这样不利于项目维护,尤其是某些计划任务要重跑时,我们需要通过接口调用等方式来触发 接下来,我们的Schedule的写法统一封装为artisan命令,调用方法为: ```dotenv /** * Define the application's command schedule. * * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { // 每五分钟执行自动取消订单任务 $schedule->command('order:auto-cancel')->everyFiveMinutes(); // 每天凌晨02:00,执行计划任务,将全部的发货成功未确认并超时的订单,设置为确认状态 $schedule->command('order:auto-confirm')->dailyAt('02:00'); // 每天两点,执行计划任务,将全部确认的收货并超时的订单设置为关闭,, $schedule->command('order:auto-close')->dailyAt('02:00'); } ``` 这样做的好处是,如果计划任务执行有问题,我们修复后,可以通过artisan命令来执行 !!!另外,在设计计划任务时,要注意不要生成脏数据/错误数据,例如:计划任务在执行前要有前置条件,例如清分任务,需要判断一些是否已经清分过了(是否已经生成记录),不要出现数据重复的情况 ## 关于数据库表、字段的命名原则 请注意,为了有效生成接口文档和数据库文档,必须进行数据库表、字段的备注填写,表名的备注为对象中文名,例如: - user:用户账户 - pms_spu:产品SPU - pms_category:产品分类 一些常规的命名:昵称/nick_name;封面/cover等 随着我们项目经验的积累,以往我们很纠结数据库的字段类型,但实施了数据分析平台项目后,发现字段类型对检索效率等的影响并不大,因此针对数据库的字段定义制定如下规范,简单可落地,如有哪位同事有疑问或者意见,我们可以探讨。 - int:一般是id字段 - varchar(255):一般input值,不需要纠结,长度无所谓,姓名、昵称、手机号等 - int:sort、sale_num等数值类型数据,请注意之前我们的排序是seq,现在改为sort - datetime:created_at、updated_at、deleted_at - json:比较好用的字段类型,mysql5.7以上都可以支持,请注意,全部json类型的字段必须以json结尾,这样gcoder生成Model层代码的时候会自动设置 - text:一般备注、描述等不需要检索的长字段用text - mediumText:富媒体,全部富媒体字段都要用_html结尾,例如content_html - decimal:金钱 - double:经纬度 此外,所有的库表不允许使用s结尾,不要使用单复数一个的单词,例如商品不要起名为good,要起名为product 库表最好分模块,例如gcoder样例中的account_为账户模块,rbac_为权限模块 针对于一对一关系的表,那么需要将外键字段设置为唯一,例如user和user_purse表,在user_purse表中的user_id就应该是unique的索引,确保唯一性 ## 项目结构 本次进行了代码层次的抽象,本质上来说,与以往的项目结构一致,即: - Controller:作为最外层,职责为使用RequestValidator进行参数格式的校验,并返回数据 - Service:新增了Service层,将原来的Manager层进行了拆分,即有一些需要事务(Transcation)的代码,要封装在Service中,Service即对上层的Controller提供业务支持,不是要求全部的Controller都通过Service来支持,但是如果在复杂的业务场景下,就需要由Service来封装服务,支持上层业务 - Manager:基础的数据库CURD类,通过该方法,可以有效地进行根据条件查询、save等方法,在Manager层中,进行了缓存的改造,可以通过ENABLE_MODEL_CACHE进行开启Model层的缓存,缓存getById和getByIdWithTrash方法 - Model:在Model层,没有过多的变化,根据分析,仍不建议再Model层写过多的逻辑,但有些基础能力要在Model层使用,例如json的转换(一些字段建议存储为json)、数据填充和默认的加密存储(数据库层面)的加密存储 此外,Commponents是定义的组件库,其中Common文件夹下是标准化的文件方法,针对于Common文件夹下的方法: - ApiResponse:标准的响应方法,目前响应报文中的数据格式为code、data和msg,没有针对接口的幂等性进行处理 - DateTool:日期函数,一般也可以使用Carbon来进行日期的处理,但是仍旧不是很语义话 - GLogger:日志函数,针对日志统一进行管理 - RequestValidator:参数校验函数 - Utils:公共的工具类 其他的SDK为封装的一些三方SDK方法,Project.php为项目自身的方法和一些常量的定义 ## 关于接口文档自动生成 经过与呱呱团队交流,通过showdoc代码注释的方案生成接口文档更好一些,目前Gcoder在生成代码时,直接将接口文档生成,建议后续可以用类似于Swagger的方案,进行接口文档的生成 请参考showdoc文档https://www.showdoc.cc/page/741656402509783,一般情况下,我会把showdoc_api.sh放置在各个模块的Controller下,以便于比较便捷地生成文档 一般只针对UserPortal、AdminPortal这样需要外放接口Controller自动生成文档 ## 队列 队列在config/queue.php中,其中请参考gcoder的队列配置 鉴于目前的项目和团队状态,一般使用mysql或者redis作为队列驱动,下面以mysql为驱动讲解(一般项目使用mysql作为队列驱动即可) ```dotenv QUEUE_CONNECTION=database ``` ``` 'default' => env('QUEUE_CONNECTION', 'database'), ``` 运行以下命令,生成队列所用的表格,一般项目我们使用mysql作为数据库 建立queue_jobs表和queue_failed_jobs表,表名已经修改,因为jobs没有前缀,不易于管理,具体参看app/config/queue.php文件 ```dotenv php artisan queue:table php artisan queue:failed-table ``` 进行库表的迁移,生成库表 ```dotenv php artisan migrate ``` 请参考SendVerifyCode方法,其中改造了GLogger,队列是异步的,因此日志的管理非常重要,在队列中,会记录Request请求中的流水号,以便于在日志打印中,可以有效看到那些是队列打印的任务,可以用顺序的方法排查问题 正常启动队列,用守护进行来进行队里的守护,也就是确保队列的监听进程一直是启动状态,具体守护进程请参考laravel的队列文档 ``` php artisan queue:work --queue=high,defalut,low ``` ## 日志管理 针对Laravel日志,已经封装了GLogger方法,可以进行日志的打印,其种在Request和Response中,都进行了日志流水的管理,在项目中使用processLog打印日志即可 建议根据级别进行日志输出,即error>info>debug 特殊说明,针对队列,请注意获取流水号,在GLogger里面存在,然后传入队列,以便知道哪个队列任务是由哪个请求触发的 提供了RecordRequestLog中间件,用于存放请求日志信息,一般情况下应该用ELK进行日志缓存,但一般项目也可以通过开启ENABLE_REQUEST_LOG的方式,将请求信息进行缓存 ## 账户体系建设 本次对账户体系统一进行管理和封装,经过历史项目的归纳和总结,基本上账户体系由以下表组成 - user、admin:用户表、管理员表,用于存储用户、管理员的基础信息,相当于之前的user、admin表,后续还可以根据需求进行扩展 - account_login:登录信息表,相当于之前的login,但将admin_login、user_login进行了整合,即f_table、f_id机制来进行不同账户表的区分,其中增加了salt字段,也就是盐(请参考密码加盐处理部分),即密码经过md5后,md5加盐后存储在数据库,具体参考app/Services/UserService.php的相关方法 - account_auth:鉴权信息,即token、key、secret等信息放置在该目录中,其中key、secret为加密用的私钥 以上库表都进行了索引和主键的控制,具体参考项目文档中的SQL,具体位置在app/项目文档/01-常用库表中,可以直接执行SQL将库表建立出来 ## 接口加密(具体客户端接口调用请参考接口调用章节) 接口加密是确保系统安全的核心要素,目前框架使用AES进行参数加密的方式进行参数和报文的加密,具体客户端调用的方式详见app/Commponenets/SDK/AES下,具体的方法在app/Http/Controllers/Api下有,路由为test下的加密、解密方法 以往我们的加密本质上已经ok了,但是此次还是进一步提升一个级别,即默认的公钥和偏移量是放置在客户端的,具体值为 ```dotenv AES_KEY=9ouxQY9ALwFhQaFj AES_IV=FQFBCcQh59HNFr2M ``` 那么当用户未登录时,一些公共的接口都可以使用公钥来加密,例如获取城市信息等,一旦Header设定了Authentication,即有了token,那么我们默认本地就有了私钥,必须使用私钥进行加密,加密后后端也会根据token值获取私钥,再进行解密 具体开启加密的方法为env的启动应用加密 ``` #接口加密,公钥信息 ENABLE_APP_ENCRYPT=false TIMESTAMP_VALID_DURATION_MINUTES=30 ``` 请注意,新版的加密更加严格,增加了时间戳校验(报文有效时间15分钟),且每个用户一个秘钥,即用户登录后,为用户生成私钥,并随token一起下发,部分接口如果没有登录前,则通过公共秘钥进行加密处理 在测试环境中,为了方便测试,可以在Header中传入Enable-App-Encrypt为1的头,一旦传入就默认走加密,这样测试环境中也可以进行加密测试了 ## 一些文件的处理 一般情况下,如果需要生成海报等图片,那么需要放置在public/storage下,这样不会引发冲突,本质上就是要做好冲突文件的处理,避免生成的文件冲突 ## RBAC的权限控制(待补充) RBAC权限控制是大型项目权限管理的标准模块,目前已经整理了库表,rbac_开头的库表为RBAC权限控制类业务表,此处将进行进一步的完善 ## 一些工具 UserInfoGenerator:可以有效生成用户信息,包括手机号、昵称、姓名、头像等 DateTool:日期方法,可以尝试使用 ## 一些写法 一些基础的编码规范要遵循,具体如下: ### getInfoByLevel的level设定 以往,我们的level设定没有语义,都是用0,1,2的模式来支持,这里不好,后续我们的level设置为有语义的,即例如UserPurse表中,如果带用户信息,那么level的具体值为user ``` //ems_company: 公司信息 if (in_array('ems_company', $level_arr)) { //在Utils中有hidePhonenum等方法,可以进行数据脱敏 $ems_company = EmsCompanyManager::getByIdWithTrashed($info->ems_company_id); if ($ems_company) { $ems_company = EmsCompanyManager::getInfoByLevel($ems_company, "Y"); } $info->ems_company = $ems_company; } ``` ### 计划任务统一调用command 每一个计划任务都要封装为artisan的命令,这样做的好处是可以通过手动执行artisan命令的方式重跑计划任务 ``` // 每五分钟执行自动取消订单任务 $schedule->command('order:auto-cancel')->everyFiveMinutes(); // 每天凌晨02:00,执行计划任务,将全部的发货成功未确认并超时的订单,设置为确认状态 $schedule->command('order:auto-confirm')->dailyAt('02:00'); // 每天两点,执行计划任务,将全部确认的收货并超时的订单设置为关闭,, $schedule->command('order:auto-close')->dailyAt('02:00'); ``` ## env说明 ```dotenv #基础信息 #应用名称 APP_NAME=gcoder #应用中文名,会关联缓存,即生成的缓存的key为{APP_EN_NAME}:user:1 APP_EN_NAME=gcoder #应用环境,应用环境有local、develop、release、product四套环境,其中本地、开发、演示环境可以不加密,生产环境必须加密 APP_ENV=local #本地环境,与测试环境区别不大 #APP_ENV=develop #开发环境,也就是测试华逆境 #APP_ENV=release #演示环境,演示用的环境 #APP_ENV=product #生产环境,生产用环境 APP_KEY=base64:6Bua82n4JJz+su/EFNUkQ7Gf+8V5A7XrVilWaj6A/EU= #debug状态 APP_DEBUG=true #应用域名 APP_URL= #是否开启请求日志,日志将存入request_log表中,一般情况下可以不开启,如果开启日志记录,请创建request_log表 ENABLE_REQUEST_LOG=false #接口加密,公钥信息 ENABLE_APP_ENCRYPT=false #!!!请注意,全部项目不允许替换公钥 AES_KEY=9ouxQY9ALwFhQaFj AES_IV=FQFBCcQh59HNFr2M #开发公司名称 DEVEOPLER=北京炬牛科技有限公司 #默认密码Aa123456,一般建议生成随机密码,然后通过短信通知 DEFAULT_PASSWORD=afdd0b4ad2ec172c586e2150770fbf9e #测试环境的中文标识 APP_TEST_TITLE=测试环境- #日志通道相关 LOG_CHANNEL=single #缓存相关 #!!!慎重开启缓存,目前缓存使用Cache门面实现 #如果清理缓存请使用 php artisan cache:clear #Token缓存,一般都建议开启 ENABLE_TOKEN_CACHE=true #Model级别的缓存 ENABLE_MODEL_CACHE=true #Api接口幂等性管理,主要针对接口部分进行管理,接口不应该 ENABLE_API_UNIQUE_IN_3_SECOND=false #数据库连接 mysql-测试 DB_CONNECTION=mysql DB_HOST=152.136.115.180 DB_PORT=3306 DB_DATABASE=gcoder DB_USERNAME=gcoder DB_PASSWORD=JG5YEp2WF7ieGbRB BROADCAST_DRIVER=log CACHE_DRIVER=file QUEUE_CONNECTION=database SESSION_DRIVER=file SESSION_LIFETIME=3600 REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null #七牛存储 QINIU_DOMAIN= QINIU_ACCESS_ QINIU_SECRET_KEY= QINIU_BUCKET= #验证码模板 VERIFY_SMS_TEMPLATE_ID= #小程序账号 WECHAT_MINI_PROGRAM_APPID= WECHAT_MINI_PROGRAM_SECRET= WECHAT_MINI_PROGRAM_TOKEN= WECHAT_MINI_PROGRAM_AES_KEY= #腾讯定位 TENCENT_LOCATION_SERVICE_KEY=PYFBZ-IGC6W-3LWRO-RDSIZ-NZUEQ-A7FAQ TENCENT_LOCATION_SERVICE_SECRET_KEY=EiZUUydVx9XGpQqUPQSuFAvSMa4Thto #高德地图 AMAP_KEY=240a1e1819a5a45266b8c44f55e44cc7 AMAP_WEB_API_KEY=5b13738d16d3b917c105792114211ae0 #微信支付 WECHAT_PAYMENT_APPID= WECHAT_PAYMENT_MCH_ID= WECHAT_PAYMENT_KEY= WECHAT_PAYMENT_NOTIFY_URL= WECHAT_SUB_ORDER_REFUND_NOTIFY_NOTIFY_URL= ``` ## 一些公共的方法 公共方法没有放置到模块中,而是在主项目中,请注意查看 ### 验证码下发 在app/Http/Controllers/CommonController中,sendVerifyCode方法,验证码下发使用了队列,可以提升下发效率 ### 管理后台登录页面的验证码功能