LBS相关应用总结

根据ip获取位置机构信息

geoip

https://github.com/Loyalsoldier/geoip

本项目每周四自动生成 GeoIP 文件,同时提供命令行界面(CLI)供用户自行定制 GeoIP 文件,包括但不限于 V2Ray dat 格式路由规则文件 geoip.dat 和 MaxMind mmdb 格式文件 Country.mmdb

https://github.com/Hackl0us/GeoIP2-CN

小巧精悍、准确、实用 GeoIP2 数据库

https://github.com/maxmind/GeoIP2-php

geoip2 php sdk

https://github.com/Torann/laravel-geoip/tree/master/src

geoip laravel 扩展包

geoip2数据库官方下载地址

相关文章

深入浅出 Symfony2 – 结合 MongoDB 开发 LBS 应用

电子围栏

Laravel发送邮件操作流程笔记

官方文档描述比较全面,但是不够简洁以及没有操作流程,所以记录一下备忘。

本文是简单实用的快捷配置方式,使用smtp服务,具体需求看官方文档。

申请发件邮箱开启SMTP服务

以qq邮箱为例,授权码即使邮箱发送时验证的密码

网易163邮箱配置

以客户留言接收邮件通知为例 markdown邮件文档

//生成Mailables Markdown 邮件
php artisan make:mail CustomerFeedbackMail --markdown=emails.customer.feedback
如图会生成两个文件,然后编写代码

配置在浏览器中预览邮件 文档

//添加路由
Route::get('mailable', 'Pc\PageController@mailable');//预览mark邮件

//控制器方法
public function mailable(Request $request)
{
   return new CustomerFeedbackMail();
}

//简单编辑邮件模板feedback.blade.php
@component('mail::message')
# 邮件通知

The body of your message.

@component('mail::button', ['url' => ''])
进入官网后台管理
@endcomponent

@endcomponent

引入通知用到模型数据 文档

//预览页面控制器
class PageController extends Controller
{
    public function mailable(Request $request)
    {
        $message = Message::find(1);

        return new CustomerFeedbackMail($message);
    }
}

//邮件mailable
class CustomerFeedbackMail extends Mailable
{
    use Queueable, SerializesModels;

    public $message;

    public function __construct(Message $message)
    {
        $this->message = $message;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {   
        //emails.customer.feedback是视图文件目录和文件
        return $this->markdown('emails.customer.feedback', ['feedback' => $this->message])->subject('邮件标题');
    }
}

//邮件视图文件markdown
@component('mail::message')
# 客户留言通知
## 姓名:{{$feedback->name}},
## 电话:{{$feedback->mobile}},
## 留言:{{$feedback->content}},
## 提交终端:{{$feedback->terminal}},
## 提交页面名称:{{$feedback->page_name}},

@component('mail::button', ['url' => ''])
进入官网后台管理
@endcomponent

@endcomponent
预览效果

发送到指定邮箱 遍历收件人列表

//修改config/mail.php 配置,增加默认收件邮箱
'to' => [
   'address' => explode(',', env('MAIL_DEFAULT_TO_ADDRESS', '')),
   'name' => env('MAIL_TO_NAME', ''),
],

//修改env配置
MAIL_MAILER=smtp
MAIL_HOST=smtp.qq.com
MAIL_PORT=465
MAIL_USERNAME=xxxx@qq.com
MAIL_PASSWORD=xxxx
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=xxx@qq.com
MAIL_FROM_NAME=xxx
MAIL_DEFAULT_TO_ADDRESS=xxx@foxmail.com
MAIL_TO_NAME=留言通知

//发送代码,此处使用terminable中间件延时发送
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Mail\CustomerFeedbackMail;
use App\Models\Message;
class SendFeedbackMailTerminable
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request);
    }

    public function terminate($request, $response)
    {
        $response_data = json_decode($response->getContent(), true);

        if (!isset($response_data['data']['message_id']) || empty($response_data['data']['message_id'])) {
            return;
        }

        Log::info('留言id=>'.$response_data['data']['message_id']);

        $message = Message::find($response_data['data']['message_id']);

        if (!$message instanceof Message) {
            return;
        }

        $toAddress = config('mail.to.address');

        foreach ($toAddress  as $value) {
            try {
                Mail::to($value)->send(new CustomerFeedbackMail($message));
            } catch (\Throwable $th) {
                Log::channel('sendmail')->info('发送邮件失败'.$th->getMessage());
                continue;
            }

            Log::channel('sendmail')->info('[官网留言邮件通知记录]:', ['email' => $value, 'message_id' => $response_data['data']['message_id']]);
        }
    }
}

消息功能,站内信的设计实现思路汇总

消息功能和站内信的需求概括

B站消息功能界面截图

一般项目的消息功能都会有多种类型,比如订单状态推送,点赞推送,收藏,系统通知,到期提醒,系统公告(后台会有发送消息的需求)。同时可能会伴随多种渠道(短信,邮件,app push)的消息推送。

按推送用户数量分为,发送给全部用户,发送给部分用户,实现发送消息和已读状态更能,在用户数量不多(中小型项目)的情况下,最适合的方法是存中间表。

当用户数量增长之后,比如100万人同时点赞,每天产生的数据量非常大,不存数据库该如何实现?

方案一 使用 redis 的 bitmap

参考 一看就懂系列之 详解redis的bitmap在亿级项目中的应用

redis bitmap介绍

bitmap不是一个实际的数据类型,而是一组定义在String类型上的面向位的操作。由于字符串是二进制安全的blobs,其最大长度为512MB,所以它们适合设置2^32个不同的位。

位操作分为两组:恒定时间的单位操作,如将一个位设置为1或0,或获得其值,以及对位组的操作,如在给定的位范围内计算设置的位的数量(如人口计数)。

位图最大的优点之一是,它们在存储信息时往往能极大地节省空间。例如在一个系统中,不同的用户由递增的用户ID代表,只需使用512MB的内存就可以记住40亿用户的一个比特信息(例如,知道一个用户是否想收到通讯)。

位是用SETBIT和GETBIT命令来设置和检索的。

setbit key 10 1
(整数) 1
getbit key 10
(整数) 1
getbit key 11
(整数) 0
SETBIT命令的第一个参数是位号,第二个参数是要将该位设置为的值,即1或0。

GETBIT只是返回指定索引处的位的值。超出范围的位(寻址的位在存储到目标键的字符串长度之外)总是被认为是零。

有三个命令对位组进行操作。

BITOP在不同的字符串之间进行位的操作。提供的操作有AND、OR、XOR和NOT。
BITCOUNT执行群体计数,报告设置为1的位的数量。
BITPOS找到第一个具有指定值0或1的位。
BITPOS和BITCOUNT都能对字符串的字节范围进行操作,而不是对字符串的整个长度进行操作。下面是一个调用BITCOUNT的微不足道的例子。

setbit key 0 1
(integer) 0
setbit key 100 1
(整数) 0
bitcount key
(整数) 2
位图的常见用户案例有。

各种类型的实时分析。
储存与对象ID相关的节省空间但性能高的布尔信息。
例如,想象一下,你想知道你的网站用户每天访问的最长连贯时间。你从零开始计算天数,也就是你公开网站的那一天,并在每次用户访问网站的时候用SETBIT设置一个位。作为一个比特索引,你只需取当前的unix时间,减去初始偏移量,然后除以3600*24。

这样,对于每个用户,你都有一个包含每天访问信息的小字符串。通过BITCOUNT可以很容易地得到一个给定的用户访问网站的天数,而通过几个BITPOS调用,或者简单地获取和分析客户端的位图,就可以很容易地计算出最长的连绵时间。

将位图分割成多个键是很容易的,例如,为了分片数据集,以及在一般情况下,最好避免使用巨大的键。为了将一个位图分割成不同的键,而不是将所有的位设置成一个键,一个简单的策略就是每个键存储M个位,用位数/M获得键的名称,用位数MOD M获得键内的第N位。

//设置用户已读:
$redis->setBit('message:'.$msg_id, $uid, 1);

//获取是否读取状态:
$redis->getBit('message:'.$msg_id, $uid);

//支持千万级用户,并且不会有数据存储方面的压力

另外:bitmap 还可以做签到,活跃统计,在线状态等等

参考文章

站内信需求设计——人人都是产品经理

技术方案参考文章

消息系统(功能)的设计与实现

消息功能中系统通知这一类信息的已读未读除了用数据表之外有没有比较好的解决方案

站内信设计总结——掘金

站内信设计思路——人手一本PHP工具书

37 | 计数系统设计(一):面对海量数据的计数器要如何做?——极客时间

38 | 计数系统设计(二):50万QPS下如何设计未读数系统?——极客时间

一看就懂系列之 详解redis的bitmap在亿级项目中的应用

如何准确的评估工期

——视频出资微博视频大佬 蛋疼的axb

问题1,过于乐观评估,导致延期,会被leader认为不靠谱

估计三天写完,但是遇到了问题,又花了三天,又遇到问题,又…

问题2,过于保守的评估,也会被认为不靠谱,leader预期一两天就完成的任务

你处于保守估计给出了一周甚至更多的工期,问你哪里有问题,你又说不明白, leader觉得还不如自己做。

以上问题会导致有重要任务就不找你了。

保守评估时给出技术难点困难, leader信服。

解决方案

1.客观正确的评价自己的能力

不要一上来就编码,花2/3的时间设计和测试(测试驱动开发),1/3最多1/2的时间用来编码

2.正确的认清你要做什么事情

如果事情以前不是由你主导到,首先要花时间了解来龙去脉,尤其是注意有没有坑。然后再花时间去预估工期

3. 将开发评估工时当做一种必要的习惯来训练。相似度非常高的项目可以套用,工期评估更准确。

具体的方法

将具体需要的完成的工作列出来,比如划分成多少个模块,需要建多少个表,完成多少个接口,写多少测试。

如何避免成为一个不靠谱的菜鸡程序员

软件开发的绝大部分时间都在和风险打交道,要提升自己预防和对抗风险的能力。

知道自己的能力有限(比如理解慢反应慢思考慢),掌握的知识有限,掌握的信息有限,在做相应事情之前要做充足的提前准备

在对方案之前你需要了解更多的关于方案的背景知识;

在提交测试之前,一定要自己多测试几遍,因为你写的代码一定会有bug;

做设计之前提前想好各种问题的解决方案;

过于追求完美导致的风险

想做代码重构导致上线推迟了;

想修复bug发现不是bug是feature;

越是追求100分的程序员越是需要知道60分的可贵,先做到可用的状态然后再进行优化,避免过早优化,过早优化是万恶之源

举例说明花十天工期完成1个项目

方案一,花9天开发到一个完美的状态,1天内完成测试联调上线这一系列的工作,

方案二6天完成一个基本版本的开发测试联调上线,剩下4天时间逐渐做迭代,应对风险能力强。

风险优先开发原则

按风险评估开发的顺序,风险越高,后果越严重的越需要往前排,这样能让你更早的遇到问题,即使你解决不聊这个问题,你也可以提前去查询资料,求助同事,或者把这个风险告诉主管提前得到预警。

程序员如何优雅的讨论排期

和产品达成共识,目标是提升整体事情的运行效率,推动事情发展。并不是一直加班就能解决问题,提示效率和兼顾质量和可维护性才是重点。

权限系统设计汇总

可能是史上最全的权限系统设计

权限系统设计模型分析(DAC,MAC,RBAC,ABAC)

有赞权限系统(SAM)

用户管理系统 – 用户权限设计从入门到精通

B端产品如何设计权限系统?4个要素,5个模型,2个行业案例

基于RBAC模型的权限系统设计(github开源项目)

权限模型

基于角色的访问控制(RBAC: Role-Based Access Control)

RBAC0模型

最简单常用的模型

RBAC1模型

RBAC2模型

基于核心模型的基础上,进行了角色的约束控制,RBAC2模型中添加了责任分离关系,其规定了权限被赋予角色时,或角色被赋予用户时,以及当用户在某一时刻激活一个角色时所应遵循的强制性规则。责任分离包括静态责任分离和动态责任分离。主要包括以下约束:

  • 互斥角色: 同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。比如财务部有会计和审核员两个角色,他们是互斥角色,那么用户不能同时拥有这两个角色,体现了职责分离原则
  • 基数约束: 一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配
  • 先决条件角色: 即用户想获得某上级角色,必须先获得其下一级的角色
rbac2

RBAC3模型

即最全面的权限管理,它是基于RBAC0,将RBAC1和RBAC2进行了整合

rbac3

基于属性的权限验证(ABAC: Attribute-Based Access Control)

自主访问控制(DAC: Discretionary Access Control)

强制访问控制(MAC: Mandatory Access Control)

PHP生态中成熟的权限工具包

laravel-permission

文档 中文翻译

相关文章

[扩展推荐] spatie/Laravel-permission Laravel 应用中的角色和权限控制

php-casbin 分享会-访问控制框架Casbin(社区版)本.pdf

Casbin 是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,支持多种语言

laravel-authz  think-authz 

文档

laravel-admin

文档

dcat-admin

文档

实现数学四则运算功能

需求概括

后台录入最小值,最大值公式,前台筛选时如果满足条件则进行公式计算

解决思路

公式的CRUD

数据库保存公式时,公式中的参数用{参数id}替代,例如公式为P/1+2,P的id为5 则公式写作{5}/1+2

查询后将参数id替换成具体参数名,删除时,将包含{id}的公式字段清空

公式验证

验证公式时,先解析出{id}的参数,验证数据是否存在,然后在验证公式是否合法

/**
 * 解析公式表达式中的参数
 *
 * @param string $formula 公式表达式
 * @return mixed array|bool
 */
public function parseParamsIds(string $formula = '')
{
        $value = $formula ? $formula : $this->formula_exp;
        preg_match_all('/\{(\d+)\}/', $value, $matches);

        if (! isset($matches[1]) || count($matches[1]) === 0) {
            $this->error_message = '公式中缺少数字类型产品参数';

            return false;
        }

        return $matches[1];
}
/**
  * 公式验证
  * @param string $formula_exp 公式表达式
  * @param array $params 公式参数
  * @return bool
  */
public function validateLegitimacy()
{
        $formula_exp = trim($this->formula_exp);

        //为空
        if ($formula_exp === '') {
            $this->error_message = '公式为空';

            return false;
        }

        //错误情况,运算符连续
        if (preg_match_all('/[\+\-\*\/]{2,}/', $formula_exp)) {
            $this->error_message = '公式错误,运算符连续';

            return false;
        }

        //空括号
        if (preg_match_all('/\(\)/', $formula_exp)) {
            $this->error_message = '公式错误,存在空括号';

            return false;
        }

        //错误情况,(后面是运算符
        if (preg_match_all('/\([\+\-\*\/]/', $formula_exp)) {
            $this->error_message = '公式错误,(后面是运算符';

            return false;
        }

        // 错误情况,)前面是运算符
        if (preg_match_all('/[\+\-\*\/]\)/', $formula_exp)) {
            $this->error_message = '公式错误,)前面是运算符';

            return false;
        }

        //错误情况,(前面不是运算符
        if (preg_match_all('/[^\+\-\*\/]\(/', $formula_exp)) {
            $this->error_message = '公式错误,(前面不是运算符';

            return false;
        }

        //错误情况,)后面不是运算符
        if (preg_match_all('/\)[^\+\-\*\/]/', $formula_exp)) {
            $this->error_message = '公式错误,)后面不是运算符';

            return false;
        }

        //错误情况,使用除()+-*/之外的字符
        if (preg_match_all('/[^\+\-\*\/0-9.a-zA-Z\(\)]/', $formula_exp)) {
            $this->error_message = '公式错误,使用除()+-*/之外的字符';

            return false;
        }

        //运算符号不能在首末位
        if (preg_match_all('/^[\+\-\*\/.]|[\+\-\*\/.]$/', $formula_exp)) {
            $this->error_message = '公式错误,运算符号不能在首末位';

            return false;
        }

        //错误情况,括号不配对
        $str_len = strlen($formula_exp);
        $stack = [];

        for ($i = 0; $i < $str_len; $i++) {
            $item = $formula_exp[$i];
            if ($item === '(') {
                array_push($stack, $item);
            } elseif ($item === ')') {
                if (count($stack) > 0) {
                    array_pop($stack);
                } else {
                    $this->error_message = '公式错误,括号不配对';

                    return false;
                }
            }
        }

        if (count($stack) > 0) {
            $this->error_message = '公式错误,括号不配对';

            return false;
        }

        //错误情况,变量没有来自“待选公式变量”
        $arr = preg_split('/[\(\)\+\-\*\/]{1,}/', $formula_exp);

        foreach ($arr as $key => $value) {
            if (preg_match_all('/[A-Z]/i', $value) && ! isset($this->params[$value])) {
                $this->error_message = '公式错误,参数不配';

                return false;
            }
        }

        return true;
}

公式计算方案

1.逆波兰表达式也叫后缀表达

class FormulaCalculate
{
    //正则表达式,用于将表达式字符串,解析为单独的运算符和操作项
    public const PATTERN_EXP = '/((?:[a-zA-Z0-9_]+)|(?:[\(\)\+\-\*\/])){1}/';

    public const EXP_PRIORITIES = ['+' => 1, '-' => 1, '*' => 2, '/' => 2, '(' => 0, ')' => 0];

    /**
     * 公式计算
     *
     * @param string $exp 普通表达式,例如 a+b*(c+d)
     * @param array $exp_values 表达式对应数据内容,例如 ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]
     * @return int
     */
    public static function calculate($exp, $exp_values)
    {
        $exp_arr = self::parseExp($exp); //将表达式字符串解析为列表

        if (! is_array($exp_arr)) {
            return 0;
        }

        $output_queue = self::nifix2rpn($exp_arr);

        return self::calculateValue($output_queue, $exp_values);
    }

    /**
     * 将字符串中每个操作项和预算符都解析出来
     *
     * @param string $exp 普通表达式
     * @return mixed
     */
    protected static function parseExp($exp)
    {
        $match = [];
        preg_match_all(self::PATTERN_EXP, $exp, $match);

        if ($match) {
            return $match[0];
        } else {
            return null;
        }
    }

    /**
     * 将中缀表达式转为后缀表达式
     *
     * @param array  $input_queue 输入队列
     * @return array
     */
    protected static function nifix2rpn($input_queue)
    {
        $exp_stack = [];
        $output_queue = [];

        foreach ($input_queue as $input) {
            if (in_array($input, array_keys(self::EXP_PRIORITIES))) {
                if ($input == '(') {
                    array_push($exp_stack, $input);
                    continue;
                }

                if ($input == ')') {
                    $tmp_exp = array_pop($exp_stack);
                    while ($tmp_exp && $tmp_exp != '(') {
                        array_push($output_queue, $tmp_exp);
                        $tmp_exp = array_pop($exp_stack);
                    }
                    continue;
                }

                foreach (array_reverse($exp_stack) as $exp) {
                    if (self::EXP_PRIORITIES[$input] <= self::EXP_PRIORITIES[$exp]) {
                        array_pop($exp_stack);
                        array_push($output_queue, $exp);
                    } else {
                        break;
                    }
                }

                array_push($exp_stack, $input);
            } else {
                array_push($output_queue, $input);
            }
        }

        foreach (array_reverse($exp_stack) as $exp) {
            array_push($output_queue, $exp);
        }

        return $output_queue;
    }

    /**
     * 传入后缀表达式队列、各项对应值的数组,计算出结果
     *
     * @param array $output_queue 后缀表达式队列
     * @param array $exp_values 表达式对应数据内容
     * @return mixed
     */
    protected static function calculateValue($output_queue, $exp_values)
    {
        $res_stack = [];

        foreach ($output_queue as $out) {
            if (in_array($out, array_keys(self::EXP_PRIORITIES))) {
                $a = array_pop($res_stack);
                $b = array_pop($res_stack);
                switch ($out) {
                case '+':
                    $res = $b + $a;
                    break;
                case '-':
                    $res = $b - $a;
                    break;
                case '*':
                    $res = $b * $a;
                    break;
                case '/':
                    $res = $b / $a;
                    break;
                }
                array_push($res_stack, $res);
            } else {
                if (is_numeric($out)) {
                    array_push($res_stack, intval($out));
                } else {
                    array_push($res_stack, $exp_values[$out]);
                }
            }
        }

        return count($res_stack) == 1 ? $res_stack[0] : null;
    }
}

2.使用eval()运行代码,要注意代码安全性,做特别严格的验证

数据用例,前台输入值P 输入5 ,L输入6,

$eval = '$result = ';
//计算公式
$formula = '{P}/2+{L}*3+23';
//接收输入值
$params = ['{P}','{L}'];
$input = [5,6];
$eval .= str_replace($params, $input, $formula);
$eval .= ';';
eval($eval);
echo $result,PHP_EOL;

3.两个堆栈,一个用来存储数字,一个用来存储运算符,遇到括号以后就递归进入括号内运算

参考

用PHP实现的四则运算表达式计算

PHP 实现后缀表达式(接受四则运算字符串,输出计算结果,附代码)

结合正则表达式验证数学公式(含变量,js版)

TDD测试驱动开发笔记

教程

learnku社区 Laravel TDD

TDD 构建 Laravel 论坛笔记

Testing Laravel 单元测试入门笔记

总结

在使用 TDD 进行开发时,请牢记以下三项法则:

  1. 在编写失败的测试之前,不要编写任何业务代码;
  2. 只要有一个单元测试失败了,就不要再写测试代码。无法通过编译也是一种失败情况;
  3. 业务代码恰好能够让当前失败的测试成功通过即可,不要多写;

要掌握PHPUnit的用法,如何测试方法,如何测试类,等等…

根据需求结果逆向分析开发的方式。先假设已经实现了需求的方法,然后一步步倒退,缺少什么就创建什么。

需求结果 => 结果方法 => 按需(缺少创建)代码的元素

根据需求准备好测试用例数据

非常适合复杂逻辑需求的推理分析。对于简单的一下就能想出方案的需求,处于节省时间的考虑,可以不用。

测试覆盖率不一定要100%,个人想法,测试要首先覆盖核心复杂迭代频率较高主要业务,简单的业务可以跳过。

和普通开发方式一样,首先要对项目的整体需求和完整逻辑有详细了解,要有基本的解决方案思路

使用渐进式的编码思路,在mvc框架做业务开发时,可以先在控制器方法写出完整的逻辑,即使代码非常多,然后再根据需求迭代使用设计模式或面向对象设计方法,按需要拆分封装成类或方法,完成由面向过程=>面向对象的演变

项目案例说明

要实现一个产品型号搜索候选项功能

需要给前端提供一个http api ,并且要满足各种情景的搜索条件,如图

1.假设这个接口已经存在了,然后创建一个商品接口测试类,写好接口测试方法和各种情境下的测试用例数据。

namespace Tests\Feature;
class GoodsPcApiTest extends TestCase
{ 
/**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_goods_search_option()
    {
        //只有分隔符
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'-',
        ]);
       //使用dump打印结果是为了方便调试,调试完毕可以注释掉
        dump('只有分隔符', $response->json());
        $response->assertStatus(200);

        //只有产品型号
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'AC013',
        ]);
        dump('只有产品型号', $response->json());
        $response->assertStatus(200);

        //产品型号+1分隔符
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'AC013-',
        ]);
        dump('产品型号+1分隔符', $response->json());
        $response->assertStatus(200);

        //产品型号+1分隔符和参数
        $response = $this->post('/api/goods/search-option', [
            'keywords'=>'AC013-d',
        ]);
        dump('产品型号+1分隔符和1参数', $response->json());
        $response->assertStatus(200);
       ...
    }
...
}

然后运行测试,此时测试必然报错,就需要去创建这个接口的控制器和方法以及路由

//路由文件routes/api.php
Route::prefix('goods')->group(function () {
...
    Route::post('search-option', [GoodsController::class, 'searchOption']); //搜索候选项
...
});
//控制器
namespace App\Http\Controllers\Api;
class GoodsController extends Controller
{
  ...
  public function searchOption(Request $request)
    {   
        //验证参数
        $request->validate([
            'keywords'=>'bail|required|string',
        ]);
        //需要根据分隔符-拆分数据
        $keywords = $request->input('keywords');
        $attribute = array_filter(explode($sep, $keywords));
        //要从模型查询,直接调用,如果测试时报错模型不存在就去创建模型
        //如果运行测试报错数据表不存在,就创建数据迁移
        GoodsParamsSku::select()->where()->get();
        //需要一个方法或类来实现某个功能,先调用,然后运行测试,报错functionName方法不存在
        $this->functionName()
 
    }
  ...
}

根据报错创建上述functionName的单元测试

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class xxxxFunctionTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function test_function_name()
    {
        $this->assertTrue(true);
    }
}

在mvc框架中如何封装业务和功能代码

在laravel中如何使用repository模式

单一的CRUD使用ORM,没有封装的必要,比如banner的增删改查

将单一的功能封装成类,比如验证码的发送和验证,数学公式计算,

具有复杂逻辑的同一类需求,购物车整套逻辑, 商品SKU筛选,订单价格计算器(优惠,抵扣,积分等),这里需求可能需要查询多个模型,或者需要复杂的处理数据

一般在框架中封装成单个类放到services目录中

//验证码肯定具有两个方法,发送和验证
//这种固定方法的类可以使用静态方法方便调用
class VerificationCode
{
    const KEY_TEMPLATE = 'verify_code_of_%s';

    /**
     * 创建并存储验证码
     *
     * @param string $phone
     * @return int
     */
    public static function create($phone)
    {
        ...
    }

    /**
     * 检查手机号与验证码是否匹配.
     *
     * @param string $phone
     * @param int    $code
     *
     * @return bool
     */
    public static function validate($phone, $code)
    {
        ...
    }
}

第三方接口调用的封装

只有几个接口,封装成单个类,放到services目录中

需要调用很多接口,有场景和功能的划分,新建SDK目录,按接口功能类型或需求,使用设计模式(也可以不用)封装成简易SDK

一个项目示例

需要快速调用第三方接口完成业务需求,但是又没有顺手的sdk,或者过于复杂文档不全,学习成本过高,可以使用trait快速完成业务分割解耦,实现功能同时又易于维护。

使用http客户端guzzle,主文件sdkclient,实现签名认证参数组合 ,发送请求等方法。

<?php
/**
 * 契约锁SDK
 * https://open.qiyuesuo.com/document?id=2279605053767548928
 */

namespace App\Services\QiYueSuo;

use App\Exceptions\QiYueSuoException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Storage;

class SDKClient
{
   //使用trait 引用template业务相关接口
    use Template,Helps;

    public $config;

    public function __construct()
    {
        $this->config = config('services.qiyuesuo');
    }

    /**
     * 发送请求
     * 使用laravel封装的http客户端,DOC:https://learnku.com/docs/laravel/7.x/http-client/7487#introduction
     *
     * @param string $method 请求方法 'get', 'post', 'put', 'patch', 'delete'
     * @param string $subUrl 接口url,不包含域名
     * @param array $params 请求参数
     * @param string $content_type 请求类型 'asJson', 'asForm', 'asMultipart'
     * @param array $files 文件上传 $files['name']上传字段名称 $files['contents']文件内容,$files['filename']文件名,可选
     * @param bool $debug 是否开启debug日志,true是 false否
     * @return mixed
     */
    public function sendRequest(string $method, string $subUrl, array $params, string $content_type = '', array $files = [], bool $debug = false)
    {
        if (! in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) {
            throw new QiYueSuoException('Error Request Method');
        }

        if ($content_type && ! in_array($content_type, ['asJson', 'asForm', 'asMultipart'])) {
            throw new QiYueSuoException('Error Method');
        }

        $url = $this->config['api_host'].$subUrl;
        $headers = $this->playloadHeader();

        $http = Http::withHeaders($headers)->timeout($this->config['timeout']);

        //上传文件附件
        if ($files) {
            $http = $http->attach($files['name'], $files['contents'], $files['filename'] ?? null);
        }

        //请求类型
        if ($content_type) {
            $http = $http->$content_type();
        }

        //请求方法
        $response = $http->$method($url, $params);
        $headers = $response->headers();

        //记录debug日志
        if ($debug) {
            Log::channel('qiyueuso')
            ->info($url.'---->',
             [
                 'params' => $params,
                 'response_json'=>$response->json(),
                 'response_status' => $response->status(),
             ]);
        }

        //响应json
        if (isset($headers['Content-Type'][0]) && strpos($headers['Content-Type'][0], 'application/json') !== false) {
            $result_arr = $response->json();
        } else {
            abort(422, '契约锁,接口响应数据类型不是json');
        }

        //dd(json_encode($params), $result_arr);
        if (! $response->successful()) {
            //转换成422http异常
            abort(422, $result_arr['message']);
        } else {
            //状态码不是成功的响应错误信息
            if (isset($result_arr['code']) && $result_arr['code'] != 0) {
                abort(422, $result_arr['message']);
            } else {
                return $result_arr['result'] ?? [];
            }
        }
    }

    /**
     * 下载文件
     *
     * @param string $method 请求方法 get
     * @param string $subUrl 接口路径
     * @param array $params 请求参数
     * @param mixed $save_path 保存路径
     * @param string $disk
     * @return void
     */
    public function downloadFile(string $subUrl, array $params, string $save_path, string $disk = 'public')
    {
        $url = $this->config['api_host'].$subUrl;
        $headers = $this->playloadHeader();
        $response = Http::withHeaders($headers)
            ->timeout($this->config['timeout'])
            ->send(
                'get',
                $url,
                [
                    'query' => $params,
                    'save_to' => $save_path,
                ]
            );

        if (isset($headers['Content-Type'][0]) && strpos($headers['Content-Type'][0], 'application/json') !== false) {
            $result_arr = $response->json();

            if (isset($result_arr['code']) && $result_arr['code'] != 0) {
                //响应错误信息
                abort(422, $result_arr['message']);
            }
        }

        if ($response->successful()) {
            if ($save_path) {
                Storage::disk($disk)->put($save_path, $response->body());

                return $save_path;
            } else {
                return $response->body();
            }
        }

        return false;
    }

    protected function playloadHeader()
    {
        $headers['x-qys-open-timestamp'] = $this->timestamp();
        $headers['x-qys-open-accesstoken'] = $this->config['app_token'];
        $headers['x-qys-open-nonce'] = $this->nonce();
        $headers['x-qys-open-signature'] = $this->signature(
            $this->config['app_token'],
            $this->config['app_secret'],
            $headers['x-qys-open-timestamp'],
            $headers['x-qys-open-nonce']
        );

        return $headers;
    }

    private function timestamp()
    {
        $timestamp = time() * 1000;

        return $timestamp;
    }

    private function nonce()
    {
        return (string) Str::uuid();
    }

    private function signature($app_token, $app_secret, $timestamp, $nonce)
    {
        return md5($app_token.$app_secret.$timestamp.$nonce);
    }
}

使用trait解耦业务方法,模板管理相关业务,可以在方法中进行相关业务处理

<?php
/**
 * 模板管理
 */

namespace App\Services\QiYueSuo;

trait Template
{
    /**
     * 创建word模板
     * DOC:https://open.qiyuesuo.com/document?id=2784729992477544720
     *
     * @param array $params 请求参数
     * @param array $files 合同文件
     * @return array
     */
    public function v3TemplateCreatebyword(array $params, array $files)
    {
        return $this->sendRequest('post', '/v3/template/createbyword', $params, 'asMultipart', $files);
    }

    /**
     * 模板列表
     * DOC:https://open.qiyuesuo.com/document?id=2657160660600767485
     *
     * @param int $page 	  查询起始位置,默认为0 第几页
     * @param int $per_page   查询列表大小,默认1000
     * @param string $tenant_name  	查询条件:子公司名称,若传递了则查询子公司下的模板
     * @param string $modify_time_start  数据变更(创建、变更)时间的起始时间,格式为yyyy-MM-dd HH:mm:ss,默认无限制
     * @param string $modify_time_end  数据变更(创建、更新)时间的结束时间,格式为yyyy-MM-dd HH:mm:ss,默认无限制
     * @return array
     */
    public function v2TemplateList(int $page = 1, int $per_page = 15, string $tenant_name = '', string $modify_time_start = '', string $modify_time_end = '')
    {
        return $this->sendRequest('get', '/v2/template/list', [
            'selectOffset' => $page,
            'selectLimit' => $per_page,
            'tenantName' => $tenant_name,
            'modify_time_start' => $modify_time_start,
            'modify_time_end' => $modify_time_end,
        ]);
    }

    /**
     * 模板详情接口
     * DOC:https://open.qiyuesuo.com/document?id=2657160735708155950
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateDetail(int $templateId)
    {
        return $this->sendRequest('get', '/v2/template/detail', ['templateId'=> $templateId]);
    }

    /**
     * 编辑模板
     * DOC:https://open.qiyuesuo.com/document?id=2784730224355451710
     *
     * @param int $templateId 模板id
     * @param array $params
     *
     * @return array
     */
    public function v2TemplateEdit(int $templateId, array $params)
    {
        return $this->sendRequest('post', '/v2/template/edit',
            array_merge(['templateId' => $templateId], $params),
        );
    }

    /**
     * 下载模板
     * DOC:https://open.qiyuesuo.com/document?id=2784730224355451710
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateDownload(int $templateId)
    {
        return $this->downloadFile('/v2/template/download', ['templateId' => $templateId], false);
    }

    /**
     * 删除模板
     * DOC:https://open.qiyuesuo.com/document?id=2786490624780538307
     *
     * @param int $templateId 模板id
     *
     * @return array
     */
    public function v2TemplateRemove(int $templateId)
    {
        return $this->sendRequest('get', '/v2/template/remove', ['templateId' => $templateId]);
    }
}

以单一模型为主逻辑或功能封装到model中模型中封装方法过多时,可以用trait进行拆分

商品扣库存封装到商品模型中,

添加实体有很多关联表必须依赖主表,将添加主表数据和关联数据的方法封装到主表对应的模型中,举例:添加商品时,要添加商品规格,商品属性等等,将添加完整数据的方法,封装到商品模型中

更新商品的销量统计,需要查询其他关联表,在商品模型封装一个刷新统计的方法

Laravel中使用Repository的相关文章和总结

参考文章

在 Laravel 5.8 中正确地应用 Repository 设计模式

Laravel repository

Laravel 设计模式:Repository + Service 实战

关于 Repository 的设计模式

Laravel 中的存储库模式(Repository)

laravel5-repository 怎么拼接搜索条件

推荐:好用的 Laravel Repository 包

关于项目中 Repository 层的思考

为什么要在 Laravel 中使用存储库模式(Repository)?

在 Laravel 5 中使用仓库模式

使用该模式的项目

大商创电商

应用场景

抽象通用的查询,很多页面或接口,要展示相同内容,每次都要select筛选一堆字段和with一堆关联,还有加相同的查询条件,在需要的控制器中直接调用repository就能复用数据

todo,写个试用demo