
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、商品を販売する際に
気をつけておかないといけないのが、
顧客満足度
ですよね。
最近は誰でもスマホを持っているので、
レビューで★1つだけ、なんてことになると
後続の販売が難しくなるわけです。
しかし、そう考えると教育業界は
難しいものがあります。
なぜなら、いくら良いものを販売していても、
努力せずに悪いレビューだけ書く
という人が一定数出てくるからです。
つまり、「優良とは言えない顧客」ですね。
マーケティングでは、こういった顧客を
事前に除外(スクリーニング)しておくテクニックがあります。
例えば、
- 何回目か以降のメルマガだけでセールスする
- いくつか動画を送り、すべて視聴した人だけにセールスする
- メルマガの最後に質問フォームを用意しておき、全部送信した人にセールスする
つまり、ある程度の努力ができる人だけに販売する
というテクニックです。
※ ちなみに、有名マーケターの イングリッシュおさる さんは、さらに個別面談までする徹底ぶりです!
そこで
今回はこの「見込み客のスクリーニング」をLaravel
で作ってみることにしました。
ぜひ何かの参考になりましたら嬉しいです。
「迫佑樹 社長の
(PR) Twitterマーケティングマスター講座」
(59,800円)購入!ガチで良いです(感謝)
開発環境: Laravel 11.x
目次 [非表示]
やりたいこと
今回は、以下の手順でスクリーニングしてみます。
- LINE メッセージで記事のURLを送る
- 記事の最後に質問フォームを用意
- 送信してくれたらそれを保存
そして、3回全てに回答してくれていたら
商品のURL
を送信してセールスする
という流れです。
ただし、LINE
を含めると複雑になりすぎてしまうので、
実際にLINEへ送信はせず、メール送信で代替します。
では、楽しんでやっていきましょう
DB 周りをつくる
今回はメインではない部分なので
サクッといきます。
以下のコマンドを実行してください。
php artisan make:model LineUser -ms
php artisan make:model LineNewsletter -ms
php artisan make:model LineUserNewsletter -m
そして、作成したファイルを以下のように変更します。
app/Models/LineUser.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LineUser extends Model
{
use HasFactory;
// Relationship
public function newsletters()
{
return $this->belongsToMany(Newsletter::class, 'line_user_newsletters', 'line_user_id', 'newsletter_id');
}
}
app/Models/Newsletter.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Newsletter extends Model
{
use HasFactory;
// Relationship
public function line_user()
{
return $this->belongsToMany(LineUser::class, 'line_user_newsletters', 'newsletter_id', 'line_user_id');
}
// Others
public static function getTestNewsletterIds()
{
// TODO: テスト用。本来ここは DB から取得するべきです
return [1, 2, 3]; // 送信するメルマガのID(この順番に送信される)
}
}
database/migrations/****_**_**_******_create_line_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('line_users', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique()->comment('UUID');
$table->string('line_id')->unique()->comment('LINEのID');
$table->string('mode')->comment('チャネルの状態');
$table->string('display_name')->comment('LINEの名前');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('line_users');
}
};
database/migrations/****_**_**_******_create_newsletters_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('newsletters', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique()->comment('UUID');
$table->string('line_body')->comment('LINEの本文');
$table->string('web_body')->comment('ウェブページの本文');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('newsletters');
}
};
database/migrations/****_**_**_******_create_line_user_newsletters_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('line_user_newsletters', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('line_user_id')->comment('LINEユーザーID');
$table->unsignedBigInteger('newsletter_id')->comment('メルマガID');
$table->text('answer')->nullable()->comment('回答');
$table->dateTime('answered_at')->nullable()->comment('回答日時');
$table->timestamps();
$table->foreign('line_user_id')->references('id')->on('line_users');
$table->foreign('newsletter_id')->references('id')->on('newsletters');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('line_user_newsletters');
}
};
database/seeders/LineUserSeeder.php
<?php
namespace Database\Seeders;
use App\Models\LineUser;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class LineUserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
for($i = 1; $i <= 10; $i++) {
$line_user = new LineUser();
$line_user->uuid = Str::uuid();
$line_user->line_id = Str::random();
$line_user->mode = 'active';
$line_user->display_name = 'ユーザー' . $i;
$line_user->save();
}
}
}
database/seeders/NewsletterSeeder.php
<?php
namespace Database\Seeders;
use App\Models\Newsletter;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class NewsletterSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
for($i = 1; $i <= 3; $i++) {
$newsletter = new Newsletter();
$newsletter->uuid = Str::uuid();
$newsletter->line_body = $i .'つ目のメルマガの内容(LINE)';
$newsletter->web_body = $i .'つ目のメルマガの内容(Web)' ." \n質問に答えてね\n↓↓↓";
$newsletter->save();
}
}
}
そして、Seeder
をLaravel
へセット。
database/seeders/DatabaseSeeder.php
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
$this->call([
LineUserSeeder::class,
NewsletterSeeder::class,
]);
}
}
では、DB
の初期化します。
php artisan migrate:fresh --seed
すると実際のテーブルはこうなりました。
メール送信部分をつくる
今回は「LINEの代替」としてMailable
をつくります。
以下のコマンドを実行して下さい。
php artisan make:mail SendNewsletter
すると、ファイルが作成されるので中身を変更します。
app/Mail/SendNewsletter.php
<?php
namespace App\Mail;
use App\Models\LineUser;
use App\Models\Newsletter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class SendNewsletter extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(private Newsletter $newsletter, private LineUser $line_user)
{}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '【 Console dot Log メルマガ 】',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.newsletter',
with: [
'newsletter' => $this->newsletter,
'line_user' => $this->line_user,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}
では、Mailable
にセットしたビューをつくります。
php artisan make:view emails.newsletter
ファイルの中身はこうなります。
resources/views/emails/newsletter.blade.php
{{ $newsletter->line_body }}<br><br>
<a href="{{ route('newsletter.show', [$newsletter->uuid, $line_user->uuid]) }}">本文はこちら</a>
コントローラーをつくる
では、(受信したメールにあるURL
をクリックしたときの)
メルマガ本文を表示するためにコントローラーをつくります。
以下のコマンドを実行して下さい。
php artisan make:controller NewsletterController
中身をこうします。
app/Http/Controllers/NewsletterController.php
<?php
namespace App\Http\Controllers;
use App\Models\LineUser;
use App\Models\LineUserNewsletter;
use App\Models\Newsletter;
use Illuminate\Http\Request;
class NewsletterController extends Controller
{
public function show($newsletter_uuid, $line_user_uuid)
{
$newsletter = Newsletter::where('uuid', $newsletter_uuid)->first();
$line_user = LineUser::where('uuid', $line_user_uuid)->first();
return view('newsletters.show', [
'newsletter' => $newsletter,
'line_user' => $line_user,
]);
}
public function storeAnswer(Request $request)
{
// 注意: バリデーションは省略しています
$line_user = LineUser::where('uuid', $request->line_user_uuid)->first();
$newsletter = Newsletter::where('uuid', $request->newsletter_uuid)->first();
$line_user_newsletter = LineUserNewsletter::query()
->where('line_user_id', $line_user->id)
->where('newsletter_id', $newsletter->id)
->first();
$line_user_newsletter->answer = $request->answer;
$line_user_newsletter->answered_at = now();
$line_user_newsletter->save();
$newsletter_ids = Newsletter::getTestNewsletterIds();
$answer_count = LineUserNewsletter::query()
->whereIn('newsletter_id', $newsletter_ids)
->where('line_user_id', $line_user->id)
->whereNotNull('answer')
->count();
if ($answer_count === count($newsletter_ids)) { // 全ての質問に回答したら
return 'この人は優良な顧客である可能性が高いので、ここでセールスメールを送る!';
}
return '回答を受け付けました!';
}
}
ビューをつくる
次に先ほどのコントローラー内でセットしたビューをつくります。
php artisan make:view newsletters.show
作成されたファイルはこうします。
resources/views/newsletters/show.blade.php
<html>
<head>
<title>メルマガ</title>
</head>
<body>
<p style="white-space:pre-wrap;">{{ $newsletter->web_body }}</p>
<div>
<form method="post" action="{{ route('newsletter.store_answer') }}">
@csrf
<input type="hidden" name="newsletter_uuid" value="{{ $newsletter->uuid }}">
<input type="hidden" name="line_user_uuid" value="{{ $line_user->uuid }}">
<label>
<textarea name="answer" rows="4" cols="40"></textarea>
</label>
<br><br>
<button type="submit">送信</button>
</form>
</div>
</body>
</html>
Artisan コマンドをつくる
続いて、LINE
(今回はメール)送信する
Artisan
コマンドをつくります。
php artisan make:command NewsletterCommand
そして、中身をこうします。
app/Console/Commands/NewsletterCommand.php
<?php
namespace App\Console\Commands;
use App\Mail\SendNewsletter;
use App\Models\LineUser;
use App\Models\Newsletter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class NewsletterCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:newsletter';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send newsletters to line users';
/**
* Execute the console command.
*/
public function handle()
{
$newsletter_ids = Newsletter::getTestNewsletterIds();
$newsletters = Newsletter::query()
->whereIn('id', $newsletter_ids)
->get();
$sent_line_user_ids = [];
foreach ($newsletter_ids as $newsletter_id) {
$newsletter = $newsletters->firstWhere('id', $newsletter_id);
$prev_newsletter_index = array_search($newsletter_id, $newsletter_ids) - 1;
$prev_newsletter_id = data_get($newsletter_ids, $prev_newsletter_index, null);
$Line_users = LineUser::query()
->whereDoesntHave('newsletters', function ($query) use ($newsletter) { // まだ送信していないユーザー
$query->where('newsletter_id', $newsletter->id);
})
->whereNotIn('id', $sent_line_user_ids) // 一度の複数の送信はしない
->when($prev_newsletter_id, function($query, $prev_newsletter_id) { // 前回のメルマガに回答してるユーザーだけ
$query->whereHas('newsletters', function($q) use($prev_newsletter_id) {
$q->where('newsletter_id', $prev_newsletter_id)
->whereNotNull('answer');
});
})
->get();
foreach ($Line_users as $Line_user) {
$Line_user->newsletters()->attach($newsletter->id, [
'created_at' => now(),
'updated_at' => now(),
]);
$sent_line_user_ids[] = $Line_user->id;
// 本来はここで LINE にメッセージを送信する処理が入る
Mail::to('test@example.com')->send(new SendNewsletter($newsletter, $Line_user));
$this->info($Line_user->display_name .' にメルマガ(ID:'. $newsletter->id .')を送信');
}
}
}
}
ルートをつくる
では、最後にルートです。
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\NewsletterController;
Route::prefix('newsletter')->controller(NewsletterController::class)->group(function () {
Route::get('{newsletter_uuid}/{line_user_uuid}', 'show')->name('newsletter.show');
Route::post('answer', 'storeAnswer')->name('newsletter.store_answer');
});
テストしてみる
では、まずは作成したArtisan
コマンドを実行してみましょう。
php artisan send:newsletter
すると・・・・・・
はい
メルマガが(最初は全員に)送信されました。
では、「ID:1」の人に送ったメッセージを見てみましょう。
うまくメッセージが届いていますね。
では、リンクをクリックしてみます。
どうなるでしょうか・・・・・・
はい
メルマガの内容とフォームが表示されました。
ここに入力して送信すると
「ちゃんと回答をしたユーザー」として次のメルマガも送信します。
では以下のようにして入力してみましょう。
すると・・・・・・
はい
保存が完了しました。
ではデータベースの方も確認しておきましょう。
はい
テーブルに回答とその日時が保存されています。
成功です
では、先ほどのArtisan
コマンドを実行して2通目のメルマガも送信してみましょう。
すると・・・・・・
はい
今回は、「前回のメルマガで回答をした」ID:1の人だけに
送信されていますね。
では、これを繰り返して3通目のメルマガにある
回答フォームを送信してみます。
うまくいくでしょうか・・・・・・
はい
3通のメルマガを読み、フォーム送信したので、
「優良な顧客」と判断され、メッセージが変化しました。
すべて成功です
企業様へのご提案
冒頭に書いたとおり「見込み客は集めれば集めるほどいい」
というわけではない時代になってきているかと思います。
そして、顧客満足度を最大限に高めることは、
以下のような流れにつながります。
- 高評価につながり口コミになる(集客の増加)
- 満足しているので、他の商品も購入してもらえる(成約率の増加、クロスセル、LTVの増加)
- 同じ理由から、より高いバージョンの商品が購入される(アップセル、LTVの増加)
- 炎上の可能性は低くなる(リスクの軽減)
もしこういったマーケティングを実施するシステムをご希望でしたら、
ぜひお問い合わせからご相談下さい。
お待ちしております。
おわりに
ということで、今回は「見込み客をスクリーニングする」機能
を実装してみました。
ちなみに、マーケティングを学べば学ぶほど、
知らないことや感心することがいっぱいです。
となると、知識がつくので逆に悪質な売り方をしていると
すぐに気づくことができるという話を聞いてナルホド納得でした。
やはり何でも知識がないのはリスク、ということでしょうか。
特にエライ目にあったことがあるわけじゃないですが、
こういうのは、もっと早く勉強しておくべきだったと感じる今日この頃です。
そして、今回Twitter
運用に関する「59,800円 の教材」を
買ってみましたが、将来の成果を考えると、
安いと言わざるを得ませんでした。
今年は、知識投資への「課金元年」になりそうです。
ぜひ皆さんも時間を短縮して、知識を獲得してみてくださいね。
ではでは〜
「こんな高い教材買うの
マジでビビッた…でも、
何倍も回収できそうなんですよね」