Laravel 支付解决方案之 Laravel Cashier (二)—— 付费会员&分期付款&生成发票

上一节我们简单讲述了Laravel Cashier的安装配置,这一节我们将使用Laravel Cashier来实现一个常见的功能——付费会员。比如QQ、微博、优酷等应用都有这一功能,并且该功能已然成为许多网站收入的重要来源,可见其地位之重要,而在Laravel中我们可以借助Cashier通过Stripe轻松实现该功能,正如我们前面提到的,Laravel Cashier为我们封装了支付功能,所以我们不需要处理如何支付,也不需要关心任何与支付相关的细节,而只需要关心具体的业务逻辑。

1、在Stripe中创建收费计划

正所谓兵马未动,粮草先行,在正式编写业务逻辑代码之前,我们先要到Stripe个人中心创建订购计划。导航到https://dashboard.stripe.com/test/plans页面创建两个付费会员级别:Silver(银牌会员)和Gold(金牌会员):

在Stripe中创建银牌会员

在Stripe中创建金牌会员

 

注意页面左上角的TEST,我们目前是在Stripe的测试环境进行操作。

2、定义路由

创建好订购计划后,我们在routes.php中为业务逻辑定义好相关路由:

//用户个人主页
Route::get('profile','UserController@profile');
//用户会员级别
Route::get('service','UserController@service');
//付费会员页面
Route::get('subscription','UserController@subscription');
//处理付费逻辑
Route::post('subscribe','UserController@subscribe');
//升级到更高级别
Route::get('upgrade','UserController@upgrade');

3、付费会员实现

接下来自然而然就是到控制器UserController中编写业务逻辑代码,由于之前我们已经讲过如何实现登录认证,这里我们使用GitHub进行登录认证。认证完成后跳转到/profile页面,然后我们的业务逻辑由此开始。

用户中心

首先在profile页面会直接跳转到查看会员级别页面:

public function profile(Request $request)
{
    $user = $request->user();
    return redirect('service');
}

会员级别

然后在service页面我们编写控制器代码如下:

public function service(Request $request){
    $user = $request->user();

    if (!$user->subscribed()) {
        return redirect('subscription');
    }
    if($user->onPlan('silver')){
        $service = ['type'=>1,'name'=>'银牌会员'];
    }else{
        $service = ['type'=>2,'name'=>'金牌会员'];
    }
    return view('user.service',['service'=>$service]);
}

首先,我们从请求中取出认证用户实例,然后调用该实例上的subscribed方法判断用户是否是付费会员,如果不是的话跳转到付费页面,否则通过onPlan方法判断用户是银牌会员还是金牌会员,并渲染对应视图resources/views/user/service.blade.php

你已经是{{$service['name']}}<br>
@if ($service['type'] == 1)
    <a href="/upgrade">升级为金牌会员</a>
@endif

购买付费会员

升级的事情我们先按下不表,先来看看购买付费会员的实现逻辑。我们在UserController中编写subscription对应的代码如下:

public function subscription(Request $request)
{
    $user = $request->user();
    if($user->subscribed()){
        return redirect('service');
    }
    return view('user.subscription');
}

该页面逻辑很简单,先判断是否已经是付费会员,是的话跳转到会员级别页面,否则才会渲染购买付费页面,接下来我们定义其对应视图文件resources/views/user/subscription.blade.php如下:

<form action="/subscribe" method="post" id="subscription-form">
    <span class="payment-errors"></span>
    {!! csrf_field() !!}
    <div>
        付费计划:
        <select name="plan">
            <option value="silver">银牌会员</option>
            <option value="gold">金牌会员</option>
        </select>
    </div>
    <div>
        信用卡号:
        <input type="text" name="number" id="number">
    </div>
    <div>
        过期时间:
        月份:<input type="text" name="exp_month" id="exp_month">
        年份:<input type="text" name="exp_year" id="exp_year">
    </div>

    <button type="submit">订购服务</button>
</form>
<script type="text/javascript" src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script>
    Stripe.setPublishableKey('{{config("services.stripe.key")}}');
    jQuery(function($) {
        $('#subscription-form').submit(function(event) {
            var form = $(this);
            form.find('button').prop('disabled', true);
            Stripe.card.createToken({
                number: $('#number').val(),
                exp_month: $('#exp_month').val(),
                exp_year: $('#exp_year').val()
            }, stripeResponseHandler);

            return false;
        });
    });

    var stripeResponseHandler = function(status, response) {
        var form = $('#subscription-form');

        if (response.error) {
            form.find('.payment-errors').text(response.error.message);
            form.find('button').prop('disabled', false);
        } else {
            var token = response.id;
            form.append($('<input type="hidden" name="stripeToken" />').val(token));
            form.get(0).submit();
        }
    };
</script>

该视图文件相对比较复杂,在该视图中我们要选择购买的会员级别,填写信用卡号,以及信用卡对应的过期时间。会员级别对应的值即为我们在Stripe中创建订购计划时对应的计划ID,至于信用卡号和过期时间是用来生成stripeToken的,这个stripeToken就是我们在调用create创建订购计划时要传入的值。Stripe还贴心的为我们提供了一系列可用的信用卡测试账号:

卡号 类型
4242424242424242 Visa
4012888888881881 Visa
4000056655665556 Visa (debit)
5555555555554444 MasterCard
5200828282828210 MasterCard (debit)
5105105105105100 MasterCard (prepaid)
378282246310005 American Express
371449635398431 American Express
6011111111111117 Discover
6011000990139424 Discover
30569309025904 Diners Club
38520000023237 Diners Club
3530111333300000 JCB
3566002020360505 JCB

至于过期时间嘛,只要大于当前时间就好了。可能在别处你还会看到需要输入CVC,这是生成stripeToken的可选参数,既然是可选,这里我们就不费那个事了。

这个页面在浏览器中看上去是这样的:

付费会员支付页面

在点击“订购服务”之前,我们先来编写subscribe对应的代码:

public function subscribe(Request $request)
{
    $plan = $request->input('plan');
    $creditCardToken = $request->input('stripeToken');

    $user = $request->user();
    $user->subscription($plan)->create($creditCardToken);
    if($user->save()){
        return redirect('service');
    }else{
        return back()->withInput();
    }
}

可以看到我们调用:

$user->subscription($plan)->create($creditCardToken);

完成付费操作实现订购。

回到subscription视图页面点击“订购服务”,成功后页面跳转到之前的service页面,页面显示如下:

会员级别页面

升级付费会员

最后我们来处理升级逻辑,编写upgrade对应业务逻辑代码如下:

public function upgrade(Request $request){
    $user = $request->user();
    if (!$user->subscribed()) {
        return redirect('subscription');
    }
    if($user->onPlan('gold')){
        exit('您已经是金牌会员了');
    }
    try{
        $user->subscription('gold')->swap();
    }catch(Exception $ex){
        exit('升级失败!');
    }
    return redirect('service');

}

通过

$user->subscription('gold')->swap();

即可实现升级操作。

回到service页面点击“升级到金牌会员”,成功后页面跳转回service页面并显示如下信息:

你已经是金牌会员

好了,至此我们已经完成了付费会员功能的一般逻辑。后续我们将讨论除信用卡外如何支持更多支付方式,比如银联、支付宝、微信支付等。

4、分期付款实现

走完上面的所有流程后,你会发现该实现逻辑和另外一个应用场景非常类似,就是分期付款,我们可以将商品/服务总价格+利息分成若干期,然后算出每月应还款额,然后在Stripe中创建对应分期付款计划。剩下的业务逻辑实现和付费会员并无二致,将会员级别换成商品规格或对应服务级别,剩下的支付&升级参考其实现即可,这里不再赘述。

一次性付清

对于有时候我们需要对分期付款的物品进行一次性付清,比如剩余总额为100美元,可以通过调用如下方法实现:

$user->charge(100);

该方法有返回值,支付成功返回true,否则返回false

生成发票并下载

支付完成后,如果想要获取发票信息,Laravel Cashier也对此提供了支持。

首先通过调用用户实例上的invoices方法获取该用户所有有效发票信息:

$invoices = $user()->invoices();

然后在视图中将获取到的$invoices循环显示出来:

<table>
    @foreach ($invoices as $invoice)
    <tr>
        <td>{{ $invoice->dateString() }}</td>
        <td>{{ $invoice->dollars() }}</td>
        <td><a href="/order/invoice/{{ $invoice->id }}">下载发票</a></td>
    </tr>
    @endforeach
</table>

我们将这段代码放到上述resources/views/user/service.blade.php之后:

你已经是{{$service['name']}}<br>
@if ($service['type'] == 1)
 <a href="/upgrade">升级为金牌会员</a>
@endif
<div>
    发票信息:
    <table>
         @foreach ($invoices as $invoice)
         <tr>
             <td>{{ $invoice->dateString() }}</td>
             <td>{{ $invoice->dollars() }}</td>
             <td><a href="/user/invoice/{{ $invoice->id }}">下载发票</a></td>
         </tr>
         @endforeach
    </table>
</div>

这样在我们付费成功后,页面显示如下:

Laravel Cashier 发票信息

最后我们在路由中对点击“下载发票”进行处理:

Route::get('user/invoice/{invoice}', function ($invoiceId) {
    return Auth::user()->downloadInvoice($invoiceId, [
        'vendor'  => 'Laravel Academy',
        'product' => 'Gold Member',
    ]);
});

该代码会将对应发票生成PDF文档并下载。我们点击上述的“下载发票按钮”,下载的PDF发票截图如下:

Laravel Cashier 生成的PDF发票

学院君

学院君 has written 550 articles

资深PHP工程师,Laravel学院院长