预订汽车——带移动应用程序的租车平台

目录

介绍​编辑

特征

先决条件

快速概览

前端

移动应用

后端

背景

安装

先决条件

说明

Docker

Docker镜像

SSL

API

后端

前端

docker-compose.yml

更改货币

前端

后端

移动应用

添加新语言

后端和前端

移动应用

演示数据库

Windows、Linux和macOS

Docker

构建移动应用

先决条件

配置

生产构建

说明

Android

EAS构建

本地构建

iOS

EAS构建

本地构建

从源代码运行

先决条件

说明

运行移动应用

推送通知

使用代码

API

index.ts

server.ts

路由

前端

移动应用

后端

兴趣点


介绍

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

BookCars是用户友好的,直接的,对XSS, XST, CSRFMITM安全,并且精心制作。

BookCars旨在与多个供应商一起工作。每个供应商都可以在后端管理自己的车队和预订。BookCars也可以只与一个供应商合作。

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

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

客户可以通过前端或移动应用程序注册,根据接送点和时间搜索可用车辆,选择车辆并完成结账过程。

一个关键的设计决定是使用TypeScript而不是JavaScript,因为它有很多优点。TypeScript提供了强大的类型、工具和集成,从而产生高质量、可扩展、更易读和可维护的代码,并且易于调试和测试。

在本文中,您将了解如何制作BookCars,包括对源代码和软件体系结构的主要部分的描述、如何部署它以及如何运行源代码。但在我们深入研究之前,我们将从平台的快速概述开始。

特征

  • 供应商管理
  • 准备一个或多个供应商
  • 车队管理
  • 预订管理
  • 客户端管理
  • 多种支付方式(信用卡、延期付款)
  • 多语言支持(英语、法语)
  • 多个分页选项(带有“下一步”和“上一步”按钮的经典分页,无限滚动)
  • 响应式后端和前端
  • 适用于安卓和iOS的原生移动应用程序
  • 推送通知
  • 防止XSS,XST,CSRF和MITM

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

先决条件

  • TypeScript
  • Node.js
  • 表达
  • MognoDB
  • React
  • Expo
  • JWT
  • MVC
  • Docker
  • Git

快速概览

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

前端

从前端,用户可以搜索可用的汽车,选择汽车并结帐。

下面是前端的主页,用户可以在其中选择接送地点、日期和时间预订以及搜索可用的汽车。

以下是主页的搜索结果,用户可以在其中选择租车。

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

下面是登录页面。

下面是注册页面。

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

下面是客户可以看到预订详情的页面。

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

下面是客户可以管理其设置的页面。

下面是客户可以更改密码的页面。

就是这样!这是前端的主要页面。

移动应用

从移动应用程序中,用户可以搜索可用的汽车,选择汽车并结帐。

如果用户的预订状态已更新,用户还可以接收推送通知。

以下是移动应用程序的主页,用户可以在其中选择接送地点、日期和时间预订以及搜索可用汽车。

   

 以下是主页的搜索结果,用户可以在其中选择租车和结账。

    

 以下是登录和注册页面。

   

以下是用户可以查看和管理其预订的页面。

    

以下是户可以更新其个人资料信息,更改密码并查看其通知的页面。

   

移动应用程序的主页就是这样。

后端

BookCars以供应商为导向。这意味着有三种类型的用户:

  • Admin:他具有对后端的完全访问权限。他无所不能。
  • Supplier:他限制了对后端的访问。他只能管理他的汽车和预订。
  • User:他只能访问前端和移动应用程序。他无法访问后端。

BookCars旨在与多个供应商合作。每个供应商都可以从后端管理他的车队和预订。BookCars也可以只与一个供应商合作。

在后端,管理员用户可以创建和管理供应商、汽车、位置、用户和预订。

当管理员用户创建新供应商时,供应商将收到一封自动电子邮件,用于创建其帐户以访问后端,以便他可以管理他的车队和预订。

下面是后端的登录页面。

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

下面是显示车队并可以管理的页面。

下面是管理员和供应商可以通过提供图像和汽车信息来创建新汽车的页面。

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

下面是管理员可以管理平台用户的页面。

以下是编辑预订的页面。

就是这样!这是后端的主页。

背景

BookCars背后的基本思想非常简单:

  • 后端:管理员从中创建新供应商。每个供应商都会收到一封自动电子邮件,以激活他的帐户并可以访问后端,以便他可以管理他的车队和预订。
  • 前端和移动应用程序:用户可以根据某些参数(例如接送地点以及日期和时间预订)查看可用的汽车。然后,用户可以继续结帐以预订他们的汽车。

后端,前端和移动应用程序依赖于BookCars API,这是一个RESTful API,它公开了访问BookCars数据库的功能。

安装

BookCars是跨平台的,可以在WindowsLinuxmacOS上运行和安装。

以下是Linux上的安装说明。

先决条件

  1. 安装 gitNode.jsNGINXMongoDBmongosh
  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.com.key
BC_CERTIFICATE=/etc/ssl/bookcars.com.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.com.crt
BC_DB_SSL_CA=/etc/ssl/bookcars.com.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.com
BC_ADMIN_EMAIL=admin@bookcars.com
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_DEFAULT_LANGUAGE=en
BC_BACKEND_HOST=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost/
BC_MINIMUM_AGE=21
BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

您需要配置以下选项:

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=in-v3.mailjet.com
BC_SMTP_PORT=587
BC_SMTP_USER=USER
BC_SMTP_PASS=PASSWORD
BC_SMTP_FROM=admin@bookcars.com
BC_ADMIN_EMAIL=admin@bookcars.com
BC_BACKEND_HOST=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost/

BC_DB_URI中的PASSWORD替换为MongoDB密码, JWT_SECRET替换为secret token。最后,设置SMTP选项。SMTP选项是注册所必需的。您可以使用mailjetbrevosendgrid或任何其他事务性电子邮件提供商。

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

如果要在本地进行测试或将其替换为IP、主机名或FQDN,请离开localhost

如果要在移动应用程序中启用推送通知,请按照以下说明操作并设置以下选项:

BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

如果要启用HTTPS,需要设置以下选项:

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

6、创建/opt/bookcars/backend/.env 文件:

PORT=3001
REACT_APP_NODE_ENV=production
REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_DEFAULT_LANGUAGE=en
REACT_APP_BC_PAGE_SIZE=30
REACT_APP_BC_CARS_PAGE_SIZE=15
REACT_APP_BC_BOOKINGS_PAGE_SIZE=20
REACT_APP_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS=http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars
REACT_APP_BC_COMPANY_IMAGE_WIDTH=60
REACT_APP_BC_COMPANY_IMAGE_HEIGHT=30
REACT_APP_BC_CAR_IMAGE_WIDTH=300
REACT_APP_BC_CAR_IMAGE_HEIGHT=200
REACT_APP_BC_MINIMUM_AGE=21
REACT_APP_BC_PAGINATION_MODE=classic

您需要配置以下选项:


REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS=http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars

如果要在本地进行测试或将其替换为IP、主机名或FQDN,请离开localhost

REACT_APP_BC_PAGINATION_MODE:您可以在classicinfinite_scroll之间进行选择。此选项缺省为classic。如果您选择classic,您将获得一个经典的分页,在桌面上带有下一个和上一个按钮,在移动设备上无限滚动。如果您选择infinite_scroll,您将在桌面和移动设备上获得无限滚动。

7、创建/opt/bookcars/frontend/.env 文件:

REACT_APP_NODE_ENV=production
REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_RECAPTCHA_ENABLED=false
REACT_APP_BC_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY
REACT_APP_BC_DEFAULT_LANGUAGE=en
REACT_APP_BC_PAGE_SIZE=30
REACT_APP_BC_CARS_PAGE_SIZE=15
REACT_APP_BC_BOOKINGS_PAGE_SIZE=20
REACT_APP_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_COMPANY_IMAGE_WIDTH=60
REACT_APP_BC_COMPANY_IMAGE_HEIGHT=30
REACT_APP_BC_CAR_IMAGE_WIDTH=300
REACT_APP_BC_CAR_IMAGE_HEIGHT=200
REACT_APP_BC_MINIMUM_AGE=21
REACT_APP_BC_PAGINATION_MODE=classic

您需要配置以下选项:

REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars

如果要在本地进行测试或将其替换为IP、主机名或FQDN,请离开localhost

默认情况下,reCAPTCHA处于禁用状态。如果要启用它,则需要设置REACT_APP_BC_RECAPTCHA_ENABLEDtrueREACT_APP_BC_RECAPTCHA_SITE_KEYGoogle reCAPTCHA站点密钥。

8、如果要运行或构建移动应用程序,则需要创建 mobile/.env

BC_API_HOST=https://bookcars.com:4002
BC_DEFAULT_LANGUAGE=en
BC_PAGE_SIZE=20
BC_CARS_PAGE_SIZE=8
BC_BOOKINGS_PAGE_SIZE=8
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars
BC_COMPANY_IMAGE_WIDTH=60
BC_COMPANY_IMAGE_HEIGHT=30
BC_CAR_IMAGE_WIDTH=300
BC_CAR_IMAGE_HEIGHT=200
BC_MINIMUM_AGE=21

您需要配置以下选项:

BC_API_HOST=https://bookcars.com:4002
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars

替换localhostIP、主机名或FQDN

9、配置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.com.key;
    #ssl_certificate /etc/ssl/bookcars.com.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.com.key
#ssl_certificate /etc/ssl/bookcars.com.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.com.key;
    #ssl_certificate /etc/ssl/bookcars.com.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文件夹,并向在/var/www/cdn/bookcars上运行bookcars服务的用户添加完全访问权限。

如果要启用SSL,请取消注释并设置以下行:

#listen 3001 http2 ssl default_server
#ssl_certificate_key /etc/ssl/bookcars.com.key
#ssl_certificate /etc/ssl/bookcars.com.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

10、启用防火墙并打开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

11、启动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

12、部署BookCars:

bc-deploy all

BookCars后端可在端口3001上访问,前端可在端口80上访问。

13、如果您不想使用演示数据库,请导航到hostname3001/sign-up来创建管理员

14、打开 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

如果您想更改货币,请按照以下说明操作。

Docker

BookCars可以在Linux上的Docker容器和适用于WindowsMacDocker 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=in-v3.mailjet.com
BC_SMTP_PORT=587
BC_SMTP_USER=USER
BC_SMTP_PASS=PASSWORD
BC_SMTP_FROM=admin@bookcars.ma
BC_ADMIN_EMAIL=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=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost/
BC_MINIMUM_AGE=21
BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

设置以下选项:

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=in-v3.mailjet.com
BC_SMTP_PORT=587
BC_SMTP_USER=USER
BC_SMTP_PASS=PASSWORD
BC_SMTP_FROM=admin@bookcars.com
BC_ADMIN_EMAIL=admin@bookcars.com
BC_BACKEND_HOST=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost/

替换PASSWORDBC_DB_URI为您选择的强密码和JWT_SECRET为秘密令牌。最后,设置SMTP选项。SMTP选项是注册所必需的。您可以使用mailjetbrevosendgrid或任何其他事务性电子邮件提供商。

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

如果要在本地进行测试,请离开localhost,或将其替换为IP、主机名或FQDN

如果要在移动应用程序中启用推送通知,请按照以下说明操作并设置以下选项:

BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

3、创建包含以下内容的./backend/.env.docker 文件:

REACT_APP_NODE_ENV=production
REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_DEFAULT_LANGUAGE=en
REACT_APP_BC_PAGE_SIZE=30
REACT_APP_BC_CARS_PAGE_SIZE=15
REACT_APP_BC_BOOKINGS_PAGE_SIZE=20
REACT_APP_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS=http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars
REACT_APP_BC_COMPANY_IMAGE_WIDTH=60
REACT_APP_BC_COMPANY_IMAGE_HEIGHT=30
REACT_APP_BC_CAR_IMAGE_WIDTH=300
REACT_APP_BC_CAR_IMAGE_HEIGHT=200
REACT_APP_BC_MINIMUM_AGE=21
REACT_APP_BC_PAGINATION_MODE=classic

设置以下选项:

REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS=http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars

如果要在本地进行测试,请离开localhost,或将其替换为IP、主机名或FQDN

如果要更改分页模式,请更改REACT_APP_BC_PAGINATION_MODE选项。您可以在classicinfinite_scroll之间进行选择。此选项缺省为classic。如果您选择classic,您将获得一个经典的分页,在桌面上带有下一个和上一个按钮,在移动设备上无限滚动。如果您选择infinite_scroll,您将在桌面和移动设备上获得无限滚动。

4、创建包含以下内容的./frontend/.env.docker 文件:

REACT_APP_NODE_ENV=production
REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_RECAPTCHA_ENABLED=false
REACT_APP_BC_DEFAULT_LANGUAGE=en
REACT_APP_BC_PAGE_SIZE=30
REACT_APP_BC_CARS_PAGE_SIZE=15
REACT_APP_BC_BOOKINGS_PAGE_SIZE=20
REACT_APP_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_COMPANY_IMAGE_WIDTH=60
REACT_APP_BC_COMPANY_IMAGE_HEIGHT=30
REACT_APP_BC_CAR_IMAGE_WIDTH=300
REACT_APP_BC_CAR_IMAGE_HEIGHT=200
REACT_APP_BC_MINIMUM_AGE=21
REACT_APP_BC_PAGINATION_MODE=classic

设置以下选项:

REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars

如果要在本地进行测试,请离开localhost,或将其替换为IP、主机名或FQDN

如果要更改分页模式,请更改REACT_APP_BC_PAGINATION_MODE选项。

默认情况下,reCAPTCHA在前端处于禁用状态。如果要启用它,则必须设置REACT_APP_BC_RECAPTCHA_ENABLEDtrueREACT_APP_BC_RECAPTCHA_SITE_KEYGoogle reCAPTCHA站点密钥。

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:

替换PASSWORD为您在./api/.env.docker 中的BC_DB_URI中设置的密码。

6、构建并运行Docker镜像:

sudo docker compose up

要在后台运行撰写,请使用以下命令添加-d选项:

sudo docker compose up -d

如果要重新生成,请使用以下命令:

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

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

sudo docker compose logs

如果要停止和删除由组合创建的容器、数据卷、网络和镜像,并从干净的镜像开始,请使用以下命令:

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

使用上述命令时要小心。MongoDB数据库和cdn内容将被删除。因此,请确保在继续之前备份数据库和cdn内容。

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

如果您是第一次运行BookCars,您将从一个空数据库开始。因此,您必须通过填写from 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.keybookcars.crt将在./docker-compose.yml 中加载为/etc/ssl/bookcars.crt

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=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost/

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

后端

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

REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS=http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars

http://localhost替换为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 中更新以下选项:

REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars

http://localhost替换为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

更改货币

要更改货币,请按照以下说明操作:

前端

打开 frontend/src/lang/common.ts 并更改en.CURRENCYfr.CURRENCY

后端

打开 backend/src/lang/common.ts 并更改en.CURRENCYfr.CURRENCY

移动应用

打开 mobile/lang/en.ts 并更改en.CURRENCYen.CAR_CURRENCY

打开 mobile/lang/fr.ts 并更改fr.CURRENCYfr.CAR_CURRENCY

添加新语言

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

后端和前端

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

移动应用

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

演示数据库

WindowsLinuxmacOS

  • 下载并安装 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密码。

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

CDN文件夹包含以下文件夹:

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

如果您想从源代码运行BookCars或在不使用Docker的情况下将其安装在WindowsLinux上,请按以下步骤操作:

  • 在Windows上,安装IIS并将cdn文件夹的内容复制到C:\inetpub\wwwroot\cdn\bookcars中。最后,向在C:\inetpub\wwwroot\cdn\bookcars上运行BookCars API的用户添加完全访问权限。
  • 在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;
    }
}

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

后端凭据:

  • 用户名:admin@bookcars.ma
  • 密码:B00kC4r5

前端和移动应用凭据:

  • 用户名: jdoe@bookcars.ma
  • 密码:B00kC4r5

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-1API容器名称。

9、转到后端 http://localhost:3001 并使用以下凭据登录: 

用户名admin@bookcars.ma
密码B00kC4r5

10、转到前端 http://localhost 并使用以下凭据登录: 

用户名jdoe@bookcars.ma
密码B00kC4r5

构建移动应用

先决条件

要构建BookCars移动应用程序,您需要在计算机上安装以下工具:

使用以下命令安装eas-cli

npm i -g eas-cli

配置

  • 您需要下载google-services.json文件并将其放在./mobile根目录下以发送推送通知。否则,移动应用将无法生成。不要忘记按照文档中所述 expo.dev>凭据>服务凭据Google Cloud Messaging Token 中设置Firebase Server密钥。
  • 如果您没有Expo 帐户,则需要创建一个帐户来构建BookCars移动应用程序。
  • 转到 expo.dev,单击Pojects,然后单击Create a Project。将BookCars设置为项目名称,然后单击Create
  • 转到BookCars项目并复制项目ID。打开./mobile/app.json并将项目ID粘贴到extra.eas.projectId
  • expo.dev创建Expo访问令牌(帐户设置>访问令牌)并设置 api/.env BC_EXPO_ACCESS_TOKEN设置
BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
  • 创建包含以下内容的mobile/.env文件:
BC_API_HOST=https://bookcars.com:4002
BC_DEFAULT_LANGUAGE=en
BC_PAGE_SIZE=20
BC_CARS_PAGE_SIZE=8
BC_BOOKINGS_PAGE_SIZE=8
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars
BC_COMPANY_IMAGE_WIDTH=60
BC_COMPANY_IMAGE_HEIGHT=30
BC_CAR_IMAGE_WIDTH=300
BC_CAR_IMAGE_HEIGHT=200
BC_MINIMUM_AGE=21

您需要配置以下选项:

BC_API_HOST=https://bookcars.com:4002
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars

替换https://bookcars.comIP、主机名或FQDN

生产构建

如果您想在生产中使用BookCars移动应用程序,您应该在BookCars API中使用HTTPS,并通过删除plugins部分中的"./plugins/usesCleartextTraffic"行来禁用./mobile/app.json中的usesCleartextTraffic expo插件。

说明

  • 将源克隆到您的计算机:

git clone https://github.com/aelassas/bookcars.git
  • 转到移动文件夹:

cd ./mobile
  • 运行以下命令:

npm install

Android

EAS构建

若要使用EAS Build托管服务生成BookCars Android应用,请运行以下命令:

npm run build:android
本地构建

本地构建需要macOSLinux。对于Windows,应使用EAS Builds

您需要安装Android SDKJDK 11以及设置ANDROID_HOMEJAVA_HOME环境变量。然后,运行以下命令:

npm run build:android:local

iOS

EAS构建

若要生成BookCars iOS应用程序,请运行以下命令:

npm run build:ios
本地构建

您需要在macOS上安装fastlaneCocoaPods

若要在本地生成BookCars iOS应用,请运行以下命令:

npm run build:ios:local

从源代码运行

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

先决条件

  1. Windows,MongoDBmongosh上安装gitNode.jsNGINXIIS
  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 源代码下载到您的机器上。

2、添加 api/.env 文件:

NODE_ENV=development
BC_PORT=4002
BC_HTTPS=false
BC_PRIVATE_KEY=/etc/ssl/bookcars.com.key
BC_CERTIFICATE=/etc/ssl/bookcars.com.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.com.crt
BC_DB_SSL_CA=/etc/ssl/bookcars.com.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.com
BC_ADMIN_EMAIL=admin@bookcars.com
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=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost:3002/
BC_MINIMUM_AGE=21
BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

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

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

您需要配置以下选项:

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.com
BC_ADMIN_EMAIL=admin@bookcars.com

替换BC_DB_URI中的PASSWORD为您选择的强密码和JWT_SECRET为秘密令牌。最后,设置SMTP选项。SMTP选项是注册所必需的。您可以使用mailjetbrevosendgrid或任何其他事务性电子邮件提供商。

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

如果要在移动应用程序中启用推送通知,请按照以下说明操作并设置以下选项:

BC_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN

要运行api,请使用以下命令:

cd ./api
npm install
npm run dev

3、添加 backend/.env 文件:

PORT=3001
REACT_APP_NODE_ENV=development
REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_DEFAULT_LANGUAGE=en
REACT_APP_BC_PAGE_SIZE=30
REACT_APP_BC_CARS_PAGE_SIZE=15
REACT_APP_BC_BOOKINGS_PAGE_SIZE=20
REACT_APP_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS=http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars
REACT_APP_BC_COMPANY_IMAGE_WIDTH=60
REACT_APP_BC_COMPANY_IMAGE_HEIGHT=30
REACT_APP_BC_CAR_IMAGE_WIDTH=300
REACT_APP_BC_CAR_IMAGE_HEIGHT=200
REACT_APP_BC_MINIMUM_AGE=21
REACT_APP_BC_PAGINATION_MODE=classic

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

cd ./backend
npm install
npm start

4、添加 frontend/.env 文件:

PORT=3002
REACT_APP_NODE_ENV=development
REACT_APP_BC_API_HOST=http://localhost:4002
REACT_APP_BC_RECAPTCHA_ENABLED=false
REACT_APP_BC_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY
REACT_APP_BC_DEFAULT_LANGUAGE=en
REACT_APP_BC_PAGE_SIZE=30
REACT_APP_BC_CARS_PAGE_SIZE=15
REACT_APP_BC_BOOKINGS_PAGE_SIZE=20
REACT_APP_BC_BOOKINGS_MOBILE_PAGE_SIZE=10
REACT_APP_BC_CDN_USERS=http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
REACT_APP_BC_COMPANY_IMAGE_WIDTH=60
REACT_APP_BC_COMPANY_IMAGE_HEIGHT=30
REACT_APP_BC_CAR_IMAGE_WIDTH=300
REACT_APP_BC_CAR_IMAGE_HEIGHT=200
REACT_APP_BC_MINIMUM_AGE=21
REACT_APP_BC_PAGINATION_MODE=classic

默认情况下,reCAPTCHA处于禁用状态。如果要启用它,则必须设置REACT_APP_BC_RECAPTCHA_ENABLEDtrueREACT_APP_BC_RECAPTCHA_SITE_KEYGoogle reCAPTCHA 站点密钥。

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

cd ./frontend
npm install
npm start

5、如果要运行移动应用程序,则需要添加mobile/.env

BC_API_HOST=https://bookcars.com:4002
BC_DEFAULT_LANGUAGE=en
BC_PAGE_SIZE=20
BC_CARS_PAGE_SIZE=8
BC_BOOKINGS_PAGE_SIZE=8
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars
BC_COMPANY_IMAGE_WIDTH=60
BC_COMPANY_IMAGE_HEIGHT=30
BC_CAR_IMAGE_WIDTH=300
BC_CAR_IMAGE_HEIGHT=200
BC_MINIMUM_AGE=21

您需要配置以下选项:

BC_API_HOST=https://bookcars.com:4002
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars

您需要替换https://bookcars.comIP或主机名。

6、配置 http://localhost/cdn

  • 在Windows上,安装IIS,创建 C:\inetpub\wwwroot\cdn\bookcars文件夹,并为在bookcars C:\inetpub\wwwroot\cdn\bookcars文件夹上运行BookCars API的用户添加完全访问权限。
  • 在Linux上,安装NGINX,创建/var/www/cdn/bookcars文件夹,在/var/www/cdn/bookcars文件夹上为运行BookCars API的用户添加完全访问权限,并通过更改/etc/nginx/sites-available/defaultcdn文件夹添加到NGINX,如下所示:
server {
    listen 80 default_server;
    server_name _;
    
    ...

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

7、从 http://localhost:3001/sign-up 创建管理员用户

8、要运行移动应用程序,只需在您的设备上下载Expo应用程序,然后从./mobile文件夹运行以下命令:

npm install
npm start

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

您可以在此处找到有关运行移动应用程序的详细说明。

要更改货币,请按照以下说明操作。

运行移动应用

若要运行移动应用,请使用以下选项创建./mobile/.env 文件:

BC_API_HOST=https://bookcars.com:4002
BC_DEFAULT_LANGUAGE=en
BC_PAGE_SIZE=20
BC_CARS_PAGE_SIZE=8
BC_BOOKINGS_PAGE_SIZE=8
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars
BC_COMPANY_IMAGE_WIDTH=60
BC_COMPANY_IMAGE_HEIGHT=30
BC_CAR_IMAGE_WIDTH=300
BC_CAR_IMAGE_HEIGHT=200
BC_MINIMUM_AGE=21

您需要配置以下选项:

BC_API_HOST=https://bookcars.com:4002
BC_CDN_USERS=https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS=https://bookcars.com/cdn/bookcars/cars

替换https://bookcars.comIP、主机名或FQDN

按照以下说明安装演示数据库。

配置 http://localhost/cdn

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

Linux上,安装NGINX并更新/etc/nginx/sites-enabled/default,如下所示:

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

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

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

按照以下说明配置./api

使用以下命令运行./api

cd ./api
npm run dev

只需在您的设备上下载世博会应用程序并从./mobile文件夹运行以下命令,即可运行移动应用程序:

cd ./mobile
npm install
npm start

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

推送通知

如果要启用BookCars推送通知,请下载 google-services.json文件并将其放在./mobile根目录中。可以通过googleServicesFile设置选项从./mobile/app.json配置文件配置其路径。不要忘记按照文档中所述 expo.dev>凭据>服务凭据Google Cloud Messaging Token 中设置Firebase Server密钥。

使用代码

本节介绍BookCars的软件架构,包括API、前端、移动端和后端。

BookCars API是一个Node.js服务器应用程序,它使用Express公开RESTful API,该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/server.ts是建立数据库连接和加载路由的主服务器。
  • ./api/index.ts是BookCars API的主要入口点。

index.ts

index.tsBookCars 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 mongoose from 'mongoose'
import app from './server'
import * as env from './config/env.config'

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 mongoose.connection.close(true)
        console.log('MongoDB connection closed')
        process.exit(0)
    })
}

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

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

close函数被定义为在接收到终止信号时正常停止服务器。它关闭服务器和MongoDB连接,然后退出进程,状态代码为0。最后,它注册在进程接收到SIGINTSIGTERMSIGQUIT信号时要调用的close函数。

server.ts

server.ts位于主服务器中:

import express, { Express } from 'express'
import cors from 'cors'
import mongoose, { ConnectOptions } from 'mongoose'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import strings from './config/app.config'
import * as env from './config/env.config'
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'

let options: ConnectOptions = {}
if (env.DB_SSL) {
    options = {
        tls: true,
        tlsCertificateKeyFile: env.DB_SSL_CERT,
        tlsCAFile: env.DB_SSL_CA,
    }
}

mongoose.set('debug', env.DB_DEBUG)
mongoose.Promise = globalThis.Promise
try {
    await mongoose.connect(env.DB_URI, options)
    console.log('Database is connected')
} catch (err) {
    console.error('Cannot connect to the database:', err)
}

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

strings.setLanguage(env.DEFAULT_LANGUAGE)
export default app

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

路由

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

  • userRoutes:提供与用户相关的REST函数
  • companyRoutes:提供与供应商相关的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 async function create(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 async function getLocations(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 async function getNotifications(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 async function markAsRead(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.jsReactMUITypeScript构建的Web应用程序。在前端,客户可以根据上车和下车地点和时间搜索可用的汽车,选择一辆车并继续结账。

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

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

App.tsx是主要的react App

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>
)

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

我们不会涵盖前端的每一页,但您可以打开源代码并根据需要查看每个页面。

移动应用

BookCars提供适用于AndroidiOS的原生移动应用程序。该移动应用程序是使用React NativeExpoTypeScript构建的。与前端一样,移动应用程序允许客户根据上车和下车地点和时间搜索可用的汽车,选择一辆汽车并继续结账。

如果客户从后端更新其预订,则会收到推送通知。推送通知是使用Node.jsExpo Server SDKFirebase构建的。

  • ./mobile/assets/folder包含图像。
  • ./mobile/screens/folder包含主要的React Native屏幕。
  • ./mobile/components/folder包含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 DrawerNavigator from './components/DrawerNavigator'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
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(() => {
    async function register() {
      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.data

          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.jsReactMUITypeScript构建的Web应用程序。在后端,管理员可以创建和管理供应商、汽车、位置、客户和预订。当从后端创建新的供应商时,他们将收到一封电子邮件,提示他们创建一个帐户,以便访问后端并管理他们的车队和预订。

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

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

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

我们不会涵盖后端的每一页,但您可以打开源代码并根据需要查看每个页面。

兴趣点

我为一位经营汽车租赁公司的朋友开始了这个项目。他需要一个适用于AndroidiOS的汽车租赁网站和移动应用程序,以及一个用于管理车辆、预订和客户的后端。我更进一步,通过附加功能增强项目,使其更加通用。最后,我根据MIT许可条款发布了它,并在CodeProject GitHub 上向所有人开放。

使用React NativeExpo构建移动应用程序非常容易。Expo使使用React Native进行移动开发变得非常简单。

使用相同的语言(TypeScript)进行后端和前端开发非常方便。

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

https://www.codeproject.com/Articles/5346604/BookCars-Car-Rental-Platform-with-Mobile-App

内容概要:本文深入探讨了多种高级格兰杰因果检验方法,包括非线性格兰杰因果检验、分位数格兰杰因果检验、混频格兰杰因果检验以及频域因果检验。每种方法都有其独特之处,适用于不同类型的时间序列数据。非线性格兰杰因果检验分为非参数方法、双变量和多元检验,能够在不假设数据分布的情况下处理复杂的关系。分位数格兰杰因果检验则关注不同分位数下的因果关系,尤其适合经济数据的研究。混频格兰杰因果检验解决了不同频率数据之间的因果关系分析问题,而频域因果检验则专注于不同频率成分下的因果关系。文中还提供了具体的Python和R代码示例,帮助读者理解和应用这些方法。 适合人群:从事时间序列分析、经济学、金融学等领域研究的专业人士,尤其是对非线性因果关系感兴趣的学者和技术人员。 使用场景及目标:①研究复杂非线性时间序列数据中的因果关系;②分析不同分位数下的经济变量因果关系;③处理不同频率数据的因果关系;④识别特定频率成分下的因果关系。通过这些方法,研究人员可以获得更全面、细致的因果关系洞察。 阅读建议:由于涉及较多数学公式和编程代码,建议读者具备一定的统计学和编程基础,特别是对时间序列分析有一定了解。同时,建议结合具体案例进行实践操作,以便更好地掌握这些方法的实际应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值