65.9K
CodeProject 正在变化。 阅读更多。
Home

BookCars - 汽车租赁平台及移动应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (81投票s)

2022年11月10日

MIT

39分钟阅读

viewsIcon

270849

downloadIcon

8127

带移动应用的汽车租赁平台

目录

  1. 引言
  2. 特点
  3. 实时演示
  4. 必备组件
  5. 快速概览
    1. 前端
    2. 移动应用
    3. 后端
  6. 背景
  7. 安装(自托管)
  8. 安装 (VPS)
  9. 安装(Docker)
    1. Docker 镜像
    2. SSL
  10. 设置 Stripe
  11. 更改货币
  12. 添加新语言
  13. 演示数据库
    1. Windows, Linux, macOS
    2. Docker
  14. 构建移动应用
  15. 从源码运行
  16. 运行移动应用
  17. 单元测试和覆盖率
  18. 日志
  19. Using the Code
    1. API
    2. 前端
    3. 移动应用
    4. 后端
  20. 关注点
  21. 历史

引言

BookCars 是一个面向供应商的汽车租赁平台,提供后端用于管理汽车车队和预订,以及用于租赁汽车的前端和移动应用。

通过以下解决方案,您可以以极低的成本构建一个完全可定制的、针对多个供应商优化的汽车租赁网站,并且拥有一个运行中的 Stripe 支付网关,只需将其托管在至少 1GB RAM 的 Docker 虚拟机上。

BookCars 设计用于与多个供应商协作。每个供应商都可以从后端管理其汽车车队和预订。BookCars 也可以只与一个供应商合作,并用作汽车租赁聚合器。

通过后端,管理员可以创建和管理供应商、汽车、地点、客户和预订。

当创建新供应商时,他们会收到一封电子邮件,提示他们创建账户,以便访问后端并管理其汽车车队和预订。

客户可以从前端或移动应用注册,根据取车和还车地点及时间搜索可用车辆,选择车辆并完成结账流程。

一项关键的设计决策是使用 TypeScript 而不是 JavaScript,因为其具有诸多优势。TypeScript 提供了强类型、出色的工具支持和集成,从而产生了高质量、可扩展、更具可读性和可维护性的代码,易于调试和测试。

BookCars 可以在 Docker 容器中运行。在本文中,您可以找到如何构建 BookCars Docker 镜像并在 Docker 容器中运行它。

在本文中,您将学习 BookCars 是如何构建的,包括源代码的主要部分和软件架构的描述,如何部署它,以及如何运行源代码。但在深入研究之前,我们将从对平台的快速概述开始。

特点

  • 供应商管理
  • 支持单个或多个供应商
  • 车队管理
  • 地点、国家、停车位和地图功能
  • 预订管理
  • 支付管理
  • 客户管理
  • 多种支付方式(信用卡、PayPal、Google Pay、Apple Pay、Link、Pay Later)
  • 运营中的 Stripe 支付网关
  • 多语言支持(英语、法语)
  • 多种分页选项(经典的带有下一页和上一页按钮的分页,无限滚动)
  • 响应式后端和前端
  • 原生 Android 和 iOS 移动应用,代码库单一
  • 推送通知
  • 防 XSS、XST、CSRF 和 MITM 攻击
  • 支持的平台:iOS、Android、Web、Docker

实时演示

前端

后端

移动应用

您可以在任何 Android 设备上安装该 Android 应用。

用设备扫描此代码

打开相机应用,将其对准此代码。然后点击出现的通知。

如何在 Android 上安装移动应用

  • 在运行 Android 8.0 (API 级别 26) 及更高版本的设备上,您必须导航到“安装未知应用”系统设置屏幕,以允许从特定来源(即您正在下载应用的浏览器)安装应用。

  • 在运行 Android 7.1.1 (API 级别 25) 及更低版本的设备上,您应该在设备上启用“设置”>“安全”中的“未知来源”系统设置。

备用方法

您也可以通过直接下载 APK 并将其安装在任何 Android 设备上来安装 Android 应用。

  • 下载 APK
  • 登录:jdoe@bookcars.ma
  • 密码:B00kC4r5

必备组件

  • TypeScript
  • Node.js
  • Express
  • MongoDB
  • React
  • Vite
  • MUI
  • React Native
  • Expo
  • JWT
  • MVC
  • Docker
  • NGINX/Apache
  • Git

快速概览

在本节中,您将看到前端、后端和移动应用主要页面的快速概述。

前端

通过前端,客户可以搜索可用车辆,选择车辆并结账。

下图是前端的主页面,客户可以在其中选择取车和还车地点及时间,然后搜索可用车辆。

下图是主页面的搜索结果,客户可以在其中选择要租赁的车辆。

下图是结账页面,客户可以在其中设置租赁选项并结账。如果客户未注册,他可以同时结账并注册。他将收到一封确认和激活电子邮件,提示他设置密码(如果尚未注册)。

下图是登录页面。在生产环境中,身份验证 cookie 是 httpOnly、已签名、安全且严格的 sameSite。这些选项可防止 XSS、CSRF 和 MITM 攻击。通过自定义中间件,身份验证 cookie 也可免受 XST 攻击。

下图是注册页面。

下图是客户查看和管理其预订的页面。

下图是客户查看预订详情的页面。

下图是联系页面。

下图是汽车租赁地点页面。

下图是客户查看和管理其通知的页面。

还有其他页面,但这些是前端的主要页面。

移动应用

通过移动应用,客户可以搜索可用车辆,选择车辆并结账。

如果客户的预订状态从后端更新,客户还可以接收推送通知。

下图是移动应用的主要页面,客户可以在其中选择取车和还车地点及时间,然后搜索可用车辆。

下图是主页面的搜索结果,客户可以在其中选择要租赁的车辆并结账。

下图是登录和注册页面。

下图是客户查看和管理其预订的页面。

下图是客户更新其个人信息、更改密码和查看其通知的页面。

以上是移动应用的主要页面。

后端

BookCars 是面向供应商的。这意味着有三种类型的用户

  • 管理员:他拥有对后端的完全访问权限。他可以做任何事情。
  • 供应商:他拥有对后端的有限访问权限。他只能管理其汽车车队和预订。
  • 客户:他只能访问前端和移动应用。他无法访问后端。

BookCars 设计用于与多个供应商协作。每个供应商都可以从后端管理其汽车车队和预订。BookCars 也可以只与一个供应商合作。

通过后端,管理员可以创建和管理供应商、汽车、地点、客户和预订。

当创建新供应商时,他们会收到一封电子邮件,提示他们创建账户,以便访问后端并管理其汽车车队和预订。

下图是后端的登录页面。

下图是后端的仪表板页面,管理员和供应商可以在其中查看和管理预订。

下图是显示汽车车队并可进行管理的页面。

下图是管理员和供应商通过提供图像和汽车信息来创建新汽车的页面。如果汽车选项是免费包含的,则将相应汽车选项设置为 0。否则,设置选项的价格或留空如果您不想包含它。

下图是管理员和供应商编辑汽车的页面。

下图是管理员管理用户的页面。

下图是编辑预订的页面。

还有其他页面,但这些是后端的主要页面。

背景

BookCars 的基本理念非常简单

  • 后端:管理员通过后端创建新供应商。每个供应商都会收到一封自动邮件以激活其账户并获得对后端的访问权限,以便管理其汽车车队、预订和客户。
  • 前端和移动应用:客户通过前端和移动应用,可以根据取车和还车地点及时间查看可用车辆。然后,他们可以进行结账以预订车辆。

安装(自托管)

BookCars 是跨平台的,可以在 Windows、Linux 和 macOS 上运行和安装。

以下是 Linux 上的安装说明。

必备组件

  1. 安装 gitNode.jsNGINXMongoDBmongosh。如果您想使用 MongoDB Atlas,可以跳过安装和配置 MongoDB。

  2. 配置 MongoDB

    mongosh

    创建管理员用户

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD", 
    roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

    PASSWORD 替换为强密码。

    安全 MongoDB

    sudo nano /etc/mongod.conf

    按以下方式更改配置

    net:
    port: 27017
    bindIp: 0.0.0.0
    
    security:
    authorization: enabled

    重启 MongoDB 服务

    sudo systemctl restart mongod.service
    sudo systemctl status mongod.service

说明

  1. 克隆 BookCars 仓库
    cd /opt
    sudo git clone https://github.com/aelassas/bookcars.git
  2. 添加权限
    sudo chown -R $USER:$USER /opt/bookcars
    sudo chmod -R +x /opt/bookcars/__scripts
  3. 创建部署快捷方式
    sudo ln -s /opt/bookcars/__scripts/bc-deploy.sh /usr/local/bin/bc-deploy
  4. 创建 BookCars 服务
    sudo cp /opt/bookcars/__services/bookcars.service /etc/systemd/system
    sudo systemctl enable bookcars.service
    
  5. 创建 /opt/bookcars/api/.env 文件
    NODE_ENV=production
    BC_PORT=4002
    BC_HTTPS=false
    BC_PRIVATE_KEY=/etc/ssl/bookcars.ma.key
    BC_CERTIFICATE=/etc/ssl/bookcars.ma.crt
    BC_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars
    BC_DB_SSL=false
    BC_DB_SSL_CERT=/etc/ssl/bookcars.ma.crt
    BC_DB_SSL_CA=/etc/ssl/bookcars.ma.ca.pem
    BC_DB_DEBUG=false
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_JWT_SECRET=JWT_SECRET
    BC_JWT_EXPIRE_AT=86400
    BC_TOKEN_EXPIRE_AT=86400
    BC_SMTP_HOST=smtp.sendgrid.net
    BC_SMTP_PORT=587
    BC_SMTP_USER=apikey
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    BC_CDN_USERS=/var/www/cdn/bookcars/users
    BC_CDN_TEMP_USERS=/var/www/cdn/bookcars/temp/users
    BC_CDN_CARS=/var/www/cdn/bookcars/cars
    BC_CDN_TEMP_CARS=/var/www/cdn/temp/bookcars/cars
    BC_CDN_LOCATIONS=/var/www/cdn/bookcars/locations
    BC_CDN_TEMP_LOCATIONS=/var/www/cdn/bookcars/temp/locations
    BC_DEFAULT_LANGUAGE=en
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https:///
    BC_MINIMUM_AGE=21
    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    BC_ADMIN_EMAIL=admin@bookcars.ma
    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    
  6. 您需要配置以下选项

    BC_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_JWT_SECRET=JWT_SECRET
    BC_SMTP_HOST=smtp.sendgrid.net
    BC_SMTP_PORT=587
    BC_SMTP_USER=apikey
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https:///
    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    

    如果您想使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放入 BC_DB_URI,否则,请将 PASSWORD 替换为您在 BC_DB_URI 中的 MongoDB 密码。将 JWT_SECRET 替换为一个秘密令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 sendgrid 或任何其他事务性电子邮件提供商。

    如果您选择 sendgrid,请在 sendgrid.com 上创建一个账户,登录并转到仪表板。在左侧面板上,点击 **Email API**,然后点击 **Integration Guide**。接着,选择 **SMTP Relay** 并按照步骤操作。您将需要创建一个 API 密钥。一旦您创建了 API 密钥并验证了 smtp 转发,请将 API 密钥复制到 ./api/.env 中的 BC_SMTP_PASS。Sendgrid 的免费套餐允许每天发送多达 100 封电子邮件。如果您需要每天发送超过 100 封电子邮件,请切换到付费套餐或选择其他事务性电子邮件提供商。

    COOKIE_SECRETJWT_SECRET 至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。

    以下设置非常重要,如果设置不正确,身份验证将无法工作

    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https:///
    

    localhost 替换为 IP 或 FQDN。也就是说,如果您从 http://<FQDN>:3001/ 访问后端。BC_BACKEND_HOST 应该是 http://<FQDN>:3001/。BC_FRONTEND_HOST 也是如此。而 BC_AUTH_COOKIE_DOMAIN 应该是 FQDN。

    如果您想在移动应用中启用推送通知,请遵循这些 说明 并设置以下选项

    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

    如果您想启用 Stripe 支付网关,请 注册一个 Stripe 账户,填写表格并保存 Stripe 仪表板中的发布密钥和密钥。然后,在 api/.env 的以下选项中设置密钥

    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    

    不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。

    在 Stripe 中,所有账户默认总共有四个 API 密钥 - 两个用于测试模式,两个用于实时模式。

    • 测试模式密钥:默认情况下,您可以使用此密钥在测试模式下对服务器上的请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
    • 测试模式发布密钥:在 Web 或移动应用程序的客户端代码中,可以使用此密钥进行测试。
    • 实时模式密钥:在实时模式下,您可以使用此密钥在服务器上对请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
    • 实时模式发布密钥:当您准备好启动应用程序时,可以使用此密钥在 Web 或移动应用程序的客户端代码中。

    仅使用您的测试 API 密钥进行测试。这样可以确保您不会意外修改您的实时客户或交易。

    如果您想启用 HTTPS,则需要设置以下选项

    BC_HTTPS = true
    BC_PRIVATE_KEY=/etc/ssl/bookcars.ma.key
    BC_CERTIFICATE=/etc/ssl/bookcars.ma.crt

    如果您想在前端使用 Google reCAPTCHA,则需要设置

    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET

     

  7. 创建 /opt/bookcars/backend/.env 文件
    VITE_PORT=3001
    VITE_NODE_ENV=production
    VITE_BC_API_HOST=https://:4002
    VITE_BC_DEFAULT_LANGUAGE=en
    VITE_BC_PAGE_SIZE=30
    VITE_BC_CARS_PAGE_SIZE=15
    VITE_BC_BOOKINGS_PAGE_SIZE=20
    VITE_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_TEMP_USERS=https:///cdn/bookcars/temp/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_TEMP_CARS=https:///cdn/bookcars/temp/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_CDN_TEMP_LOCATIONS=https:///cdn/bookcars/temp/locations
    VITE_BC_SUPPLIER_IMAGE_WIDTH=60
    VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
    VITE_BC_CAR_IMAGE_WIDTH=300
    VITE_BC_CAR_IMAGE_HEIGHT=200
    VITE_BC_MINIMUM_AGE=21
    VITE_BC_PAGINATION_MODE=classic
    VITE_BC_CURRENCY=$
    VITE_BC_DEPOSIT_FILTER_VALUE_1=250
    VITE_BC_DEPOSIT_FILTER_VALUE_2=500
    VITE_BC_DEPOSIT_FILTER_VALUE_3=750
    

    您需要配置以下选项

    VITE_BC_API_HOST=https://:4002
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_TEMP_USERS=https:///cdn/bookcars/temp/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_TEMP_CARS=https:///cdn/bookcars/temp/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_CDN_TEMP_LOCATIONS=https:///cdn/bookcars/temp/locations
    

    如果您想本地测试,请将 localhost 保留;否则,请将其替换为 IP、主机名或 FQDN。

    VITE_BC_PAGINATION_MODE:您可以选择 classicinfinite_scroll。此选项默认为 classic。如果您选择 classic,则在桌面上会看到经典的下一页和上一页按钮分页,在移动设备上会看到无限滚动。如果您选择 infinite_scroll,则在桌面和移动设备上都会看到无限滚动。

  8. 创建 /opt/bookcars/frontend/.env 文件
    VITE_NODE_ENV=production
    VITE_BC_API_HOST=https://:4002
    VITE_BC_RECAPTCHA_ENABLED=false
    VITE_BC_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY
    VITE_BC_DEFAULT_LANGUAGE=en
    VITE_BC_PAGE_SIZE=30
    VITE_BC_CARS_PAGE_SIZE=15
    VITE_BC_BOOKINGS_PAGE_SIZE=20
    VITE_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_SUPPLIER_IMAGE_WIDTH=60
    VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
    VITE_BC_CAR_IMAGE_WIDTH=300
    VITE_BC_CAR_IMAGE_HEIGHT=200
    VITE_BC_MINIMUM_AGE=21
    VITE_BC_PAGINATION_MODE=classic
    VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_BC_STRIPE_CURRENCY_CODE=USD
    VITE_BC_SET_LANGUAGE_FROM_IP=false
    VITE_BC_GOOGLE_ANALYTICS_ENABLED=false
    VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    VITE_BC_DEPOSIT_FILTER_VALUE_1=250
    VITE_BC_DEPOSIT_FILTER_VALUE_2=500
    VITE_BC_DEPOSIT_FILTER_VALUE_3=750
    

    您需要配置以下选项

    VITE_BC_API_HOST=https://:4002
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_BC_STRIPE_CURRENCY_CODE=USD
    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    

    如果您想本地测试,请将 localhost 保留;否则,请将其替换为 IP、主机名或 FQDN。

    如果您想启用 Stripe 支付网关,请将 Stripe 发布密钥设置在 VITE_BC_STRIPE_PUBLISHABLE_KEY 中。您可以从 Stripe 仪表板中检索它。

    VITE_BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

    reCAPTCHA 默认禁用。如果您想启用它,需要将 VITE_BC_RECAPTCHA_ENABLED 设置为 true,并将 VITE_BC_RECAPTCHA_SITE_KEY 设置为 Google reCAPTCHA 站点密钥。

    通过联系表单发送的电子邮件将发送到

    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    

    如果您想启用 Google Analytics,请设置以下选项

    VITE_BC_GOOGLE_ANALYTICS_ENABLED=true
    VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX

     

  9. 如果您想运行或构建移动应用,您需要创建 mobile/.env
    BC_API_HOST=https://bookcars.ma:4002
    BC_DEFAULT_LANGUAGE=en
    BC_PAGE_SIZE=20
    BC_CARS_PAGE_SIZE=8
    BC_BOOKINGS_PAGE_SIZE=8
    BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
    BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
    BC_SUPPLIER_IMAGE_WIDTH=60
    BC_SUPPLIER_IMAGE_HEIGHT=30
    BC_CAR_IMAGE_WIDTH=300
    BC_CAR_IMAGE_HEIGHT=200
    BC_MINIMUM_AGE=21
    BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    BC_STRIPE_COUNTRY_CODE=US
    BC_STRIPE_CURRENCY_CODE=USD
    BC_CURRENCY=$
    

    您需要配置以下选项

    BC_API_HOST=https://bookcars.ma:4002
    BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
    BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
    BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    BC_STRIPE_COUNTRY_CODE=US
    BC_STRIPE_CURRENCY_CODE=USD
    BC_CURRENCY=$
    

    localhost 替换为 IP、主机名或 FQDN。

    如果您想启用 Stripe 支付网关,请将 Stripe 发布密钥设置在 BC_STRIPE_PUBLISHABLE_KEY 中。您可以从 Stripe 仪表板中检索它。

    BC_STRIPE_MERCHANT_IDENTIFIER 是您在 Apple 注册的用于 Apple Pay 的商户标识符。

    BC_STRIPE_COUNTRY_CODE 是您公司所在国家/地区的两位字母 ISO 3166 代码,例如“US”。Stripe 支付必需。

    BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

  10. 配置 NGINX
    sudo nano /etc/nginx/sites-available/default

    按以下方式更改前端的配置

    server {
      root /var/www/bookcars/frontend;
      #listen 443 http2 ssl default_server;
      listen 80 default_server;
      server_name _;
      
      #ssl_certificate_key /etc/ssl/bookcars.ma.key;
      #ssl_certificate /etc/ssl/bookcars.ma.pem;
    
      access_log /var/log/nginx/bookcars.frontend.access.log;
      error_log /var/log/nginx/bookcars.frontend.error.log;
    
      index index.html;
    
      location / {
          # First attempt to serve request as file, then as directory,
          # then as index.html, then fall back to displaying a 404.
          try_files $uri $uri/ /index.html =404;
      }
    
      location /cdn {
        alias /var/www/cdn;
      }
    }

    如果您想启用 SSL,请取消注释并设置这些行

    #listen 443 http2 ssl default_server
    #ssl_certificate_key /etc/ssl/bookcars.ma.key
    #ssl_certificate /etc/ssl/bookcars.ma.pem;

    为后端添加以下配置

    server {
      root /var/www/bookcars/backend;
      #listen 3001 http2 ssl default_server;
      listen 3001 default_server;
      server_name _;
    
      #ssl_certificate_key /etc/ssl/bookcars.ma.key;
      #ssl_certificate /etc/ssl/bookcars.ma.pem;
    
      #error_page 497 301 =307 https://$host:$server_port$request_uri;
    
      access_log /var/log/nginx/bookcars.backend.access.log;
      error_log /var/log/nginx/bookcars.backend.error.log;
    
      index index.html;
    
      location / {
          # First attempt to serve request as file, then as directory,
          # then as index.html, then fall back to displaying a 404.
          try_files $uri $uri/ /index.html =404;
      }
    }

    创建 /var/www/cdn/bookcars 文件夹,并为运行 bookcars 服务的用户在 /var/www/cdn/bookcars 上授予完全访问权限。

    如果您想启用 SSL,请取消注释并设置这些行

    #listen 3001 http2 ssl default_server
    #ssl_certificate_key /etc/ssl/bookcars.ma.key
    #ssl_certificate /etc/ssl/bookcars.ma.pem
    #error_page 497 301 =307 https://$host:$server_port$request_uri;

    然后,检查 NGINX 配置并重新启动 NGINX 服务

    sudo nginx -t
    sudo systemctl restart nginx.service
    sudo systemctl status nginx.service
  11. 启用防火墙并打开 BookCars 端口
    sudo ufw enable
    sudo ufw allow 4002/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 3001/tcp
    sudo ufw allow 27017/tcp
  12. 启动 bookcars 服务
    cd /opt/bookcars/api
    npm install --omit=dev
    sudo systemctl start bookcars.service
    

    使用以下命令确保 BookCars 服务正在运行

    sudo systemctl status bookcars.service
    

    通过检查日志确保数据库连接已建立

    tail -f /var/log/bookcars.log

    或通过打开此文件

    tail -f /opt/bookcars/api/logs/all.log
    

    错误日志写入于

    tail -f /opt/bookcars/api/logs/error.log
  13. 部署 BookCars
    bc-deploy all

    BookCars 后端可通过端口 3001 访问,前端可通过端口 80 访问。

    如果您想在 RAM 较少的 VPS 上安装 BookCars,在运行 bc-deploy all 时可能会遇到内存问题。在这种情况下,您需要按以下方式操作

    • 运行 bc-deploy api 来安装和运行 API。
    • 在您的桌面 PC 上,按前面所述设置 frontend/.env,然后从 frontend 文件夹运行以下命令
      npm install
      npm run build
    • frontend/build 的内容从您的桌面 PC 复制到 VPS 上的 /var/www/bookcars/frontend
    • 在您的桌面 PC 上,按前面所述设置 backend/.env,然后从 backend 文件夹运行以下命令
      npm install
      npm run build
    • backend/build 的内容从您的桌面 PC 复制到 VPS 上的 /var/www/bookcars/backend
    • 重启 NGINX
      sudo rm -rf /var/cache/nginx
      sudo systemctl restart nginx
      sudo systemctl status nginx
      
  14. 如果您不想使用演示数据库,请导航到 hostname:3001/sign-up 来创建一个管理员。

  15. 打开 backend/src/App.tsx 并注释掉这些行以保护后端

    const SignUp = lazy(() => import('./pages/SignUp'))
    <Route exact path='/sign-up' element={<SignUp />} />

    然后再次运行后端部署

    bc-deploy backend

如果您只想部署前端,请运行以下命令

bc-deploy frontend

如果您只想部署 API,请运行以下命令

bc-deploy api

如果您想部署 API、后端和前端,请运行以下命令

bc-deploy all

要更改货币,请遵循这些 说明

安装 (VPS)

这个 指南 展示了如何在运行 Ubuntu 22.04 的 VPS 上安装 BookCars,该 VPS 至少需要 512MB RAM 和一个 CPU。

安装(Docker)

BookCars 可以在 Linux 上的 Docker 容器以及 Windows 或 Mac 的 Docker Desktop 中运行。

Docker 镜像

本节介绍如何构建 BookCars Docker 镜像并在 Docker 容器中运行它。

  1. 克隆 BookCars 仓库
    git clone https://github.com/aelassas/bookcars.git
  2. 创建 ./api/.env.docker 文件,内容如下
    NODE_ENV=production
    BC_PORT=4002
    BC_HTTPS=false
    BC_PRIVATE_KEY=/etc/ssl/bookcars.key
    BC_CERTIFICATE=/etc/ssl/bookcars.crt
    BC_DB_URI=mongodb://admin:PASSWORD@mongo:27017/bookcars?authSource=admin&appName=bookcars
    BC_DB_SSL=false
    BC_DB_SSL_CERT=/etc/ssl/bookcars.crt
    BC_DB_SSL_CA=/etc/ssl/bookcars.ca.pem
    BC_DB_DEBUG=false
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_JWT_SECRET=JWT_SECRET
    BC_JWT_EXPIRE_AT=86400
    BC_TOKEN_EXPIRE_AT=86400
    BC_SMTP_HOST=smtp.sendgrid.net
    BC_SMTP_PORT=587
    BC_SMTP_USER=apikey
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    BC_CDN_USERS=/var/www/cdn/bookcars/users
    BC_CDN_TEMP_USERS=/var/www/cdn/bookcars/temp/users
    BC_CDN_CARS=/var/www/cdn/bookcars/cars
    BC_CDN_TEMP_CARS=/var/www/cdn/bookcars/temp/cars
    BC_CDN_LOCATIONS=/var/www/cdn/bookcars/locations
    BC_CDN_TEMP_LOCATIONS=/var/www/cdn/bookcars/temp/locations
    BC_DEFAULT_LANGUAGE=en
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https:///
    BC_MINIMUM_AGE=21
    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    BC_ADMIN_EMAIL=admin@bookcars.ma
    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    

    设置以下选项

    BC_DB_URI=mongodb://admin:PASSWORD@mongo:27017/bookcars?authSource=admin&appName=bookcars
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_JWT_SECRET=JWT_SECRET
    BC_SMTP_HOST=smtp.sendgrid.net
    BC_SMTP_PORT=587
    BC_SMTP_USER=apikey
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https:///
    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    

    如果您想使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放入 BC_DB_URI,否则,请将 PASSWORD 替换为您在 BC_DB_URI 中的 MongoDB 密码。将 JWT_SECRET 替换为一个秘密令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 sendgrid 或任何其他事务性电子邮件提供商。

    如果您选择 sendgrid,请在 sendgrid.com 上创建一个账户,登录并转到仪表板。在左侧面板上,点击 **Email API**,然后点击 **Integration Guide**。接着,选择 **SMTP Relay** 并按照步骤操作。您将需要创建一个 API 密钥。一旦您创建了 API 密钥并验证了 smtp 转发,请将 API 密钥复制到 ./api/.env 中的 BC_SMTP_PASS。Sendgrid 的免费套餐允许每天发送多达 100 封电子邮件。如果您需要每天发送超过 100 封电子邮件,请切换到付费套餐或选择其他事务性电子邮件提供商。

    COOKIE_SECRETJWT_SECRET 至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。

    以下设置非常重要,如果设置不正确,身份验证将无法工作

    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https:///
    

    localhost 替换为 IP 或 FQDN。也就是说,如果您从 http://<FQDN>:3001/ 访问后端。BC_BACKEND_HOST 应该是 http://<FQDN>:3001/。BC_FRONTEND_HOST 也是如此。而 BC_AUTH_COOKIE_DOMAIN 应该是 FQDN。

    如果您想本地测试,请将 localhost 保留。

    如果您想在移动应用中启用推送通知,请遵循这些 说明 并设置以下选项

    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    

    如果您想启用 Stripe 支付网关,请 注册一个 Stripe 账户,填写表格并保存 Stripe 仪表板中的发布密钥和密钥。然后,在 api/.env 的以下选项中设置密钥

    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    

    不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。

    在 Stripe 中,所有账户默认总共有四个 API 密钥 - 两个用于测试模式,两个用于实时模式。

    • 测试模式密钥:默认情况下,您可以使用此密钥在测试模式下对服务器上的请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
    • 测试模式发布密钥:在 Web 或移动应用程序的客户端代码中,可以使用此密钥进行测试。
    • 实时模式密钥:在实时模式下,您可以使用此密钥在服务器上对请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
    • 实时模式发布密钥:当您准备好启动应用程序时,可以使用此密钥在 Web 或移动应用程序的客户端代码中。

    仅使用您的测试 API 密钥进行测试。这样可以确保您不会意外修改您的实时客户或交易。

    如果您想在前端使用 Google reCAPTCHA,则需要设置

    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    

     

  3. 创建 ./backend/.env.docker 文件,内容如下
    VITE_NODE_ENV=production
    VITE_BC_API_HOST=https://:4002
    VITE_BC_DEFAULT_LANGUAGE=en
    VITE_BC_PAGE_SIZE=30
    VITE_BC_CARS_PAGE_SIZE=15
    VITE_BC_BOOKINGS_PAGE_SIZE=20
    VITE_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_TEMP_USERS=https:///cdn/bookcars/temp/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_TEMP_CARS=https:///cdn/bookcars/temp/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_CDN_TEMP_LOCATIONS=https:///cdn/bookcars/temp/locations
    VITE_BC_SUPPLIER_IMAGE_WIDTH=60
    VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
    VITE_BC_CAR_IMAGE_WIDTH=300
    VITE_BC_CAR_IMAGE_HEIGHT=200
    VITE_BC_MINIMUM_AGE=21
    VITE_BC_PAGINATION_MODE=classic
    VITE_BC_CURRENCY=$
    VITE_BC_DEPOSIT_FILTER_VALUE_1=250
    VITE_BC_DEPOSIT_FILTER_VALUE_2=500
    VITE_BC_DEPOSIT_FILTER_VALUE_3=750
    

    设置以下选项

    VITE_BC_API_HOST=https://:4002
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_TEMP_USERS=https:///cdn/bookcars/temp/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_TEMP_CARS=https:///cdn/bookcars/temp/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_CDN_TEMP_LOCATIONS=https:///cdn/bookcars/temp/locations
    VITE_BC_CURRENCY=$
    

    如果您想本地测试,请将 localhost 保留;否则,请将其替换为 IP、主机名或 FQDN。

    如果您想更改分页模式,请更改 VITE_BC_PAGINATION_MODE 选项。您可以选择 classicinfinite_scroll。此选项默认为 classic。如果您选择 classic,则在桌面上会看到经典的下一页和上一页按钮分页,在移动设备上会看到无限滚动。如果您选择 infinite_scroll,则在桌面和移动设备上都会看到无限滚动。

  4. 创建 ./frontend/.env.docker 文件,内容如下
    VITE_NODE_ENV=production
    VITE_BC_API_HOST=https://:4002
    VITE_BC_RECAPTCHA_ENABLED=false
    VITE_BC_DEFAULT_LANGUAGE=en
    VITE_BC_PAGE_SIZE=30
    VITE_BC_CARS_PAGE_SIZE=15
    VITE_BC_BOOKINGS_PAGE_SIZE=20
    VITE_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_SUPPLIER_IMAGE_WIDTH=60
    VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
    VITE_BC_CAR_IMAGE_WIDTH=300
    VITE_BC_CAR_IMAGE_HEIGHT=200
    VITE_BC_MINIMUM_AGE=21
    VITE_BC_PAGINATION_MODE=classic
    VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_BC_STRIPE_CURRENCY_CODE=USD
    VITE_BC_SET_LANGUAGE_FROM_IP=false
    VITE_BC_GOOGLE_ANALYTICS_ENABLED=false
    VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    VITE_BC_DEPOSIT_FILTER_VALUE_1=250
    VITE_BC_DEPOSIT_FILTER_VALUE_2=500
    VITE_BC_DEPOSIT_FILTER_VALUE_3=750
    

    设置以下选项

    VITE_BC_API_HOST=https://:4002
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_BC_STRIPE_CURRENCY_CODE=USD
    VITE_BC_SET_LANGUAGE_FROM_IP=false
    VITE_BC_GOOGLE_ANALYTICS_ENABLED=false
    VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    

    如果您想本地测试,请将 localhost 保留;否则,请将其替换为 IP、主机名或 FQDN。

    如果您想启用 Stripe 支付网关,请将 Stripe 发布密钥设置在 VITE_BC_STRIPE_PUBLISHABLE_KEY 中。您可以从 Stripe 仪表板中检索它。

    VITE_BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

    如果您想更改分页模式,请更改 VITE_BC_PAGINATION_MODE 选项。

    reCAPTCHA 默认在前端禁用。如果您想启用它,需要将 VITE_BC_RECAPTCHA_ENABLED 设置为 true,并将 VITE_BC_RECAPTCHA_SITE_KEY 设置为 Google reCAPTCHA 站点密钥。

    通过联系表单发送的电子邮件将发送到

    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    

    如果您想启用 Google Analytics,请设置以下选项

    VITE_BC_GOOGLE_ANALYTICS_ENABLED=true
    VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
    

     

  5. 打开 ./docker-compose.yml 并设置 MongoDB 密码
    version: "3.8"
    services:
    api:
      build: 
        context: .
        dockerfile: ./api/Dockerfile
      env_file: ./api/.env.docker
      restart: always
      ports:
        - 4002:4002
      depends_on:
        - mongo
      volumes:
        - cdn:/var/www/cdn/bookcars
    
    mongo:
      image: mongo:latest
      command: mongod --quiet --logpath /dev/null
      restart: always
      environment:
        # Provide your credentials here
        MONGO_INITDB_ROOT_USERNAME: admin
        MONGO_INITDB_ROOT_PASSWORD: PASSWORD
      ports:
        - 27017:27017
    
    backend:
      build: 
        context: .
        dockerfile: ./backend/Dockerfile
      depends_on:
        - api
      ports:
        - 3001:3001
    
    frontend:
      build: 
        context: .
        dockerfile: ./frontend/Dockerfile
      depends_on:
        - api
      ports:
        - 80:80
      volumes:
        - cdn:/var/www/cdn/bookcars
    
    volumes:
    cdn:

    如果您想使用 MongoDB Atlas,请移除 mongo 容器。否则,请将 PASSWORD 替换为您在 ./api/.env.dockerBC_DB_URI 中设置的密码。

  6. 构建并运行 Docker 镜像
    sudo docker compose up

    要在后台运行 compose,请在命令中添加 -d 选项

    sudo docker compose up -d

    如果您想重新构建,请使用以下命令

    docker compose up --build --force-recreate --no-deps api backend frontend

    如果您想检查容器的日志以进行故障排除,请使用以下命令

    sudo docker compose logs
    

    如果您想在不缓存的情况下重新构建,请使用以下命令

    docker compose build --no-cache api backend frontend
    docker compose up

     

就是这样!BookCars 后端可通过 http://<hostname>:3001 访问,BookCars 前端可通过 http://<hostname> 访问。

如果您是第一次运行 BookCars,您将从一个空数据库开始。因此,您需要通过 http://<hostname>:3001/sign-up 填写表单来从后端创建管理员。需要配置 SMTP 设置才能处理注册。然后,通过打开 backend/src/App.tsx 并注释掉以下行来保护后端

const SignUp = lazy(() => import('./pages/SignUp'))
<Route exact path='/sign-up' element={<Signup />} />

您需要重新构建并运行 Docker 镜像

sudo docker compose build --no-cache
sudo docker compose up

创建管理员用户后,请执行以下操作

  • 转到供应商页面并创建一名或多名供应商
  • 转到地点页面并创建一名或多个地点
  • 转到汽车页面并创建一辆或多辆汽车
  • 转到前端,注册,选择一辆车并结账。

最后,您将在后端仪表板中看到预订列表。

如果您愿意,可以使用 演示数据库

以下是 Docker 配置文件

就是这样。您可以探索后端和前端中的其他页面。

SSL

本节将指导您如何在 Docker 容器中的 API、后端和前端启用 SSL。

将您的私钥 bookcars.key 和证书 bookcars.crt 复制到 ./

bookcars.key 将作为 /etc/ssl/bookcars.key 加载,bookcars.crt 将作为 /etc/ssl/bookcars.crt 加载到 ./docker-compose.yml 中。

API

对于 API,请按以下方式更新 ./api/.env.docker 以启用 SSL

BC_HTTPS=true
BC_PRIVATE_KEY=/etc/ssl/bookcars.key
BC_CERTIFICATE=/etc/ssl/bookcars.crt
BC_BACKEND_HOST=https://:3001/
BC_FRONTEND_HOST=https:///

https:// 替换为 https://<fqdn>

后端

对于后端,请在 ./backend/.env.docker 中更新以下选项

VITE_BC_API_HOST=https://:4002
VITE_BC_CDN_USERS=https:///cdn/bookcars/users
VITE_BC_CDN_TEMP_USERS=https:///cdn/bookcars/temp/users
VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
VITE_BC_CDN_TEMP_CARS=https:///cdn/bookcars/temp/cars
VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
VITE_BC_CDN_TEMP_LOCATIONS=https:///cdn/bookcars/temp/locations

https:// 替换为 https://<fqdn>

然后,按以下方式更新 ./backend/nginx.conf 以启用 SSL

server {
  listen 3001 ssl;
  root /usr/share/nginx/html;
  index index.html;

  ssl_certificate_key /etc/ssl/bookcars.key;
  ssl_certificate /etc/ssl/bookcars.crt;

  error_page 497 301 =307 https://$host:$server_port$request_uri;

  access_log /var/log/nginx/backend.access.log;
  error_log /var/log/nginx/backend.error.log;

  location / {
      # First attempt to serve request as file, then as directory,
      # then as index.html, then fall back to displaying a 404.
      try_files $uri $uri/ /index.html =404;
  }
}

前端

对于前端,请在 ./frontend/.env.docker 中更新以下选项

VITE_BC_API_HOST=https://:4002
VITE_BC_CDN_USERS=https:///cdn/bookcars/users
VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations

https:// 替换为 https://<fqdn>

将端口 443 添加到 ./frontend/Dokerfile,如下所示

# syntax=docker/dockerfile:1

FROM node:lts-alpine as build
WORKDIR /frontend
COPY . .
COPY ./.env.docker ./.env
RUN npm install
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /frontend/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80
EXPOSE 443

然后,按以下方式更新 ./frontend/nginx.conf 以启用 SSL

server {
  listen 80;
  return 301 https://$host$request_uri;
}
server {
  listen 443 ssl;
  root /usr/share/nginx/html;
  index index.html;

  ssl_certificate_key /etc/ssl/bookcars.key;
  ssl_certificate /etc/ssl/bookcars.crt;

  access_log /var/log/nginx/frontend.access.log;
  error_log /var/log/nginx/frontend.error.log;

  location / {
    # First attempt to serve request as file, then as directory,
    # then as index.html, then fall back to displaying a 404.
    try_files $uri $uri/ /index.html =404;
  }

  location /cdn {
    alias /var/www/cdn;
  }
}

docker-compose.yml

更新 ./docker-compose.yml 以加载您的私钥 bookcars.key 和证书 bookcars.crt,并将端口 443 添加到前端,如下所示

version: "3.8"
services:
api:
  build: 
    context: .
    dockerfile: ./api/Dockerfile
  env_file: ./api/.env.docker
  restart: always
  ports:
    - 4002:4002
  depends_on:
    - mongo
  volumes:
    - cdn:/var/www/cdn/bookcars
    - ./bookcars.key:/etc/ssl/bookcars.key
    - ./bookcars.crt:/etc/ssl/bookcars.crt

mongo:
  image: mongo:latest
  restart: always
  environment:
    # Provide your credentials here
    MONGO_INITDB_ROOT_USERNAME: admin
    MONGO_INITDB_ROOT_PASSWORD: PASSWORD
  ports:
    - 27017:27017

backend:
  build: 
    context: .
    dockerfile: ./backend/Dockerfile
  depends_on:
    - api
  ports:
    - 3001:3001
  volumes:
    - ./bookcars.key:/etc/ssl/bookcars.key
    - ./bookcars.crt:/etc/ssl/bookcars.crt

frontend:
  build: 
    context: .
    dockerfile: ./frontend/Dockerfile
  depends_on:
    - api
  ports:
    - 80:80
    - 443:443
  volumes:
    - cdn:/var/www/cdn/bookcars
    - ./bookcars.key:/etc/ssl/bookcars.key
    - ./bookcars.crt:/etc/ssl/bookcars.crt

volumes:
cdn:

重新构建并运行 Docker 镜像

sudo docker compose build --no-cache
sudo docker compose up

设置 Stripe

如果您想启用 Stripe 支付网关,请 注册一个 Stripe 账户,填写表格并保存 Stripe 开发者仪表板中的发布密钥和密钥。

不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。

在 Stripe 中,所有账户默认共有四个 API 密钥 - 两个用于测试模式,两个用于实时模式

  • 测试模式密钥:默认情况下,您可以使用此密钥在测试模式下对服务器上的请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
  • 测试模式发布密钥:在 Web 或移动应用程序的客户端代码中,可以使用此密钥进行测试。
  • 实时模式密钥:在实时模式下,您可以使用此密钥在服务器上对请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
  • 实时模式发布密钥:当您准备好启动应用程序时,可以使用此密钥在 Web 或移动应用程序的客户端代码中。

您可以在 Stripe 开发者仪表板的 API 密钥页面找到您的密钥和发布密钥。

仅使用您的测试 API 密钥进行测试和开发。这样可以确保您不会意外修改您的实时客户或交易。

在生产环境中,请在 API、后端、前端和移动应用中使用 HTTPS 以便使用 Stripe 支付网关。

API

api/.env 的以下选项中设置 Stripe 密钥

BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

前端

frontend/.env 的以下选项中设置 Stripe 发布密钥和货币

VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
VITE_BC_STRIPE_CURRENCY_CODE=USD

移动应用

mobile/.env 中设置 Stripe 发布密钥和其他 Stripe 设置

BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
BC_STRIPE_COUNTRY_CODE=US
BC_STRIPE_CURRENCY_CODE=USD

BC_STRIPE_MERCHANT_IDENTIFIER 是您在 Apple 注册的用于 Apple Pay 的商户标识符。

BC_STRIPE_COUNTRY_CODE 是您公司所在国家/地区的两位字母 ISO 3166 代码,例如“US”。Stripe 支付必需。

BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

如果您想启用 Apple Pay,还需要在 mobile/app.jsonplugins 部分设置 merchantIdentifier

Google Pay

Google Pay 不支持 Expo Go。要使用 Google Pay,您必须创建 开发构建。这可以通过 EAS Build 完成,或者通过运行 npx expo run:android 在本地完成。

Apple Pay

Apple Pay 不支持 Expo Go。要使用 Apple Pay,您必须创建 开发构建。这可以通过 EAS Build 完成,或者通过运行 npx expo run:ios 在本地完成。

更改货币

要更改货币,请遵循这些说明

前端

打开 frontend/.env 并更改 VITE_BC_CURRENCYVITE_BC_STRIPE_CURRENCY_CODE 设置。

默认情况下,它设置为

VITE_BC_CURRENCY=$
VITE_BC_STRIPE_CURRENCY_CODE=USD

例如,如果您想更改为欧元

VITE_BC_CURRENCY=€
VITE_BC_STRIPE_CURRENCY_CODE=EUR

在生产环境中,您需要重新构建前端以应用更改。

后端

打开 backend/.env 并更改 VITE_BC_CURRENCY 设置。

默认情况下,它设置为

VITE_BC_CURRENCY=$

在生产环境中,您需要重新构建后端以应用更改。

移动应用

打开 mobile/.env 并更改 BC_CURRENCYBC_STRIPE_CURRENCY_CODE 设置。

默认情况下,它设置为

BC_CURRENCY=$
BC_STRIPE_CURRENCY_CODE=USD

例如,如果您想更改为欧元

BC_CURRENCY=€
BC_STRIPE_CURRENCY_CODE=EUR

在生产环境中,您需要重新构建移动应用以应用更改。

添加新语言

要添加新语言,请按以下步骤操作

API

  1. 将新的语言 ISO 639-1 代码 添加到 api/src/config/env.config.ts 中的 LANGUAGES 设置。
  2. src/lang 文件夹中创建一个新文件 <ISO 639-1 code>.ts 并在此文件中添加翻译。
  3. 将您的翻译添加到 src/lang/i18n.ts

后端和前端

  1. 将新的语言 ISO 639-1 代码 及其标签添加到 src/config/env.config.tsLANGUAGES 常量中。
  2. 将翻译添加到 src/lang/*.ts

移动应用

  1. LANGUAGES 常量中,将新的语言 ISO 639-1 代码及其标签添加到 config/env.config.ts
  2. lang 文件夹中创建一个新文件 <ISO 639-1 代码>.ts 并在此文件中添加翻译。
  3. 将您的翻译添加到 lang/i18n.ts

演示数据库

Windows、Linux 和 macOS

  • 下载并安装 MongoDB 命令行数据库工具
  • 在 Windows 上,将 MongoDB 命令行数据库工具文件夹添加到 Path 环境变量。
  • bookcars-db.zip 下载到您的机器,解压它,然后从终端进入解压后的文件夹。
  • 使用以下命令恢复 BookCars 演示数据库
    mongorestore --verbose --drop --gzip --host=127.0.0.1 --port=27017 --username=admin --password=$PASSWORD --authenticationDatabase=admin --nsInclude="bookcars.*" --archive=bookcars.gz
    

$PASSWORD 替换为您的 MongoDB 密码。

如果您正在使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放在 --uri= 命令行参数中

mongorestore --verbose --drop --gzip --uri="mongodb://admin:$PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars" --nsInclude="bookcars.*" --nsFrom="bookcars.*" --nsTo="bookcars.*" --archive=bookcars.gz

cdn 文件夹的内容复制到您的 Web 服务器上,以便可以通过 https:///cdn/bookcars/ 访问这些文件

cdn 文件夹包含以下文件夹

  • users:此文件夹包含用户的头像和供应商的图像。
  • cars:此文件夹包含汽车的图像。
  • temp:此文件夹包含临时文件。

如果您想从源代码运行 BookCars 或在 Windows 或 Linux 上安装它而无需使用 Docker,请按以下步骤操作

  • 在 Windows 上,安装 IIS 并将 cdn 文件夹的内容复制到 C:\inetpub\wwwroot\cdn\bookcars。最后,为运行 BookCars API 的用户在 C:\inetpub\wwwroot\cdn\bookcars 上添加完全访问权限。
  • 在 Linux 上,安装 NGINX 并将 cdn 文件夹的内容复制到 /var/www/cdn/bookcars。然后,按以下方式更新 /etc/nginx/sites-enabled/default
    server {
      listen 80 default_server;
      server_name _;
      
      ...
    
      location /cdn {
        alias /var/www/cdn;
      }
    }
    

    最后,为运行 BookCars API 的用户在 /var/www/cdn/bookcars 上添加完全访问权限。

后端凭证

前端和移动应用凭证

Docker

要在 Docker 容器中恢复 BookCars 演示数据库,请按以下步骤操作

  1. 确保端口 80、3001、4002 和 27017 未被任何应用程序使用。
  2. 在您的本地计算机上下载并安装 MongoDB 命令行数据库工具
  3. 将 MongoDB 命令行数据库工具文件夹添加到您本地计算机的 Path 环境变量中。
  4. bookcars-db.zip 下载到您的本地计算机并解压。
  5. 运行 compose
    docker compose up
  6. 进入 bookcars-db 文件夹,使用以下命令恢复演示数据库
    mongorestore --verbose --drop --gzip --host=127.0.0.1 --port=27017 --username=admin --password=$PASSWORD --authenticationDatabase=admin --nsInclude="bookcars.*" --archive=bookcars.gz
    $PASSWORD 替换为您在 docker-compose.yml 中设置的 MongoDB 密码。
  7. 使用以下命令获取 API Docker 容器名称
    docker container ls
    名称应类似:src-api-1
  8. 进入 bookcars-db/cdn 文件夹,使用以下命令将文件夹内容复制到 API 容器中
    docker cp ./cdn/users src-api-1:/var/www/cdn/bookcars
    docker cp ./cdn/cars src-api-1:/var/www/cdn/bookcars
    src-api-1 替换为您的 API 容器名称。
  9. 进入后端 https://:3001 并使用以下凭证登录
    用户名: admin@bookcars.ma
    密码: B00kC4r5
  10. 进入前端 https:// 并使用以下凭证登录
    用户名: jdoe@bookcars.ma
    密码: B00kC4r5

构建移动应用

必备组件

要构建 BookCars 移动应用,您需要在您的机器上安装以下工具

使用以下命令安装 eas-cli

npm i -g eas-cli

配置

  • 您需要下载 google-services.json 文件并将其放在 ./mobile 的根目录中以启用推送通知。否则,移动应用将无法构建。不要忘记按照文档中的说明,在 expo.dev > **Credentials** > **Service Credentials** > **Google Cloud Messaging Token** 中设置 Firebase 服务器密钥。

  • 如果您没有 Expo 账户,则需要创建一个账户才能构建 BookCars 移动应用。

  • 转到 expo.dev,点击 **Projects** 然后点击 **Create a Project**。将 **BookCars** 设置为项目名称,然后点击 **Create**。

  • 转到 **BookCars** 项目并复制 **project ID**。打开 ./mobile/app.json 并将 **project ID** 粘贴到 extra.eas.projectId 中。

  • expo.dev(**Account Settings** > **Access Tokens**)创建 Expo 访问令牌,并将 api/.env 中的 BC_EXPO_ACCESS_TOKEN 设置。

    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    
  • 创建 mobile/.env 文件,内容如下
    BC_API_HOST=https://bookcars.ma:4002
    BC_DEFAULT_LANGUAGE=en
    BC_PAGE_SIZE=20
    BC_CARS_PAGE_SIZE=8
    BC_BOOKINGS_PAGE_SIZE=8
    BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
    BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
    BC_SUPPLIER_IMAGE_WIDTH=60
    BC_SUPPLIER_IMAGE_HEIGHT=30
    BC_CAR_IMAGE_WIDTH=300
    BC_CAR_IMAGE_HEIGHT=200
    BC_MINIMUM_AGE=21
    BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    BC_STRIPE_COUNTRY_CODE=US
    BC_STRIPE_CURRENCY_CODE=USD
    BC_CURRENCY=$
    

    您需要配置以下选项

    BC_API_HOST=https://bookcars.ma:4002
    BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
    BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
    BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    BC_STRIPE_COUNTRY_CODE=US
    BC_STRIPE_CURRENCY_CODE=USD
    BC_CURRENCY=$
    

    https://bookcars.ma 替换为 IP 或 FQDN。

    如果您想启用 Stripe 支付网关,请将 Stripe 发布密钥设置在 BC_STRIPE_PUBLISHABLE_KEY 中。您可以从 Stripe 仪表板中检索它。

    BC_STRIPE_MERCHANT_IDENTIFIER 是您在 Apple 注册的用于 Apple Pay 的商户标识符。

    BC_STRIPE_COUNTRY_CODE 是您公司所在国家/地区的两位字母 ISO 3166 代码,例如“US”。Stripe 支付必需。

    BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

生产构建

如果您想在生产环境中使用 BookCars 移动应用,您应该在 BookCars API 中使用 HTTPS,并在 ./mobile/app.json 中禁用 usesCleartextTraffic expo 插件,方法是在 plugins 部分删除 "./plugins/usesCleartextTraffic" 这一行。

说明

  • 将源代码克隆到您的计算机
    git clone https://github.com/aelassas/bookcars.git
    
  • 进入 mobile 文件夹
    cd ./mobile
    
  • 运行以下命令
    npm install

Android

EAS Build

要使用 EAS Build 托管服务构建 BookCars Android 应用,请运行以下命令

npm run build:android
本地构建

本地构建需要 macOS 或 Linux。对于 Windows,您应该使用 EAS Builds。

您需要安装 Android Studio、openjdk-17,并设置 ANDROID_HOMEJAVA_HOME 环境变量。然后,运行以下命令

npm run build:android:local

在 macOS 上,如果遇到本地构建问题,请尝试在 ./mobile/eas.json 中设置 ANDROID_HOMEJAVA_HOME 环境变量,如下所示

{
"cli": {
  "version": ">= 0.53.0"
},
"build": {
  "development": {
    "developmentClient": true,
    "distribution": "internal",
    "android": {
      "image": "latest",
      "gradleCommand": ":app:assembleDebug"
    },
    "ios": {
      "image": "latest",
      "buildConfiguration": "Debug"
    }
  },
  "preview": {
    "distribution": "internal"
  },
  "production": {
    "env": {
      "ANDROID_HOME": "/path/to/android/sdk",
      "JAVA_HOME": "/path/to/java/home"
    },
    "android": {
      "image": "latest",
      "buildType": "apk"
    },
    "ios": {
      "image": "latest"
    }
  }
},
"submit": {
  "production": {}
}
}

iOS

EAS Build

要构建 BookCars iOS 应用,请运行以下命令

npm run build:ios
本地构建

您需要安装 fastlane 和 CocoaPods 在 macOS 上。

要本地构建 BookCars iOS 应用,请运行以下命令

npm run build:ios:local

从源码运行

以下是运行 BookCars 的源代码的说明。

必备组件

  1. 安装 gitNode.js、Windows 上的 NGINXIISMongoDBmongosh。如果您想使用 MongoDB Atlas,可以跳过安装和配置 MongoDB。

  2. 配置 MongoDB

    mongosh

    创建管理员用户

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD", 
    roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

    PASSWORD 替换为强密码。

    通过更改 mongod.conf 来保护 MongoDB,如下所示

    net:
    port: 27017
    bindIp: 0.0.0.0
    
    security:
    authorization: enabled

    重启 MongoDB 服务。

说明

  1. 克隆 BookCars 仓库
    git clone https://github.com/aelassas/bookcars.git
  2. 创建 api/.env 文件,内容如下
    NODE_ENV=development
    BC_PORT=4002
    BC_HTTPS=false
    BC_PRIVATE_KEY=/etc/ssl/bookcars.ma.key
    BC_CERTIFICATE=/etc/ssl/bookcars.ma.crt
    BC_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars
    BC_DB_SSL=false
    BC_DB_SSL_CERT=/etc/ssl/bookcars.ma.crt
    BC_DB_SSL_CA=/etc/ssl/bookcars.ma.ca.pem
    BC_DB_DEBUG=false
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_JWT_SECRET=JWT_SECRET
    BC_JWT_EXPIRE_AT=86400
    BC_TOKEN_EXPIRE_AT=86400
    BC_SMTP_HOST=smtp.sendgrid.net
    BC_SMTP_PORT=587
    BC_SMTP_USER=apikey
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    BC_CDN_USERS=/var/www/cdn/bookcars/users
    BC_CDN_TEMP_USERS=/var/www/cdn/bookcars/temp/users
    BC_CDN_CARS=/var/www/cdn/bookcars/cars
    BC_CDN_TEMP_CARS=/var/www/cdn/bookcars/temp/cars
    BC_CDN_LOCATIONS=/var/www/cdn/bookcars/locations
    BC_CDN_TEMP_LOCATIONS=/var/www/cdn/bookcars/temp/locations
    BC_DEFAULT_LANGUAGE=en
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https://:3002/
    BC_MINIMUM_AGE=21
    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    BC_ADMIN_EMAIL=admin@bookcars.ma
    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    

    在 Windows 上,安装 IIS 并使用以下值更新以下设置

    BC_CDN_USERS=C:\inetpub\wwwroot\cdn\bookcars\users
    BC_CDN_TEMP_USERS=C:\inetpub\wwwroot\cdn\bookcars\temp\users
    BC_CDN_CARS=C:\inetpub\wwwroot\cdn\bookcars\cars
    BC_CDN_TEMP_CARS=C:\inetpub\wwwroot\cdn\bookcars\temp\cars
    BC_CDN_LOCATIONS=C:\inetpub\wwwroot\cdn\bookcars\locations
    BC_CDN_TEMP_LOCATIONS=C:\inetpub\wwwroot\cdn\bookcars\temp\locations
    

    为运行 BookCars API 的用户在 C:\inetpub\wwwroot\cdn\bookcars 上添加完全访问权限。

    您需要配置以下选项

    BC_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_JWT_SECRET=JWT_SECRET
    BC_SMTP_HOST=smtp.sendgrid.net
    BC_SMTP_PORT=587
    BC_SMTP_USER=apikey
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    

    如果您想使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放入 BC_DB_URI,否则,请将 PASSWORD 替换为您在 BC_DB_URI 中的 MongoDB 密码。将 JWT_SECRET 替换为一个秘密令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 sendgrid 或任何其他事务性电子邮件提供商。

    如果您选择 sendgrid,请在 sendgrid.com 上创建一个账户,登录并转到仪表板。在左侧面板上,点击 **Email API**,然后点击 **Integration Guide**。接着,选择 **SMTP Relay** 并按照步骤操作。您将需要创建一个 API 密钥。一旦您创建了 API 密钥并验证了 smtp 转发,请将 API 密钥复制到 ./api/.env 中的 BC_SMTP_PASS。Sendgrid 的免费套餐允许每天发送多达 100 封电子邮件。如果您需要每天发送超过 100 封电子邮件,请切换到付费套餐或选择其他事务性电子邮件提供商。

    COOKIE_SECRETJWT_SECRET 至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。

    如果您想在移动应用中启用推送通知,请遵循这些 说明 并设置以下选项

    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

    如果您想启用 Stripe 支付网关,请 注册一个 Stripe 账户,填写表格并保存 Stripe 仪表板中的发布密钥和密钥。然后,在 api/.env 的以下选项中设置密钥

    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    

    不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。使用 Stripe 的测试模式。

    仅使用您的测试 API 密钥进行测试。这样可以确保您不会意外修改您的实时客户或交易。

    如果您想在前端使用 Google reCAPTCHA,则需要设置

    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET

    要运行 API,请使用以下命令

    cd ./api
    npm install
    npm run dev
  3. 添加 backend/.env 文件
    VITE_PORT=3001
    VITE_NODE_ENV=development
    VITE_BC_API_HOST=https://:4002
    VITE_BC_DEFAULT_LANGUAGE=en
    VITE_BC_PAGE_SIZE=30
    VITE_BC_CARS_PAGE_SIZE=15
    VITE_BC_BOOKINGS_PAGE_SIZE=20
    VITE_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_TEMP_USERS=https:///cdn/bookcars/temp/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_TEMP_CARS=https:///cdn/bookcars/temp/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_CDN_TEMP_LOCATIONS=https:///cdn/bookcars/temp/locations
    VITE_BC_SUPPLIER_IMAGE_WIDTH=60
    VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
    VITE_BC_CAR_IMAGE_WIDTH=300
    VITE_BC_CAR_IMAGE_HEIGHT=200
    VITE_BC_MINIMUM_AGE=21
    VITE_BC_PAGINATION_MODE=classic
    VITE_BC_CURRENCY=$
    VITE_BC_DEPOSIT_FILTER_VALUE_1=250
    VITE_BC_DEPOSIT_FILTER_VALUE_2=500
    VITE_BC_DEPOSIT_FILTER_VALUE_3=750
    

    要运行后端,请使用以下命令

    cd ./backend
    npm install
    npm start
  4. 添加 frontend/.env 文件
    VITE_PORT=3002
    VITE_NODE_ENV=development
    VITE_BC_API_HOST=https://:4002
    VITE_BC_RECAPTCHA_ENABLED=false
    VITE_BC_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY
    VITE_BC_DEFAULT_LANGUAGE=en
    VITE_BC_PAGE_SIZE=30
    VITE_BC_CARS_PAGE_SIZE=15
    VITE_BC_BOOKINGS_PAGE_SIZE=20
    VITE_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
    VITE_BC_CDN_USERS=https:///cdn/bookcars/users
    VITE_BC_CDN_CARS=https:///cdn/bookcars/cars
    VITE_BC_CDN_LOCATIONS=https:///cdn/bookcars/locations
    VITE_BC_SUPPLIER_IMAGE_WIDTH=60
    VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
    VITE_BC_CAR_IMAGE_WIDTH=300
    VITE_BC_CAR_IMAGE_HEIGHT=200
    VITE_BC_MINIMUM_AGE=21
    VITE_BC_PAGINATION_MODE=classic
    VITE_BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    VITE_BC_STRIPE_CURRENCY_CODE=USD
    VITE_BC_SET_LANGUAGE_FROM_IP=false
    VITE_BC_GOOGLE_ANALYTICS_ENABLED=false
    VITE_BC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
    VITE_BC_CONTACT_EMAIL=info@bookcars.ma
    VITE_BC_DEPOSIT_FILTER_VALUE_1=250
    VITE_BC_DEPOSIT_FILTER_VALUE_2=500
    VITE_BC_DEPOSIT_FILTER_VALUE_3=750
    

    如果您想启用 Stripe 支付网关,请将 Stripe 发布密钥设置在 VITE_BC_STRIPE_PUBLISHABLE_KEY 中。您可以从 Stripe 仪表板中检索它。

    VITE_BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

    reCAPTCHA 默认禁用。如果您想启用它,需要将 VITE_BC_RECAPTCHA_ENABLED 设置为 true,并将 VITE_BC_RECAPTCHA_SITE_KEY 设置为 Google reCAPTCHA 站点密钥。

    要运行前端,请使用以下命令

    cd ./frontend
    npm install
    npm start
  5. 如果您想运行移动应用,您需要添加 mobile/.env
    BC_API_HOST=https://bookcars.ma:4002
    BC_DEFAULT_LANGUAGE=en
    BC_PAGE_SIZE=20
    BC_CARS_PAGE_SIZE=8
    BC_BOOKINGS_PAGE_SIZE=8
    BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
    BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
    BC_SUPPLIER_IMAGE_WIDTH=60
    BC_SUPPLIER_IMAGE_HEIGHT=30
    BC_CAR_IMAGE_WIDTH=300
    BC_CAR_IMAGE_HEIGHT=200
    BC_MINIMUM_AGE=21
    BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    BC_STRIPE_COUNTRY_CODE=US
    BC_STRIPE_CURRENCY_CODE=USD
    BC_CURRENCY=$
    

    您需要配置以下选项

    BC_API_HOST=https://bookcars.ma:4002
    BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
    BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
    BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
    BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
    BC_STRIPE_COUNTRY_CODE=US
    BC_STRIPE_CURRENCY_CODE=USD
    BC_CURRENCY=$
    

    您需要将 https://bookcars.ma 替换为 IP 或主机名。

  6. 配置 https:///cdn
    • 在 Windows 上,安装 IIS,创建 C:\inetpub\wwwroot\cdn\bookcars 文件夹,并为运行 BookCars API 的用户在 bookcarsC:\inetpub\wwwroot\cdn\bookcars 文件夹上添加完全访问权限。
    • 在 Linux 上,安装 NGINX,创建 /var/www/cdn/bookcars 文件夹,为运行 BookCars API 的用户在 /var/www/cdn/bookcars 文件夹上添加完全访问权限,并通过更改 /etc/nginx/sites-available/defaultcdn 文件夹添加到 NGINX,如下所示
    server {
      listen 80 default_server;
      server_name _;
      
      ...
    
      location /cdn {
        alias /var/www/cdn;
      }
    }
  7. https://:3001/sign-up 创建管理员用户
  8. 要运行移动应用,只需在您的设备上下载 Expo 应用,并在 ./mobile 文件夹中运行以下命令
    npm install
    npm start

您需要下载 google-services.json 文件并将其放在 ./mobile 的根目录中以启用推送通知。

您可以在 这里 找到运行移动应用的详细说明。

要更改货币,请遵循这些 说明

运行移动应用

要运行移动应用,请创建 ./mobile/.env 文件,内容如下

BC_API_HOST=https://bookcars.ma:4002
BC_DEFAULT_LANGUAGE=en
BC_PAGE_SIZE=20
BC_CARS_PAGE_SIZE=8
BC_BOOKINGS_PAGE_SIZE=8
BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
BC_SUPPLIER_IMAGE_WIDTH=60
BC_SUPPLIER_IMAGE_HEIGHT=30
BC_CAR_IMAGE_WIDTH=300
BC_CAR_IMAGE_HEIGHT=200
BC_MINIMUM_AGE=21
BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
BC_STRIPE_COUNTRY_CODE=US
BC_STRIPE_CURRENCY_CODE=USD
BC_CURRENCY=$

您需要配置以下选项

BC_API_HOST=https://bookcars.ma:4002
BC_CDN_USERS=https://bookcars.ma/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.ma/cdn/bookcars/cars
BC_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
BC_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
BC_STRIPE_COUNTRY_CODE=US
BC_STRIPE_CURRENCY_CODE=USD
BC_CURRENCY=$

https://bookcars.ma 替换为 IP、主机名或 FQDN。

如果您想启用 Stripe 支付网关,请将 Stripe 发布密钥设置在 BC_STRIPE_PUBLISHABLE_KEY 中。您可以从 Stripe 仪表板中检索它。使用 Stripe 的测试模式。

BC_STRIPE_MERCHANT_IDENTIFIER 是您在 Apple 注册的用于 Apple Pay 的商户标识符。

BC_STRIPE_COUNTRY_CODE 是您公司所在国家/地区的两位字母 ISO 3166 代码,例如“US”。Stripe 支付必需。

BC_STRIPE_CURRENCY_CODE 是三位字母的 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是支持的货币:https://docs.stripe.com/currencies

通过遵循这些 说明 来安装演示数据库。

配置 https:///cdn

在 Windows 上,安装 IIS 并为运行 BookCars API 的用户在 C:\inetpub\wwwroot\cdn\bookcars 上添加完全访问权限。

在 Linux 上,安装 NGINX 并按以下方式更新 /etc/nginx/sites-enabled/default

server {
  listen 80 default_server;
  server_name _;
  
  ...

  location /cdn {
    alias /var/www/cdn;
  }
}

最后,为运行 BookCars API 的用户在 /var/www/cdn/bookcars 上添加完全访问权限。

通过遵循这些 说明 来配置 ./api

使用以下命令运行 ./api

cd ./api
npm run dev

通过在您的设备上下载 Expo 应用并从 ./mobile 文件夹中运行以下命令来运行移动应用

cd ./mobile
npm install
npm start

在您的设备上打开 Expo 应用并扫描 QR 码以运行 BookCars 移动应用。

推送通知

如果您想启用 BookCars 推送通知,请下载 google-services.json 文件并将其放在 ./mobile 的根目录中以启用推送通知。其路径可以在 ./mobile/app.json 配置文件中的 googleServicesFile 设置选项中配置。不要忘记在 Firebase Console 中生成新的 私钥,并将 JSON 文件上传到 Expo 仪表板

单元测试和覆盖率

以下是运行单元测试和构建覆盖率报告的说明。

必备组件

  1. 安装 gitNode.jsNGINXIISMongoDBmongosh

  2. 配置 MongoDB

    mongosh
    
  3. 创建管理员用户

    db = db.getSiblingDB('admin')
    db.createUser({ user: "admin" , pwd: "PASSWORD", roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})
    

    PASSWORD 替换为强密码。

  4. 通过更改 mongod.conf 以如下方式安全 MongoDB

    net:
    port: 27017
    bindIp: 0.0.0.0
    
    security:
    authorization: enabled
    
  5. 重启 MongoDB 服务。

说明

  1. 克隆 Movin' In 仓库
    git clone https://github.com/aelassas/bookcars.git
    
  1. 创建 api/.env 文件,内容如下
    NODE_ENV=development
    BC_PORT=4002
    BC_HTTPS=false
    BC_PRIVATE_KEY=/etc/ssl/bookcars.ma.key
    BC_CERTIFICATE=/etc/ssl/bookcars.ma.crt
    BC_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars
    BC_DB_SSL=false
    BC_DB_SSL_CERT=/etc/ssl/bookcars.ma.crt
    BC_DB_SSL_CA=/etc/ssl/bookcars.ma.ca.pem
    BC_DB_DEBUG=false
    BC_COOKIE_SECRET=COOKIE_SECRET
    BC_AUTH_COOKIE_DOMAIN=localhost
    BC_JWT_SECRET=JWT_SECRET
    BC_JWT_EXPIRE_AT=86400
    BC_TOKEN_EXPIRE_AT=86400
    BC_SMTP_HOST=in-v3.mailjet.com
    BC_SMTP_PORT=587
    BC_SMTP_USER=USER
    BC_SMTP_PASS=PASSWORD
    BC_SMTP_FROM=admin@bookcars.ma
    BC_CDN_USERS=/var/www/cdn/bookcars/users
    BC_CDN_TEMP_USERS=/var/www/cdn/bookcars/temp/users
    BC_CDN_CARS=/var/www/cdn/bookcars/cars
    BC_CDN_TEMP_CARS=/var/www/cdn/bookcars/temp/cars
    BC_DEFAULT_LANGUAGE=en
    BC_BACKEND_HOST=https://:3001/
    BC_FRONTEND_HOST=https://:3002/
    BC_MINIMUM_AGE=21
    BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
    BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
    BC_STRIPE_SESSION_EXPIRE_AT=82800
    BC_ADMIN_EMAIL=admin@bookcars.ma
    BC_RECAPTCHA_SECRET=RECAPTCHA_SECRET
    

在 Windows 上,安装 IIS 并使用以下值更新以下设置

BC_CDN_USERS=C:\inetpub\wwwroot\cdn\bookcars\users
BC_CDN_TEMP_USERS=C:\inetpub\wwwroot\cdn\bookcars\temp\users
BC_CDN_CARS=C:\inetpub\wwwroot\cdn\bookcars\cars
BC_CDN_TEMP_CARS=C:\inetpub\wwwroot\cdn\bookcars\temp\cars

为运行 Movin' In API 的用户在 C:\inetpub\wwwroot\cdn\bookcars 上添加完全访问权限。

然后,设置以下选项

BC_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/bookcars?authSource=admin&appName=bookcars
BC_COOKIE_SECRET=COOKIE_SECRET
BC_JWT_SECRET=JWT_SECRET
BC_SMTP_HOST=in-v3.mailjet.com
BC_SMTP_PORT=587
BC_SMTP_USER=USER
BC_SMTP_PASS=PASSWORD
BC_SMTP_FROM=admin@bookcars.ma
BC_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY

PASSWORD 替换为 BC_DB_URI 中的 MongoDB 密码,将 JWT_SECRET 替换为一个秘密令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 mailjet、brevo、sendgrid 或任何其他事务性电子邮件提供商。

STRIPE_SECRET_KEY 替换为您的 Stripe 测试模式密钥。您可以在 Stripe 开发者仪表板中找到它。

COOKIE_SECRETJWT_SECRET 至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。

如果您想在移动应用中启用推送通知,请遵循这些 说明 并设置以下选项

BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

运行 Movin' In API 单元测试

cd ./api
npm install
npm test

覆盖率

运行单元测试后,会自动构建覆盖率报告

./api/coverage

您也可以在 codecov 上查看覆盖率报告。

日志

所有 API 日志都写入 ./api/logs/all.log

API 错误日志也写入 ./api/logs/error.log

Using the Code

本节描述了 BookCars 的软件架构,包括 API、前端、移动应用和后端。

BookCars API 是一个 Node.js 服务器应用程序,使用 Express 公开 RESTful API,从而访问 BookCars MongoDB 数据库。

BookCars 前端是一个 React Web 应用程序,是用于预订汽车的主要 Web 界面。

Bookcars 后端是一个 React Web 应用程序,允许管理员和供应商管理汽车车队、预订和客户。

BookCars 移动应用是一个 React Native 应用程序,是用于预订汽车的主要移动应用。

一项关键的设计决策是使用 TypeScript 而不是 JavaScript,因为其具有诸多优势。TypeScript 提供了强类型、出色的工具支持和集成,从而产生了高质量、可扩展、更具可读性和可维护性的代码,易于调试和测试。

API

BookCars API 公开后端、前端和移动应用所需的所有 BookCars 功能。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 是 BookCars API 的主入口点。

index.ts

index.ts 是 BookCars API 的主入口点

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https from 'node:https'
import * as env from './config/env.config'
import * as DatabaseHelper from './common/DatabaseHelper'
import app from './app'

if (await DatabaseHelper.Connect(env.DB_DEBUG)) {
  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 = { key: privateKey, cert: certificate }
      server = https.createServer(credentials, app)

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

  const close = () => {
      console.log('\nGracefully stopping...')
      server.close(async () => {
          console.log(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
          await DatabaseHelper.Close(true)
          console.log('MongoDB connection closed')
          process.exit(0)
      })
  }

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

这是一个 TypeScript 文件,它使用 Node.js 和 Express 启动服务器。它导入了多个模块,包括 dotenv、process、fs、http、https、mongoose 和 app。然后,它检查 HTTPS 环境变量是否设置为 true,如果是,则使用 https 模块和提供的私钥及证书创建一个 HTTPS 服务器。否则,它使用 http 模块创建一个 HTTP 服务器。服务器监听 PORT 环境变量中指定的端口。

定义了 close 函数,用于在接收到终止信号时优雅地停止服务器。它关闭服务器和 MongoDB 连接,然后以状态码 0 退出进程。最后,它注册 close 函数,以便在进程接收到 SIGINTSIGTERMSIGQUIT 信号时调用。

app.ts

app.ts 位于主服务器中

import express, { Express } from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import strings from './config/app.config'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import supplierRoutes from './routes/supplierRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import carRoutes from './routes/carRoutes'
import userRoutes from './routes/userRoutes'
import * as Helper from './common/Helper'

const app: Express = 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('/', supplierRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', carRoutes)
app.use('/', userRoutes)

strings.setLanguage(env.DEFAULT_LANGUAGE)

Helper.mkdir(env.CDN_USERS)
Helper.mkdir(env.CDN_TEMP_USERS)
Helper.mkdir(env.CDN_CARS)
Helper.mkdir(env.CDN_TEMP_CARS)

export default app

首先,我们检索 MongoDB 连接字符串,然后与 BookCars MongoDB 数据库建立连接。然后,我们创建一个 Express app 并加载中间件,如 cors、compression、helmet 和 nocache。我们使用 helmet 中间件库设置了各种安全措施。我们还导入了应用程序不同部分的各种路由文件,例如 supplierRoutesbookingRouteslocationRoutesnotificationRoutescarRoutesuserRoutes。最后,我们加载 Express 路由并导出 app

路由

BookCars API 中有六个路由。每个路由都有自己的控制器,遵循 MVC 设计模式和 SOLID 原则。以下是主要路由

  • userRoutes:提供与用户相关的 REST 函数
  • supplierRoutes:提供与供应商相关的 REST 函数
  • locationRoutes:提供与地点相关的 REST 函数
  • carRoutes:提供与汽车相关的 REST 函数
  • bookingRoutes:提供与预订相关的 REST 函数
  • notificationRoutes:提供与通知相关的 REST 函数

我们不打算逐一解释每个路由。我们将以 locationRoutes 为例,看看它是如何实现的

import express from 'express'
import routeNames from '../config/locationRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as locationController from '../controllers/locationController'

const routes = express.Router()

routes
.route(routeNames.validate)
.post(authJwt.verifyToken, locationController.validate)

routes
.route(routeNames.create)
.post(authJwt.verifyToken, locationController.create)

routes
.route(routeNames.update)
.put(authJwt.verifyToken, locationController.update)

routes
.route(routeNames.delete)
.delete(authJwt.verifyToken, locationController.deleteLocation)

routes
.route(routeNames.getLocation)
.get(locationController.getLocation)

routes
.route(routeNames.getLocations)
.get(locationController.getLocations)

routes
.route(routeNames.checkLocation)
.get(authJwt.verifyToken, locationController.checkLocation)

export default routes

首先,我们创建一个 Express Router。然后,我们使用路由名称、方法、中间件和控制器来创建路由。

routeNames 包含 locationRoutes 路由名称

export default {
  validate: '/api/validate-location',
  create: '/api/create-location',
  update: '/api/update-location/:id',
  delete: '/api/delete-location/:id',
  getLocation: '/api/location/:id/:language',
  getLocations: '/api/locations/:page/:size/:language',
  checkLocation: '/api/check-location/:id',
}

locationController 包含有关地点的核心业务逻辑。由于控制器代码量很大,我们不打算展示所有源代码,但将以 creategetLocations 控制器函数为例。

以下是 Location 模型

import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const locationSchema = new Schema<env.Location>(
{
  values: {
    type: [Schema.Types.ObjectId],
    ref: 'LocationValue',
    validate: (value: any): boolean => Array.isArray(value) && value.length > 1,
  },
},
{
  timestamps: true,
  strict: true,
  collection: 'Location',
},
)

const locationModel = model<env.Location>('Location', locationSchema)

locationModel.on('index', (err) => {
if (err) {
  console.error('Location index error: %s', err)
} else {
  console.info('Location indexing complete')
}
})

export default locationModel

以下是 env.Location TypeScript 类型

export interface Location extends Document {
  values: Types.ObjectId[]
  name?: string
}

一个 Location 有多个值。每个语言一个。默认支持英语和法语。

以下是 LocationValue 模型

import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const locationValueSchema = new Schema<env.LocationValue>(
{
  language: {
    type: String,
    required: [true, "can't be blank"],
    index: true,
    trim: true,
    lowercase: true,
    minLength: 2,
    maxLength: 2,
  },
  value: {
    type: String,
    required: [true, "can't be blank"],
    index: true,
    trim: true,
  },
},
{
  timestamps: true,
  strict: true,
  collection: 'LocationValue',
},
)

const locationValueModel = model<env.LocationValue>('LocationValue', locationValueSchema)

locationValueModel.on('index', (err) => {
if (err) {
  console.error('LocationValue index error: %s', err)
} else {
  console.info('LocationValue indexing complete')
}
})

export default locationValueModel

以下是 env.LocationValue TypeScript 类型

export interface LocationValue extends Document {
  language: string
  value: string
}

一个 LocationValue 包含一个 language 代码(ISO 639-1)和一个字符串 value

以下是 create 控制器函数

export const create = async (req: Request, res: Response) => {
const body: bookcarsTypes.LocationName[] = req.body
const names = body

try {
  const values = []
  for (let i = 0; i < names.length; i++) {
    const name = names[i]
    const locationValue = new LocationValue({
      language: name.language,
      value: name.name,
    })
    await locationValue.save()
    values.push(locationValue._id)
  }

  const location = new Location({ values })
  await location.save()
  return res.sendStatus(200)
} catch (err) {
  console.error(`[location.create] ${strings.DB_ERROR} ${req.body}`, err)
  return res.status(400).send(strings.DB_ERROR + err)
}
}

在此函数中,我们检索请求体,迭代请求体中提供的值(每个语言一个值),然后创建一个 LocationValue。最后,我们根据创建的地点值来创建地点。

以下是 getLocations 控制器函数

export const getLocations = async (req: Request, res: Response) => {
try {
  const page = Number.parseInt(req.params.page)
  const size = Number.parseInt(req.params.size)
  const language = req.params.language
  const keyword = escapeStringRegexp(String(req.query.s || ''))
  const options = 'i'

  const locations = await Location.aggregate(
    [
      {
        $lookup: {
          from: 'LocationValue',
          let: { values: '$values' },
          pipeline: [
            {
              $match: {
                $and: [
                  { $expr: { $in: ['$_id', '$$values'] } },
                  { $expr: { $eq: ['$language', language] } },
                  {
                    $expr: {
                      $regexMatch: {
                        input: '$value',
                        regex: keyword,
                        options,
                      },
                    },
                  },
                ],
              },
            },
          ],
          as: 'value',
        },
      },
      { $unwind: { path: '$value', preserveNullAndEmptyArrays: false } },
      { $addFields: { name: '$value.value' } },
      {
        $facet: {
          resultData: [{ $sort: { name: 1 } }, { $skip: (page - 1) * size }, { $limit: size }],
          pageInfo: [
            {
              $count: 'totalRecords',
            },
          ],
        },
      },
    ],
    { collation: { locale: env.DEFAULT_LANGUAGE, strength: 2 } },
  )

  return res.json(locations)
} catch (err) {
  console.error(`[location.getLocations] ${strings.DB_ERROR} ${req.query.s}`, err)
  return res.status(400).send(strings.DB_ERROR + err)
}
}

在此控制器函数中,我们使用 aggregate MongoDB 函数和 facet 从数据库检索地点以实现分页。

以下是另一个简单的路由,notificationRoutes

import express from 'express'
import routeNames from '../config/notificationRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as notificationController from '../controllers/notificationController'

const routes = express.Router()

routes
.route(routeNames.notificationCounter)
.get(authJwt.verifyToken, notificationController.notificationCounter)

routes
.route(routeNames.notify)
.post(authJwt.verifyToken, notificationController.notify)

routes
.route(routeNames.getNotifications)
.get(authJwt.verifyToken, notificationController.getNotifications)

routes
.route(routeNames.markAsRead)
.post(authJwt.verifyToken, notificationController.markAsRead)

routes
.route(routeNames.markAsUnRead)
.post(authJwt.verifyToken, notificationController.markAsUnRead)

routes
.route(routeNames.delete)
.post(authJwt.verifyToken, notificationController.deleteNotifications)

export default routes

以下是 Notification 模型

import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const notificationSchema = new Schema<env.Notification>(
{
  user: {
    type: Schema.Types.ObjectId,
    required: [true, "can't be blank"],
    ref: 'User',
    index: true,
  },
  message: {
    type: String,
    required: [true, "can't be blank"],
  },
  booking: {
    type: Schema.Types.ObjectId,
    ref: 'Booking',
  },
  isRead: {
    type: Boolean,
    default: false,
  },
},
{
  timestamps: true,
  strict: true,
  collection: 'Notification',
},
)

const notificationModel = model<env.Notification>('Notification', notificationSchema)

notificationModel.on('index', (err) => {
if (err) {
  console.error('Notification index error: %s', err)
} else {
  console.info('Notification indexing complete')
}
})

export default notificationModel

以下是 env.Notification TypeScript 类型

export interface Notification extends Document {
  user: Types.ObjectId
  message: string
  booking: Types.ObjectId
  isRead?: boolean
}

一个 Notification 由对 user 的引用、一个 message、对 booking 的引用和一个 isRead 标志组成。

以下是 getNotifications 控制器函数

export const getNotifications = async (req: Request, res: Response) => {
const { userId: _userId, page: _page, size: _size } = req.params

try {
  const userId = new mongoose.Types.ObjectId(_userId)
  const page = Number.parseInt(_page)
  const size = Number.parseInt(_size)

  const notifications = await Notification.aggregate([
    { $match: { user: userId } },
    {
      $facet: {
        resultData: [{ $sort: { createdAt: -1 } }, { $skip: (page - 1) * size }, { $limit: size }],
        pageInfo: [
          {
            $count: 'totalRecords',
          },
        ],
      },
    },
  ])

  return res.json(notifications)
} catch (err) {
  console.error(`[notification.getNotifications] ${strings.DB_ERROR} ${_userId}`, err)
  return res.status(400).send(strings.DB_ERROR + err)
}
}

在这个简单的控制器函数中,我们使用 MongoDB aggregate 函数以及 pagesize 参数来检索通知。

以下是 markAsRead 控制器函数

export const markAsRead = async (req: Request, res: Response) => {
try {
  const body: { ids: string[] } = req.body
  const { ids: _ids } = body
  const ids = _ids.map((id) => new mongoose.Types.ObjectId(id))
  const { userId: _userId } = req.params
  const userId = new mongoose.Types.ObjectId(_userId)

  const bulk = Notification.collection.initializeOrderedBulkOp()
  const notifications = await Notification.find({
    _id: { $in: ids },
    isRead: false,
  })
  const length = notifications.length

  bulk.find({ _id: { $in: ids }, isRead: false }).update({ $set: { isRead: true } })
  const result = await bulk.execute()

  if (result.modifiedCount !== length) {
    console.error(`[notification.markAsRead] ${strings.DB_ERROR}`)
    return res.status(400).send(strings.DB_ERROR)
  }
  const counter = await NotificationCounter.findOne({ user: userId })
  if (!counter || typeof counter.count === 'undefined') {
    return res.sendStatus(204)
  }
  counter.count -= length
  await counter.save()

  return res.sendStatus(200)
} catch (err) {
  console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
  return res.status(400).send(strings.DB_ERROR + err)
}
}

在此控制器函数中,我们批量更新通知并将其标记为已读。

前端

前端是一个使用 Node.js、React、MUI 和 TypeScript 构建的 Web 应用程序。通过前端,客户可以根据取车和还车地点及时间搜索可用车辆,选择车辆并进行结账。

  • ./frontend/src/assets/ 文件夹包含 CSS 和图像。
  • ./frontend/src/pages/ 文件夹包含 React 页面。
  • ./frontend/src/components/ 文件夹包含 React 组件。
  • ./frontend/src/services/ 包含 BookCars API 客户端服务。
  • ./frontend/src/App.tsx 是包含路由的主 React 应用。
  • ./frontend/src/index.tsx 是前端的主入口点。

TypeScript 类型定义在包 ./packages/bookcars-types 中定义。

App.tsx 是主 React 应用

import React, { lazy, Suspense } from 'react'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

const SignIn = lazy(() => import('./pages/SignIn'))
const SignUp = lazy(() => import('./pages/SignUp'))
const Activate = lazy(() => import('./pages/Activate'))
const ForgotPassword = lazy(() => import('./pages/ForgotPassword'))
const ResetPassword = lazy(() => import('./pages/ResetPassword'))
const Home = lazy(() => import('./pages/Home'))
const Cars = lazy(() => import('./pages/Cars'))
const Checkout = lazy(() => import('./pages/Checkout'))
const Bookings = lazy(() => import('./pages/Bookings'))
const Booking = lazy(() => import('./pages/Booking'))
const Settings = lazy(() => import('./pages/Settings'))
const Notifications = lazy(() => import('./pages/Notifications'))
const ToS = lazy(() => import('./pages/ToS'))
const About = lazy(() => import('./pages/About'))
const ChangePassword = lazy(() => import('./pages/ChangePassword'))
const Contact = lazy(() => import('./pages/Contact'))
const NoMatch = lazy(() => import('./pages/NoMatch'))

const App = () => (
<Router>
  <div className="App">
    <Suspense fallback={<></>}>
      <Routes>
        <Route path="/sign-in" element={<SignIn />} />
        <Route path="/sign-up" element={<SignUp />} />
        <Route path="/activate" element={<Activate />} />
        <Route path="/forgot-password" element={<ForgotPassword />} />
        <Route path="/reset-password" element={<ResetPassword />} />
        <Route path="/" element={<Home />} />
        <Route path="/cars" element={<Cars />} />
        <Route path="/checkout" element={<Checkout />} />
        <Route path="/bookings" element={<Bookings />} />
        <Route path="/booking" element={<Booking />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/notifications" element={<Notifications />} />
        <Route path="/change-password" element={<ChangePassword />} />
        <Route path="/about" element={<About />} />
        <Route path="/tos" element={<ToS />} />
        <Route path="/contact" element={<Contact />} />

        <Route path="*" element={<NoMatch />} />
      </Routes>
    </Suspense>
  </div>
</Router>
)

export default App

我们使用 React 懒加载来加载每个路由。

我们不会介绍前端的每个页面,但您可以打开源代码查看每个页面。

移动应用

BookCars 提供原生 Android 和 iOS 移动应用。该移动应用使用 React Native、Expo 和 TypeScript 构建。与前端一样,移动应用允许客户根据取车和还车地点及时间搜索可用车辆,选择车辆并进行结账。

如果客户的预订从后端更新,客户会收到推送通知。推送通知使用 Node.js、Expo Server SDK 和 Firebase 构建。

  • ./mobile/assets/ 文件夹包含图像。
  • ./mobile/screens/ 文件夹包含主要的 React Native 屏幕。
  • ./mobile/components/ 文件夹包含 React Native 组件。
  • ./mobile/services/ 包含 BookCars API 客户端服务。
  • ./mobile/App.tsx 是主 React Native 应用。

TypeScript 类型定义在

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

./mobile/types/./mobile/tsconfig.json 中按以下方式加载

{
"extends": "expo/tsconfig.base",
"compilerOptions": {
  "strict": true,
  "typeRoots": [
    "./types"
  ]
}
}

App.tsx 是主 React Native 应用

import 'react-native-gesture-handler'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RootSiblingParent } from 'react-native-root-siblings'
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import DrawerNavigator from './components/DrawerNavigator'
import * as Helper from './common/Helper'
import * as NotificationService from './services/NotificationService'
import * as UserService from './services/UserService'

Notifications.setNotificationHandler({
handleNotification: async () => ({
  shouldShowAlert: true,
  shouldPlaySound: true,
  shouldSetBadge: true,
}),
})

// Prevent native splash screen from autohiding before App component declaration
SplashScreen.preventAutoHideAsync()
.then((result) => console.log(`SplashScreen.preventAutoHideAsync() succeeded: ${result}`))
.catch(console.warn) // it's good to explicitly catch and inspect any error

const App = () => {
const [appIsReady, setAppIsReady] = useState(false)
const responseListener = useRef<Notifications.Subscription>()
const navigationRef = useRef<NavigationContainerRef<StackParams>>(null)

useEffect(() => {
  const register = async () => {
    const loggedIn = await UserService.loggedIn()
    if (loggedIn) {
      const currentUser = await UserService.getCurrentUser()
      if (currentUser?._id) {
        await Helper.registerPushToken(currentUser._id)
      } else {
        Helper.error()
      }
    }
  }

  // Register push notifiations token
  register()

  // This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
  responseListener.current = Notifications.addNotificationResponseReceivedListener(async (response) => {
    try {
      if (navigationRef.current) {
        const { data } = response.notification.request.content

        if (data.booking) {
          if (data.user && data.notification) {
            await NotificationService.markAsRead(data.user, [data.notification])
          }
          navigationRef.current.navigate('Booking', { id: data.booking })
        } else {
          navigationRef.current.navigate('Notifications', {})
        }
      }
    } catch (err) {
      Helper.error(err, false)
    }
  })

  return () => {
    if (responseListener.current) {
      Notifications.removeNotificationSubscription(responseListener.current)
    }
  }
}, [])

setTimeout(() => {
  setAppIsReady(true)
}, 500)

const onReady = useCallback(async () => {
  if (appIsReady) {
    // This tells the splash screen to hide immediately! If we call this after
    // `setAppIsReady`, then we may see a blank screen while the app is
    // loading its initial state and rendering its first pixels. So instead,
    // we hide the splash screen once we know the root view has already
    // performed layout.
    await SplashScreen.hideAsync()
  }
}, [appIsReady])

if (!appIsReady) {
  return null
}

return (
  <SafeAreaProvider>
    <Provider>
      <RootSiblingParent>
        <NavigationContainer ref={navigationRef} onReady={onReady}>
          <ExpoStatusBar style="light" backgroundColor="rgba(0, 0, 0, .9)" />
          <DrawerNavigator />
        </NavigationContainer>
      </RootSiblingParent>
    </Provider>
  </SafeAreaProvider>
)
}

export default App

我们不会介绍移动应用的每个屏幕,但您可以打开源代码查看每个屏幕。

后端

后端是一个使用 Node.js、React、MUI 和 TypeScript 构建的 Web 应用程序。通过后端,管理员可以创建和管理供应商、汽车、地点、客户和预订。当从后端创建新供应商时,他们将收到一封电子邮件,提示他们创建账户,以便访问后端并管理其汽车车队和预订。

  • ./backend/assets/ 文件夹包含 CSS 和图像。
  • ./backend/pages/ 文件夹包含 React 页面。
  • ./backend/components/ 文件夹包含 React 组件。
  • ./backend/services/ 包含 BookCars API 客户端服务。
  • ./backend/App.tsx 是包含路由的主 React 应用。
  • ./backend/index.tsx 是后端的入口点。

TypeScript 类型定义在包 ./packages/bookcars-types 中定义。

后端的 App.tsx 的逻辑与前端的 App.tsx 类似。

我们不会介绍后端的每个页面,但您可以打开源代码查看每个页面。

关注点

使用 React Native 和 Expo 构建移动应用非常简单。Expo 使 React Native 的移动开发变得非常简单。

在后端、前端和移动开发中使用相同的语言(TypeScript)非常方便。

TypeScript 是一种非常有趣的语言,具有许多优点。通过为 JavaScript 添加静态类型,我们可以避免许多错误,并生成高质量、可扩展、更具可读性和可维护性的代码,易于调试和测试。

就是这样!希望您喜欢阅读这篇文章。

历史

© . All rights reserved.