一、资源推荐接口
Larabbs 的侧边栏有个推荐资源的功能,这一节我们来开发对应的接口。因为该功能已经在上一本教程中完成,我们只是为其写个接口,实现起来非常方便。
1. 添加路由
推荐资源是游客可以访问的接口
routes/api.php
.
.
.
//每个用户的回复列表下方增加 资源推荐
$api->get('users/{user}/replies', 'RepliesController@userIndex')
->name('api.users.replies.index');
//资源推荐
$api->get('links', 'LinksController@index')
->name('api.links.index');
.
.
.
2. 添加 Transformer
$ touch app/Transformers/LinkTransformer.php
app/Transformers/LinkTransformer.php
<?php
namespace App\Transformers;
use App\Models\Link;
use League\Fractal\TransformerAbstract;
class LinkTransformer extends TransformerAbstract
{
public function transform(Link $link)
{
return [
'id' => $link->id,
'title' => $link->title,
'link' => $link->link,
];
}
}
3. 添加 Controller
$ php artisan make:controller Api/LinksController
app/Http/Controllers/Api/LinksController.php
<?php
namespace App\Http\Controllers\Api;
use App\Models\Link;
use Illuminate\Http\Request;
use App\Transformers\LinkTransformer;
class LinksController extends Controller
{
public function index(Link $link)
{
$links = $link->getAllCached();
return $this->response->collection($links, new LinkTransformer());
}
}
Link 模型已经存在,getAllCached 会对结果进行缓存,可以看一下代码:
app\Models\Link.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Cache;
class Link extends Model
{
protected $fillable = ['title', 'link'];
public $cache_key = 'larabbs_links';
protected $cache_expire_in_minutes = 1440;
public function getAllCached()
{
// 尝试从缓存中取出 cache_key 对应的数据。如果能取到,便直接返回数据。
// 否则运行匿名函数中的代码来取出 links 表中所有的数据,返回的同时做了缓存。
return Cache::remember($this->cache_key, $this->cache_expire_in_minutes, function(){
return $this->all();
});
}
}
4. PostMan 调试
初始化整个项目 的时候,我们执行了 php artisan migrate --seed
命令,links 表中应该已经生成了部分假数据,如果你想填充更多的数据,可以单独执行:
$ php artisan db:seed --class=LinksTableSeeder
可以看到接口返回了推荐的资源数据,结果正确。
5. 保存接口
可以将接口保存在 其他接口
目录中:
Git 版本控制
$ git add -A
$ git commit -m "links index"
二、活跃用户接口
Larabbs 的边栏会显示活跃用户,这一节我们会为这个功能开发接口:
1. 生成测试数据
如果你的 Homestead 的 Cron 没有配置,你可能会发现,你本地的 Larabbs 并没有显示活跃用户
这是因为活跃用户数据,是 artisan 命令生成的,如果 Cron 配置正确,会每个小时执行一次,计算出活跃用户。
算法
算法参考 Laravel China 社区: 关于「活跃用户」的算法。
系统 每一个小时 计算一次,统计 最近 7 天 所有用户发的 帖子数 和 评论数,用户每发一个帖子则得 4 分,每发一个回复得 1 分,计算出所有人的『得分』后再倒序,排名前八的用户将会显示在「活跃用户」列表里。
假设用户 A 在 7 天内发了 10 篇帖子,发了 5 条评论,则其得分为
10 * 4 + 5 * 1 = 45
执行 Artisan 命令
由于算法计算的是 7 天内的用户发帖和评论数据,我们可以在填充一些帖子数据
$ php artisan db:seed --class=TopicsTableSeeder
执行生成活跃用户命令
$ php artisan larabbs:calculate-active-user
再次访问 larabbs.test,应该能看到活跃用户数据了
2. 添加路由
routes/api.php
.
.
.
// 资源推荐下方添加 活跃用户
$api->get('links', 'LinksController@index')
->name('api.links.index');
// 活跃用户
$api->get('actived/users', 'UsersController@activedIndex')
->name('api.actived.users.index');
.
.
.
该接口也是游客可以访问的。
3. 修改 Controller
app/Http/Controllers/Api/UsersController.php
.
.
.
public function activedIndex(User $user)
{
return $this->response->collection($user->getActiveUsers(), new UserTransformer());
}
.
.
.
可以看到代码非常的简单,直接调用 $user->getActiveUsers()
即可,活跃用户的逻辑代码放置于在 Trait —— app/Models/Traits/ActiveUserHelper.php
中,算法的讲解,代码里有注释,这里便不再做过多讲解,购买过第二本教程的用户可以复习一下 8.1. 边栏活跃用户 这一节。
4. PostMan 调试
代码版本控制
$ git add -A
$ git commit -m 'actived users'
运行项目时,报错502 Bad Gateway
Unable to round-trip http request to upstream: lookup larabbs.test on 127.0.0.1:53: no such host
解决方案:关闭蓝灯软件
三、本地化
这一节我们来实现接口的本地化。本地化主要的是客户端的工作,切换语言后,客户端显示不同的界面,例如下面就是微信 中文 和 英文 语言下的界面。
除了界面显示之外,还有一些报错信息需要做本地化,举个例子,用户登录时,密码错误:
- 英文客户端,提示
invalid username or password
- 中文客户端,提示
用户名和密码错误
报错信息本地化的处理方式,一般有两种:
- 客户端通过服务器端返回的状态码和错误码,自行翻译为错误信息;
- 服务器端返回状态码时,返回已经格式化了的错误消息。
接下来我们会一一讲解。
1. 本地化完全交给客户端
因为我们是 RESTFul 风格的接口,返回了标准的状态码,大部分情况下,客户端可以根据状态码,以及语言设置提示给用户不同语言的报错信息。例如上面的例子,客户端调用 登录
接口时,报错信息中的 message 统一为中文,客户端根据状态码等标识进行本地化提示。
{
"message": "用户名和密码错误",
"status_code": 401
}
但是某些情况下,只有状态码是不够的,比如下面这个场景 发布话题
,我们增加了以下限制:
错误原因 | 状态码 | 错误描述 |
---|---|---|
被加入黑名单的用户不能发帖 | 403 | 您已被加入黑名单 |
会员用户才能发帖 | 403 | 您还不是会员 |
实名认证的用户才能发帖 | 403 | 您还没有通过认证 |
状态码都是 403 但是报错信息却不同,这时就需要定义不同的错误码,以便让客户端进行判断:
错误原因 | 状态码 | 错误描述 | 自定义错误码 |
---|---|---|---|
被加入黑名单的用户不能发帖 | 403 | 您已被加入黑名单 | 1001 |
会员用户才能发帖 | 403 | 您还不是会员 | 1002 |
实名认证的用户才能发帖 | 403 | 您还没有通过认证 | 1003 |
接口响应类似下面这样:
{
"message": "您还没有通过认证",
"status_code": 403,
"code": 1003
}
错误响应中增加 code 错误码字段
我们需要提前定义好『错误码』,并且在响应中增加 code 字段,这样客户端才能错误码,提示出正确的报错信息。DingoApi 会自动将异常中的异常码 (code)添加到响应中,所以我们只需要抛出异常的时候增加自定义的异常码即可,例如:
throw new \Symfony\Component\HttpKernel\Exception\HttpException(
401,
'您还没有通过认证',
null,
[],
1003);
进行封装
我们就会得到上面的响应结果。当然我们应该对上述代码进行封装:
app/Http/Controllers/Api/Controller.php
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Dingo\Api\Routing\Helpers;
use App\Http\Controllers\Controller as BaseController;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Controller extends BaseController
{
use Helpers;
public function errorResponse($statusCode, $message=null, $code=0)
{
throw new HttpException($statusCode, $message, null, [], $code);
}
}
我们在 Api/Controller
中加了 errorResponse
方法,所以我们在任意 API 控制器中直接使用 $this->errorResponse
即可。
添加测试代码
比如我们在 发布话题
接口的代码中增加下面的测试代码:
app/Http/Controllers/Api/TopicsController.php
.
.
.
public function store(TopicRequest $request, Topic $topic)
{
return $this->errorResponse(403, '您还没有通过认证', 1003);
.
.
.
由于是我们假设的业务,所以直接抛出异常,观察响应。
使用 PostMan 调试
可以看到响应中增加了 code 字段。记得还原测试代码:
$ git checkout app/Http/Controllers/Api/TopicsController.php
2. 接口根据客户端语言切换错误信息
如果接口根据客户端语言设置,返回对应语言的报错信息,客户端就可以直接将服务器报错提示给用户。由于每个接口都是无状态的,客户端需要在每次请求接口的时候增加参数,告诉接口支持的语言。我们可以利用 HTTP 的 Accept-Language
头信息。
- Accept-Language zh-CN —— 简体中文
- Accept-Language en —— 英文
增加 middleware
通过以下命令创建一个中间件。
$ php artisan make:middleware ChangeLocale
打开文件,填入如下代码:
app/Http/Middleware/ChangeLocale.php
<?php
namespace App\Http\Middleware;
use Closure;
class ChangeLocale
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$language = $request->header('accept-language');
if ($language) {
\App::setLocale($language);
}
return $next($request);
}
}
逻辑很简单,获取请求头中的 accept-language
,然后设置语言。
注册中间件
app/Http/Kernel.php
protected $routeMiddleware = [
.
.
.
// 访问节流,类似于 『1 分钟只能请求 10 次』的需求,一般在 API 中使用
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
// 接口语言设置
'change-locale' => \App\Http\Middleware\ChangeLocale::class,
.
.
.
];
routes/api.php
$api->version('v1', [
'namespace' => 'App\Http\Controllers\Api',
'middleware' => ['serializer:array', 'bindings', 'change-locale']
], function ($api) {
我们注册了 change-locale
这个中间件,并且在 v1 版本的接口中增加该中间件。
PostMan 调试
接下来我们以 登录
接口为例。
由于我们已经在 config/app.php 中设置了默认的语言为'locale' => 'zh-CN',
简体中文,所以我们直接访问登录接口,密码只填写 3 位。
可以看到中文的报错信息:
增加 accept-language
头,值为 en
切换为英文。
可以切换语言,并且得到了正确的报错信息,看一下登录的代码:
app/Http/Controllers/Api/AuthorizationsController.php
.
.
.
if (!$token = \Auth::guard('api')->attempt($credentials)) {
return $this->response->errorUnauthorized('用户名或密码错误');
}
.
.
.
发现用户名密码错误的时候,报错并没有进行本地化处理,修改代码如下:
.
.
.
if (!$token = \Auth::guard('api')->attempt($credentials)) {
return $this->response->errorUnauthorized(trans('auth.failed'));
}
.
.
.
输入错误的密码,访问登录接口来测试。
方案总结
第一种方案比较专业,大部分的第三方 API 平台接口提供方都会提供状态码,如 新浪微博 错误码,但是你需要一个个地将错误码写出。在某些开发需求中,错误码可能不仅影响本地化,有时客户端需要服务器返回的特定状态码做不同的业务处理,比如跳转到不同的页面。
第二种方案比较便捷,客户端可以直接将服务器的报错消息显示给用户,从快速实现的角度来看,省去了错误码的定义,并且后期可通过服务器代码来控制错误消息内容,也带来一定的灵活性。
从接口的可扩展性、适用性和专业性上考虑,最合理的也是最推荐的做法是将两种方案合并 —— API 既提供错误码,又提供错误消息。
另外,错误消息 默认 返回英文,也会是比较合理的最佳实践,当然最终视 API 业务逻辑而定。
代码版本控制
$ git add -A
$ git commit -m 'locale'
四、消息推送
消息推送是 APP 开发中非常重要的功能,可以让不在前台运行的 APP,及时进行消息通知,应用于新闻内容、促销活动、产品信息、版本更新提醒、订单状态提醒等多种场景。
因为我们没有客户端的配合,不方便测试,这一节我们主要以 iOS 为例,介绍一下消息推送的机制,以及实现一些基础代码。
推送原理
APNs (英文全称:Apple Push Notification service),即苹果推送通知服务。
消息推送分为本地通知和远程通知,本地通知是由本地应用触发的,是基于时间行为的一种通知形式,例如闹钟定时、待办事项提醒,又或者一个应用在一段时候后不使用通常会提示用户使用此应用等都是本地通知,因为本地通知没有服务器的参与,所以我们在这里不展开讨论,主要来了解一下远程通知。
远程通知主要步骤为:
- 在 APNs 下载 push 证书,设置在服务器中
- APP 获取 APNs 分配的 deviceToken (应用的唯一标识,不同设备,不同的应用 deviceToken 都会不同),提交给服务器,服务器记录下 deviceToken
- 消息推送时,服务器将 deviceToken 和要发送的消息提交给 APNs,并使用 push 证书签名
- APNs 会在自己已经注册的 iPhone 列表中根据 deviceToken 查找目标 iPhone。然后将消息发给目标 iPhone。
- iPhone 会把消息发送给相应的程序,并且已设定的方式弹出。
自己实现远程推送的功能会有很多的配置工作,再加上以后还有 Android 设备的推送,有很多繁琐的工作要处理,所以我们一般使用第三方的推送服务,省去了很多开发工作,第三方推送的统计也同时满足了运营的需求。
注册极光推送
极光推送 是我们常用的第三方推送服务商,以下简称 Jpush。Jpush 同时支持 iOS 和 Android 平台的消息推送,服务器只需要实现一套代码即可。我们先来 注册 一个 Jpush 账号。
填写必填信息,注册:
注册成功后,需要验证注册邮箱:
邮箱验证成功后,我们会进入极光的控制台,接下来创建一个 Larabbs 应用:
最后我们会看到 Jpush 的 key 和 Secret,记录下这两个数据,下面会用到。
由于使用了第三方服务,远程通知的流程变为:
- 服务器配置 Jpush 的 AppKey 和 Secret;
- APP 获取 APNs 分配的 deviceToken,去 Jpush 注册并获取 RegistrationID;
- APP 将 RegistrationID 提交到服务器,服务器记录下对应用户的 RegistrationID;
- 消息推送时,服务器通过 Jpush sdk 提交 RegistrationID 及消息到 Jpush 服务器;
- Jpush 负责之后的消息推送。
原来是将 deviceToken 提交至服务器,现在是将 RegistrationID 提交至服务器,RegistrationID 相当于设备在 Jpush 中的唯一标识,服务器需要将 RegistrationID 与单个用户绑定,这样就能推送消息给指定用户。
注册 Registration ID
添加 registration_id 字段
为 users
表增加 registration_id
字段:
$ php artisan make:migration add_registration_id_to_users_table --table=users
database/migrations/< your_date >_add_registration_id_to_users_table.php 注意替换日期变量
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddRegistrationIdToUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('registration_id')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('registration_id');
});
}
}
执行 migrate
命令:
$ php artisan migrate
修改编辑个人信息接口
APP 需要将 RegistrationID 提交到服务器,只需要调整 编辑个人信息
接口即可。
app/Http/Controllers/Api/UsersController.php
.
.
.
public function update(UserRequest $request)
{
$user = $this->user();
$attributes = $request->only(['name', 'email', 'introduction', 'registration_id']);
.
.
.
修改模型的 fillable
:
.
.
.
protected $fillable = [
'name', 'phone', 'email', 'password', 'introduction', 'avatar',
'weixin_openid', 'weixin_unionid', 'registration_id'
];
.
.
.
使用 PostMan 调试
只提交 registration_id ,查看数据库中数据,保存成功。
推送消息通知
回忆一下 Larabbs 的 消息通知 功能,我们在有消息消息通知的时候,推送给对应的用户。
安装 Jpush SDK
$ composer require jpush/jpush
封装 SDK
为了方便使用,进行一下简单的封装:
$ php artisan make:provider JpushServiceProvider
app/Providers/JpushServiceProvider.php
<?php
namespace App\Providers;
use JPush\Client;
use Illuminate\Support\ServiceProvider;
class JpushServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(Client::class, function ($app) {
return new Client(config('jpush.key'), config('jpush.secret'));
});
$this->app->alias(Client::class, 'jpush');
}
}
config/app.php
.
.
.
App\Providers\EasySmsServiceProvider::class,
App\Providers\JpushServiceProvider::class,
.
.
.
创建配置文件:
$ touch config/jpush.php
config/jpush.php
<?php
return [
'key' => env('JPUSH_KEY'),
'secret' => env('JPUSH_SECRET'),
];
在 env 文件中填写 Jpush 的 key 和 secret
.env
.
.
.
# jpush
JPUSH_KEY=9c6f53edad67db7ec24bfe32
JPUSH_SECRET=deeb2a04669ab79******
这样我们可以直接依赖注入 JPush\Client
或者 app('jpush')
来使用 Jpush 的 SDK。
监听消息通知
$ php artisan make:listener PushNotification
监听事件,这里我们可以在 DatabaseNotification 创建时,推送消息给客户端
app/Providers/EventServiceProvider.php
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// add your listeners (aka providers) here
'SocialiteProviders\Weixin\WeixinExtendSocialite@handle'
],
'eloquent.created: Illuminate\Notifications\DatabaseNotification' => [
'App\Listeners\PushNotification',
],
];
app/Listeners/PushNotification.php
<?php
namespace App\Listeners;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\DatabaseNotification;
use JPush\Client;
class PushNotification implements ShouldQueue
{
protected $client;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* Handle the event.
*
* @param NotificationSent $event
* @return void
*/
public function handle(DatabaseNotification $notification)
{
// 本地环境默认不推送
if (app()->environment('local')) {
return;
}
$user = $notification->notifiable;
// 没有 registration_id 的不推送
if (!$user->registration_id) {
return;
}
// 推送消息
$this->client->push()
->setPlatform('all')
->addRegistrationId($user->registration_id)
->setNotificationAlert(strip_tags($notification->data['reply_content']))
->send();
}
}
逻辑很简单,当通知存入数据库后,也就是监听 eloquent.created: Illuminate\Notifications\DatabaseNotification
这个事件,如果用户已经有了 Jpush 的 registration_id,则使用 Jpush SDK 将消息内容推送到目标用户的 APP 中,注意我们使用了 strip_tags 去除了 notificaiton 数据中的 HTML 标签。
代码版本控制
$ git add -A
$ git commit -m 'push notification'