0

Xây dựng Quản lý kho hàng (Listing Management) trong hệ thống Bất Động Sản

Trong ngành Bất động sản (Proptech), việc quản lý kho hàng (Listing Management) không đơn giản là CRUD mà là bài toán về Tính nhất quán dữ liệu và Quản lý trạng thái.

Nếu thiết kế kém, dữ liệu sẽ bị trùng lặp (nhiều môi giới cùng đăng một căn nhà) hoặc thông tin bị sai lệch (nhà đã bán nhưng vẫn hiện "Đang bán"). Chúng ta sẽ xây dựng module này với tư duy Enterprise: Sử dụng Spatial Data (tọa độ bản đồ), JSON Media, và Action Pattern để đảm bảo quy trình kiểm duyệtListing chặt chẽ.

Bước 1: Khởi tạo dự án và Thiết kế Cơ sở dữ liệu

Tạo dự án mới:

laravel new proptech-listings
cd proptech-listings

1. Tạo Model và Migration:

php artisan make:model Listing -m

2. Thiết kế bảng listings:

Chúng ta cần lưu trữ thông tin kỹ thuật, pháp lý và dữ liệu đa phương tiện (JSON).

// database/migrations/xxxx_create_listings_table.php
public function up(): void
{
    Schema::create('listings', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->string('slug')->unique();
        $table->text('description');
        
        // Thông số kỹ thuật
        $table->decimal('area', 10, 2); // Diện tích (m2)
        $table->decimal('price', 15, 2); // Giá bán
        
        // Vị trí & Bản đồ
        $table->string('address');
        $table->decimal('lat', 10, 8)->nullable(); // Vĩ độ
        $table->decimal('lng', 11, 8)->nullable(); // Kinh độ
        
        // Pháp lý & Phân loại
        $table->string('legal_status'); // Sổ hồng, sổ đỏ, đang chờ...
        $table->string('category'); // Nhà phố, Chung cư, Đất nền
        $table->string('status')->default('available'); // available, deposited, sold
        
        // Media (Lưu JSON các link ảnh/video 360)
        $table->json('media_assets')->nullable(); 

        $table->timestamps();
    });
}

Bước 2: Chuẩn hóa dữ liệu bằng PHP Enums

Để tránh việc nhập sai trạng thái (ví dụ: gõ nhầm 'da ban' thay vì 'sold'), ta dùng Enums.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Enums\ListingStatus;
use App\Enums\ListingCategory;
use Illuminate\Support\Str;

class Listing extends Model
{
    protected $fillable = [
        'title', 'slug', 'description', 'area', 'price', 'address', 
        'lat', 'lng', 'legal_status', 'category', 'status', 'media_assets'
    ];

    protected $casts = [
        'status' => ListingStatus::class,
        'category' => ListingCategory::class,
        'media_assets' => 'array', // Tự động convert JSON sang Array
    ];

    protected static function boot()
    {
        parent::boot();
        static::creating(fn ($listing) => $listing->slug = Str::slug($listing->title));
    }
}

Bước 3: Action Pattern - Tạo mới Bất động sản

Sử dụng Action giúp chúng ta dễ dàng thêm các logic kiểm tra trùng lặp vị trí hoặc tiêu đề sau này.

// app/Actions/CreateListingAction.php
namespace App\Actions;

use App\Models\Listing;
use Illuminate\Validation\ValidationException;

class CreateListingAction
{
    public function execute(array $data): Listing
    {
        // Logic kiểm tra trùng lặp đơn giản (Ví dụ: Trùng địa chỉ chính xác)
        if (Listing::where('address', $data['address'])->exists()) {
            throw ValidationException::withMessages([
                'address' => 'Bất động sản tại địa chỉ này đã tồn tại trong kho hàng.'
            ]);
        }

        return Listing::create($data);
    }
}

Bước 4: API Resource - Định dạng dữ liệu trả về

Frontend cần dữ liệu đẹp, ví dụ: Diện tích phải có đơn vị "m2", giá tiền phải được format.

// app/Http/Resources/ListingResource.php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class ListingResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'specifications' => [
                'area' => $this->area . ' m²',
                'price' => number_format($this->price) . ' VND',
                'category' => $this->category->value,
            ],
            'location' => [
                'full_address' => $this->address,
                'coords' => [
                    'lat' => (float) $this->lat,
                    'lng' => (float) $this->lng,
                ]
            ],
            'legal' => $this->legal_status,
            'status' => $this->status->value,
            'gallery' => $this->media_assets ?? [],
            'created_at' => $this->created_at->diffForHumans(),
        ];
    }
}

Bước 5: Controller & Routing

// app/Http/Controllers/Api/ListingController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Actions\CreateListingAction;
use App\Models\Listing;
use App\Http\Resources\ListingResource;

class ListingController extends Controller
{
    public function index()
    {
        $listings = Listing::latest()->paginate(10);
        return ListingResource::collection($listings);
    }

    public function store(Request $request, CreateListingAction $action)
    {
        $data = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'required|string',
            'area' => 'required|numeric',
            'price' => 'required|numeric',
            'address' => 'required|string',
            'category' => 'required|string',
            'legal_status' => 'required|string',
            'media_assets' => 'nullable|array',
            'lat' => 'nullable|numeric',
            'lng' => 'nullable|numeric',
        ]);

        $listing = $action->execute($data);

        return response()->json([
            'success' => true,
            'data' => new ListingResource($listing)
        ], 201);
    }
}

Routes (routes/api.php):

use App\Http\Controllers\Api\ListingController;

Route::get('/listings', [ListingController::class, 'index']);
Route::post('/listings', [ListingController::class, 'store']);

Bước 6: Thử lửa với Postman

Khởi động server: php artisan serve

  1. Tạo Listing mới (POST):
  • URL: http://127.0.0.1:8000/api/listings
  • Body (JSON):
{
    "title": "Căn hộ Landmark 81 - View sông Sài Gòn",
    "description": "Căn hộ cao cấp đầy đủ nội thất, tầng cao thoáng mát.",
    "area": 85.5,
    "price": 7500000000,
    "address": "Landmark 81, Vinhomes Central Park, Bình Thạnh",
    "category": "apartment",
    "legal_status": "Sổ hồng riêng",
    "media_assets": [
        "https://example.com/img1.jpg",
        "https://example.com/tour360.mp4"
    ],
    "lat": 10.7948,
    "lng": 106.7218
}

2. Kết quả mong đợi:

Hệ thống trả về JSON đã được format qua Resource, slug tự động sinh ra là can-ho-landmark-81-view-song-sai-gon.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí