Chinese (Simplified) (中文(简体)) translation by honeymmmm (you can also view the original English article)

介绍
本教程是Envato Tuts +上使用PHP构建您的启动系列的一部分。 在本系列中,我将引导您使用我的 Meeting Planner应用程序作为一个真实的示例,从概念到现实启动创业。 在此过程中的每一步,我都会将会议策划代码作为您可以学习的开源示例发布。 我还将解决与启动相关的业务问题。
这集的封面是什么?
在上一个教程中,我们开始通过电子邮件发送会议邀请函,其中包括众多参与者回复的链接,即查看会议页面,接受所有地点和时间,拒绝某个地点或时间等。
在本教程中,我将回顾如何以安全,实用的方式构建和处理这些链接。 大多数会议参与者(特别是最初的参与者)之前不会使用会议筹办者 - 他们将不为我们所知。 然而,我们希望安全地对它们进行身份验证,以允许它们查看会议请求并与之交互,并为将来创建自己的会议请求。 我们还希望有一些保障措施,当人们用他们的安全代码转发会议请求而不考虑后果(新手!或许,或者只是每天的人)。
提醒一下,Meeting Planner的所有代码都是用Yii2 Framework for PHP编写的。 如果您想了解更多关于Yii2的信息,请查看我在Envato Tuts +上的并行系列Programming with Yii2。
当您阅读本文时,您可能会开始在实时网站MeetingPlanner.io上尝试会议邀请(请记住,仍有许多用户体验改进工作和抛光工作)。 我参与下面的评论主题,如果您有其他想法或想要为未来的教程建议主题,我特别感兴趣。 您也可以通过Twitter @reifman与我联系。
会议计划程序命令
命令的重要性
在我的设计过程中,我将电子邮件中的命令视为会议计划流程和实际事件前的时间的元素。
当参与者收到电子邮件邀请时,他们需要获得安全权限才能查看会议页面,还需要回答特定地点和时间是否适合他们。
会议结束后,我们可能会开始向提供专门命令的参与者发送提醒,例如“我迟到了”,这会给您困境的其他方发短信,或者“请求更改到位”或“取消”。
所有这些命令都需要对收件人进行身份验证,并为他们提供对网站的安全访问,以便Meeting Planner可以正确处理他们的回复。 但是,如果网站错误地将会议邀请电子邮件转发给另一方,然后点击链接,该网站还需要保护不发布会员的整个联系人列表。 次要方可以轻松登录收件人的会议计划员帐户,并能够查看他们的所有会议和个人信息。
需要什么命令?
当我进一步考虑应用程序的愿景时,会有大量潜在的命令。 以下是初始邀请中的一些内容(我可能会在某些时候简化以改善用户体验):
- 查看会议
- 接受所有的地方和时间
- 拒绝邀请
- 接受或拒绝特定地点
- 接受或拒绝特定的日期和时间
- 完成会议*
- 建议另一个地方*
- 建议另一个日期和时间*
- 选择最终地点*
- 选择最终日期和时间*
- 添加或回复会议记录
- 查看会议上下文中地点位置的地图
- 查看您的电子邮件设置
- 阻止此组织者向您发送电子邮件
- 取消订阅所有会议筹办者电子邮件
注意:已加星标(*)项目的外观取决于组织者的会议设置。
计划会议后,还有各种后续命令:
- 重新安排会议
- 取消会议
- 给我看一张地图
- 获取行车路线
- 请求更改时间
- 请求更改地点
- 告知会议人员你迟到了
建筑考虑因素
鉴于各种命令过多,我觉得在单个控制器中对所有命令进行相同的身份验证和处理会很有用。
现在,我在MeetingController中创建了一个处理点,但我希望稍后我会创建一个专用的CommandController。 我还考虑过将来创建一个API访问控制器,并通过这个单一的安全入口点引导所有应用程序的功能。 现在,我会推迟。
首先,我在Meeting.php模型中为每个命令指定了一个特定的常量定义:
const COMMAND_HOME = 5; const COMMAND_VIEW = 10; const COMMAND_VIEW_MAP = 20; const COMMAND_FINALIZE = 50; const COMMAND_CANCEL = 60; const COMMAND_ACCEPT_ALL = 70; const COMMAND_ACCEPT_PLACE = 100; const COMMAND_REJECT_PLACE = 110; const COMMAND_ACCEPT_ALL_PLACES = 120; const COMMAND_CHOOSE_PLACE = 150; const COMMAND_ACCEPT_TIME = 200; const COMMAND_REJECT_TIME = 210; const COMMAND_ACCEPT_ALL_TIMES = 220; const COMMAND_CHOOSE_TIME = 250; const COMMAND_ADD_PLACE = 300; const COMMAND_ADD_TIME = 310; const COMMAND_ADD_NOTE = 320; const COMMAND_FOOTER_EMAIL = 400; const COMMAND_FOOTER_BLOCK = 410; const COMMAND_FOOTER_BLOCK_ALL = 420;
构建命令链接
我决定,目前,每个命令都有以下URL参数:
- meeting_Id的
$id
- 命令操作的
$cmd
(来自上面的常量) -
$obj_id
可以对任何对象采取行动,即地点或日期时间 - 调用命令的user_id的
$actor_id
-
$k
用于验证$ actor_id到其帐户的密钥
大多数参与者最初都没有注册,但我们会在创建会议时创建与其邀请电子邮件相关联的身份验证密钥。 这就是我们如何通过电子邮件邀请验证他们的链接。
以下是电子邮件中嵌入的示例网址链接:
http://meetingplanner.io/meeting/command?id=27&cmd=70&actor_id=18&k=9cHGl...1x
鉴于从代码中的许多地方创建带有各种参数的URL的复杂性,我创建了一个/common/components/MiscHelpers.php
库,它以buildCommand
开头:
<?php namespace common\components; use yii\helpers\Url; use common\models\User; //use \yii\helpers\FormatConverter; class MiscHelpers { public static function buildCommand($meeting_id,$cmd=0,$obj_id=0,$actor_id=0,$auth_key='') { return Url::to(['meeting/command','id'=>$meeting_id,'cmd'=>$cmd,'actor_id'=>$actor_id,'k'=>$auth_key,'obj_id'=>$obj_id,],true); } } ?>
这是我们的invitation-html.php视图文件的一个例子,它调用buildCommand()
来显示地方行。 每个地方都有必须在URL中提供所有这些参数的命令:
<?php foreach($places as $p) { ?> <tr> <td width="300"> <p> <?php echo $p->place->name; ?> <br/ > <span style="font-size:75%;"><?php echo $p->place->vicinity; ?> <?php echo HTML::a(Yii::t('frontend','view map'), MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_VIEW_MAP,$p->id,$user_id,$auth_key)); ?></span> </p> </td> <td width="300" > <?php echo HTML::a(Yii::t('frontend','acceptable'),MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_ACCEPT_PLACE,$p->id,$user_id,$auth_key)); ?> | <?php echo HTML::a(Yii::t('frontend','reject'),MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_REJECT_PLACE,$p->id,$user_id,$auth_key)); ?> <?php if ($meetingSettings->participant_choose_place) { ?> | <?php echo HTML::a(Yii::t('frontend','choose'),MiscHelpers::buildCommand($meeting_id,Meeting::COMMAND_CHOOSE_PLACE,$p->id,$user_id,$auth_key)); ?> <?php } ?> </td> </tr> <?php } ?>
您可以在下面看到它们的样子:

处理命令
然后,我构建了控制器功能,用于验证和处理命令。 这是第一部分:
public function actionCommand($id,$cmd=0,$obj_id=0,$actor_id=0,$k=0) { $performAuth = true; $authResult = false; // Manage the incoming session if (!Yii::$app->user->isGuest) { if (Yii::$app->user->getId()!=$actor_id) { // to do: give user a choice of not logging out Yii::$app->user->logout(); } else { // user actor_id is already logged in $authResult = true; $performAuth = false; } }
最初,我想为我自己的测试提供保护,以及人们使用他们的身份验证链接转发电子邮件。
我检查的一个事件是$actor_id
是否是与当前登录用户不同的用户。 这可能发生在使用多个帐户进行测试期间,或者如果参与者将其邀请转发给组织者,则可能发生这种情况。 最后,我将提供有关情况的信息并为人们提供选择。 但是,就目前而言,我只是在验证请求用户之前记录当前用户。
如果用户已经以$actor_id
身份登录,则会对其进行身份验证。 如果未经过身份验证,我们会运行身份验证检查:
if ($performAuth) { //echo 'guest'; $person = new \common\models\User; $identity = $person->findIdentity($actor_id); if ($identity->validateAuthKey($k)) { Yii::$app->user->login($identity); // echo 'authenticated'; $authResult=true; } else { // echo 'fail'; $authResult=false; } }
我们使用Yii的内置findIdentity和validateAuthKey函数。
在不久的将来,我计划通过电子邮件进行身份验证,提供对帐户功能的有限访问权限。 例如,每当用户没有登录但点击命令链接时,我们就会将他们的活动限制在该会议和一些相关功能上。 他们将无法看到其他会议,帐户持有人的朋友等。 但是,我们将提供友好链接,以便他们通过密码或社交登录登录其帐户。 这将最大限度地减少人们转发邀请的安全影响。
同样,如果在点击命令链接之前从未注册的新用户,我们将提示他们注册并创建密码或社交登录。 User.php模型具有状态字段,其指示用户是否曾经自己注册,或者是否被动地被邀请参加会议。
目前,如果身份验证成功,我们可以只处理每个命令:
if (!$authResult) { $this->redirect(['site/authfailure']); } else { // TO DO check if user is PASSIVE // if active, set SESSION to indicate log in through command // if PASSIVE login // - if no password, setflash to link to create password // - meeting page - flash to security limitation of that meeting view // - meeting index - redirect to view only that meeting (do this on other index pages too) $meeting = $this->findModel($id); switch ($cmd) { case Meeting::COMMAND_HOME: $this->goHome(); break; case Meeting::COMMAND_VIEW: $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_VIEW_MAP: $this->redirect(['meeting/viewplace','id'=>$id,'meeting_place_id'=>$obj_id]); break; case Meeting::COMMAND_FINALIZE: $this->redirect(['meeting/finalize','id'=>$id]); break; case Meeting::COMMAND_CANCEL: $this->redirect(['meeting/cancel','id'=>$id]); break; case Meeting::COMMAND_ACCEPT_ALL: MeetingTimeChoice::setAll($id,$actor_id); MeetingPlaceChoice::setAll($id,$actor_id); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_ACCEPT_ALL_PLACES: MeetingPlaceChoice::setAll($id,$actor_id); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_ACCEPT_ALL_TIMES: MeetingTimeChoice::setAll($id,$actor_id); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_ADD_PLACE: $this->redirect(['meeting-place/create','meeting_id'=>$id]); break; case Meeting::COMMAND_ADD_TIME: $this->redirect(['meeting-time/create','meeting_id'=>$id]); break; case Meeting::COMMAND_ADD_NOTE: $this->redirect(['meeting-note/create','meeting_id'=>$id]); break; case Meeting::COMMAND_ACCEPT_PLACE: $mpc = MeetingPlaceChoice::find()->where(['meeting_place_id'=>$obj_id,'user_id'=>$actor_id])->one(); MeetingPlaceChoice::set($mpc->id,MeetingPlaceChoice::STATUS_YES); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_REJECT_PLACE: $mpc = MeetingPlaceChoice::find()->where(['meeting_place_id'=>$obj_id,'user_id'=>$actor_id])->one(); MeetingPlaceChoice::set($mpc->id,MeetingPlaceChoice::STATUS_NO); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_CHOOSE_PLACE: MeetingPlace::setChoice($id,$obj_id,$actor_id); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_ACCEPT_TIME: $mtc = MeetingTimeChoice::find()->where(['meeting_time_id'=>$obj_id,'user_id'=>$actor_id])->one(); MeetingTimeChoice::set($mtc->id,MeetingTimeChoice::STATUS_YES); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_REJECT_TIME: $mtc = MeetingTimeChoice::find()->where(['meeting_time_id'=>$obj_id,'user_id'=>$actor_id])->one(); MeetingTimeChoice::set($mtc->id,MeetingTimeChoice::STATUS_NO); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_CHOOSE_TIME: MeetingTime::setChoice($id,$obj_id,$actor_id); $this->redirect(['meeting/view','id'=>$id]); break; case Meeting::COMMAND_FOOTER_EMAIL: case Meeting::COMMAND_FOOTER_BLOCK: case Meeting::COMMAND_FOOTER_BLOCK_ALL: $this->redirect(['site\unavailable','meeting_id'=>$id]); break; default: $this->redirect(['site\error','meeting_id'=>$id]); break; } }
对于我尚未构建的功能,我创建了一个视图以指示该功能不可用,例如 /views/site/unavailable.php
,或者如果命令被误解,则/views/site/error.php
。
两个示例命令
我们来看两个示例命令。 首先,让我们看看另一个地方:
case Meeting::COMMAND_ADD_PLACE: $this->redirect(['meeting-place/create','meeting_id'=>$id]); break;
在这种情况下,该功能要求用户返回我们的网站以填写他们可以选择新地点的表格。 因此,我们只是将它们重定向到该meeting_id
的创建会议地点页面。 它们已经过身份验证并从上面登录。
这是一个例子 - 通知面包屑菜单反映了会议的背景,例如: 早餐会:

其次,让我们看看接受所有日期和时间:
case Meeting::COMMAND_ACCEPT_ALL_TIMES: MeetingTimeChoice::setAll($id,$actor_id); $this->redirect(['meeting/view','id'=>$id]); break;
在这种情况下,我们需要接受该会议的所有时间和$actor_id
。 接受在幕后透明地完成。 之后,我们可以重定向他们以查看会议。
这是在到达会议视图时所接受的一切,例如, 好吧,好吧,好吧,下面的地方和时间:

一个有趣的故事
实施所有这些命令肯定需要一些时间,但Meeting Planner的功能真正开始生命。 我能够将我的第一份邀请函发送到世界各地。
我一直在约会的女人知道我已经接近完成这项功能所以她决定激励我更快地完成它。 她说:
“我不知道下次什么时候能见到你,因为我还没有收到会议筹办者的邀请。”
经过几天的额外工作,我给她发了第二次会议策划邀请函 - 第一次去了朋友进行测试。
令人印象深刻的是,当我的约会收到她的邀请时,她很快就要求了两个有用的功能。 首先,她说她不确定她是否能出现我们的约会,除非活动在她手机的谷歌日历中(通常我更喜欢约会iOS用户,而不是Android)。 下一个教程将讲述构建用于导入的iCal(.ics)文件的故事(因此我的日期将知道去哪里)。 我不会让你陷入悬念 - 我及时完成了这个功能。
其次,她要求我想到一个功能,但没有意识到它的重要性。 她希望能够指定一个有时间的地方。 换句话说,Canlis餐厅周五晚上7点,但Paseo周六晚上8点。 目前,地点和时间是单独提供的,而不是组合提供的。 我将为将来的一集保存此功能。
这引发了一个普遍问题,即如何在启动过程中定期收集人们的反馈并将其集成到您的需求和开发计划中。 并非所有用户都会为您提供数据以换取他们喜欢的功能。 我有一个教程剧集计划在未来讨论如何做到这一点,尽管缺乏次要动机。
下一步是什么?
在下一集中,我将详细介绍如何使用邀请详细信息构建日历文件(.ics)以导入到Google日历,Outlook和Apple日历中。 包括联系方式和地图以及管理时区问题都是其中的关键方面。
观看我使用PHP系列构建您的初创公司即将推出的教程 - 我希望您渴望尝试使用Meeting Planner。 立即试一试!
请随时在下面添加您的问题和评论; 我试着定期参加讨论。 您也可以通过Twitter @reifman与我联系。
相关链接
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post