Rails×Reactでアフィリエイトシステムを作る


以前携わった案件でアフィリエイト機能を付け足そうか議論になったことがあり、(結局はやらなかった)それを思い出したので実装してみました。
ちなみにアフィリエイトとはAmazonや楽天で販売されている商品を自身のSNSやメディアなどに掲載し、そこから発生した売上の一部を還元されるという仕組みです。代表的な例としてAmazonアソシエイト楽天アフィリエイトなどがあります。
皆さんがブログなどでよく見る商品のバナー広告などのことです。アフィリエイターはアフィリエイトリンクをブログなどに差し込んでそのリンクから発生したうりあげの一部がアフィリエイターに還元されます。

※ 今回はcorsの設定やユーザーの認証周りなど色々省略しているので、そのままだと動かないかもしれませんがアフィリエイトの簡単な仕組みの紹介なので今回はご容赦ください🙇‍♂️
ちなみに筆者は認証周りのライブラリはdevice_token_authdeviseを使用しました。

フロントエンド

_app.tsx
import Cookies from 'js-cookie';

~ 中略 ~

  React.useEffect(() => {
    if (!ref || Cookies.get('_affiliater_id')) return;

    Cookies.set('_affiliater_id', ref, { expires: 30 });
  });

例えばhttps://example.com/?ref=[紹介者ID] というリンクをブログから踏んできたユーザーの端末のcookieに_affiliater_idに紹介者IDがセットされます。

api.ts
import axios from 'axios';
import Cookies from 'js-cookie';

type SignUpParams = {
 email: string;
 password: string;
}

const signUp = async (params: SignUpParams) => {
  const response = await axios.post('/api/auth', params);
  if (Cookies.get('_affiliater_id')) {
    await registerAffiliater({identifier: Cookies.get('_affiliater_id')})
  }
  return response;
}

const registerAffiliater = async (params: { identifier: string }) => {
  const response = await axios.post('/api/users/affiliates', params);

  return response;
};

フォームからemailとpasswordを入力してサインアップ後、Cookieに_affiliater_idが存在する場合、registerAffiliater関数が実行されアフィリエイターが登録される仕組みです。

バックエンド

モデル設計

User

※ 本来はpasswordはハッシュ化したものを保存するべきだが今回は割愛。

db/migrate/xxxxxx.rb
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email
      t.string :password
      t.string :identifier

      t.timestamps
    end
  end
end
app/models/user.rb
class User < ApplicationRecord
  has_one :affiliater, class_name: 'Affiliate', foreign_key: 'affiliated_id', dependent: :destroy
  has_one :affiliate_user, through: :affiliater, source: :affiliater
  has_many :affiliated, class_name: 'Affiliate', foreign_key: 'affiliater_id', dependent: :destroy
  has_many :affiliated_users, through: :affiliated, source: :affiliated
end

紹介者は一人にすべきなので、affiliaterはhas_oneを使いました。affiliate_user、affiliated_usersでユーザーを直接参照できるように。

Affiliate

db/migrate/xxxxxx.rb
class CreateAffiliates < ActiveRecord::Migration[6.1]
  def change
    create_table :affiliates do |t|
      t.references :affiliater, null: false, foreign_key: { to_table: :users }
      t.references :affiliated, null: false, foreign_key: { to_table: :users }

      t.timestamps
    end
  end
end
app/models/affiliater.rb
class Affiliate < ApplicationRecord
  belongs_to :affiliater, class_name: 'User'
  belongs_to :affiliated, class_name: 'User'
  validates :affiliater_id, uniqueness: { scope: :affiliated_id }
  validate :cannot_be_affiliater_mine

  def cannot_be_affiliater_mine
    return unless affiliated == affiliater

    errors.add(:base, '自分のアフィリエイターにはなれません')
  end
end

Affiliateモデルにはaffiliaterとaffiliatedが一致するパターンとaffiliterとaffiliatedが同ユーザーである場合にバリデーションを設定しました。

コントローラー

app/controllers/api/affiliates_controller.rb
module Api
  class AffiliatesController < ApplicationController
    before_action :find_affiliter, only: %i[create]

    def create
      if @affiliater
        affiliate = Affiliate.create(affiliater: @affiliater, affiliated: current_user)
        render json: {data: affiliate, message: "成功しました", status: 200}
      else
        render json: {data: nil, message: "失敗しました", status: 500}
      end
    end

    private

    def find_affiliater
      @affiliater = User.find_by(identifier: params[:identifier])
    end
  end
end

ルーティング

config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :users do
      collection do
        resources :affiliates, only: %i[create]
      end
    end
  end
end

以上です、ありがとうございました!