Laravel で優良な顧客だけ残すスクリーニング機能をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、商品を販売する際に
気をつけておかないといけないのが、

顧客満足度

ですよね。

最近は誰でもスマホを持っているので、
レビューで★1つだけ、なんてことになると
後続の販売が難しくなるわけです。

しかし、そう考えると教育業界は
難しいものがあります。

なぜなら、いくら良いものを販売していても、

努力せずに悪いレビューだけ書く

という人が一定数出てくるからです。

つまり、「優良とは言えない顧客」ですね。

マーケティングでは、こういった顧客を
事前に除外(スクリーニング)しておくテクニックがあります。

例えば、

  • 何回目か以降のメルマガだけでセールスする
  • いくつか動画を送り、すべて視聴した人だけにセールスする
  • メルマガの最後に質問フォームを用意しておき、全部送信した人にセールスする

つまり、ある程度の努力ができる人だけに販売する
というテクニックです。

※ ちなみに、有名マーケターの イングリッシュおさる さんは、さらに個別面談までする徹底ぶりです!

そこで❗

今回はこの「見込み客のスクリーニング」をLaravelで作ってみることにしました。

ぜひ何かの参考になりましたら嬉しいです。😊✨

「迫佑樹 社長の
(PR) Twitterマーケティングマスター講座
(59,800円)購入!ガチで良いです(感謝😊)

開発環境: Laravel 11.x

やりたいこと

今回は、以下の手順でスクリーニングしてみます。

  1. LINE メッセージで記事のURLを送る
  2. 記事の最後に質問フォームを用意
  3. 送信してくれたらそれを保存

そして、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();

        }
    }
}

そして、SeederLaravelへセット。

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の増加)
  • 炎上の可能性は低くなる(リスクの軽減)

もしこういったマーケティングを実施するシステムをご希望でしたら、
ぜひお問い合わせからご相談下さい。

お待ちしております。😊

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回は「見込み客をスクリーニングする」機能
を実装してみました。

ちなみに、マーケティングを学べば学ぶほど、
知らないことや感心することがいっぱいです。

となると、知識がつくので逆に悪質な売り方をしていると
すぐに気づくことができるという話を聞いてナルホド納得でした。

やはり何でも知識がないのはリスク、ということでしょうか。

特にエライ目にあったことがあるわけじゃないですが、
こういうのは、もっと早く勉強しておくべきだったと感じる今日この頃です。

そして、今回Twitter運用に関する「59,800円 の教材」を
買ってみましたが、将来の成果を考えると、
安いと言わざるを得ませんでした。

今年は、知識投資への「課金元年」になりそうです。

ぜひ皆さんも時間を短縮して、知識を獲得してみてくださいね。

ではでは〜❗

「こんな高い教材買うの
マジでビビッた…でも、
何倍も回収できそうなんですよね👍」

このエントリーをはてなブックマークに追加       follow us in feedly