共计 11006 个字符,预计需要花费 28 分钟才能阅读完成。
最近接到一个波及领取的需要,旧代码看的有拍板大,所以捋了捋逻辑,看了下工夫,还是足够的,所以就重写了一遍领取模块,抽空记录一下过程。
问题所在
- 全副领取走对立的二维码生成接口,导致须要通过 type 辨别接管不同的字段,随着领取形式越来越多,参数判断越来越多,难以保护
- 代码解构凌乱,一个
$data
变量贯通整个办法,导致最初不晓得$data
变量外面什么数据,开发、排错越来越简单 - 异样解决,业务代码处处抛出
\Exception
和捕捉\Exception
,导致如果程序遇到了零碎异样也不能及时的告诉谬误
革新前的一段伪代码
- 所有业务逻辑谬误也抛出
\Exception
异样,捕捉\Exception
后返回下单失败
导致如果程序遇到真正谬误时,无奈及时排查谬误 - 单看
checkVerifyType()
办法名会认为只是查看领取type
是否正确,但却不是,这个办法把所有该干不该干的事都干完了 - 传参用
0
,1
也不能明确晓得是代表什么货色 qrcode
接口参数也很简单,例:type
= 1 时,必须要code
参数;type
= 2 时,必须要price
参数;type
= 3 时 ….$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;
}
着手革新
- 波及领取的模块有:开明会员、充值、购买单个商品等
开发领取流程:
- 生成二维码(生成长期订单
redis
,返回redis
零时订单key
)- 手机端确认购买信息(展现购买商品信息)
- 手机端确认领取(通过长期订单的
key
,创立一条订单数据到数据库)- 依据长期订单的
key
创立订单- 拉起领取
- 回调
波及到的设计模式
- 策略模式
- 简略工厂模式
后期筹备
原来的返回格局:
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
来辨别参数
- 开明会员:
/buy/vip
- 充值:
/buy/recharge
- 购买商品:
/buy/goods
创立长期订单策略
-
创立一个订单的
形象策略
,定义算法的接口,所有策略必须实现长期订单的接口,app/Http/Services/PayOrder/PayOrderStrategy.php
abstract class PayOrderStrategy {abstract function createTemporaryOrder($request); }
-
创立一个
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); } }
-
根底的策略框架曾经搭建好,当初就须要具体的策略了
开明 vip 策略
$request
是开明 vip 接口中传入的$request
app/Http/Services/PayOrder/Strategy/VipStrategy.php
// 开明 vip class 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); } }
-
创立一个订单服务类,写一些创立订单的公共办法
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"}
}
长期订单已生成,当初须要须要开发手机扫码后的预览接口
预览订单
失常来说预览订单是每个领取都须要有的性能,所以减少一个形象办法
-
在
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
并抛出一个异样,开发的时候如果谬误的调用了这个办法就会晓得,以后领取形式不反对订单的预览 -
开明 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; }
-
预览订单接口
这个接口返回一个页面,手机扫码收能够预览并且有下单按钮
路由: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); }
-
生成二维码
前端申请 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>
-
接下来就是立刻领取了,但立刻领取前还有个问题,预览订单接口的
策略抉择
仿佛有点问题,这里设计的预览订单接口不论是开明 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); }
发动领取
-
和创立订单策略同样,咱们也创立一个领取策略
/Users/tuju/Project/pay/app/Http/Services/Payment
Payment ├── PaymentContext.php ├── PaymentFactory.php ├── PaymentStrategy.php └── Strategy ├── AlipayStrategy.php ├── UnionStrategy.php └── WechatStrategy.php
-
定义领取接口
PaymentStrategy.php
interface PaymentStrategy {public function pay(array $order); }
-
上下文分割
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); } }
-
获取领取策略的工厂
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; } }
-
制订具体领取策略
-
支付宝策略
Strategy/AlipayStrategy.phpclass 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/"; } }
- 微信策略
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/"; } }
-
-
确认领取接口
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); }
-
最初列出下最终策略模块的树状图
├── 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
中的数据全副切分到每个不同的策略中去,而不是在办法中应用大量的if
和switch
来解决,再减少类型时只须要关注新增的策略即可 - 把接口数据用
laravel
表单申请验证 来判断,而不是在controller
和service
层用if
来判断 - 把
\Exception
代码全副替换成相应的业务异样
不足之处
$request
我认为还是不要往下传递比拟好,最好在控制器中解决,但整体领取逻辑还是比较复杂,传参的话又须要传入很多参数,临时也没有想出什么好的办法,所以还是决定将$request
往下传递了。- 创立订单中的零时订单存入到 redis 后再获取,还是不能明确晓得数组里具体存入了什么数据,在 GO 中在序列化
json
时须要一个struct
来反对,明确表名 json 中有什么字段,这样开发时既不容易出错,也缩小很多梳理代码的工夫;我认为能够新建一个 class 来模仿 GO 中的struct
来明确 json 外面有什么数据。
总结
- 不要抛出
\Exception
异样,业务上的谬误异样应该抛出自定义异样 - 尽量不要去捕捉
\Exception
异样,\Exception
异样应该由顶层的Handel
去解决;如遇到事务须要rollback
的话,捕捉Exception
后,在返回错误信息前,须要手动记录下异样的详细信息。 - 一段代码如果有两处以上用到,应该独立出一个公共办法。
- 参数的验证在管制层面就校验实现,不要再传到 service 中解决。
-
不要用
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;}
- 善用设计模式
最初,大家有什么改良之处,或者疑难之处欢送大家提出、斧正。