Movin' In - 租赁房产管理平台,附带移动应用程序






4.99/5 (21投票s)
租赁房产管理平台,附带移动应用程序
目录
- 引言
- 特点
- 实时演示
- 必备组件
- 快速概览
- 安装(自托管)
- 安装(VPS)
- 安装(Docker)
- 设置 Stripe
- 更改货币
- 添加新语言
- 演示数据库
- 构建移动应用
- 从源码运行
- 运行移动应用
- 单元测试和覆盖率
- 日志
- Using the Code
- 关注点
- 历史
引言
Movin' In 是一个面向代理的租赁房产管理平台,包含一个用于管理房产、客户和预订的后端,以及一个用于租赁房产的前端和移动应用程序。
Movin' In 设计用于支持多个代理机构。代理机构可以从后端管理他们的房产和预订。Movin' In 也可以只支持一个代理机构,并用作房产租赁聚合器。
通过后端,管理员可以创建和管理代理机构、房产、地点、用户和预订。
当创建新的代理机构时,它们会收到一封电子邮件,提示它们创建账户以访问后端并管理它们的房产、客户和预订。
客户可以通过前端或移动应用程序注册,根据地点和时间搜索可用房产,选择房产并完成结账流程。
一项重要的设计决策是使用 TypeScript 而非 JavaScript,因为其具有诸多优势。TypeScript 提供强类型、强大的工具和集成,从而实现高质量、可扩展、更易读、更易维护且易于调试和测试的代码。
Movin' In 可以运行在 Docker 容器中。在本文中,您可以找到如何构建 Movin' In Docker 镜像并在 Docker 容器中运行它。
在本文中,您将了解 Movin' In 的创建过程,包括源代码主要部分和软件架构的描述,如何部署它,以及如何运行源代码。但在深入研究之前,我们将从对平台的快速概览开始。
特点
- 代理机构管理
- 支持一个或多个代理机构
- 房产管理
- 预订管理
- 支付管理
- 客户管理
- 多种支付方式(信用卡、PayPal、Google Pay、Apple Pay、Link、Pay Later)
- 运营中的 Stripe 支付网关
- 多语言支持(英语、法语)
- 多种分页选项(经典分页,带上一页和下一页按钮;无限滚动)
- 响应式后端和前端
- 适用于 Android 和 iOS 的原生移动应用,单一代码库
- 推送通知
- 防范 XSS、XST、CSRF 和 MITM 攻击
- 支持平台:iOS、Android、Web、Docker
实时演示
前端
- URL:https://movinin.dynv6.net:3004/
- 登录:jdoe@movinin.io
- 密码:M00vinin
后端
- URL:https://movinin.dynv6.net:3003/
- 登录:admin@movinin.io
- 密码:M00vinin
移动应用
您可以在任何 Android 设备上安装 Android 应用。
扫描此代码
使用设备扫描此代码
如何在 Android 上安装移动应用
-
在运行 Android 8.0 (API 级别 26) 及更高版本的设备上,您必须导航到“安装未知应用”系统设置屏幕,以允许从特定位置(即您下载应用的浏览器)安装应用。
-
在运行 Android 7.1.1 (API 级别 25) 及更低版本的设备上,您应该在设备的“设置”>“安全”中启用“未知来源”系统设置。
备用方法
您还可以通过直接下载 APK 文件并在任何 Android 设备上安装来安装 Android 应用。
- 下载 APK
- 登录:jdoe@movinin.io
- 密码:M00vinin
必备组件
- TypeScript
- Node.js
- Express
- MongoDB
- React
- MUI
- React Native
- Expo
- JWT
- MVC
- Docker
- NGINX
- Git
快速概览
在本节中,您将看到前端、后端和移动应用程序主要页面的快速概览。
前端
通过前端,客户可以搜索可用房产,选择房产并结账。
下方是前端主页,客户可以在其中选择地点和时间,并搜索可用房产。
下方是主页的搜索结果,客户可以在其中选择要租赁的房产。
下方是客户可以查看房产详情的页面
下方是房产图片视图
下方是结账页面,客户可以在其中设置租赁选项并结账。如果客户未注册,他可以同时结账和注册。如果客户尚未注册,他将收到一封确认和激活电子邮件,提示他设置密码。
下方是登录页面。在生产环境中,身份验证 cookie 是 httpOnly、签名、安全且具有严格的 sameSite。这些选项可防止 XSS、CSRF 和 MITM 攻击。身份验证 cookie 也通过自定义中间件来防止 XST 攻击。
下方是注册页面。
下方是客户可以查看和管理其预订的页面。
下方是客户可以查看预订详情的页面。
下方是客户可以查看和管理其通知的页面。
下方是客户可以管理其设置的页面。
下方是客户可以更改其密码的页面。
还有其他页面,但这些是前端的主要页面。
移动应用
通过移动应用程序,客户可以搜索可用房产,选择房产并结账。
如果客户的预订状态从后端更新,客户将收到推送通知。
以下是移动应用程序的主要页面,客户可以在其中选择地点和时间,并搜索可用房产。
以下是主页的搜索结果,客户可以在其中选择要租赁的房产并结账。
以下是登录和注册页面。
以下是客户可以查看和管理其预订的页面。
以下是客户可以更新其个人信息、更改密码和管理其通知的页面。
以上是移动应用程序主要页面的内容。
后端
Movin' In 面向代理机构。这意味着有三种用户类型:
- 管理员:他们可以完全访问后端。他们可以做任何事情。
- 代理机构:他们对后端有有限的访问权限。他们只能管理自己的房产、预订和客户。
- 客户:他们只能访问前端和移动应用程序。他们无法访问后端。
Movin' In 设计用于支持多个代理机构。代理机构可以从后端管理它们的房产、客户和预订。Movin' In 也可以只支持一个代理机构。
通过后端,管理员可以创建和管理代理机构、房产、地点、客户和预订。
当创建新的代理机构时,它们会收到一封电子邮件,提示它们创建一个账户,以便访问后端并管理它们的房产和预订。
下方是后端的登录页面。
下方是后端的仪表板页面,管理员和代理机构可以在其中查看和管理预订。
如果预订状态发生变化,相关客户将收到推送通知和电子邮件。
下方是显示房产并可进行管理的页面。
下方是管理员和代理机构可以通过提供图片和房产信息来创建新房产的页面。要免费包含取消功能,请将其设置为 0。否则,设置选项的价格或留空如果您不想包含它。
下方是管理员和代理机构可以编辑房产的页面。
下方是管理员可以管理客户的页面。
下方是代理机构和管理员可以创建预订(如果他们想)的页面。否则,当从前端或移动应用程序完成结账过程时,预订会自动创建。
下方是编辑预订的页面。
下方是管理代理机构的页面。
下方是创建新代理机构的页面。
下方是编辑代理机构的页面。
下方是查看代理机构房产的页面。
下方是查看客户预订的页面。
下方是管理员和代理机构可以管理其设置的页面。
还有其他页面,但这些是后端的主要页面。
安装(自托管)
Movin' In 是跨平台的,可以在 Windows、Linux 和 macOS 上运行和安装。
下方是 Linux 上的安装说明。
必备组件
-
安装 git、Node.js、NGINX、MongoDB 和 mongosh。如果您想使用 MongoDB Atlas,可以跳过安装和配置 MongoDB。
-
配置 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
说明
- 克隆 movinin 仓库
cd /opt sudo git clone https://github.com/aelassas/movinin.git
- 添加权限
sudo chown -R $USER:$USER /opt/movinin sudo chmod -R +x /opt/movinin/__scripts
- 创建部署快捷方式
sudo ln -s /opt/movinin/__scripts/mi-deploy.sh /usr/local/bin/mi-deploy
- 创建 movinin 服务
sudo cp /opt/movinin/__services/movinin.service /etc/systemd/system sudo systemctl enable movinin.service
- 创建 /opt/movinin/api/.env 文件
NODE_ENV=production MI_PORT=4004 MI_HTTPS=false MI_PRIVATE_KEY=/etc/ssl/movinin.key MI_CERTIFICATE=/etc/ssl/movinin.pem MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin MI_DB_SSL=false MI_DB_SSL_CERT=/etc/ssl/movinin.pem MI_DB_SSL_CA=/etc/ssl/movinin.pem MI_DB_DEBUG=false MI_COOKIE_SECRET=COOKIE_SECRET MI_AUTH_COOKIE_DOMAIN=localhost MI_JWT_SECRET=JWT_SECRET MI_JWT_EXPIRE_AT=86400 MI_TOKEN_EXPIRE_AT=86400 MI_SMTP_HOST=smtp.sendgrid.net MI_SMTP_PORT=587 MI_SMTP_USER=apikey MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=no-reply@movinin.io MI_CDN_USERS=/var/www/cdn/movinin/users MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties MI_CDN_TEMP_PROPERTIES=/var/www/cdn/temp/movinin/properties MI_CDN_LOCATIONS=/var/www/cdn/movinin/locations MI_CDN_TEMP_LOCATIONS=/var/www/cdn/movinin/temp/locations MI_DEFAULT_LANGUAGE=en MI_BACKEND_HOST=https://:3003/ MI_FRONTEND_HOST=https:/// MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN MI_MINIMUM_AGE=21 MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY MI_ADMIN_EMAIL=admin@movinin.io MI_RECAPTCHA_SECRET=RECAPTCHA_SECRET
您需要配置以下选项
MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin MI_COOKIE_SECRET=COOKIE_SECRET MI_AUTH_COOKIE_DOMAIN=localhost MI_JWT_SECRET=JWT_SECRET MI_SMTP_HOST=smtp.sendgrid.net MI_SMTP_PORT=587 MI_SMTP_USER=apikey MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=no-reply@movinin.io MI_BACKEND_HOST=https://:3004/ MI_FRONTEND_HOST=https:///
如果您想使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放在
MI_DB_URI
中,否则请将MI_DB_URI
中的PASSWORD
替换为您的 MongoDB 密码。将JWT_SECRET
替换为密钥令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 sendgrid 或任何其他事务性电子邮件提供商。如果您选择 sendgrid,请在 sendgrid.com 上创建一个账户,登录并转到仪表板。在左侧面板中,单击“Email API”,然后单击“Integration Guide”。然后,选择“SMTP Relay”并按照步骤操作。系统会提示您创建一个 API 密钥。一旦您创建了 API 密钥并验证了 smtp 中继,请将 API 密钥复制到 ./api/.env 中的
MI_SMTP_PASS
。Sendgrid 的免费套餐允许每天发送最多 100 封电子邮件。如果您需要每天发送超过 100 封电子邮件,请升级到付费套餐或选择其他事务性电子邮件提供商。COOKIE_SECRET
和JWT_SECRET
至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。以下设置非常重要,如果设置不正确,身份验证将无法工作
MI_AUTH_COOKIE_DOMAIN=localhost MI_BACKEND_HOST=https://:3001/ MI_FRONTEND_HOST=https:///
将
localhost
替换为 IP 或 FQDN。也就是说,如果您从 http://<FQDN>:3001/ 访问后端。MI_BACKEND_HOST
应为 http://<FQDN>:3001/。MI_FRONTEND_HOST
也是如此。而MI_AUTH_COOKIE_DOMAIN
应为 FQDN。如果您想在移动应用程序中启用推送通知,请遵循这些 说明 并设置以下选项
MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
如果您想启用 Stripe 支付网关,请注册一个 stripe 账户,填写表格并保存 Stripe 开发者仪表板中的发布密钥和密钥。然后,在 api/.env 中的以下选项中设置密钥
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。
在 Stripe 中,所有账户默认共有四个 API 密钥 - 两个用于测试模式,两个用于实时模式
- 测试模式密钥:默认情况下,您可以使用此密钥在测试模式下对服务器上的请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
- 测试模式发布密钥:在 Web 或移动应用程序的客户端代码中,可以使用此密钥进行测试。
- 实时模式密钥:在实时模式下,您可以使用此密钥在服务器上对请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
- 实时模式发布密钥:当您准备好启动应用程序时,可以使用此密钥在 Web 或移动应用程序的客户端代码中。
仅使用您的测试 API 密钥进行测试。这样可以确保您不会意外修改您的实时客户或交易。
如果您想启用 HTTPS,则需要设置以下选项
MI_HTTPS = true MI_PRIVATE_KEY=/etc/ssl/movinin.key MI_CERTIFICATE=/etc/ssl/movinin.pem
- 创建 /opt/movinin/backend/.env 文件
VITE_NODE_ENV=production VITE_MI_API_HOST=https://:4004 VITE_MI_DEFAULT_LANGUAGE=en VITE_MI_PAGE_SIZE=30 VITE_MI_PROPERTIES_PAGE_SIZE=15 VITE_MI_BOOKINGS_PAGE_SIZE=20 VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_TEMP_USERS=https:///cdn/movinin/temp/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_TEMP_PROPERTIES=https:///cdn/movinin/temp/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_CDN_TEMP_LOCATIONS=https:///cdn/movinin/temp/locations VITE_MI_AGENCY_IMAGE_WIDTH=60 VITE_MI_AGENCY_IMAGE_HEIGHT=30 VITE_MI_PROPERTY_IMAGE_WIDTH=300 VITE_MI_PROPERTY_IMAGE_HEIGHT=200 VITE_MI_MINIMUM_AGE=21 VITE_MI_PAGINATION_MODE=classic
您需要配置以下选项
VITE_MI_API_HOST=https://:4004 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_TEMP_USERS=https:///cdn/movinin/temp/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_TEMP_PROPERTIES=https:///cdn/movinin/temp/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_CDN_TEMP_LOCATIONS=https:///cdn/movinin/temp/locations
如果您想本地测试,请将
localhost
保留;否则,请将其替换为 IP、主机名或 FQDN。
VITE_MI_PAGINATION_MODE
:您可以在classic
或infinite_scroll
之间选择。此选项默认为classic
。如果您选择classic
,在桌面端您将获得经典的带上一页和下一页按钮的分页,在移动端获得无限滚动。如果您选择infinite_scroll
,您将在桌面端和移动端都获得无限滚动。 - 创建 /opt/movinin/frontend/.env 文件
VITE_NODE_ENV=production VITE_MI_API_HOST=https://:4004 VITE_MI_RECAPTCHA_ENABLED=false VITE_MI_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY VITE_MI_DEFAULT_LANGUAGE=en VITE_MI_PAGE_SIZE=30 VITE_MI_PROPERTIES_PAGE_SIZE=15 VITE_MI_BOOKINGS_PAGE_SIZE=20 VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_AGENCY_IMAGE_WIDTH=60 VITE_MI_AGENCY_IMAGE_HEIGHT=30 VITE_MI_PROPERTY_IMAGE_WIDTH=300 VITE_MI_PROPERTY_IMAGE_HEIGHT=200 VITE_MI_MINIMUM_AGE=21 VITE_MI_PAGINATION_MODE=classic VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY VITE_MI_STRIPE_CURRENCY_CODE=USD VITE_MI_CURRENCY=$ VITE_MI_SET_LANGUAGE_FROM_IP=false VITE_MI_GOOGLE_ANALYTICS_ENABLED=false VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX VITE_MI_FB_APP_ID=XXXXXXXXXX VITE_MI_APPLE_ID=XXXXXXXXXX VITE_MI_GG_APP_ID=XXXXXXXXXX
您需要配置以下选项
VITE_MI_API_HOST=https://:4004 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY VITE_MI_STRIPE_CURRENCY_CODE=USD VITE_MI_CURRENCY=$ VITE_MI_SET_LANGUAGE_FROM_IP=false VITE_MI_GOOGLE_ANALYTICS_ENABLED=false VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX VITE_MI_FB_APP_ID=XXXXXXXXXX VITE_MI_APPLE_ID=XXXXXXXXXX VITE_MI_GG_APP_ID=XXXXXXXXXX
如果您想本地测试,请将
localhost
保留;否则,请将其替换为 IP、主机名或 FQDN。如果您想启用 Stripe 支付网关,请在
VITE_MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。
VITE_MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currenciesreCAPTCHA 默认禁用。如果您想启用它,需要将
VITE_MI_RECAPTCHA_ENABLED
设置为true
,并将VITE_MI_RECAPTCHA_SITE_KEY
设置为 Google reCAPTCHA 站点密钥。 - 如果您想运行或构建移动应用程序,则需要创建 mobile/.env
MI_API_HOST=https://movinin.io:4004 MI_DEFAULT_LANGUAGE=en MI_PAGE_SIZE=20 MI_PROPERTIES_PAGE_SIZE=8 MI_BOOKINGS_PAGE_SIZE=8 MI_CDN_USERS=https://movinin.io/cdn/movinin/users MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties MI_AGENCY_IMAGE_WIDTH=60 MI_AGENCY_IMAGE_HEIGHT=30 MI_PROPERTY_IMAGE_WIDTH=300 MI_PROPERTY_IMAGE_HEIGHT=200 MI_MINIMUM_AGE=21 MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER MI_STRIPE_COUNTRY_CODE=US MI_STRIPE_CURRENCY_CODE=USD
您需要配置以下选项
MI_API_HOST=https://movinin.io:4004 MI_CDN_USERS=https://movinin.io/cdn/movinin/users MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER MI_STRIPE_COUNTRY_CODE=US MI_STRIPE_CURRENCY_CODE=USD
将
https://movinin.io
替换为 IP、主机名或 FQDN。如果您想启用 Stripe 支付网关,请在
MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。MI_STRIPE_MERCHANT_IDENTIFIER
是您在 Apple 注册以与 Apple Pay 一起使用的商户标识符。MI_STRIPE_COUNTRY_CODE
是您企业的两字母 ISO 3166 国家代码,例如“US”。Stripe 支付必需。MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currencies - 配置 NGINX
sudo nano /etc/nginx/sites-available/default
修改前端的配置如下
server { root /var/www/movinin/frontend; #listen 443 http2 ssl default_server; listen 80 default_server; server_name _; #ssl_certificate_key /etc/ssl/movinin.key; #ssl_certificate /etc/ssl/movinin.pem; access_log /var/log/nginx/movinin.frontend.access.log; error_log /var/log/nginx/movinin.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/movinin.com.key #ssl_certificate /etc/ssl/movinin.com.pem;
为后端添加以下配置
server { root /var/www/movinin/backend; #listen 3003 http2 ssl default_server; listen 3003 default_server; server_name _; #ssl_certificate_key /etc/ssl/movinin.key; #ssl_certificate /etc/ssl/movinin.pem; #error_page 497 301 =307 https://$host:$server_port$request_uri; access_log /var/log/nginx/movinin.backend.access.log; error_log /var/log/nginx/movinin.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/movinin 文件夹,并为运行 movinin 服务的用户授予对 /var/www/cdn/movinin 的完全访问权限。
如果您想启用 SSL,请取消注释并设置这些行#listen 3003 http2 ssl default_server #ssl_certificate_key /etc/ssl/movinin.com.key #ssl_certificate /etc/ssl/movinin.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
- 启用防火墙并打开 movinin 端口
sudo ufw enable sudo ufw allow 4004/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 3003/tcp sudo ufw allow 27017/tcp
- 启动 movinin 服务
cd /opt/movinin/api npm install --omit=dev sudo systemctl start movinin.service
确保 movinin 服务正在运行,使用以下命令
sudo systemctl status movinin.service
通过检查日志来确保数据库连接已建立,使用以下命令
tail -f /var/log/movinin.log
或者这个命令
sudo journalctl -xfu movinin.service
或者通过打开此文件
tail -f /opt/movinin/api/logs/all.log
错误日志写入在
tail -f /opt/movinin/api/logs/error.log
- 使用以下命令部署 movinin
mi-deploy all
Movin' In 后端可通过端口 3003 访问,前端可通过端口 80 访问。
如果您想在内存量较少的 VPS 上安装 Movin' In,在运行
mi-deploy all
时可能会遇到内存问题。在这种情况下,您需要按以下步骤进行- 运行
mi-deploy api
来安装和运行 API。 - 在您的桌面 PC 上,按照之前的说明设置 frontend/.env,然后从 frontend 文件夹运行以下命令
npm install npm run build
- 将 frontend/build 的内容从您的桌面 PC 复制到 VPS 上的 /var/www/movinin/frontend。
- 在您的桌面 PC 上,按照之前的说明设置 backend/.env,然后从 backend 文件夹运行以下命令
npm install npm run build
- 将 backend/build 的内容从您的桌面 PC 复制到 VPS 上的 /var/www/movinin/backend。
- 重启 NGINX
sudo rm -rf /var/cache/nginx sudo systemctl restart nginx sudo systemctl status nginx
- 运行
-
如果您不想使用演示数据库,请导航至 hostname:3003/sign-up 创建管理员。
-
打开 backend/src/App.tsx 并注释掉这些行以保护后端
const SignUp = lazy(() => import('./pages/SignUp')) <Route exact path='/sign-up' element={<SignUp />} />
然后再次运行后端部署
mi-deploy backend
如果您只想部署前端,请运行以下命令
mi-deploy frontend
如果您只想部署 API,请运行以下命令
mi-deploy api
如果您想部署 API、后端和前端,请运行以下命令
mi-deploy all
要更改货币,请遵循这些 说明。
安装(VPS)
此 指南 展示了如何在运行 Ubuntu 22.04 的 VPS 上安装 BookCars,该 VPS 至少拥有 512MB RAM 和一个 CPU。
安装(Docker)
Movin' In 可以在 Linux 上的 Docker 容器中运行,也可以在 Windows 或 Mac 的 Docker Desktop 中运行。
Docker 镜像
本节介绍了如何构建 Movin' In Docker 镜像并在 Docker 容器中运行它。
- 克隆 Movin' In 仓库
git clone https://github.com/aelassas/movinin.git
- 创建 ./api/.env.docker 文件,内容如下
NODE_ENV=production MI_PORT=4004 MI_HTTPS=false MI_PRIVATE_KEY=/etc/ssl/movinin.key MI_CERTIFICATE=/etc/ssl/movinin.crt MI_DB_URI=mongodb://admin:PASSWORD@mongo:27017/movinin?authSource=admin&appName=movinin MI_DB_SSL=false MI_DB_SSL_CERT=/etc/ssl/movinin.crt MI_DB_SSL_CA=/etc/ssl/movinin.ca.pem MI_DB_DEBUG=false MI_COOKIE_SECRET=COOKIE_SECRET MI_AUTH_COOKIE_DOMAIN=localhost MI_JWT_SECRET=JWT_SECRET MI_JWT_EXPIRE_AT=86400 MI_TOKEN_EXPIRE_AT=86400 MI_SMTP_HOST=smtp.sendgrid.net MI_SMTP_PORT=587 MI_SMTP_USER=apikey MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=admin@movinin.io MI_CDN_USERS=/var/www/cdn/movinin/users MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties MI_CDN_TEMP_PROPERTIES=/var/www/cdn/movinin/temp/properties MI_CDN_LOCATIONS=/var/www/cdn/movinin/locations MI_CDN_TEMP_LOCATIONS=/var/www/cdn/movinin/temp/locations MI_DEFAULT_LANGUAGE=en MI_BACKEND_HOST=https://:3003/ MI_FRONTEND_HOST=https:/// MI_MINIMUM_AGE=21 MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY MI_ADMIN_EMAIL=admin@movinin.io MI_RECAPTCHA_SECRET=RECAPTCHA_SECRET
设置以下选项
MI_DB_URI=mongodb://admin:PASSWORD@mongo:27017/movinin?authSource=admin&appName=movinin MI_COOKIE_SECRET=COOKIE_SECRET MI_AUTH_COOKIE_DOMAIN=localhost MI_JWT_SECRET=JWT_SECRET MI_SMTP_HOST=smtp.sendgrid.net MI_SMTP_PORT=587 MI_SMTP_USER=apikey MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=admin@movinin.io MI_BACKEND_HOST=https://:3003/ MI_FRONTEND_HOST=https:///
如果您想使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放在
MI_DB_URI
中,否则请将MI_DB_URI
中的PASSWORD
替换为您的 MongoDB 密码。将JWT_SECRET
替换为密钥令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 sendgrid 或任何其他事务性电子邮件提供商。如果您选择 sendgrid,请在 sendgrid.com 上创建一个账户,登录并转到仪表板。在左侧面板中,单击“Email API”,然后单击“Integration Guide”。然后,选择“SMTP Relay”并按照步骤操作。系统会提示您创建一个 API 密钥。一旦您创建了 API 密钥并验证了 smtp 中继,请将 API 密钥复制到 ./api/.env 中的
MI_SMTP_PASS
。Sendgrid 的免费套餐允许每天发送最多 100 封电子邮件。如果您需要每天发送超过 100 封电子邮件,请升级到付费套餐或选择其他事务性电子邮件提供商。COOKIE_SECRET
和JWT_SECRET
至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。MI_AUTH_COOKIE_DOMAIN=localhost MI_BACKEND_HOST=https://:3001/ MI_FRONTEND_HOST=https:///
将
localhost
替换为 IP 或 FQDN。也就是说,如果您从 http://<FQDN>:3001/ 访问后端。MI_BACKEND_HOST
应为 http://<FQDN>:3001/。MI_FRONTEND_HOST
也是如此。而MI_AUTH_COOKIE_DOMAIN
应为 FQDN。如果您想本地测试,请将
localhost
保留。如果您想在移动应用程序中启用推送通知,请遵循这些 说明 并设置以下选项
MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
如果您想启用 Stripe 支付网关,请注册一个 stripe 账户,填写表格并保存 Stripe 开发者仪表板中的发布密钥和密钥。然后,在 api/.env 中的以下选项中设置密钥
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。
在 Stripe 中,所有账户默认共有四个 API 密钥 - 两个用于测试模式,两个用于实时模式
- 测试模式密钥:默认情况下,您可以使用此密钥在测试模式下对服务器上的请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
- 测试模式发布密钥:在 Web 或移动应用程序的客户端代码中,可以使用此密钥进行测试。
- 实时模式密钥:在实时模式下,您可以使用此密钥在服务器上对请求进行身份验证。默认情况下,您可以使用此密钥不受限制地执行任何 API 请求。
- 实时模式发布密钥:当您准备好启动应用程序时,可以使用此密钥在 Web 或移动应用程序的客户端代码中。
仅使用您的测试 API 密钥进行测试。这样可以确保您不会意外修改您的实时客户或交易。
- 创建 ./backend/.env.docker 文件,内容如下
VITE_NODE_ENV=production VITE_MI_API_HOST=https://:4004 VITE_MI_DEFAULT_LANGUAGE=en VITE_MI_PAGE_SIZE=30 VITE_MI_PROPERTIES_PAGE_SIZE=15 VITE_MI_BOOKINGS_PAGE_SIZE=20 VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_TEMP_USERS=https:///cdn/movinin/temp/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_TEMP_PROPERTIES=https:///cdn/movinin/temp/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_CDN_TEMP_LOCATIONS=https:///cdn/movinin/temp/locations VITE_MI_AGENCY_IMAGE_WIDTH=60 VITE_MI_AGENCY_IMAGE_HEIGHT=30 VITE_MI_PROPERTY_IMAGE_WIDTH=300 VITE_MI_PROPERTY_IMAGE_HEIGHT=200 VITE_MI_MINIMUM_AGE=21 VITE_MI_PAGINATION_MODE=classic
设置以下选项
VITE_MI_API_HOST=https://:4004 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_TEMP_USERS=https:///cdn/movinin/temp/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_TEMP_PROPERTIES=https:///cdn/movinin/temp/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_CDN_TEMP_LOCATIONS=https:///cdn/movinin/temp/locations
如果您想本地测试,请将
localhost
保留,或将其替换为 IP、主机名或 FQDN。如果您想更改分页模式,请更改
VITE_MI_PAGINATION_MODE
选项。您可以在classic
或infinite_scroll
之间选择。此选项默认为classic
。如果您选择classic
,在桌面端您将获得经典的带上一页和下一页按钮的分页,在移动端获得无限滚动。如果您选择infinite_scroll
,您将在桌面端和移动端都获得无限滚动。 - 创建 ./frontend/.env.docker 文件,内容如下
VITE_NODE_ENV=production VITE_MI_API_HOST=https://:4004 VITE_MI_RECAPTCHA_ENABLED=false VITE_MI_DEFAULT_LANGUAGE=en VITE_MI_PAGE_SIZE=30 VITE_MI_PROPERTIES_PAGE_SIZE=15 VITE_MI_BOOKINGS_PAGE_SIZE=20 VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_AGENCY_IMAGE_WIDTH=60 VITE_MI_AGENCY_IMAGE_HEIGHT=30 VITE_MI_PROPERTY_IMAGE_WIDTH=300 VITE_MI_PROPERTY_IMAGE_HEIGHT=200 VITE_MI_MINIMUM_AGE=21 VITE_MI_PAGINATION_MODE=classic VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY VITE_MI_STRIPE_CURRENCY_CODE=USD VITE_MI_CURRENCY=$ VITE_MI_SET_LANGUAGE_FROM_IP=false VITE_MI_GOOGLE_ANALYTICS_ENABLED=false VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX VITE_MI_FB_APP_ID=XXXXXXXXXX VITE_MI_APPLE_ID=XXXXXXXXXX VITE_MI_GG_APP_ID=XXXXXXXXXX
设置以下选项
VITE_MI_API_HOST=https://:4004 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_STRIPE_CURRENCY_CODE=USD VITE_MI_CURRENCY=$ VITE_MI_SET_LANGUAGE_FROM_IP=false VITE_MI_GOOGLE_ANALYTICS_ENABLED=false VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX VITE_MI_FB_APP_ID=XXXXXXXXXX VITE_MI_APPLE_ID=XXXXXXXXXX VITE_MI_GG_APP_ID=XXXXXXXXXX
如果您想本地测试,请将
localhost
保留,或将其替换为 IP、主机名或 FQDN。如果您想启用 Stripe 支付网关,请在
VITE_MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。
VITE_MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currencies如果您想更改分页模式,请更改
VITE_MI_PAGINATION_MODE
选项。reCAPTCHA 默认在前端禁用。如果您想启用它,必须将
VITE_MI_RECAPTCHA_ENABLED
设置为true
,并将VITE_MI_RECAPTCHA_SITE_KEY
设置为 Google reCAPTCHA 站点密钥。 - 打开 ./docker-compose.yml 并设置 MongoDB 密码
version: "3.8" services: api: build: context: . dockerfile: ./api/Dockerfile env_file: ./api/.env.docker restart: always ports: - 4004:4004 depends_on: - mongo volumes: - cdn:/var/www/cdn/movinin 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: - 3003:3003 frontend: build: context: . dockerfile: ./frontend/Dockerfile depends_on: - api ports: - 80:80 volumes: - cdn:/var/www/cdn/movinin volumes: cdn:
如果您想使用 MongoDB Atlas,请删除
mongo
容器。否则,请将PASSWORD
替换为您在 ./api/.env.docker 中的MI_DB_URI
中设置的密码。 - 构建并运行 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 --force-recreate --no-deps api backend frontend
就是这样!Movin' In 后端可以从 http://<hostname>:3003 访问,Movin' In 前端可以从 http://<hostname> 访问。
如果您是第一次运行 Movin' In,您将从一个空的数据库开始。因此,您必须通过在 http://<hostname>:3003/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 配置文件
- API:Dockerfile
- 后端:Dockerfile
- 前端:Dockerfile
- Movin' In:docker-compose.yml
就是这样!您可以在后端和前端探索其他页面。
SSL
本节将引导您完成在 Docker 容器中的 API、后端和前端中启用 SSL 的方法。
将您的私钥 movinin.key 和证书 movinin.crt 复制到 ./ 旁边 docker-
compose.yml。
movinin.key 将被加载为 /etc/ssl/movinin.key,movinin.crt 将被加载为 /etc/ssl/movinin.crt 在 ./docker-compose.yml 中。
API
对于 API,更新 ./api/.env.docker 如下以启用 SSL
MI_HTTPS=true
MI_PRIVATE_KEY=/etc/ssl/movinin.key
MI_CERTIFICATE=/etc/ssl/movinin.crt
MI_BACKEND_HOST=https://:3003/
MI_FRONTEND_HOST=https:///
将 https:// 替换为 https://<fqdn>。
后端
对于后端,更新 ./backend/.env.docker 中的以下选项
VITE_MI_API_HOST=https://:4004
VITE_MI_CDN_USERS=https:///cdn/movinin/users
VITE_MI_CDN_TEMP_USERS=https:///cdn/movinin/temp/users
VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties
VITE_MI_CDN_TEMP_PROPERTIES=https:///cdn/movinin/temp/properties
将 https:// 替换为 https://<fqdn>。
然后,更新 ./backend/nginx.conf 如下以启用 SSL
server {
listen 3003 ssl;
root /usr/share/nginx/html;
index index.html;
ssl_certificate_key /etc/ssl/movinin.key;
ssl_certificate /etc/ssl/movinin.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_MI_API_HOST=https://:4004
VITE_MI_CDN_USERS=https:///cdn/movinin/users
VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties
将 https:// 替换为 https://<fqdn>。
将端口 443 添加到 ./frontend/Dokerfile 如下
# syntax=docker/dockerfile:1
FROM node:lts-alpine as build
WORKDIR /movinin/frontend
COPY ./frontend ./
COPY ./frontend/.env.docker .env
COPY ./packages /movinin/packages
RUN npm install
RUN npm run build
FROM nginx:stable-alpine
WORKDIR /usr/share/nginx/html
RUN rm -rf -- *
COPY --from=build /movinin/frontend/build .
COPY ./frontend/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/movinin.key;
ssl_certificate /etc/ssl/movinin.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 以加载您的私钥 movinin.key 和证书 movinin.crt,并将端口 443 添加到前端,如下所示
version: "3.8"
services:
api:
build:
context: .
dockerfile: ./api/Dockerfile
env_file: ./api/.env.docker
restart: always
ports:
- 4004:4004
depends_on:
- mongo
volumes:
- cdn:/var/www/cdn/movinin
- ./movinin.key:/etc/ssl/movinin.key
- ./movinin.crt:/etc/ssl/movinin.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:
- 3003:3003
volumes:
- ./movinin.key:/etc/ssl/movinin.key
- ./movinin.crt:/etc/ssl/movinin.crt
frontend:
build:
context: .
dockerfile: ./frontend/Dockerfile
depends_on:
- api
ports:
- 80:80
- 443:443
volumes:
- cdn:/var/www/cdn/movinin
- ./movinin.key:/etc/ssl/movinin.key
- ./movinin.crt:/etc/ssl/movinin.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 密钥
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
前端
在 frontend/.env 中的以下选项中设置 Stripe 发布密钥和货币
VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
VITE_MI_STRIPE_CURRENCY_CODE=USD
移动应用
在 mobile/.env 中设置 Stripe 发布密钥和其他 Stripe 设置
MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
MI_STRIPE_COUNTRY_CODE=US
MI_STRIPE_CURRENCY_CODE=USD
MI_STRIPE_MERCHANT_IDENTIFIER
是您在 Apple 注册以与 Apple Pay 一起使用的商户标识符。
MI_STRIPE_COUNTRY_CODE
是您企业的两字母 ISO 3166 国家代码,例如“US”。Stripe 支付必需。
MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currencies
如果您想启用 Apple Pay,还需要在 mobile/app.json 的 plugins
部分设置 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_MI_CURRENCY
和 VITE_MI_STRIPE_CURRENCY_CODE
设置。
默认情况下,它设置为
VITE_MI_CURRENCY=$ VITE_MI_STRIPE_CURRENCY_CODE=USD
例如,如果您想更改为欧元
VITE_MI_CURRENCY=€ VITE_MI_STRIPE_CURRENCY_CODE=EUR
在生产环境中,您需要重新构建前端以应用更改。
后端
打开 backend/.env 并更改 VITE_MI_CURRENCY
设置。
默认情况下,它设置为
VITE_MI_CURRENCY=$
在生产环境中,您需要重新构建后端以应用更改。
移动应用
打开 mobile/.env 并更改 MI_CURRENCY
和 MI_STRIPE_CURRENCY_CODE
设置。
默认情况下,它设置为
MI_CURRENCY=$ MI_STRIPE_CURRENCY_CODE=USD
例如,如果您想更改为欧元
MI_CURRENCY=€ MI_STRIPE_CURRENCY_CODE=EUR
在生产环境中,您需要重新构建移动应用程序以应用更改。
添加新语言
要添加新语言,请按以下步骤操作
API
- 将新的语言 ISO 639-1 代码 添加到 api/src/config/env.config.ts 中的
LANGUAGES
设置。 - 在 src/lang 文件夹中创建一个新文件 <ISO 639-1 code>.ts 并在此文件中添加翻译。
- 将您的翻译添加到 src/lang/i18n.ts
后端和前端
- 将新的语言 ISO 639-1 代码 及其标签添加到 src/config/env.config.ts 的
LANGUAGES
常量中。 - 将翻译添加到 src/lang/*.ts。
移动应用
- 将新的语言 ISO 639-1 代码 及其标签添加到 config/env.config.ts 的
LANGUAGES
常量中。 - 在 lang 文件夹中创建一个新文件 <ISO 639-1 code>.ts 并在此文件中添加翻译。
- 将您的翻译添加到 lang/i18n.ts
演示数据库
Windows, Linux 和 macOS
- 下载并安装 MongoDB 命令行数据库工具。
- 在 Windows 上,将 MongoDB 命令行数据库工具文件夹添加到
Path
环境变量。 - 下载 movinin-db.zip 到您的本地机器,解压缩并从终端进入解压缩的文件夹。
- 使用以下命令恢复 Movin' In 演示数据库
mongorestore --verbose --drop --gzip --host=127.0.0.1 --port=27017 --username=admin --password=$PASSWORD --authenticationDatabase=admin --nsInclude="movinin.*" --archive=movinin.gz
将 $PASSWORD 替换为您的 MongoDB 密码。
如果您正在使用 MongoDB Atlas,请在 --uri= 命令行参数中放入您的 MongoDB Atlas URI
mongorestore --verbose --drop --gzip --uri=mongodb://admin:$PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin --nsInclude="movinin.*" --nsFrom="movinin.*" --nsTo="movinin.*" --archive=movinin.gz
将 cdn 文件夹的内容复制到您的 Web 服务器,以便可以通过 https:///cdn/movinin/ 访问这些文件。
cdn 文件夹包含以下文件夹
- users:此文件夹包含用户的头像和代理机构的图像。
- properties:此文件夹包含房产的图像。
- temp:此文件夹包含临时文件。
如果您想从源代码运行 Movin' In 或在 Windows 或 Linux 上安装它而不使用 Docker,请按以下步骤操作
- 在 Windows 上,安装 IIS 并将 cdn 文件夹的内容复制到 C:\inetpub\wwwroot\cdn\movinin。最后,为运行 Movin' In API 的用户授予对 C:\inetpub\wwwroot\cdn\movinin 的完全访问权限。
- 在 Linux 上,安装 NGINX 并将 cdn 文件夹的内容复制到 /var/www/cdn/movinin。然后,按如下方式更新 /etc/nginx/sites-enabled/default
server { listen 80 default_server; server_name _; ... location /cdn { alias /var/www/cdn; } }
最后,为运行 Movin' In API 的用户授予对 /var/www/cdn/movinin 的完全访问权限。
后端凭据
- 用户名:admin@movinin.io
- 密码:M00vinin
前端和移动应用程序凭据
- 用户名:jdoe@movinin.io
- 密码:M00vinin
Docker
要恢复 Docker 容器中的 Movin' In 演示数据库,请按以下步骤操作
- 确保端口 80、3003、4004 和 27017 未被任何应用程序使用。
- 在您的本地机器上下载并安装 MongoDB 命令行数据库工具。
- 在您的本地机器上的
Path
环境变量中添加 MongoDB 命令行数据库工具文件夹。 - 下载 movinin-db.zip 到您的本地机器并解压缩。
- 运行 compose
docker compose up
- 转到 movinin-db 文件夹并使用以下命令恢复演示数据库
mongorestore --verbose --drop --gzip --host=127.0.0.1 --port=27017 --username=admin --password=$PASSWORD --authenticationDatabase=admin --nsInclude="movinin.*" --archive=movinin.gz
将$PASSWORD
替换为您在 docker-compose.yml 中设置的 MongoDB 密码。 - 使用以下命令获取 API Docker 容器名称
docker container ls
名称应类似于:src-api-1
- 转到 movinin-db/cdn 文件夹,并使用以下命令将该文件夹的内容复制到 API 容器中
docker cp ./cdn/users src-api-1:/var/www/cdn/movinin docker cp ./cdn/properties src-api-1:/var/www/cdn/movinin
将src-api-1
替换为您的 API 容器名称。 - 转到后端 https://:3003 并使用以下凭据登录
用户名:admin@movinin.io
密码:M00vinin - 转到前端 https:// 并使用以下凭据登录
用户名:jdoe@movinin.io
密码:M00vinin
构建移动应用
必备组件
要构建 Movin' In 移动应用程序,您需要在您的机器上安装以下工具
使用以下命令安装 eas-cli
npm i -g eas-cli
配置
-
您需要下载 google-services.json 文件并将其放在 ./mobile 的根目录中以启用推送通知。否则,移动应用程序将无法构建。请勿忘记在 expo.dev > Credentials > Service Credentials > Google Cloud Messaging Token 中设置 Firebase 服务器密钥,如文档中所述。
-
如果您没有 Expo 账户,您需要创建一个账户才能构建 Movin' In 移动应用程序。
-
转到 expo.dev,点击 Projects,然后点击 Create a Project。将 Movin' In 设置为项目名称,然后单击 Create。
-
转到 Movin' In 项目并复制 project ID。打开 ./mobile/app.json 并将 project ID 粘贴到
extra.eas.projectId
中。 -
从 expo.dev (Account Settings > Access Tokens) 创建一个 Expo Access Token,并在 api/.env 中设置
MI_EXPO_ACCESS_TOKEN
设置。MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
- 创建 mobile/.env 文件,内容如下
MI_API_HOST=https://movinin.io:4004 MI_DEFAULT_LANGUAGE=en MI_PAGE_SIZE=20 MI_PROPERTIES_PAGE_SIZE=8 MI_BOOKINGS_PAGE_SIZE=8 MI_CDN_USERS=https://movinin.io/cdn/movinin/users MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties MI_AGENCY_IMAGE_WIDTH=60 MI_AGENCY_IMAGE_HEIGHT=30 MI_PROPERTY_IMAGE_WIDTH=300 MI_PROPERTY_IMAGE_HEIGHT=200 MI_MINIMUM_AGE=21 MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER MI_STRIPE_COUNTRY_CODE=US MI_STRIPE_CURRENCY_CODE=USD
您需要配置以下选项
MI_API_HOST=https://movinin.io:4004 MI_CDN_USERS=https://movinin.io/cdn/movinin/users MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER MI_STRIPE_COUNTRY_CODE=US MI_STRIPE_CURRENCY_CODE=USD
将
https://movinin.io
替换为 IP、主机名或 FQDN。如果您想启用 Stripe 支付网关,请在
MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。MI_STRIPE_MERCHANT_IDENTIFIER
是您在 Apple 注册以与 Apple Pay 一起使用的商户标识符。MI_STRIPE_COUNTRY_CODE
是您企业的两字母 ISO 3166 国家代码,例如“US”。Stripe 支付必需。MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currencies
生产构建
如果您想在生产环境中使用 Movin' In 移动应用程序,您应该在 Movin' In API 中使用 HTTPS,并在 ./mobile/app.json 中禁用 usesCleartextTraffic
expo 插件,通过删除 plugins
部分中的行 "./plugins/usesCleartextTraffic" 来实现。
说明
- 将源代码克隆到您的计算机
git clone https://github.com/aelassas/movinin.git
- 转到 mobile 文件夹
cd ./mobile
- 运行以下命令
npm install
Android
EAS Build
要使用 EAS Build 托管服务构建 Movin' In Android 应用程序,请运行以下命令
npm run build:android
本地构建
本地构建需要 macOS 或 Linux。对于 Windows,您应该使用 EAS Builds。
您需要安装 Android Studio、openjdk-17,并设置 ANDROID_HOME
和 JAVA_HOME
环境变量。然后,运行以下命令
npm run build:android:local
在 macOS 上,如果您在本地构建时遇到问题,请尝试在 ./mobile/eas.json 中设置 ANDROID_HOME
和 JAVA_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
您需要付费的 Apple 开发者账户才能构建 EAS 构建和本地构建的 iOS 应用程序。
EAS Build
要构建 Movin' In iOS 应用程序,请运行以下命令
npm run build:ios
本地构建
您需要为 macOS 安装 fastlane 和 CocoaPods。
要本地构建 Movin' In iOS 应用程序,请运行以下命令
npm run build:ios:local
从源码运行
以下是从源代码运行 Movin' In 的说明。
必备组件
-
安装 git、Node.js、Windows 上的 NGINX 或 IIS、MongoDB 和 mongosh。如果您想使用 MongoDB Atlas,可以跳过安装和配置 MongoDB。
-
配置 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 服务。
说明
- 克隆 Movin' In 仓库
git clone https://github.com/aelassas/movinin.git
- 创建 api/.env 文件,内容如下
NODE_ENV=development MI_PORT=4004 MI_HTTPS=false MI_PRIVATE_KEY=/etc/ssl/movinin.key MI_CERTIFICATE=/etc/ssl/movinin.crt MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin MI_DB_SSL=false MI_DB_SSL_CERT=/etc/ssl/movinin.crt MI_DB_SSL_CA=/etc/ssl/movinin.ca.pem MI_DB_DEBUG=false MI_COOKIE_SECRET=COOKIE_SECRET MI_AUTH_COOKIE_DOMAIN=localhost MI_JWT_SECRET=JWT_SECRET MI_JWT_EXPIRE_AT=86400 MI_TOKEN_EXPIRE_AT=86400 MI_SMTP_HOST=smtp.sendgrid.net MI_SMTP_PORT=587 MI_SMTP_USER=apikey MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=admin@movinin.io MI_CDN_USERS=/var/www/cdn/movinin/users MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties MI_CDN_TEMP_PROPERTIES=/var/www/cdn/movinin/temp/properties MI_CDN_LOCATIONS=/var/www/cdn/movinin/locations MI_CDN_TEMP_LOCATIONS=/var/www/cdn/movinin/temp/locations MI_DEFAULT_LANGUAGE=en MI_BACKEND_HOST=https://:3003/ MI_FRONTEND_HOST=https://:3004/ MI_MINIMUM_AGE=21 MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY MI_ADMIN_EMAIL=admin@movinin.io MI_RECAPTCHA_SECRET=RECAPTCHA_SECRET
在 Windows 上,安装 IIS 并使用以下值更新以下设置
MI_CDN_USERS=C:\inetpub\wwwroot\cdn\movinin\users MI_CDN_TEMP_USERS=C:\inetpub\wwwroot\cdn\movinin\temp\users MI_CDN_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\properties MI_CDN_TEMP_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\temp\properties
为运行 Movin' In API 的用户授予对 C:\inetpub\wwwroot\cdn\movinin 的完全访问权限。
您需要配置以下选项
MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin MI_COOKIE_SECRET=COOKIE_SECRET MI_JWT_SECRET=JWT_SECRET MI_SMTP_HOST=smtp.sendgrid.net MI_SMTP_PORT=587 MI_SMTP_USER=apikey MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=admin@movinin.io
如果您想使用 MongoDB Atlas,请将您的 MongoDB Atlas URI 放在
MI_DB_URI
中,否则请将MI_DB_URI
中的PASSWORD
替换为您的 MongoDB 密码。将JWT_SECRET
替换为密钥令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 sendgrid 或任何其他事务性电子邮件提供商。如果您选择 sendgrid,请在 sendgrid.com 上创建一个账户,登录并转到仪表板。在左侧面板中,单击“Email API”,然后单击“Integration Guide”。然后,选择“SMTP Relay”并按照步骤操作。系统会提示您创建一个 API 密钥。一旦您创建了 API 密钥并验证了 smtp 中继,请将 API 密钥复制到 ./api/.env 中的
MI_SMTP_PASS
。Sendgrid 的免费套餐允许每天发送最多 100 封电子邮件。如果您需要每天发送超过 100 封电子邮件,请升级到付费套餐或选择其他事务性电子邮件提供商。COOKIE_SECRET
和JWT_SECRET
至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。如果您想在移动应用程序中启用推送通知,请遵循这些 说明 并设置以下选项
MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
如果您想启用 Stripe 支付网关,请注册一个 stripe 账户,填写表格并保存 Stripe 开发者仪表板中的发布密钥和密钥。然后,在 api/.env 中的以下选项中设置密钥
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
不要在网站上公开 Stripe 密钥或将其嵌入移动应用程序中。它必须保密并安全地存储在服务器端。使用 Stripe 的测试模式。
仅使用您的测试 API 密钥进行测试。这样可以确保您不会意外修改您的实时客户或交易。
要运行 API,请使用以下命令
cd ./api npm install npm run dev
- 添加 backend/.env 文件
VITE_PORT=3003 VITE_NODE_ENV=development VITE_MI_API_HOST=https://:4004 VITE_MI_DEFAULT_LANGUAGE=en VITE_MI_PAGE_SIZE=30 VITE_MI_PROPERTIES_PAGE_SIZE=15 VITE_MI_BOOKINGS_PAGE_SIZE=20 VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_TEMP_USERS=https:///cdn/movinin/temp/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_TEMP_PROPERTIES=https:///cdn/movinin/temp/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_CDN_TEMP_LOCATIONS=https:///cdn/movinin/temp/locations VITE_MI_AGENCY_IMAGE_WIDTH=60 VITE_MI_AGENCY_IMAGE_HEIGHT=30 VITE_MI_PROPERTY_IMAGE_WIDTH=300 VITE_MI_PROPERTY_IMAGE_HEIGHT=200 VITE_MI_MINIMUM_AGE=21 VITE_MI_PAGINATION_MODE=classic
要运行后端,请使用以下命令
cd ./backend npm install npm start
- 添加 frontend/.env 文件
VITE_PORT=3004 VITE_NODE_ENV=development VITE_MI_API_HOST=https://:4004 VITE_MI_RECAPTCHA_ENABLED=false VITE_MI_RECAPTCHA_SITE_KEY=GOOGLE_RECAPTCHA_SITE_KEY VITE_MI_DEFAULT_LANGUAGE=en VITE_MI_PAGE_SIZE=30 VITE_MI_PROPERTIES_PAGE_SIZE=15 VITE_MI_BOOKINGS_PAGE_SIZE=20 VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10 VITE_MI_CDN_USERS=https:///cdn/movinin/users VITE_MI_CDN_PROPERTIES=https:///cdn/movinin/properties VITE_MI_CDN_LOCATIONS=https:///cdn/movinin/locations VITE_MI_AGENCY_IMAGE_WIDTH=60 VITE_MI_AGENCY_IMAGE_HEIGHT=30 VITE_MI_PROPERTY_IMAGE_WIDTH=300 VITE_MI_PROPERTY_IMAGE_HEIGHT=200 VITE_MI_MINIMUM_AGE=21 VITE_MI_PAGINATION_MODE=classic VITE_MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY VITE_MI_STRIPE_CURRENCY_CODE=USD VITE_MI_CURRENCY=$ VITE_MI_SET_LANGUAGE_FROM_IP=false VITE_MI_GOOGLE_ANALYTICS_ENABLED=false VITE_MI_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXXX VITE_MI_FB_APP_ID=XXXXXXXXXX VITE_MI_APPLE_ID=XXXXXXXXXX VITE_MI_GG_APP_ID=XXXXXXXXXX
如果您想启用 Stripe 支付网关,请在
VITE_MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。
VITE_MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currenciesreCAPTCHA 默认禁用。如果您想启用它,必须将
VITE_MI_RECAPTCHA_ENABLED
设置为true
,并将VITE_MI_RECAPTCHA_SITE_KEY
设置为 Google reCAPTCHA 站点密钥。要运行前端,请使用以下命令
cd ./frontend npm install npm run dev
- 如果您想运行移动应用程序,则需要添加 mobile/.env
MI_API_HOST=https://movinin.io:4004 MI_DEFAULT_LANGUAGE=en MI_PAGE_SIZE=20 MI_PROPERTIES_PAGE_SIZE=8 MI_BOOKINGS_PAGE_SIZE=8 MI_CDN_USERS=https://movinin.io/cdn/movinin/users MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties MI_AGENCY_IMAGE_WIDTH=60 MI_AGENCY_IMAGE_HEIGHT=30 MI_PROPERTY_IMAGE_WIDTH=300 MI_PROPERTY_IMAGE_HEIGHT=200 MI_MINIMUM_AGE=21 MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER MI_STRIPE_COUNTRY_CODE=US MI_STRIPE_CURRENCY_CODE=USD
您需要配置以下选项
MI_API_HOST=https://movinin.io:4004 MI_CDN_USERS=https://movinin.io/cdn/movinin/users MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER MI_STRIPE_COUNTRY_CODE=US MI_STRIPE_CURRENCY_CODE=USD
您需要将 https://movinin.io 替换为 IP 或主机名。
如果您想启用 Stripe 支付网关,请在
MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。使用 Stripe 的测试模式。 - 配置 https:///cdn
- 在 Windows 上,安装 IIS,创建 C:\inetpub\wwwroot\cdn\movinin 文件夹,并为运行 Movin' In API 的用户授予对 movininC:\inetpub\wwwroot\cdn\movinin 文件夹的完全访问权限。
- 在 Linux 上,安装 NGINX,创建 /var/www/cdn/movinin 文件夹,为运行 Movin' In API 的用户授予对 /var/www/cdn/movinin 文件夹的完全访问权限,并通过修改 /etc/nginx/sites-available/default 将 cdn 文件夹添加到 NGINX,如下所示
server { listen 80 default_server; server_name _; ... location /cdn { alias /var/www/cdn; } }
- 从 https://:3003/sign-up 创建管理员用户
- 要运行移动应用程序,只需在您的设备上下载 Expo 应用程序,然后从 ./mobile 文件夹运行以下命令
npm install npm run dev
您需要下载 google-services.json 文件并将其放在 ./mobile 的根目录中以启用推送通知。
您可以在 此处 找到有关运行移动应用程序的详细说明。
要更改货币,请遵循这些 说明。
运行移动应用
要运行移动应用程序,请创建 ./mobile/.env 文件,内容如下
MI_API_HOST=https://movinin.io:4004
MI_DEFAULT_LANGUAGE=en
MI_PAGE_SIZE=20
MI_PROPERTIES_PAGE_SIZE=8
MI_BOOKINGS_PAGE_SIZE=8
MI_CDN_USERS=https://movinin.io/cdn/movinin/users
MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
MI_AGENCY_IMAGE_WIDTH=60
MI_AGENCY_IMAGE_HEIGHT=30
MI_PROPERTY_IMAGE_WIDTH=300
MI_PROPERTY_IMAGE_HEIGHT=200
MI_MINIMUM_AGE=21
MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
MI_STRIPE_COUNTRY_CODE=US
MI_STRIPE_CURRENCY_CODE=USD
您需要配置以下选项
MI_API_HOST=https://movinin.io:4004
MI_CDN_USERS=https://movinin.io/cdn/movinin/users
MI_CDN_PROPERTIES=https://movinin.io/cdn/movinin/properties
MI_STRIPE_PUBLISHABLE_KEY=STRIPE_PUBLISHABLE_KEY
MI_STRIPE_MERCHANT_IDENTIFIER=MERCHANT_IDENTIFIER
MI_STRIPE_COUNTRY_CODE=US
MI_STRIPE_CURRENCY_CODE=USD
将 https://movinin.io
替换为 IP、主机名或 FQDN。
如果您想启用 Stripe 支付网关,请在 MI_STRIPE_PUBLISHABLE_KEY
中设置 Stripe 发布密钥。您可以从 Stripe 仪表板中检索它。使用 Stripe 的测试模式。
MI_STRIPE_MERCHANT_IDENTIFIER
是您在 Apple 注册以与 Apple Pay 一起使用的商户标识符。
MI_STRIPE_COUNTRY_CODE
是您企业的两字母 ISO 3166 国家代码,例如“US”。Stripe 支付必需。
MI_STRIPE_CURRENCY_CODE
是三位字母 ISO 4217 货币代码,例如“USD”或“EUR”。Stripe 支付必需。必须是受支持的货币:https://docs.stripe.com/currencies
通过遵循这些 说明 来安装演示数据库。
配置 https:///cdn
在 Windows 上,安装 IIS 并为运行 Movin' In API 的用户授予对 C:\inetpub\wwwroot\cdn\movinin 的完全访问权限。
在 Linux 上,安装 NGINX 并按如下方式更新 /etc/nginx/sites-enabled/default
server {
listen 80 default_server;
server_name _;
...
location /cdn {
alias /var/www/cdn;
}
}
最后,为运行 Movin' In API 的用户授予对 /var/www/cdn/movinin 的完全访问权限。
通过遵循这些 说明 来配置 ./api。
使用以下命令运行 ./api
cd ./api
npm run dev
通过在您的设备上下载 Expo 应用程序并从 ./mobile 文件夹运行以下命令来运行移动应用程序
cd ./mobile
npm install
npm start
打开设备上的 Expo 应用程序并扫描 QR 码以运行 Movin' In 移动应用程序。
推送通知
如果您想启用 BookCars 推送通知,请下载 google-services.json 文件并将其放在 ./mobile 的根目录中以启用推送通知。其路径可以在 ./mobile/app.json 配置文件中通过 googleServicesFile
设置选项进行配置。不要忘记在 Firebase Console 中生成一个新的 私钥,并将 JSON 文件上传到 Expo 仪表板。
单元测试和覆盖率
以下是运行单元测试和构建覆盖率报告的说明。
必备组件
-
配置 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 服务。
说明
- 克隆 Movin' In 仓库
git clone https://github.com/aelassas/movinin.git
- 创建 api/.env 文件,内容如下
NODE_ENV=development MI_PORT=4004 MI_HTTPS=false MI_PRIVATE_KEY=/etc/ssl/movinin.key MI_CERTIFICATE=/etc/ssl/movinin.crt MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin MI_DB_SSL=false MI_DB_SSL_CERT=/etc/ssl/movinin.crt MI_DB_SSL_CA=/etc/ssl/movinin.ca.pem MI_DB_DEBUG=false MI_COOKIE_SECRET=COOKIE_SECRET MI_AUTH_COOKIE_DOMAIN=localhost MI_JWT_SECRET=JWT_SECRET MI_JWT_EXPIRE_AT=86400 MI_TOKEN_EXPIRE_AT=86400 MI_SMTP_HOST=in-v3.mailjet.com MI_SMTP_PORT=587 MI_SMTP_USER=USER MI_SMTP_PASS=PASSWORD MI_SMTP_FROM=admin@movinin.io MI_CDN_USERS=/var/www/cdn/movinin/users MI_CDN_TEMP_USERS=/var/www/cdn/movinin/temp/users MI_CDN_PROPERTIES=/var/www/cdn/movinin/properties MI_CDN_TEMP_PROPERTIES=/var/www/cdn/movinin/temp/properties MI_DEFAULT_LANGUAGE=en MI_BACKEND_HOST=https://:3003/ MI_FRONTEND_HOST=https://:3004/ MI_MINIMUM_AGE=21 MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
在 Windows 上,安装 IIS 并使用以下值更新以下设置
MI_CDN_USERS=C:\inetpub\wwwroot\cdn\movinin\users
MI_CDN_TEMP_USERS=C:\inetpub\wwwroot\cdn\movinin\temp\users
MI_CDN_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\properties
MI_CDN_TEMP_PROPERTIES=C:\inetpub\wwwroot\cdn\movinin\temp\properties
为运行 Movin' In API 的用户授予对 C:\inetpub\wwwroot\cdn\movinin
的完全访问权限。
然后,设置以下选项
MI_DB_URI=mongodb://admin:PASSWORD@127.0.0.1:27017/movinin?authSource=admin&appName=movinin
MI_COOKIE_SECRET=COOKIE_SECRET
MI_JWT_SECRET=JWT_SECRET
MI_SMTP_HOST=in-v3.mailjet.com
MI_SMTP_PORT=587
MI_SMTP_USER=USER
MI_SMTP_PASS=PASSWORD
MI_SMTP_FROM=admin@movinin.io
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
将 MI_DB_URI
中的 PASSWORD
替换为 MongoDB 密码,将 JWT_SECRET
替换为密钥令牌。最后,设置 SMTP 选项。SMTP 选项对于注册是必需的。您可以使用 mailjet、sendgrid、brevo 或任何其他事务性电子邮件提供商。
将 STRIPE_SECRET_KEY
替换为您测试模式下的 Stripe 密钥。您可以在 Stripe 开发者仪表板中找到它。
COOKIE_SECRET
和 JWT_SECRET
至少应为 32 个字符长,但越长越好。您可以使用在线密码生成器并将密码长度设置为 32 个或更长。
如果您想在移动应用程序中启用推送通知,请遵循这些 说明 并设置以下选项
MI_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
本节介绍了 Movin' In 的软件架构,包括 API、前端、移动应用程序和后端。
Movin' In API 是一个 Node.js 服务器应用程序,它使用 Express 公开 RESTful API,该 API 提供对 Movin' In MongoDB 数据库的访问。
Movin' In 前端是一个 React Web 应用程序,是用于预订房产的主要 Web 界面。
Movin' In 后端是一个 React Web 应用程序,允许管理员和代理机构管理房产、预订和客户。
Movin' In 移动应用程序是一个 React Native 应用程序,是用于预订房产的主要移动应用程序。
一项重要的设计决策是使用 TypeScript 而非 JavaScript,因为其具有诸多优势。TypeScript 提供强类型、强大的工具和集成,从而实现高质量、可扩展、更易读、更易维护且易于调试和测试的代码。
API
Movin' In API 公开了后端、前端和移动应用程序所需的所有 Movin' In 功能。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 是 Movin' In API 的主入口点。
index.ts
index.ts 是 Movin' In 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 app from './app'
import * as DatabaseHelper from './common/DatabaseHelper'
import * as env from './config/env.config'
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
函数,以便在进程接收到 SIGINT
、SIGTERM
或 SIGQUIT
信号时调用。
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 agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import * 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('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
strings.setLanguage(env.DEFAULT_LANGUAGE)
Helper.mkdir(env.CDN_USERS)
Helper.mkdir(env.CDN_TEMP_USERS)
Helper.mkdir(env.CDN_PROPERTIES)
Helper.mkdir(env.CDN_TEMP_PROPERTIES)
export default app
首先,我们检索 MongoDB 连接字符串,然后与 Movin' In MongoDB 数据库建立连接。然后,我们创建一个 Express app
并加载 cors、compression、helmet 和 nocache 等中间件。我们使用 helmet 中间件库设置了各种安全措施。我们还导入了应用程序不同部分的各种路由文件,例如 supplierRoutes
、bookingRoutes
、locationRoutes
、notificationRoutes
、propertyRoutes
和 userRoutes
。最后,我们加载了 Express 路由并将 app
导出。
路由
Movin' In API 中有六个路由。每个路由都有自己的控制器,遵循 MVC 设计模式和 SOLID 原则。以下是主要路由
userRoutes
:提供与用户相关的 REST 函数agencyRoutes
:提供与代理机构相关的 REST 函数locationRoutes
:提供与地点相关的 REST 函数propertyRoutes
:提供与房产相关的 REST 函数bookingRoutes
:提供与预订相关的 REST 函数notificationRoutes
:提供与通知相关的 REST 函数
以下是 Property
类型
import { Document, Types } from 'mongoose'
export interface Property extends Document {
name: string
type: movininTypes.PropertyType
agency: Types.ObjectId
description: string
image: string
images?: string[]
bedrooms: number
bathrooms: number
kitchens?: number
parkingSpaces?: number,
size?: number
petsAllowed: boolean
furnished: boolean
minimumAge: number
location: Types.ObjectId
address?: string
price: number
hidden?: boolean
cancellation?: number
aircon?: boolean
available?: boolean
rentalTerm: movininTypes.RentalTerm
}
一个房产由以下部分组成:
- 名称
- 类型(
Apartment
、Commercial
、Farm
、House
、Industrial
、Plot
、Townhouse
) - 创建它的代理机构的引用
- 描述
- 主图
- 附加图片
- 卧室数量
- 浴室数量
- 厨房数量
- 停车位数量
- 大小
- 租赁最低年龄
- 地点
- 地址(可选)
- 价格
- 租赁期限(
Monthly
、Weekly
、Daily
、Yearly
) - 取消价格(设置为
0
表示免费包含,留空表示不包含,或设置取消价格) - 指示是否允许宠物的标志
- 指示房产是否带家具的标志
- 指示房产是否隐藏的标志
- 指示是否有空调的标志
- 指示房产是否可供租赁的标志
以下是 create
控制器函数
export const create = async (req: Request, res: Response) => {
const body: movininTypes.CreatePropertyPayload = req.body
try {
if (!body.image) {
console.error(`[property.create] ${strings.PROPERTY_IMAGE_REQUIRED} ${body}`)
return res.status(400).send(strings.PROPERTY_IMAGE_REQUIRED)
}
const {
name,
type,
agency,
description,
image: imageFile,
images,
bedrooms,
bathrooms,
kitchens,
parkingSpaces,
size,
petsAllowed,
furnished,
minimumAge,
location,
address,
price,
hidden,
cancellation,
aircon,
rentalTerm
} = body
const _property = {
name,
type,
agency,
description,
bedrooms,
bathrooms,
kitchens,
parkingSpaces,
size,
petsAllowed,
furnished,
minimumAge,
location,
address,
price,
hidden,
cancellation,
aircon,
rentalTerm
}
const property = new Property(_property)
await property.save()
// image
if (!await Helper.exists(env.CDN_PROPERTIES)) {
await fs.mkdir(env.CDN_PROPERTIES, { recursive: true })
}
const _image = path.join(env.CDN_TEMP_PROPERTIES, imageFile)
if (await Helper.exists(_image)) {
const filename = `${property._id}_${Date.now()}${path.extname(imageFile)}`
const newPath = path.join(env.CDN_PROPERTIES, filename)
await fs.rename(_image, newPath)
property.image = filename
} else {
await Property.deleteOne({ _id: property._id })
const err = 'Image file not found'
console.error(strings.ERROR, err)
return res.status(400).send(strings.ERROR + err)
}
// images
property.images = []
if (images) {
for (let i = 0; i < images.length; i++) {
const imageFile = images[i]
const _image = path.join(env.CDN_TEMP_PROPERTIES, imageFile)
if (await Helper.exists(_image)) {
const filename =
`${property._id}_${uuid()}_${Date.now()}_${i}${path.extname(imageFile)}`
const newPath = path.join(env.CDN_PROPERTIES, filename)
await fs.rename(_image, newPath)
property.images.push(filename)
} else {
await Property.deleteOne({ _id: property._id })
const err = 'Image file not found'
console.error(strings.ERROR, err)
return res.status(400).send(strings.ERROR + err)
}
}
}
await property.save()
return res.json(property)
} catch (err) {
console.error(`[property.create] ${strings.DB_ERROR} ${body}`, err)
return res.status(400).send(strings.ERROR + err)
}
}
在此函数中,我们从 HTTP 请求体创建房产,并构建主图以及附加图。
以下是 Booking
类型
import { Document, Types } from 'mongoose'
export interface Booking extends Document {
agency: Types.ObjectId
location: Types.ObjectId
property: Types.ObjectId
renter: Types.ObjectId
from: Date
to: Date
status: movininTypes.BookingStatus
cancellation?: boolean
cancelRequest?: boolean
price: number
}
一个预订由以下部分组成:
- 与预订相关的代理机构的引用
- 与预订相关的地点的引用
- 与预订相关的房产的引用
- 与预订相关的客户的引用
- 租赁开始日期
- 租赁结束日期
- 价格
- 状态(
Void
、Pending
、Deposit
、Paid
、Reserved
、Cancelled
) - 指示是否可取消的标志
- 指示客户是否已发出取消请求的标志
以下是 create
控制器函数
export const create = async (req: Request, res: Response) => {
try {
const body: movininTypes.Booking = req.body
const booking = new Booking(body)
await booking.save()
return res.json(booking)
} catch (err) {
console.error(`[booking.create] ${strings.DB_ERROR} ${req.body}`, err)
return res.status(400).send(strings.DB_ERROR + err)
}
}
在此函数中,我们根据 HTTP 请求体创建一个 Booking
。
以下是 update
控制器函数
const notifyRenter = async (booking: env.Booking) => {
const renter = await User.findById(booking.renter)
if (!renter) {
console.log(`Renter ${booking.renter} not found`)
return
}
if (renter.language) {
strings.setLanguage(renter.language)
}
const message = `${strings.BOOKING_UPDATED_NOTIFICATION_PART1} ${booking._id}
${strings.BOOKING_UPDATED_NOTIFICATION_PART2}`
const notification = new Notification({
user: renter._id,
message,
booking: booking._id,
})
await notification.save()
let counter = await NotificationCounter.findOne({ user: renter._id })
if (counter && typeof counter.count !== 'undefined') {
counter.count++
await counter.save()
} else {
counter = new NotificationCounter({ user: renter._id, count: 1 })
await counter.save()
}
// mail
const mailOptions = {
from: env.SMTP_FROM,
to: renter.email,
subject: message,
html: `<p>${strings.HELLO}${renter.fullName},<br><br>
${message}<br><br>
${Helper.joinURL(env.FRONTEND_HOST, `booking?b=${booking._id}`)}<br><br>
${strings.REGARDS}<br></p>`,
}
await MailHelper.sendMail(mailOptions)
// push notification
const pushNotification = await PushNotification.findOne({ user: renter._id })
if (pushNotification) {
const pushToken = pushNotification.token
const expo = new Expo({ accessToken: env.EXPO_ACCESS_TOKEN })
if (!Expo.isExpoPushToken(pushToken)) {
console.log(`Push token ${pushToken} is not a valid Expo push token.`)
return
}
const messages: ExpoPushMessage[] = [
{
to: pushToken,
sound: 'default',
body: message,
data: {
user: renter._id,
notification: notification._id,
booking: booking._id,
},
},
]
// The Expo push notification service accepts batches of notifications so
// that you don't need to send 1000 requests to send 1000 notifications. We
// recommend you batch your notifications to reduce the number of requests
// and to compress them (notifications with similar content will get
// compressed).
const chunks = expo.chunkPushNotifications(messages)
const tickets = [];
(async () => {
// Send the chunks to the Expo push notification service. There are
// different strategies you could use. A simple one is to send one chunk at a
// time, which nicely spreads the load out over time:
for (const chunk of chunks) {
try {
const ticketChunk = await expo.sendPushNotificationsAsync(chunk)
tickets.push(...ticketChunk)
// NOTE: If a ticket contains an error code in ticket.details.error, you
// must handle it appropriately. The error codes are listed in the Expo
// documentation:
// https://docs.expo.io/push-notifications/sending-notifications/#individual-errors
} catch (error) {
console.error(error)
}
}
})()
}
}
export const update = async (req: Request, res: Response) => {
try {
const body: movininTypes.Booking = req.body
const booking = await Booking.findById(body._id)
if (booking) {
const {
agency,
location,
property,
renter,
from,
to,
status,
cancellation,
price,
} = req.body
const previousStatus = booking.status
booking.agency = agency
booking.location = location
booking.property = property
booking.renter = renter
booking.from = from
booking.to = to
booking.status = status
booking.cancellation = cancellation
booking.price = price
await booking.save()
if (previousStatus !== status) {
// notify renter
await notifyRenter(booking)
}
return res.sendStatus(200)
} else {
console.error('[booking.update] Booking not found:', req.body._id)
return res.sendStatus(204)
}
} catch (err) {
console.error(`[booking.update] ${strings.DB_ERROR} ${req.body}`, err)
return res.status(400).send(strings.DB_ERROR + err)
}
}
在此函数中,我们根据 HTTP 请求体更新 Booking
,并通过电子邮件和推送通知通知客户。
我们不会逐一解释每个路由。我们将以 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
包含与地点相关的主要业务逻辑。由于控制器相当大,我们不会查看其所有源代码,但我们将以 create
和 getLocations
控制器函数为例。
以下是 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: movininTypes.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)
}
}
在此控制器函数中,我们使用 MongoDB 的 aggregate
函数和 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
函数、page
和 size
参数检索通知。
以下是 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/ 包含 Movin' In API 客户端服务。
- ./frontend/src/App.tsx 是包含路由的主 React App。
- ./frontend/src/index.tsx 是前端的主入口点。
TypeScript 类型定义在包 ./packages/movinin-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 Properties = lazy(() => import('./pages/Properties'))
const Property = lazy(() => import('./pages/Property'))
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="/properties" element={<Properties />} />
<Route path="/property" element={<Property />} />
<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 懒加载来加载每个路由。
我们不会介绍前端的每个页面,但您可以打开源代码查看每个页面(如果您愿意)。
移动应用
Movin' In 提供适用于 Android 和 iOS 的原生移动应用程序。该移动应用程序使用 React Native、Expo 和 TypeScript 构建。与前端一样,移动应用程序允许客户根据地点和时间搜索可用房产,选择房产并进行结账。
如果客户的预订从后端更新,客户将收到推送通知。推送通知使用 Node.js、Expo Server SDK 和 Firebase 构建。
- ./mobile/assets/ 文件夹包含图像。
- ./mobile/screens/ 文件夹包含主要的 React Native 屏幕。
- ./mobile/components/ 文件夹包含 React Native 组件。
- ./mobile/services/ 包含 Movin' In API 客户端服务。
- ./mobile/App.tsx 是主 React Native App。
TypeScript 类型定义在
- ./mobile/types/index.d.ts
- ./mobile/types/env.d.ts
- ./mobile/miscellaneous/movininTypes.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/src/assets/ 文件夹包含 CSS 和图像。
- ./backend/src/pages/ 文件夹包含 React 页面。
- ./backend/src/components/ 文件夹包含 React 组件。
- ./backend/src/services/ 包含 Movin' In API 客户端服务。
- ./backend/src/App.tsx 是包含路由的主 React App。
- ./backend/src/index.tsx 是后端的入口点。
TypeScript 类型定义在包 ./packages/movinin-types 中定义。
后端的 App.tsx 与前端的 App.tsx 类似。
我们不会介绍后端的每个页面,但您可以打开源代码查看每个页面(如果您愿意)。
关注点
我在此项目中构建了许多有用的 React 和 React Native 组件,用于处理图像、文本、下拉菜单、自动完成、单选按钮等。如果您愿意,可以在您的 React 或 React Native 项目中使用这些组件。
使用 React Native 和 Expo 构建移动应用程序非常简单。Expo 使 React Native 的移动开发变得非常简单。
对后端、前端和移动开发使用相同的语言(TypeScript)非常方便。
TypeScript 是一种非常有趣的语言,具有许多优点。通过为 JavaScript 添加静态类型,我们可以避免许多错误,并生成高质量、可扩展、更易读、更易维护且易于调试和测试的代码。
好了!希望您喜欢阅读这篇文章。