未熟学生エンジニアのブログ

TetsuFeの個人開発ブログ

TetsuFeはテツエフイー と読みます。FlutterやWeb周り全般、チーム開発について語るブログ

Django製フリマサイト「ホクマ」ができるまで part5

今回は、 4.2 「ホクマ」の機能構成・技術の話 について話します。

「ホクマができるまで」シリーズについて

シリーズの目次はこちらにあります。今回は第5回です。

swiftfe0.hatenablog.com

この度「ホクマ」という北大生限定フリマWebサービスを作りました。(GitHub)

hufurima.com

その作り方について大雑把に解説していくことで、(基本個人)学生によるWebサービス開発の流れを説明していきたいと思います。

(ただし、ビジネス的な観点はあまり考えて作っていません。 収益化の方法などは書いていかない予定なので気をつけてください。

使った技術

f:id:swiftfe:20181006223428j:plain

サービス開発の流れ(簡単に)

今回は「 4.2 「ホクマ」の機能構成・技術の話 」について書いていきます。

4.2 「ホクマ」の機能構成・技術の話

前回までは、開発初期の話をしました。具体的には以下のような話でした。

  • どんなものを作るかなんとなく決める
  • 具体的にどんなものを作るのかを考える・必須なことの列挙
  • ユーザーが操作する一連の流れをシンプルに作る
  • githubやweb上にある似たようなサービスを参考にしよう

今回は、もう少し開発が進んだ時点での話や、「ホクマ」の詳しい機能の構成や技術構成について話していきます。今回は、以下の二つの話をします

  • フリマサイト「ホクマ」のモデル設計(相当拙いものですが)
  • インフラを含めた技術構成

さらに次回で、以下の二つの話をする予定です

  • その他Djangoについての細かいこと
  • サイトのデザインについて

4.2.1 「ホクマ」のモデル設計

pygraphvizを使って、djangoのモデルのクラス図を自動生成してみました。

f:id:swiftfe:20181013023341p:plain

ごちゃごちゃしていてわかりませんね・・・一つずつ簡単に説明していきます

「ホクマ」で使ったモデルクラス

  • User: ユーザアカウント
  • Product: 商品
  • ProductImage: 商品の画像(複数あるため)
  • Chat: チャットルーム一つ
  • Talk: チャットメッセージ一通
  • UserRating: ユーザ評価
  • Notification: 通知(お知らせ)
  • TODO: TODO
    • RatingTodo: ユーザ評価を督促するTODO
    • ReportToRecieveTodo: 商品受け取りの報告を督促するTODO
  • Contact: お問い合わせ

そんなに多くないですね。ここだけ見ると、すぐに作れる人もいると思います。実際、そこまで複雑な機能が求められるかはユーザヒアリングをしないとわからないので、まずはこれくらいでいいのではないでしょうか。

一つずつ書いていきます。djangoのコードになっていますが、なんとなく構造はわかると思います

Userモデル

DjangoのAbstractBaseUserクラスを継承して作成しました。

class User(PermissionsMixin, AbstractBaseUser):
    username = models.CharField(
        ('username'),
        max_length=150,
        unique=True,
        help_text=(
            '英数字と@, ., +, -, _が使えます'),
        validators=[username_validator],
        error_messages={
            'unique': ("このユーザ名は既に登録されています"),
        },
    )
    email = models.EmailField(('email'), unique=True)
    intro = models.TextField(('intro'), max_length=200, blank=True)
    date_joined = models.DateTimeField(default=timezone.now, editable=False)
    is_active = models.BooleanField(default=False)
    icon = VersatileImageField('',upload_to='account',blank=True)
    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'

Productモデル

商品画像に関しては、ProductImageモデルが管理しています。Djangoは若干この関連付けが特殊jなような気がしてます。Productモデルは画像関係のカラムを持っていないんですね。しかし、実はこれでもProductインスタンスから、ProductImageを取り出すことができるんです。

詳しくは以下をどうぞ

多対一 (many-to-one) 関係 | Django documentation | Django

class Product(models.Model):
    seller = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='seller')
    title  = models.CharField('商品名', max_length=200, validators=[has_no_singlequote])
    description = models.TextField('説明文', max_length=2000)
    price = models.PositiveIntegerField('値段(円)', default=0)
    is_sold = models.BooleanField(default=False)
    created_date = models.DateTimeField(default=timezone.now)
    updated_date = models.DateTimeField(blank=True, null=True)
    wanting_users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='wanting_users')
    buyer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, related_name='buyer')
    access_level = models.CharField('公開/非公開',
        max_length=10,
        choices=[(level.name, level.value) for level in AccessLevelChoice],
        default='public'
    )

    def update(self):
        self.updated_date = timezone.now()
        self.save()

    def __str__(self):
        return self.title

ProductImageモデル

class ProductImage(models.Model):
    image = VersatileImageField(
        '',
        upload_to='product', 
        blank=True,
        null=True
    )
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    def update(self):
        self.save()

    @property
    def thumbnail_url(self):
        if self.image and hasattr(self.image, 'url'):
            return self.image.thumbnail['600x600'].url

Chatモデル

商品の値段交渉や受け渡し方法の相談などをするためのチャットのためのモデルです。

ChatモデルとTalkモデルで分けています。

Chatモデルは以下の情報を持っています。チャットルームのような扱いです。

  • 商品(どの商品についてのチャットか)
  • 商品の売り手
  • 商品の買い手

Talkが実際のメッセージになります

class Chat(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True) 
    product_seller = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='product_seller') 
    product_wanting_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='product_wanting_user') 
    created_date = models.DateTimeField(default=timezone.now)
    updated_date = models.DateTimeField(blank=True, null=True)

    def update(self):
        self.updated_date = timezone.now()
        self.save()

Talkモデル

Talkモデルは以下の情報を持っています。実際のメッセージになります。

  • 話し手
  • チャットID(これでどのチャットルームで話されているか判定)
  • メッセージ本文
class Talk(models.Model):
    talker = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    chat = models.ForeignKey(Chat, on_delete=models.CASCADE, null=True)
    sentence = models.TextField(max_length=200)
    created_date = models.DateTimeField(default=timezone.now)
    updated_date = models.DateTimeField(blank=True, null=True)

    def update(self):
        self.updated_date = timezone.now()
        self.save()

UserRatingモデル

ユーザ評価のモデルです。

  • 商品ID
  • レビュアーID
  • レビュイーID
  • 評価値
class UserRating(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True)
    rating_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='rating_user')
    rated_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='rated_user')
    rating = models.CharField(max_length=6)

Notificationモデル

お知らせ・通知のモデルです。商品が売れたりした時の通知などに使います。

  • 通知の受け手
  • 既読フラグ
  • 通知メッセージ本文
  • 通知にあるURL(このように分離しておくとHTMLとして表現しやすくなる)
class Notification(models.Model):
    reciever = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    unread = models.BooleanField(default=True)
    message = models.TextField(max_length=256, null=True) 
    relative_url = models.TextField(max_length=256, null=True) 

TODOモデル(親クラス)

通知とほぼ同じなので省略

class Todo(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
    is_done = models.BooleanField(default=False)
    message = models.TextField(max_length=256, null=True) 
    relative_url = models.TextField(max_length=256, null=True) 
    created_date = models.DateTimeField(default=timezone.now)

    def done(self):
        self.is_done = True

    def update(self):
        self.save()

ReportToRecieveTodo

通知とほぼ同じなので省略

class ReportToRecieveTodo(Todo):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True)

    def set_template_message(self):
        self.message = '「'+self.product.title+'」の受け取りが完了しましたら、「商品を受け取りました」をクリックしてください'

RatingTodo

通知とほぼ同じなので省略

class RatingTodo(Todo):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True)

    def set_template_message(self):
        if self.user == self.product.seller:
            self.message = self.product.buyer.username+'との間での「'+self.product.title+'」の受け渡しの完了を確認しました。最後に購入者を評価してください。'
        else:
            self.message = self.product.seller.username+'との間での「'+self.product.title+'」の受け渡しの完了を確認しました。最後に出品者を評価してください。'

Contactモデル

問い合わせ(質問)についてのモデル。

  • 問い合わせ本文
  • メールアドレス
class Contact(models.Model):
    text = models.TextField(('お問い合わせ内容'), max_length=1000)
    email = models.EmailField(('メールアドレス'), default='')

4.2.2 インフラを含めた技術構成

技術構成図を再掲します。

f:id:swiftfe:20181006223428j:plain

だいたい以下のようにレイヤ分けされると思います。ここを詳しく話すと枝葉の話になるのでとりあえずここでは省略します。

ここについてはまた今度かきたいとおもいます。開発環境/QA環境/本番環境の3つに分けてdockerを使った開発について語ります多分。(11月)

  • アプリケーション
  • データベース
    • Postgres
  • Webサーバ
    • Nginx
    • Let's Encrypt(HTTPS対応)
  • その他
    • CentOS7
    • Docker
    • お名前.com(ドメイン取得)
    • ConohaVPS(配信サーバ)
  • メールサーバ

次回

次回は、以下の二つの話をする予定です

  • その他Djangoについての細かいこと
  • サイトのデザインについて

ついで: 僕のプログラミング歴について

以下の過去投稿にあります

swiftfe0.hatenablog.com

twitter.com