首頁  >  文章  >  web前端  >  從零到英雄:我建立房產租賃網站和行動應用程式的旅程

從零到英雄:我建立房產租賃網站和行動應用程式的旅程

Susan Sarandon
Susan Sarandon原創
2024-11-11 16:09:031021瀏覽

內容

  1. 簡介
  2. 技術堆疊
  3. 快速概述
  4. 現場示範
  5. API
  6. 前端
  7. 行動應用程式
  8. 管理儀表板
  9. 興趣點
  10. 資源

原始碼:https://github.com/aelassas/movinin

示範:https://movinin.dynv6.net:3004

介紹

這個想法源於建立無邊界的願望 - 一個完全可定制和可操作的房產租賃網站和移動應用程序,其中每個方面都在您的控制之下:

  • 擁有 UI/UX:設計獨特的客戶體驗,而無需克服模板限制
  • 控制後端:實現完美匹配需求的自訂業務邏輯和資料結構
  • 掌握 DevOps:使用首選工具和工作流程部署、擴展和監控應用程式
  • 自由擴充:增加新功能和集成,無需平台限製或額外費用

技術需求:

  • 支付網關
    • 實施安全、國際支援的支付網關
    • 確保跨多個國家和貨幣的兼容性
  • DevOps
    • 使用 Docker 容器進行部署以實現一致性和可擴展性
    • 託管在最小的基礎設施上(1GB RAM 伺服器)
    • 使用 Hetzner 或 DigitalOcean 等提供者將每月託管成本維持在 5 美元以下
    • 最佳化資源使用以實現高效運作

技術堆疊

這是使其成為可能的技術堆疊:

  • 打字稿
  • Node.js
  • MongoDB
  • 反應
  • MUI
  • React Native
  • 世博會
  • 條紋
  • 碼頭工人

由於 TypeScript 具有眾多優點,因此做出了使用 TypeScript 的關鍵設計決定。 TypeScript 提供強大的類型、工具和集成,從而產生高品質、可擴展、更具可讀性和可維護性的程式碼,並且易於調試和測試。

我選擇React是因為它強大的渲染能力,MongoDB是為了靈活的資料建模,而Stripe是為了安全的支付處理。

選擇此堆疊,您不僅僅是在建立網站和行動應用程式 - 您正在投資一個可以根據您的需求不斷發展的基礎,並得到強大的開源技術和不斷發展的開發者社群的支持。

React 因其以下優點而成為絕佳選擇:

  1. 基於組件的架構
    • 讓您將複雜的 UI 分解為更小的、可重複使用的部分
    • 使程式碼更易於維護且更易於測試
    • 實現更好的程式碼組織和可重複使用性
  2. 虛擬 DOM 效能
    • React 的虛擬 DOM 有效率地僅更新必要的內容
    • 帶來更快的頁面載入和更好的使用者體驗
    • 減少不必要的重新渲染
  3. 豐富的生態系統
    • 龐大的預建元件庫
    • 豐富的工具
    • 提供支持和資源的大型社區
  4. 豐富的開發人員經驗
    • 熱重載以取得即時回饋
    • 優秀的調試工具
    • JSX 讓寫 UI 程式碼更直覺
  5. 業界支持
    • 由 Meta(以前的 Facebook)支援
    • 被許多大公司使用
    • 持續開發與改進
  6. 靈活性
    • 適用於小型和大型應用程式
    • 可以逐步整合到現有專案中
    • 支援多種渲染策略(客戶端、服務端、靜態)

快速概覽

在本部分中,您將看到前端、管理儀表板和行動應用程式的主頁。

前端

在前端,客戶可以搜尋可用房產、選擇房產並結​​帳。

下面是前端的主頁,客戶可以在其中輸入位置點和時間,並搜尋可用的屬性。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是主頁的搜尋結果,客戶可以在其中選擇出租房產。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是客戶可以查看房產詳情的頁面:

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是該物業的圖片視圖:

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

下面是結帳頁面,客戶可以在其中設定租賃選項和結帳。如果顧客未註冊,可以同時結帳和註冊。如果他尚未註冊,他將收到一封確認和啟動電子郵件以設定密碼。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是登入頁面。在生產中,身份驗證 cookie 是 httpOnly、簽署的、安全且嚴格的 sameSite。這些選項可防止 XSS、CSRF 和 MITM 攻擊。身份驗證 cookie 也可以透過自訂中間件免受 XST 攻擊。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是註冊頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是客戶可以查看和管理他的預訂的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是客戶可以查看預訂詳細資訊的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是客戶可以看到他的通知的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是客戶可以管理其設定的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是客戶可以更改密碼的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

就是這樣。這是前端的主要頁面。

管理儀表板

三類使用者:

  • 管理員:他們擁有對管理儀表板的完全存取權。他們什麼都能做。
  • 機構:他們對管理儀表板的存取權限有限。他們只能管理自己的財產、預訂和客戶。
  • 客戶:他們只能存取前端和行動應用程式。他們無法存取管理儀表板。

該平台旨在與多個機構合作。每個機構都可以透過管理儀表板管理其財產、客戶和預訂。該平台也可以只與一個機構合作。

管理員可以從後端建立和管理代理商、飯店、地點、客戶和預訂。

建立新代理商時,他們會收到一封電子郵件,提示他們建立帳戶以存取管理儀表板,以便他們可以管理其財產、客戶和預訂。

以下是管理儀表板的登入頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

下面是儀表板頁面,管理員和代理商可以在其中查看和管理預訂。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

如果預訂狀態發生變化,相關客戶將收到通知和電子郵件。

下面是顯示和管理屬性的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是管理員和機構可以透過提供圖像和房產資訊來建立新房產的頁面。如需免費取消,請將其設為 0。否則,請設定該選項的價格,或者如果您不想包含它,則將其留空。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是管理員和機構可以編輯屬性的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是管理員可以管理客戶的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

如果代理商想要從管理儀表板建立預訂,以下是建立預訂的頁面。否則,當從前端或行動應用程式完成結帳流程時,會自動建立預訂。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是編輯預訂的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是管理代理商的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是建立新代理商的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是編輯機構的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是查看代理商屬性的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是查看客戶預訂的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是管理員和機構可以管理其設定的頁面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

還有其他頁面,但這些是管理儀表板的主頁。

就是這樣。這是管理儀表板的主要頁面。

現場演示

前端

  • 網址:https://movinin.dynv6.net:3004/
  • 登入:jdoe@movinin.io
  • 密碼:M00vinin

管理儀表板

  • 網址:https://movinin.dynv6.net:3003/
  • 登入:admin@movinin.io
  • 密碼:M00vinin

手機應用程式

您可以在任何 Android 裝置上安裝 Android 應用程式。

使用設備掃描此代碼

開啟相機應用程式並將其指向此程式碼。然後點選出現的通知。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

如何在 Android 上安裝行動應用程式

  • 在運行Android 8.0(API 等級26)及更高版本的裝置上,您必須導航至「安裝未知應用程式」系統設定畫面,才能從特定位置(即您下載應用程式的網路瀏覽器)啟用應用程式安裝.

  • 在運行 Android 7.1.1(API 等級 25)及更低版本的裝置上,您應該啟用「未知來源」系統設置,可在「設定」>「設定」中找到。您設備的安全性。

另類方式

您也可以透過直接下載APK並將其安裝在任何Android設備上來安裝Android應用程式。

  • 下載APK
  • 登入:jdoe@movinin.io
  • 密碼:M00vinin

應用程式介面

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

API 公開了管理儀表板、前端和行動應用程式所需的所有功能。 API遵循MVC設計模式。 JWT 用於身份驗證。有些功能需要身份驗證,例如與管理屬性、預訂和客戶相關的功能,而其他功能則不需要身份驗證,例如檢索未經身份驗證的用戶的位置和可用屬性:

  • ./api/src/models/ 資料夾包含 MongoDB 模型。
  • ./api/src/routes/ 資料夾包含 Express 路線。
  • ./api/src/controllers/ 資料夾包含控制器。
  • ./api/src/middlewares/ 資料夾包含中間件。
  • ./api/src/config/env.config.ts 包含設定和 TypeScript 類型定義。
  • ./api/src/lang/ 資料夾包含在地化內容。
  • ./api/src/app.ts 是載入路由的主伺服器。
  • ./api/index.ts 是 API 的主要入口點。

index.ts 是 API 的主要入口點:

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https, { ServerOptions } from 'node:https'
import app from './app'
import * as databaseHelper from './common/databaseHelper'
import * as env from './config/env.config'
import * as logger from './common/logger'

if (
  await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) 
  && await databaseHelper.initialize()
) {
  let server: http.Server | https.Server

  if (env.HTTPS) {
    https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
    const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
    const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
    const credentials: ServerOptions = { key: privateKey, cert: certificate }
    server = https.createServer(credentials, app)

    server.listen(env.PORT, () => {
      logger.info('HTTPS server is running on Port', env.PORT)
    })
  } else {
    server = app.listen(env.PORT, () => {
      logger.info('HTTP server is running on Port', env.PORT)
    })
  }

  const close = () => {
    logger.info('Gracefully stopping...')
    server.close(async () => {
      logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
      await databaseHelper.close(true)
      logger.info('MongoDB connection closed')
      process.exit(0)
    })
  }

  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

這是一個使用 Node.js 和 Express 啟動伺服器的 TypeScript 檔案。它導入了多個模組,包括 dotenv、process、fs、http、https、mongoose 和 app。然後,它檢查 HTTPS 環境變數是否設為 true,如果是,則使用 https 模組以及提供的私密金鑰和憑證建立 HTTPS 伺服器。否則,它使用 http 模組建立一個 HTTP 伺服器。伺服器監聽 PORT 環境變數中指定的連接埠。

close 函數被定義為在收到終止訊號時優雅地停止伺服器。它關閉伺服器和 MongoDB 連接,然後以狀態碼 0 退出進程。最後,它註冊當進程收到 SIGINT、SIGTERM 或 SIGQUIT 訊號時要呼叫的 close 函數。

app.ts 是 api 的主要入口點:

import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import i18n from './lang/i18n'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import stripeRoutes from './routes/stripeRoutes'
import countryRoutes from './routes/countryRoutes'
import * as helper from './common/helper'

const app = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

helper.mkdir(env.CDN_USERS)
helper.mkdir(env.CDN_TEMP_USERS)
helper.mkdir(env.CDN_PROPERTIES)
helper.mkdir(env.CDN_TEMP_PROPERTIES)
helper.mkdir(env.CDN_LOCATIONS)
helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app

首先,我們檢索 MongoDB 連接字串,然後與 MongoDB 資料庫建立連線。然後我們建立一個 Express 應用並載入 cors、compression、helmet 和 nocache 等中間件。我們使用頭盔中間件庫設置了各種安全措施。我們也為應用程式的不同部分匯入各種路由文件,例如:supplierRoutes、bookingRoutes、locationRoutes、notificationRoutes、propertyRoutes 和 userRoutes。最後,我們載入 Express 路線並匯出應用程式。

API中有8條路由。每條路線都有自己的控制器,遵循 MVC 設計模式和 SOLID 原則。主要路線如下:

  • userRoutes:提供與使用者相關的REST功能
  • agencyRoutes:提供與機構相關的REST功能
  • countryRoutes:提供與國家相關的REST功能
  • locationRoutes:提供與位置相關的REST函數
  • propertyRoutes:提供與屬性相關的REST函數
  • bookingRoutes:提供與預訂相關的REST功能
  • notificationRoutes:提供與通知相關的REST功能
  • stripeRoutes:提供與Stripe支付網關相關的REST功能

我們不會一一解釋每條路線。例如,我們將以 propertyRoutes 為例,看看它是如何製作的。您可以瀏覽原始程式碼並查看所有路由。

這是 propertyRoutes.ts:

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https, { ServerOptions } from 'node:https'
import app from './app'
import * as databaseHelper from './common/databaseHelper'
import * as env from './config/env.config'
import * as logger from './common/logger'

if (
  await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) 
  && await databaseHelper.initialize()
) {
  let server: http.Server | https.Server

  if (env.HTTPS) {
    https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
    const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
    const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
    const credentials: ServerOptions = { key: privateKey, cert: certificate }
    server = https.createServer(credentials, app)

    server.listen(env.PORT, () => {
      logger.info('HTTPS server is running on Port', env.PORT)
    })
  } else {
    server = app.listen(env.PORT, () => {
      logger.info('HTTP server is running on Port', env.PORT)
    })
  }

  const close = () => {
    logger.info('Gracefully stopping...')
    server.close(async () => {
      logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
      await databaseHelper.close(true)
      logger.info('MongoDB connection closed')
      process.exit(0)
    })
  }

  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

首先,我們建立一個 Express Router。然後,我們使用名稱、方法、中間件和控制器來建立路由。

routeNames 包含 propertyRoutes 路由名稱:

import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import i18n from './lang/i18n'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import stripeRoutes from './routes/stripeRoutes'
import countryRoutes from './routes/countryRoutes'
import * as helper from './common/helper'

const app = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

helper.mkdir(env.CDN_USERS)
helper.mkdir(env.CDN_TEMP_USERS)
helper.mkdir(env.CDN_PROPERTIES)
helper.mkdir(env.CDN_TEMP_PROPERTIES)
helper.mkdir(env.CDN_LOCATIONS)
helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app

propertyController 包含有關位置的主要業務邏輯。我們不會看到控制器的所有原始程式碼,因為它相當大,但我們將以創建控制器函數為例。

以下是房產模型:

import express from 'express'
import multer from 'multer'
import routeNames from '../config/propertyRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as propertyController from '../controllers/propertyController'

const routes = express.Router()

routes.route(routeNames.create).post(authJwt.verifyToken, propertyController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, propertyController.update)
routes.route(routeNames.checkProperty).get(authJwt.verifyToken, propertyController.checkProperty)
routes.route(routeNames.delete).delete(authJwt.verifyToken, propertyController.deleteProperty)
routes.route(routeNames.uploadImage).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('image')], propertyController.uploadImage)
routes.route(routeNames.deleteImage).post(authJwt.verifyToken, propertyController.deleteImage)
routes.route(routeNames.deleteTempImage).post(authJwt.verifyToken, propertyController.deleteTempImage)
routes.route(routeNames.getProperty).get(propertyController.getProperty)
routes.route(routeNames.getProperties).post(authJwt.verifyToken, propertyController.getProperties)
routes.route(routeNames.getBookingProperties).post(authJwt.verifyToken, propertyController.getBookingProperties)
routes.route(routeNames.getFrontendProperties).post(propertyController.getFrontendProperties)

export default routes

以下是房產類型:

const routes = {
  create: '/api/create-property',
  update: '/api/update-property',
  delete: '/api/delete-property/:id',
  uploadImage: '/api/upload-property-image',
  deleteTempImage: '/api/delete-temp-property-image/:fileName',
  deleteImage: '/api/delete-property-image/:property/:image',
  getProperty: '/api/property/:id/:language',
  getProperties: '/api/properties/:page/:size',
  getBookingProperties: '/api/booking-properties/:page/:size',
  getFrontendProperties: '/api/frontend-properties/:page/:size',
  checkProperty: '/api/check-property/:id',
}

export default routes

屬性由以下部分組成:

  • 名字
  • A 類型(公寓、商業、農場、住宅、工業、地塊、聯排別墅)
  • 創建它的機構的參考
  • 描述
  • 主圖
  • 其他圖片
  • 臥室數量
  • 浴室數量
  • 廚房數量
  • 停車位數量
  • A 尺寸
  • 租最低年齡
  • 地點
  • 位址(可選)
  • 價格
  • 租賃期間(每月、每週、每日、每年)
  • 取消價格(設定為0免費包含,不想包含則留空,或設定取消價格)
  • 表示是否允許攜帶寵物的標誌
  • 指示房產是否配備家具的標誌
  • 指示屬性是否隱藏的標誌
  • 指示空調是否可用的標誌
  • 指示該房產是否可供出租的標誌

以下是建立控制器函數:

import { Schema, model } from 'mongoose'
import * as movininTypes from ':movinin-types'
import * as env from '../config/env.config'

const propertySchema = new Schema<env.Property>(
  {
    name: {
      type: String,
      required: [true, "can't be blank"],
    },
    type: {
      type: String,
      enum: [
        movininTypes.PropertyType.House,
        movininTypes.PropertyType.Apartment,
        movininTypes.PropertyType.Townhouse,
        movininTypes.PropertyType.Plot,
        movininTypes.PropertyType.Farm,
        movininTypes.PropertyType.Commercial,
        movininTypes.PropertyType.Industrial,
      ],
      required: [true, "can't be blank"],
    },
    agency: {
      type: Schema.Types.ObjectId,
      required: [true, "can't be blank"],
      ref: 'User',
      index: true,
    },
    description: {
      type: String,
      required: [true, "can't be blank"],
    },
    available: {
      type: Boolean,
      default: true,
    },
    image: {
      type: String,
    },
    images: {
      type: [String],
    },
    bedrooms: {
      type: Number,
      required: [true, "can't be blank"],
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    bathrooms: {
      type: Number,
      required: [true, "can't be blank"],
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    kitchens: {
      type: Number,
      default: 1,
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    parkingSpaces: {
      type: Number,
      default: 0,
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    size: {
      type: Number,
    },
    petsAllowed: {
      type: Boolean,
      required: [true, "can't be blank"],
    },
    furnished: {
      type: Boolean,
      required: [true, "can't be blank"],
    },
    minimumAge: {
      type: Number,
      required: [true, "can't be blank"],
      min: env.MINIMUM_AGE,
      max: 99,
    },
    location: {
      type: Schema.Types.ObjectId,
      ref: 'Location',
      required: [true, "can't be blank"],
    },
    address: {
      type: String,
    },
    price: {
      type: Number,
      required: [true, "can't be blank"],
    },
    hidden: {
      type: Boolean,
      default: false,
    },
    cancellation: {
      type: Number,
      default: 0,
    },
    aircon: {
      type: Boolean,
      default: false,
    },
    rentalTerm: {
      type: String,
      enum: [
        movininTypes.RentalTerm.Monthly,
        movininTypes.RentalTerm.Weekly,
        movininTypes.RentalTerm.Daily,
        movininTypes.RentalTerm.Yearly,
      ],
      required: [true, "can't be blank"],
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'Property',
  },
)

const Property = model<env.Property>('Property', propertySchema)

export default Property

前端

前端是一個使用 Node.js、React、MUI 和 TypeScript 建立的 Web 應用程式。在前端,客戶可以根據接送點和時間搜尋可用的汽車,選擇汽車並繼續結帳:

  • ./frontend/src/assets/ 資料夾包含 CSS 和圖片。
  • ./frontend/src/pages/ 資料夾包含 React 頁面。
  • ./frontend/src/components/ 資料夾包含 React 元件。
  • ./frontend/src/services/ 包含 api 客戶端服務。
  • ./frontend/src/App.tsx 是包含路由的主要 React 應用程式。
  • ./frontend/src/index.tsx 是前端的主要入口點。

TypeScript 類型定義在套件 ./packages/movinin-types 中定義。

App.tsx 是主要的 React 應用程式:

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https, { ServerOptions } from 'node:https'
import app from './app'
import * as databaseHelper from './common/databaseHelper'
import * as env from './config/env.config'
import * as logger from './common/logger'

if (
  await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) 
  && await databaseHelper.initialize()
) {
  let server: http.Server | https.Server

  if (env.HTTPS) {
    https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
    const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
    const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
    const credentials: ServerOptions = { key: privateKey, cert: certificate }
    server = https.createServer(credentials, app)

    server.listen(env.PORT, () => {
      logger.info('HTTPS server is running on Port', env.PORT)
    })
  } else {
    server = app.listen(env.PORT, () => {
      logger.info('HTTP server is running on Port', env.PORT)
    })
  }

  const close = () => {
    logger.info('Gracefully stopping...')
    server.close(async () => {
      logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
      await databaseHelper.close(true)
      logger.info('MongoDB connection closed')
      process.exit(0)
    })
  }

  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

我們使用 React 延遲載入來載入每個路由。

我們不會涵蓋前端的每一頁,但您可以瀏覽原始程式碼並查看每一頁。

手機應用程式

該平台提供適用於 Android 和 iOS 的本機行動應用程式。這個行動應用程式是使用 React Native、Expo 和 TypeScript 建構的。與前端一樣,行動應用程式允許客戶根據接送點和時間搜尋可用的汽車,選擇汽車並繼續結帳。

如果他的預訂從後端更新,客戶會收到推播通知。推播通知是使用 Node.js、Expo Server SDK 和 Firebase 建構的。

  • ./mobile/assets/ 資料夾包含影像。
  • ./mobile/screens/ 資料夾包含主要的 React Native 螢幕。
  • ./mobile/components/ 資料夾包含 React Native 元件。
  • ./mobile/services/ 包含 api 客戶端服務。
  • ./mobile/App.tsx 是主要的 React Native 應用程式。

TypeScript 類型定義定義於:

  • ./mobile/types/index.d.ts
  • ./mobile/types/env.d.ts
  • ./mobile/miscellaneous/movininTypes.ts

./mobile/types/ 載入到 ./mobile/tsconfig.json 中,如下所示:

import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import i18n from './lang/i18n'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import stripeRoutes from './routes/stripeRoutes'
import countryRoutes from './routes/countryRoutes'
import * as helper from './common/helper'

const app = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

helper.mkdir(env.CDN_USERS)
helper.mkdir(env.CDN_TEMP_USERS)
helper.mkdir(env.CDN_PROPERTIES)
helper.mkdir(env.CDN_TEMP_PROPERTIES)
helper.mkdir(env.CDN_LOCATIONS)
helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app

App.tsx 是 React Native 應用程式的主要入口點:

導入'react-native-gesture-handler'
從 'react' 導入 React, { useCallback, useEffect, useRef, useState }
從 'react-native-root-siblings' 導入 { RootSiblingParent }
從'@react-navigation/native'導入{NavigationContainer,NavigationContainerRef}
從“expo-status-bar”導入 { StatusBar as ExpoStatusBar }
從 'react-native-safe-area-context' 導入 { SafeAreaProvider }
從“react-native-paper”導入{Provider}
從“expo-splash-screen”導入 * as SplashScreen
導入 * 作為來自“expo-notifications”的通知
從 '@stripe/stripe-react-native' 導入 { StripeProvider }
從 './components/DrawerNavigator' 導入 DrawerNavigator
從 './common/helper' 導入 * 作為助手
從'./services/NotificationService'導入*作為NotificationService
從 './services/UserService' 導入 * 作為 UserService
從 './context/GlobalContext' 導入 { GlobalProvider }
從 './config/env.config' 導入 * 作為 env

通知.setNotificationHandler({
  handleNotification: async() =>; ({
    應該會顯示警報:真,
    應該播放聲音:真,
    應該要設定徽章:真,
  }),
})

//
// 防止本機啟動畫面在應用程式元件宣告之前自動隱藏
//
SplashScreen.preventAutoHideAsync()
  .then((結果) => console.log(`SplashScreen.preventAutoHideAsync() 成功:${result}`))
  .catch(console.warn) // 最好明確捕獲並檢查任何錯誤

const App = () =>; {
  const [appIsReady, setAppIsReady] = useState(false)

  const responseListener = useRef<notifications.subscription>()
  const navigationRef = useRef<navigationcontainerref>>(null)

  useEffect(() => {
    const 暫存器 = async() => {
      const LoggedIn = 等待 UserService.loggedIn()
      如果(登入){
        const currentUser = 等待 UserService.getCurrentUser()
        if (目前使用者?._id) {
          等待 helper.registerPushToken(currentUser._id)
        } 別的 {
          helper.error()
        }
      }
    }

    //
    // 註冊推播通知令牌
    //
    登記()

    //
    // 每當使用者點擊通知或與通知互動時就會觸發此偵聽器(當應用程式處於前台、背景或終止時有效)
    //
    responseListener.current = Notifications.addNotificationResponseReceivedListener(async (response) => {
      嘗試 {
        如果(navigationRef.current){
          const { 資料 } = 回應.通知.請求.內容

          如果(資料.預訂){
            if (data.user && data.notification) {
              等待NotificationService.markAsRead(data.user, [data.notification])
            }
            navigationRef.current.navigate('預訂', { id: data.booking })
          } 別的 {
            navigationRef.current.navigate('通知', {})
          }
        }
      } 捕獲(錯誤){
        helper.error(錯誤,錯誤)
      }
    })

    返回() => {
      Notifications.removeNotificationSubscription(responseListener.current!)
    }
  }, [])

  setTimeout(() => {
    設定應用程式已就緒(true)
  }, 500)

  const onReady = useCallback(async () => {
    如果(應用程式已就緒){
      //
      // 這告訴啟動畫面立即隱藏!如果我們之後調用這個
      // `setAppIsReady`,那麼當應用程式運行時我們可能會看到一個空白螢幕
      // 載入其初始狀態並渲染其第一個像素。所以相反,
      // 一旦我們知道根視圖已經隱藏了啟動畫面
      // 執行佈局。
      //
      等待 SplashScreen.hideAsync()
    }
  }, [應用程式已就緒])

  如果(!appIsReady){
    傳回空值
  }

  返回 (
    
      
        
          <stripeproviderpublishablekey>;
            <rootsiblingparent>
              <navigationcontainer ref="{navigationRef}" onready="{onReady}">
                



<p>我們不會涵蓋行動應用程式的每個螢幕,但您可以瀏覽原始程式碼並查看每個螢幕。 </p>

<h2>
  
  
  管理儀表板
</h2>

<p>管理儀表板是一個使用 Node.js、React、MUI 和 TypeScript 建立的 Web 應用程式。管理員可以從後端建立和管理供應商、汽車、位置、客戶和預訂。當從後端建立新的供應商時,他們將收到一封電子郵件,提示他們建立帳戶,以便存取管理儀表板並管理他們的車隊和預訂。 </p>

<ul>
<li>./backend/assets/ 資料夾包含 CSS 和圖片。 </li>
<li>./backend/pages/ 資料夾包含 React 頁面。 </li>
<li>./backend/components/ 資料夾包含 React 元件。 </li>
<li>./backend/services/ 包含 api 客戶端服務。 </li>
<li>./backend/App.tsx 是包含路由的主要 React 應用程式。 </li>
<li>./backend/index.tsx 是管理儀表板的主要入口點。 </li>
</ul>

<p>TypeScript 類型定義在套件 ./packages/movinin-types 中定義。 </p>

<p>管理儀表板的 App.tsx 遵循與前端的 App.tsx 類似的邏輯。 </p>

<p>我們不會涵蓋管理儀表板的每一頁,但您可以瀏覽原始程式碼並查看每一頁。 </p>

<h2>
  
  
  興趣點
</h2>

<p>使用 React Native 和 Expo 建立行動應用程式非常簡單。 Expo 讓使用 React Native 進行行動開發變得非常簡單。 </p>

<p>後端、前端和行動裝置開發使用同一種語言(TypeScript)非常方便。 </p>

<p>TypeScript 是一門非常有趣的語言,並且有很多優點。透過向 JavaScript 添加靜態類型,我們可以避免許多錯誤並產生高品質、可擴展、更具可讀性和可維護性的程式碼,並且易於調試和測試。 </p>

<p>就是這樣!我希望您喜歡閱讀這篇文章。 </p>
<h2>
  
  
  資源
</h2>

<ol>
<li>概述</li>
<li>建築</li>
<li>安裝(自架)</li>
<li>安裝(VPS)</li>
<li>
安裝(Docker)

<ol>
<li>Docker 映像</li>
<li>SSL</li>
</ol>


</li>

<li>設定條紋</li>

<li>建立行動應用程式</li>

<li>

演示資料庫

<ol>
<li>Windows、Linux 和 macOS</li>
<li>碼頭工人</li>
</ol>


</li>

<li>從源頭運行</li>

<li>

運行行動應用程式

<ol>
<li>先決條件</li>
<li>使用說明</li>
<li>推播通知</li>
</ol>


</li>

<li>更改貨幣</li>

<li>新增語言</li>

<li>單元測試和覆蓋率</li>

<li>日誌</li>

</ol>


          

            
        </navigationcontainer></rootsiblingparent></stripeproviderpublishablekey></navigationcontainerref></notifications.subscription>

以上是從零到英雄:我建立房產租賃網站和行動應用程式的旅程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn