[Series] Xây dựng Hệ thống Bất động sản với Node.js & TypeScript - Bài 1: Thiết kế Database "Chuẩn chỉnh"
Chào các anh em Gophers (và cả các anh em đang muốn lấn sân sang Node.js)! Trong series này, chúng ta sẽ cùng nhau xây dựng một hệ thống Website Bất động sản từ con số 0. Một hệ thống thực tế không chỉ là CRUD (Create-Read-Update-Delete) đơn giản, mà nó còn liên quan đến quản lý tin đăng, gói thành viên (Pricing), phân loại tài sản và tương tác người dùng.
Móng nhà có chắc thì nhà mới bền. Vì vậy, Bài 1 chúng ta sẽ tập trung hoàn toàn vào việc thiết kế và hiện thực hóa Database.
1. Phân tích yêu cầu hệ thống
Dựa trên yêu cầu, hệ thống của chúng ta sẽ xoay quanh các thực thể chính:
- Users: Người dùng (chủ nhà, môi giới hoặc người tìm kiếm).
- Posts: Các tin đăng bất động sản (thông tin chi tiết về diện tích, vị trí, giá...).
- Pricings: Các gói dịch vụ để đẩy tin hoặc hiển thị ưu tiên.
- Interactions: Rating (đánh giá), Comments (bình luận), và WishLists (tin lưu lại).
- Tags: Phân loại tin đăng theo các từ khóa linh hoạt.
2. Hiện thực hóa bằng Code
Để đảm bảo tính nhất quán và dễ quản lý trong dự án TypeScript, mình khuyến khích các bạn sử dụng TypeORM hoặc Prisma. Tuy nhiên, để anh em có cái nhìn tổng quan nhất về cấu trúc dữ liệu, mình sẽ cung cấp file SQL khởi tạo và các Interface tương ứng trong TypeScript.
A. Định nghĩa Enums (TypeScript) Trong TypeScript, việc sử dụng Enum giúp code của chúng ta cực kỳ "clear" và tránh lỗi typo.
// types/enums.ts
export enum ListingTypes {
SALE = "Bán",
RENT = "Cho thuê"
}
export enum PropertyTypes {
APARTMENT = "Căn hộ chung cư",
STREET_HOUSE = "Nhà mặt phố",
PRIVATE_HOUSE = "Nhà riêng",
SHOPHOUSE = "Nhà phố thương mại",
VILLA = "Biệt thự",
LAND = "Đất nền",
FARM = "Trang trại",
RESORT = "Khu nghỉ dưỡng",
WAREHOUSE = "Kho",
FACTORY = "Nhà xưởng",
OTHER = "Khác"
}
export enum Directions {
NORTH_EAST = "Đông - Bắc",
SOUTH_WEST = "Tây - Nam",
SOUTH_EAST = "Đông - Nam",
NORTH_WEST = "Tây - Bắc",
EAST = "Đông",
WEST = "Tây",
SOUTH = "Nam",
NORTH = "Bắc"
}
export enum PostStatus {
AVAILABLE = "Còn trống",
NEGOTIATING = "Đang đàm phán",
HANDED_OVER = "Đã bàn giao"
}
B. SQL Schema (PostgreSQL/MySQL) Dưới đây là mã SQL để bạn có thể chạy trực tiếp vào database của mình:
-- 1. Create Enums
CREATE TYPE ListingTypes AS ENUM ('Bán', 'Cho thuê');
CREATE TYPE PropertyTypes AS ENUM ('Căn hộ chung cư', 'Nhà mặt phố', 'Nhà riêng', 'Nhà phố thương mại', 'Biệt thự', 'Đất nền', 'Bán đất', 'Trang trại', 'Khu nghỉ dưỡng', 'Kho', 'Nhà xưởng', 'Khác');
CREATE TYPE Directions AS ENUM ('Đông - Bắc', 'Tây - Nam', 'Đông - Nam', 'Tây - Bắc', 'Đông', 'Tây', 'Nam', 'Bắc');
CREATE TYPE PostStatus AS ENUM ('Còn trống', 'Đang đàm phán', 'Đã bàn giao');
-- 2. Create Tables
CREATE TABLE pricings (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
isDisplayImmedialy BOOLEAN DEFAULT TRUE,
isShowDescription BOOLEAN DEFAULT TRUE,
priority INTEGER DEFAULT 0,
requireScore INTEGER DEFAULT 0,
price INTEGER NOT NULL,
expiredDay INTEGER NOT NULL
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
fullname VARCHAR(255),
phone VARCHAR(20),
emailVerified BOOLEAN DEFAULT FALSE,
phoneVerified BOOLEAN DEFAULT FALSE,
password VARCHAR(255) NOT NULL,
avatar TEXT,
balance BIGINT DEFAULT 0,
score INTEGER DEFAULT 0,
resetPwdToken VARCHAR(255),
resetPwdExpiry TIMESTAMP,
idPricing INTEGER REFERENCES pricings(id)
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
idPost UUID DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
address TEXT,
province VARCHAR(100),
district VARCHAR(100),
ward VARCHAR(100),
price BIGINT,
avgStar INTEGER DEFAULT 0,
size INTEGER,
description TEXT, -- Rich text/Markdown
floor INTEGER,
bedroom INTEGER,
bathroom INTEGER,
isFurniture BOOLEAN DEFAULT FALSE,
listingType ListingTypes,
propertyType PropertyTypes,
direction Directions,
verified BOOLEAN DEFAULT FALSE,
expiredData TIMESTAMP,
expiredPost TIMESTAMP,
status PostStatus DEFAULT 'Còn trống',
idUser INTEGER REFERENCES users(id)
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
idPost INTEGER REFERENCES posts(id),
idUser INTEGER REFERENCES users(id),
content TEXT NOT NULL,
idParent INTEGER -- For nested comments
);
CREATE TABLE ratings (
id SERIAL PRIMARY KEY,
idPost INTEGER REFERENCES posts(id),
idUser INTEGER REFERENCES users(id),
content TEXT,
star INTEGER CHECK (star >= 1 AND star <= 5)
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
tag VARCHAR(100) UNIQUE NOT NULL
);
CREATE TABLE tags_posts (
id SERIAL PRIMARY KEY,
idPost INTEGER REFERENCES posts(id),
idTag INTEGER REFERENCES tags(id)
);
CREATE TABLE wishlists (
id SERIAL PRIMARY KEY,
idPost INTEGER REFERENCES posts(id),
idUser INTEGER REFERENCES users(id),
content TEXT,
idParent INTEGER
);
3. Giải thích các điểm cần lưu ý
Gói dịch vụ (Pricings): Bảng này rất quan trọng để thương mại hóa ứng dụng. Cột priority giúp chúng ta logic hóa việc hiển thị tin nào lên đầu trang.
Địa chỉ (Province/District/Ward): Mình tách ra 3 cột để sau này dễ dàng làm tính năng Filter (Lọc) theo khu vực.
Hệ thống điểm (Score/Balance): Người dùng có thể nạp tiền (balance) để mua gói (idPricing) hoặc tích lũy điểm thưởng (score) để nâng cấp tài khoản.
Bình luận lồng nhau (idParent): Trong bảng Comments, việc có idParent cho phép chúng ta làm tính năng Reply (Phản hồi) bình luận, giúp cộng đồng tương tác tốt hơn.
4. Tổng kết
Vậy là chúng ta đã hoàn thành bản thiết kế Database cho dự án Bất động sản. Việc thiết kế kỹ lưỡng ngay từ đầu sẽ giúp bạn tránh được việc phải ALTER TABLE liên tục khi dự án phình to.
All rights reserved