最近接到一个波及领取的需要,旧代码看的有拍板大,所以捋了捋逻辑,看了下工夫,还是足够的,所以就重写了一遍领取模块,抽空记录一下过程。

问题所在

  • 全副领取走对立的二维码生成接口,导致须要通过 type 辨别接管不同的字段,随着领取形式越来越多,参数判断越来越多,难以保护
  • 代码解构凌乱,一个 $data 变量贯通整个办法,导致最初不晓得 $data 变量外面什么数据,开发、排错越来越简单
  • 异样解决,业务代码处处抛出 \Exception 和捕捉 \Exception ,导致如果程序遇到了零碎异样也不能及时的告诉谬误

革新前的一段伪代码

  1. 所有业务逻辑谬误也抛出 \Exception 异样,捕捉 \Exception 后返回 下单失败 导致如果程序遇到真正谬误时,无奈及时排查谬误
  2. 单看 checkVerifyType() 办法名会认为只是查看领取 type 是否正确, 但却不是,这个办法把所有该干不该干的事都干完了
  3. 传参用 01 也不能明确晓得是代表什么货色
  4. qrcode 接口参数也很简单,例:type = 1时,必须要 code 参数; type = 2 时,必须要 price 参数;type = 3 时 ....
  5. $data 外面各种数据,有:申请数据,订单长期数据,订单预览数据,依据购买商品的不同又放入不同的数据,后果 $data 就是个大杂烩,批改起来切实一言难尽
// 所有购买入口获取二维码的入口public function qrcode(Request $request){    try {          // ...        $key = $this->checkVerifyType(0, 1);          // ...        return $key;    } catch (\Exception $e) {        return '下单失败';    }}

*/Service/PayService.php

public function checkVerifyType($payType1 = 0, $payType2 = 0){    $data = request()->all();    if (!ctype_digit(strval($data['type']))) {        throw new \Exception('type err');    }        // ..... 还有一堆的参数验证    switch ($data['type']) {        case 'vip':            // ... 验证            $data['vip_info'] = Vip::where('code', $data['code'])->first();            break;        case 'recharge':            // ... 验证            $data['money'] = $data['money'];            break;        // case...    }    // 优惠券判断    if ($data['coupon_id']) {        $money = Coupon::where('id', $data['coupon_id'])->value("money");        $data['reduce'] = $money;        // ....    }    // 订单预览信息    $data['show_title'] = "购买一个会员";    $data['show_money'] = 100;      $key = "abcdefg";    Redis::set($key, $data);    return $key;}

着手革新

  1. 波及领取的模块有:开明会员、充值、购买单个商品等
  2. 开发领取流程:

    1. 生成二维码(生成长期订单 redis,返回 redis 零时订单 key
    2. 手机端确认购买信息(展现购买商品信息)
    3. 手机端确认领取 (通过长期订单的 key ,创立一条订单数据到数据库)
    4. 依据长期订单的 key 创立订单
    5. 拉起领取
    6. 回调
  3. 波及到的设计模式

    1. 策略模式
    2. 简略工厂模式

后期筹备

原来的返回格局:

public function json($code, $msg, $data){    return ['status' => $code, 'message' => $msg, 'data' => $data];}// 调用json(200, "Ok", []);

尽管没什么大问题,但调用起来不太不便,也不直观,每次还须要传入一些不必要的参数,这里减少一些罕用的返回办法

BaseContrller 中减少几个返回数据的办法,不便调用

const SUCCESS_CODE = 200;const SUCCESS_FAIL = 100;protected function success($msg = 'ok', $data = [], $code = self::SUCCESS_CODE){    return ['status' => $code, 'message' => $msg, 'data' => $data];}protected function data($data = [], $msg = 'ok', $code = self::SUCCESS_CODE){    return ['status' => $code, 'message' => $msg, 'data' => $data];}protected function fail($msg = 'ok', $data = [], $code = self::SUCCESS_FAIL){    return ['status' => $code, 'message' => $msg, 'data' => $data];}

按模块辨别不同的下单链接

由原来的对立 qrcode 链接分出 3 个接口,每个接口只须要接管本人须要的参数就行,不须要原来的 type 来辨别参数
  1. 开明会员: /buy/vip
  2. 充值:/buy/recharge
  3. 购买商品:/buy/goods

创立长期订单策略

  1. 创立一个订单的 形象策略,定义算法的接口,所有策略必须实现长期订单的接口,

    app/Http/Services/PayOrder/PayOrderStrategy.php

    abstract class PayOrderStrategy{    abstract function createTemporaryOrder($request);}
  2. 创立一个 Context

    app/Http/Services/PayOrder/PayOrderStrategy.php

    class PayOrderContext{    private $strategy;    public function __construct(PayOrderStrategy $payOrderStrategy)    {        return $this->strategy = $payOrderStrategy;    }    public function createOrder(Request $request)    {        return $this->strategy->createTemporaryOrder($request);    }}
  3. 根底的策略框架曾经搭建好,当初就须要具体的策略了

    开明 vip 策略

    $request 是开明 vip 接口中传入的 $request

    app/Http/Services/PayOrder/Strategy/VipStrategy.php

    // 开明 vipclass VipStrategy extends PayOrderStrategy{      // 组装长期订单的数据,而后存入 redis      // 这里是 vip 策略,所以只专一 vip 须要的数据就好    function createTemporaryOrder(Request $request)    {        $packageCode = $request['code'];        $package     = app(PayOrderService::class)->getVipByCode($packageCode);        // 长期订单数据           $tmpOrder = [            'package_cope' => $package->toArray(),            'type'         => PayOrderService::TYPE_VIP,            'uid'          => 1,            'ip'           => $request->ip(),            // ....        ];              return app(PayOrderService::class)->saveTemporaryOrder($tmpOrder);    }}
  4. 创立一个订单服务类,写一些创立订单的公共办法

    app/Http/Services/PayOrderService.php

    use Ramsey\Uuid\Uuid;class PayOrderService{    const TYPE_VIP      = 1; // 购买 vip    const TYPE_RECHARGE = 2; // 充值    const TYPE_GOODS    = 3; // 购买商品    // 通过 code 查问 vip 套餐信息    public function getVipByCode(string $code)    {        // 这里应是从数据库获取数据返回        return collect(['id' => 1, 'code' => 'vip1', 'price' => 100, 'vip_day' => 30]);    }    // 保留长期订单    public function saveTemporaryOrder(array $tmpOrder)    {        $key = Uuid::uuid4()->toString();        Cache::set($key, $tmpOrder, 3);        return $key;    }}

    目前的目录解构

    app/Http/Services/

    ├── PayOrder│   ├── PayOrderContext.php│   ├── PayOrderStrategy.php│   └── Strategy│       └── VipStrategy.php└── PayOrderService.php

实现开明vip接口

所有接口的数据都是通过 laravel 表单申请验证

路由:routes/web.php

Route::get('/buy/vip', "PayController@vip")->name('vip');

app/Http/Controllers/PayController.php

public function vip(Request $request){    $strategy = new VipStrategy();    $tmpOrderKey = (new PayOrderContext($strategy))->createOrder($request);    return $this->data(['key' => $tmpOrderKey]);}
curl http://127.0.0.1:8000/buy/vip?code=vip1 | json{  "status": 200,  "message": "ok",  "data": {    "key": "35349845-0e76-4973-b240-67e7b3cdda42"  }}

长期订单已生成,当初须要须要开发手机扫码后的预览接口

预览订单

失常来说预览订单是每个领取都须要有的性能,所以减少一个形象办法

  1. app/Http/Services/PayOrder/PayOrderStrategy.php 新增一个 preview 的形象办法

    abstract class PayOrderStrategy{    // 创立长期订单    abstract function createTemporaryOrder(Request $request);         // 预览订单    protected function preview(array $tmpOrder)    {        throw new UnsupportedOperationException("不反对的办法");    }}

    你可能会好奇,这里预览订单为什么要抛出一个异样呢?因为有些第三方领取没有手机领取,只能 pc 端跳转,所以就不会波及预览这一说

    如果定义成 abstract 上面继承的办法有必须实现,这个非必须的就间接定义成 protected 并抛出一个异样,开发的时候如果谬误的调用了这个办法就会晓得,以后领取形式不反对订单的预览

  2. 开明 vip 策略实现 preview 办法,参数是长期订单的信息

    app/Http/Services/PayOrder/Strategy/VipStrategy.php

    function createTemporaryOrder(Request $request){ /*...*/ }function preview(array $tmpOrder){    $preview = [        'title'   => '开明会员',        'price'   => $tmpOrder['price'],        'vip_day' => $tmpOrder['vip_day']    ];    return $preview;}
  3. 预览订单接口

    这个接口返回一个页面,手机扫码收能够预览并且有下单按钮

    路由:routes/web.php

    Route::get('/buy/preview', "PayController@preview")->name('preview');

    app/Http/Controllers/PayController.php

    // 预览订单接口public function preview(Request $request){    // 申请下单接口后返回的长期订单 key    $tmpOrderKey = $request->get('key');    // 获取长期订单    $tmpOrder = app(PayOrderService::class)->getTemporaryOrder($tmpOrderKey);    if (!$tmpOrder) {        throw new TemporaryOrderException("订单已过期");    }    $strategy = new VipStrategy();    $preview = (new PayOrderContext($strategy))->preview($tmpOrder);    return view('preview', $preview);}
  4. 生成二维码

    前端申请 vip 接口之后应用返回的长期订单 key,作为 query 参数申请 预览订单 接口

    http://127.0.0.1:8000/buy/preview?key=35349845-0e76-4973-b240-67e7b3cdda42

    <div center="left">

     ![](https://cdn.jsdelivr.net/gh/zxr615/md-images/images/2020image-20210317142724525.png) ![](https://cdn.jsdelivr.net/gh/zxr615/md-images/images/2020image-20210317142241260.png)

    </div>

  5. 接下来就是立刻领取了,但立刻领取前还有个问题,预览订单接口的 策略抉择 仿佛有点问题,这里设计的预览订单接口不论是 开明 vip 还是 充值 都是申请这个接口,所以这里还须要判断一下购买的类型来调用不同的策略。

    这里用长期订单中的 type 来判断领取的类型,依据 type 来抉择策略

    public function preview(Request $request){    ...        $strategy = new \stdClass();    switch ($tmpOrder['type']) {        case PayOrderService::TYPE_VIP:            $strategy = new VipStrategy();            break;        case PayOrderService::TYPE_RECHARGE:            // $strategy = new RechargeStrategy();            break;        // ...    }    $preview  = (new PayOrderContext($strategy))->preview($tmpOrder);    return view('preview', $preview);}

    好嘛~,问题又来了,这 switch 看着有点不爽,再把它独立进去吧,加一个获取策略的 简略工厂 ,接下来优化这段抉择策略的代码

    创立 PreviewFactory 简略工厂

    touch app/Http/Services/PayOrder/PreviewFactory.php

    namespace App\Http\Services\PayOrder;use App\Exceptions\BusinessException;use App\Exceptions\TemporaryOrderException;use App\Http\Services\PayOrder\Strategy\VipStrategy;use App\Http\Services\PayOrderService;class PreviewFactory{    public static function strategy(string $key)    {        // 获取长期订单        $tmpOrder = app(PayOrderService::class)->getTemporaryOrder($key);        if (!$tmpOrder) {            throw new TemporaryOrderException("订单已过期");        }        $strategy = new \stdClass();        switch ($tmpOrder['type']) {            case PayOrderService::TYPE_VIP:                $strategy = new VipStrategy();                break;            case PayOrderService::TYPE_RECHARGE:                // return new Recharge();                break;            // ...            default:                throw new BusinessException('订单类型谬误.');        }        return $strategy;    }}

    当初再来看看 preview 接口

    public function preview(Request $request){    $tmpOrderKey = $request->get('key');    try {        $preview = PreviewFactory::strategy($tmpOrderKey)->preview($tmpOrderKey);    } catch (TemporaryOrderException $e) {        return $this->fail($e->getMessage());    }    return view('preview', $preview);}

发动领取

  1. 和创立订单策略同样,咱们也创立一个领取策略

    /Users/tuju/Project/pay/app/Http/Services/Payment

    Payment├── PaymentContext.php├── PaymentFactory.php├── PaymentStrategy.php└── Strategy    ├── AlipayStrategy.php    ├── UnionStrategy.php    └── WechatStrategy.php
  2. 定义领取接口

    PaymentStrategy.php

    interface PaymentStrategy{    public function pay(array $order);} 
  3. 上下文分割

    PaymentContext.php

    class PaymentContext{    private $strategy;    public function __construct(PaymentStrategy $paymentStrategy)    {        return $this->strategy = $paymentStrategy;    }    public function pay(array $order)    {        return $this->strategy->pay($order);    }}
  4. 获取领取策略的工厂

    PaymentFactory.php

    class PaymentFactory{    public static function strategy(string $payType)    {        switch ($payType) {            case 'wechat':                $strategy = new WechatStrategy();                break;            case 'alipay':                $strategy = new AlipayStrategy();                break;            case 'union':                $strategy = new UnionStrategy();                break;            // case...            default:                throw new BusinessException("领取形式不存在");        }        return $strategy;    }}
  5. 制订具体领取策略

    1. 支付宝策略
      Strategy/AlipayStrategy.php

      class AlipayStrategy implements PaymentStrategy{    public function pay(array $order)    {        /**        * 向支付宝申请        * @see 支付宝官网sdk https://github.com/alipay/alipay-easysdk/tree/master/php        * @see 第三方sdk https://github.com/lokielse/omnipay-alipay        */        return "https://www.alipay.com/";    }}
    2. 微信策略

    Strategy/WechatStrategy.php

    class WechatStrategy implements PaymentStrategy{    public function pay(array $order)    {        /**        *        * @see 微信官网 https://github.com/wechatpay-apiv3/wechatpay-guzzle-middleware        * @see 官网文档 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_6_2.shtml        * @see 第三方sdk https://github.com/lokielse/omnipay-wechatpay        */        return "https://pay.weixin.qq.com/";    }}
  6. 确认领取接口

    app/Http/Controllers/PayController.php

    public function pay(Request $request){    $tmpOrderKey = $request->get('key');    // pay_type=wechat|alipay|union    $payType = $request->get('pay_type');    // 解决订单数据、创立订单    $tmpOrder = app(PayOrderService::class)->getTemporaryOrder($tmpOrderKey);    $order = [ /** ... */];    $created = app(PayOrderService::class)->createOrder($order);    if (!$created) {        return $this->fail("领取失败, 请从新生成订单.");    }    // 发动领取    try {        // 后面咱们定义了一个领取策略工厂模式,帮忙咱们实例化策略,所以这里传入咱们的领取形式        // 工厂就会帮咱们对应领取策略返回回来,而后咱们再对立调用 pay() 这个办法        $strategy = PaymentFactory::strategy($payType);        // 个别第三方会返回一个领取跳转链接,点击确认领取的时候用户是曾经在手机页面了        // 所以间接跳转链接就能够拉起对应的领取了。        $url = (new PaymentContext($strategy))->pay($created);    } catch (BusinessException $e) {        $this->fail($e->getMessage());    }    // 跳转    return redirect($url);}
  7. 最初列出下最终策略模块的树状图

    ├── PayOrder 领取相干策略汇合│   ├── PayOrderContext.php│   ├── PayOrderStrategy.php│   ├── PreviewFactory.php│   └── Strategy 具体策略,如果要充值,则新建一个充值策略即可,新增的形式也不会影响到开明会员的相干性能│       └── VipStrategy.php 开明 vip 策略├── PayOrderService.php 一些专用办法└── Payment 领取相干策略汇合    ├── PaymentContext.php    ├── PaymentFactory.php    ├── PaymentStrategy.php    └── Strategy 具体策略,能够减少各种第三方领取        ├── AlipayStrategy.php 支付宝        ├── UnionStrategy.php 微信        └── WechatStrategy.php 银联

解决的问题

  • 将接口细分,不再是所有订单都进入同一个办法,解决了参数凌乱问题
  • 把大杂烩 $data 中的数据全副切分到每个不同的策略中去,而不是在办法中应用大量的 ifswitch 来解决,再减少类型时只须要关注新增的策略即可
  • 把接口数据用 laravel 表单申请验证 来判断,而不是在 controllerservice 层用 if 来判断
  • \Exception 代码全副替换成相应的业务异样

不足之处

  • $request 我认为还是不要往下传递比拟好,最好在控制器中解决,但整体领取逻辑还是比较复杂,传参的话又须要传入很多参数,临时也没有想出什么好的办法,所以还是决定将 $request 往下传递了。
  • 创立订单中的零时订单存入到 redis 后再获取,还是不能明确晓得数组里具体存入了什么数据,在 GO 中在序列化 json 时须要一个 struct 来反对,明确表名 json 中有什么字段,这样开发时既不容易出错,也缩小很多梳理代码的工夫;我认为能够新建一个 class 来模仿 GO 中的 struct 来明确 json 外面有什么数据。

总结

  1. 不要抛出 \Exception 异样,业务上的谬误异样应该抛出自定义异样
  2. 尽量不要去捕捉 \Exception 异样,\Exception 异样应该由顶层的 Handel 去解决;如遇到事务须要 rollback 的话,捕捉 Exception 后,在返回错误信息前,须要手动记录下异样的详细信息。
  3. 一段代码如果有两处以上用到,应该独立出一个公共办法。
  4. 参数的验证在管制层面就校验实现,不要再传到 service 中解决。
  5. 不要用 0, 1, 2 传参、判段等,不梳理上下文代码,切实是不晓得什么意思,如果扭转了其代号意思,则所有波及到的中央都须要批改判断,能够用常量来治理各种代号。

    public const PAY_STATUS_FAIL = 0;public const PAY_STATUS_OK   = 1;public const PAY_STATUS_WAIT = 2;public function give($payStatus){    // 最不明确的办法, 如果没正文真不知道什么意思    if ($payStatus == 1) {        // ...    }    // 比拟好的办法,即使没有正文,意思也比拟明确    if ($payStatus == self::PAY_STATUS_OK) {        // ...    }    // 我更喜爱用的办法,定义一个办法,看办法名知其意    if ($this->isPaid($payStatus)) {        // ...    }}// 是否已领取实现public function isPaid($payStatus){    return $payStatus == self::PAY_STATUS_OK;}
  6. 善用设计模式

最初,大家有什么改良之处,或者疑难之处欢送大家提出、斧正。