近期在学习IBM全栈应用开发微学士课程,故此记录学习笔记。

恭喜您成功完成 IBM 全栈软件开发人员专业证书的所有前面课程!现在是通过完成期末考试来检验您的新技能的时候了。

您将通过考试,了解自己在 PC 各门课程中学到的知识。现在,您应该已经熟练掌握了以下主题:

  • 云计算的核心概念
  • 网页开发语言,包括 HTML、CSS 和 JavaScript
  • Git 和 GitHub
  • Node.js、Express 和 React
  • 容器
  • Python
  • 数据库概念和相关技术,如 SQL 和 Django
  • 微服务和无服务器计算

这是 IBM 全栈软件开发人员专业证书的最后一门课程。它将测试您迄今为止所掌握的知识和技能。本课程包含分级期末考试,涵盖 PC 中各种课程的内容。

您将就以下主题接受评估:核心云计算概念;HTML、CSS、JavaScript 和 Python 等语言;Node.js、Express 和 React 等框架;以及 Docker、Kubernetes、OpenShift、SQL、Django、微服务和 Serverless 等后端技术。

在学习本课程之前,请确保您已完成 IBM 全栈开发人员专业证书中的所有先前课程。

通过附加功能丰富汽车经销商门户网站

该课程从模块开始:通过三个实验丰富汽车经销商门户网站的附加功能。

在“全栈应用程序开发项目”(可见文章)中,您创建了一个汽车经销商应用程序,需要开发前端页面、用户管理、构建数据库操作动作、创建后端服务以及配置 CI/CD 管道。前端使用的技术包括 HTML、CSS、JavaScript 和 React,后端使用的技术包括 Django、Node.js、NoSQL(Mongo)、容器化、IBM 代码引擎、Python 和 Kubernetes。

通过汽车经销商应用程序,我们可以根据经销商的 ID 查看所有经销商的详细信息和评论,还可以注册、登录为用户,并在注册或登录后为特定经销商添加新的评论,方法是将 AboutContact Us 等静态页面整合到一起。

用户注册和登录功能也是在为端点和 Django 视图配置了 Express-Mongo 后端微服务后实现的。还集成了一个情感分析微服务来分析评论。也实现了各种功能,如显示经销商列表、详细信息和评论,在 Django 应用程序中添加新的经销商评论。最后,将 CI/CD Linting 服务集成到应用程序中,并部署到 Kubernetes 上。

在本课程中,您将从增强同一个汽车经销商应用程序开始。改进的重点是前端方面,然后通过添加新的微服务转移到后端,最后在后续实验中将其与前端集成。

强烈建议先完成全栈应用程序开发项目课程,然后再学习本课程,因为本模块的实验是增强已建应用程序的一部分。虽然这些内容不属于评分标准的一部分,但它们对于增强你的理解和技能仍然很有价值。

概述

作为毕业设计项目的一部分,您已经成功创建并测试了 Car Dealerships website,确保其符合预期功能。这包括允许用户查看所有经销商的详细信息、查看对特定经销商的现有评论,以及在作为注册用户登录后发布对特定经销商的新评论。

完成上述工作后,下一步就是增强应用程序的功能。这涉及到应用程序的前端和后端方面。如下所述,您将分三部分实施这些增强功能:

  1. 前端改进

    • Dealerships 页面上的 States 下拉菜单转换为可搜索文本框,使用户能够通过输入搜索字符串来筛选经销商。
    • 改进应用程序主页上导航栏和经销商按钮的配色方案,以及 Dealerships Review 页面上审查面板和审查图标的颜色。
    • 微调经销商审查面板的视觉元素,对字体大小和字体对齐方式等方面进行调整。
  2. 汽车库存后端服务

    • 使用 MongoDB 和 Node.js 服务器建立一个新的后端微型服务,专门用于获取与汽车库存相关的各种详细信息。
    • 将新创建的微服务与 Django 应用程序的后端集成,并验证后端服务器的成功启动。
  3. 汽车库存服务的前端开发

    • 开发前端组件,并将其与第 2 部分:汽车库存后端服务中开发的后端汽车库存微服务集成。
    • 创建一个按品牌型号年份里程价格选择汽车的选项。
    • 对 Django 应用程序与集成的汽车库存服务生成的输出进行全面测试。

前端改进

Dealerships 页面上的 States 下拉菜单转换为可搜索文本框

  1. 访问 frontend/src/components/Dealers/Dealers.jsx 文件。
  2. 您会注意到,经销商下拉菜单的代码是在 <select> 下拉菜单元素中显示的:
    1
    2
    <select name="state" id="state" onChange={(e) => filterDealers(e.target.value)}>
    </select>
  3. 用包含以下属性的 <input> 字段取代现有的 <select> 元素:
    • 用户可在文本框中输入搜索州。
    • 根据输入的搜索查询过滤显示的经销商,并与州匹配。
    • 当输入框失去焦点时,将显示的经销商重置为原始列表。
    1
    <input type="text" placeholder="Search states..." onChange={handleInputChange} onBlur={handleLostFocus} value={searchQuery} />

观察输入元素中的函数。现在,让我们创建它们。

  1. 创建一个名为 handleInputChange 的新函数,用于管理输入更改,并根据输入的状态查询过滤经销商。
    1
    2
    3
    4
    5
    6
    7
    8
    const handleInputChange = (event) => {
    const query = event.target.value;
    setSearchQuery(query);
    const filtered = originalDealers.filter(dealer =>
    dealer.state.toLowerCase().includes(query.toLowerCase())
    );
    setDealersList(filtered);
    };
    • 每次输入框内的值发生变化时,都会触发该函数。
    • 它会检索用户在输入框中输入的当前值,并将其作为查询存储在 setSearchQuery 变量中,用于筛选经销商。
    • 系统会生成一个新数组,其中只包含状态与输入的查询相匹配的经销商。
    • 通过将查询和经销商状态转换为小写,使其大小写不敏感,从而确保用户获得更友好的搜索体验。
    • 然后,该函数将显示与输入的状态查询相匹配的经销商。
  2. 总之,handleInputChange 函数可根据用户在搜索栏中的输入动态过滤经销商列表。它实时更新显示的经销商列表,为用户提供响应式搜索功能。
  3. 创建 handleLostFocus 函数,以确保当用户将搜索输入留空并点击或滑动标签离开时,经销商列表会重置为原始列表。
    1
    2
    3
    4
    5
    const handleLostFocus = () => {
    if (!searchQuery) {
    setDealersList(originalDealers);
    }
    }
    • 该函数在执行时验证 searchQuery 状态是否为空。
    • 如果 searchQuery 确实为空,它就会将经销商列表恢复到开始搜索前的原始状态。
    • handleLostFocus 函数在用户点击输入框外或标签页离开输入框时调用,由 onBlur 事件触发。
  4. 将此代码与之前定义的其他状态变量一起添加:
    1
    const [searchQuery, setSearchQuery] = useState('');
    • 这将利用 useState 钩子创建一个名为 searchQuery 的状态变量和一个相应的函数 setSearchQuery 来更新其值。
    • searchQuery 状态变量实时保存用户在搜索输入框中输入的值。当用户在搜索栏中输入时,该状态变量会通过调用 setSearchQuery 进行更新,并触发组件的重新渲染,以反映对搜索查询所做的任何更改。
  5. 初始化和设置状态变量,用于管理原始经销商列表。
    • 添加以下代码,初始化 dealersListsearchQuerystates 变量及其设置函数:
      1
      const [originalDealers, setOriginalDealers] = useState([]);
      在这段代码中,状态变量 originalDealers 及其设置函数 setOriginalDealers 用于跟踪和更新原始经销商列表。
    • 将下面的代码放在 getDealers 函数中更新其他状态(setStatessetDealersList)的地方。
      1
      setOriginalDealers(all_dealers);
      这一行用从应用程序接口获取的所有经销商数组设置状态变量 originalDealers,用于存储过滤前的原始经销商列表。
  6. 确保保存所有更改。
  7. 运行以下命令来构建应用程序的客户端:
    1
    2
    cd /home/project/xrwvm-fullstack_developer_capstone/server/frontend
    npm run build
  8. 访问经销商详细信息页面,测试应用程序的输出。
  9. 请观察用于经销商搜索的搜索框的外观,它取代了之前的下拉框。
  10. 输入搜索查询,例如 Texas,搜索该州来进行测试。

更改应用程序的配色方案

在本节中,您将了解在以下区域更改应用程序配色方案的步骤:

  • 与应用程序主页有关的方面,包括:

    1. 更改导航栏的背景颜色。
    2. 修改 View Dealerships 按钮的背景颜色。
  • Dealership Review 页面有关的方面包括:

    1. 调整与 Review 面板相关的背景颜色。
    2. 自定义与 Review 图标相关的悬浮边框。
  1. 更改导航栏的背景颜色

    1. 打开文件 frontend/static/Home.html
    2. 您将看到以下代码片段,它将当前的深绿色背景添加到应用程序中的导航栏:
      1
      <nav class="navbar navbar-expand-lg navbar-light" style={{backgroundColor:"darkturquoise",height:"1in"}}>
    3. 用您喜欢的颜色(如 mediumspringgreen)代替它,以修改页眉的背景颜色。
      1
      <nav class="navbar navbar-expand-lg navbar-light" style="background-color:mediumspringgreen; height: 1in;">
    4. 保存更改。
    5. 运行提供的命令构建客户端并显示上述更改:
      1
      npm run build
    6. 刷新应用页面。
    7. 观察导航栏背景的变化。
  2. 修改 View Dealerships 按钮的背景颜色

    1. 以下代码表示应用程序主页上 View Dealerships 按钮的背景颜色:
      1
      <a href="/dealers" class="btn" style="background-color: aqua;margin:10px">View Dealerships</a>
    2. 将其调整为您喜欢的颜色(例如,plum,一种紫色)。
      1
      <a href="/dealers" class="btn" style="background-color: plum; margin:10px">View Dealerships</a>
    3. 请观察 View Dealerships 按钮的最新配色方案。
  3. 调整与 Review 面板相关的背景颜色

    1. Dealers.css 文件中的样式是为应用程序中的 DealershipsReviews 面板定制的。review_panel 类包含用于设计经销商审查面板样式的代码。
    2. 请注意,它的边框是纯灰色的。
      1
      border: solid grey;
    3. 调整代码,使其具有纯紫色边框:
      1
      border: solid purple;
    4. 你会发现 Review 面板的边框变成了纯紫色,外观也发生了变化。
  4. 自定义与 Review 图标相连的悬浮边框

    1. 当用户将鼠标悬停在 Review 图标上时,.review_icon:hover 类将为其应用样式。
    2. 请注意,悬停时它的背景是纯浅灰色。
      1
      border: solid lightgray;
    3. 将颜色调整为黑色色调,边框变细(如 2 像素),以便在悬停时呈现出鲜明而纤细的背景外观。
      1
      border: 2px solid #080808;
    4. 观察修改后的图标,它带有细细的黑色边框,悬停时会显示出明显的存在感。

更改应用程序的外观和感觉

在本节中,您将学习如何改进应用程序中 Dealer Review 面板的视觉效果。

其中包括:

  • 调整字体大小
  • 确保字体对齐正确,以改善整体外观
  1. 调整字体大小

    1. .reviewer 类定义了用户评论的样式。
    2. 当前字体大小设置为小。
    3. 将字体大小调整到特定值(例如 18 像素),以放大 Review 文本,提高可读性。
      1
      font-size: 18px;
  2. 确保字体对齐正确,改善整体外观

    1. 文本对齐属性并未被指定。
    2. 因此,浏览器的默认文本对齐方式(通常为左对齐)会被应用,从而产生左对齐的 Review 文本。
    3. 将文本居中,以确保 User reviews 显示在 Review 窗格的中心。
      1
      text-align: center;

汽车库存后端服务

使用 MongoDB 和 Node.js 开发新的后端汽车库存微服务

  1. 打开一个新的终端窗口,导航到 xrwvm-fullstack_developer_capstone/server 目录。
    生成一个名为 carsInventory 的新目录。

    1
    mkdir carsInventory
  2. 执行以下命令,初始化一个新的 Node.js 项目,并在 carsInvent 目录下创建 package.json 文件:

    1
    npm init

    将以下应用程序依赖项添加到 package.json 中:

    1
    2
    3
    4
     "cors": "^2.8.5",    
    "express": "^4.18.2",
    "mongodb": "^6.3.0",
    "mongoose": "^8.0.1"

    这些依赖项对于启用 CORS(跨源资源共享)、处理网络应用程序路由和中间件(Express)、与 MongoDB 交互(MongoDB 驱动程序)以及在使用 MongoDB 的 Node.js 应用程序中提供便捷的数据建模方式(Mongoose)至关重要。

    每个依赖关系中版本号前的 ^ 符号允许在运行 npm install 命令时安装兼容的未来更新。

    name 设置为 carsInventory,将 main 的值设置为 app.js。这样,package.json 文件看起来应该与下面相似:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     {
    "name": "carsInventory",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "mongodb": "^6.3.0",
    "mongoose": "^8.0.1"
    }
    }

    执行命令安装这些依赖项:

    1
    npm install
  3. 在名为 inventory.js 的文件中设置 MongoDB schema。现在,您将使用 mongoose 库为名为 cars 的集合建立 MongoDB schema。
    这将用于创建一个与 MongoDB 数据库交互的 mongoose 模型,使应用程序能够以更有条理、更有组织的方式对汽车文档执行 CRUD 操作。
    schema 应定义汽车文件的结构,其中应包括以下字段及其数据类型:

    • dealer_idNumber
    • makeString
    • modelString
    • bodyTypeString
    • yearNumber
    • mileageNumber
    • priceNumber

    模型将被命名为 cars,与所连接的 MongoDB 数据库中的 cars 集合相对应。MongoDB 模型将以此名称导出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    const { Int32 } = require('mongodb');
    const mongoose = require('mongoose');

    const Schema = mongoose.Schema;

    const cars = new Schema({
    dealer_id: {
    type: Number,
    required: true
    },
    make: {
    type: String,
    required: true
    },
    model: {
    type: String,
    required: true
    },
    bodyType: {
    type: String,
    required: true
    },
    year: {
    type: Number,
    required: true
    },
    mileage: {
    type: Number,
    required: true
    },
    price: {
    type: Number,
    required: true
    }
    });

    module.exports = mongoose.model('cars', cars);
  4. 获取包含汽车库存和相关详细信息的 JSON 数据集。
    首先创建一个名为 data 的文件夹并导航进入:

    1
    2
    mkdir data
    cd data

    下载汽车库存数据集:

    1
    wget https://cf-courses-data.s3.us.cloud-object-storage.appdomain.cloud/IBM-CD0321EN-SkillsNetwork/labs/v2/m6/car_records.json

    检查文件,发现每个汽车对象都有以下字段:

    1
    make, model, bodyType, year, dealer_id, mileage, price
  5. 返回 carsInvent 目录,创建更多文件。

    建立具有后端端点功能的 Node.JS 服务器。首先创建一个名为 app.js 的文件:

    1
    touch app.js

    它应具备以下功能:

    • 设置与 MongoDB 集成的 Express 服务器
    • 从文件 car_records.json 中读取数据
    • 建立与 MongoDB 的连接
    • 定义根 API 端点 /,当访问根 API 时,它会响应一条欢迎访问 Mongoose API 消息
    • 定义以下六个端点,用于根据各种条件查询汽车:
      • cars/:id 端点,可根据指定的经销商 ID 从 MongoDB 集合中检索并返回汽车文档
      • /carsbymake/:id/:make 端点,可根据经销商 ID 和汽车品牌检索并返回汽车文档
      • /carsbymake/:id/:model 端点,可根据经销商 ID 和车型检索并返回汽车文档
      • /carsbymaxmileage/:id/:mileage 端点,可根据经销商 ID 和里程限制检索并返回汽车文件,如下所示:
        • 里程数:
          • 小于或等于 50000
          • 50000 至 100000
          • 100000 至 150000
          • 150000 至 200000
          • 大于 200000
      • /carsbyprice/:id/:price 端点,可根据经销商ID和价格约束检索并返回汽车文件,如下所示:
        • 价格:
          • 小于或等于 20000
          • 20000 至 40000
          • 40000 至 60000
          • 60000 至 80000
          • 大于 80000
      • /carsbymake/:id/:year 端点,可根据经销商ID和最低年份约束检索并返回汽车文件
    • 3050 端口启动服务器。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    /*jshint esversion: 8 */

    const express = require('express');
    const mongoose = require('mongoose');
    const fs = require('fs');
    const cors = require('cors');

    const app = express();
    const port = 3050;

    app.use(cors());
    app.use(express.urlencoded({ extended: false }));

    const carsData = JSON.parse(fs.readFileSync('car_records.json', 'utf8'));

    mongoose.connect('mongodb://mongo_db:27017/', { dbName: 'dealershipsDB' });

    const Cars = require('./inventory');

    try {
    Cars.deleteMany({}).then(() => {
    Cars.insertMany(carsData.cars);
    });
    } catch (error) {
    console.error(error);
    // Handle errors properly here
    }

    app.get('/', async (req, res) => {
    res.send('Welcome to the Mongoose API');
    });

    app.get('/cars/:id', async (req, res) => {
    try {
    const documents = await Cars.find({dealer_id: req.params.id});
    res.json(documents);
    } catch (error) {
    res.status(500).json({ error: 'Error fetching reviews' });
    }
    });

    app.get('/carsbymake/:id/:make', async (req, res) => {
    try {
    const documents = await Cars.find({dealer_id: req.params.id, make: req.params.make});
    res.json(documents);
    } catch (error) {
    res.status(500).json({ error: 'Error fetching reviews by car make and model' });
    }
    });

    app.get('/carsbymodel/:id/:model', async (req, res) => {
    try {
    const documents = await Cars.find({ dealer_id: req.params.id, model: req.params.model });
    res.json(documents);
    } catch (error) {
    res.status(500).json({ error: 'Error fetching dealers by ID' });
    }
    });

    app.get('/carsbymaxmileage/:id/:mileage', async (req, res) => {
    try {
    let mileage = parseInt(req.params.mileage)
    let condition = {}
    if(mileage === 50000) {
    condition = { $lte : mileage}
    } else if (mileage === 100000){
    condition = { $lte : mileage, $gt : 50000}
    } else if (mileage === 150000){
    condition = { $lte : mileage, $gt : 100000}
    } else if (mileage === 200000){
    condition = { $lte : mileage, $gt : 150000}
    } else {
    condition = { $gt : 200000}
    }
    const documents = await Cars.find({ dealer_id: req.params.id, mileage : condition });
    res.json(documents);
    } catch (error) {
    res.status(500).json({ error: 'Error fetching dealers by ID' });
    }
    });

    app.get('/carsbyprice/:id/:price', async (req, res) => {
    try {
    let price = parseInt(req.params.price)
    let condition = {}
    if(price === 20000) {
    condition = { $lte : price}
    } else if (price=== 40000){
    console.log("\n \n \n "+ price)
    condition = { $lte : price, $gt : 20000}
    } else if (price === 60000){
    condition = { $lte : price, $gt : 40000}
    } else if (price === 80000){
    condition = { $lte : price, $gt : 60000}
    } else {
    condition = { $gt : 80000}
    }
    const documents = await Cars.find({ dealer_id: req.params.id, price : condition });
    res.json(documents);
    } catch (error) {
    res.status(500).json({ error: 'Error fetching dealers by ID' });
    }
    });

    app.get('/carsbyyear/:id/:year', async (req, res) => {
    try {
    const documents = await Cars.find({ dealer_id: req.params.id, year : { $gte :req.params.year }});
    res.json(documents);
    } catch (error) {
    res.status(500).json({ error: 'Error fetching dealers by ID' });
    }
    });

    app.listen(port, () => {
    console.log(`Server is running on http://localhost:${port}`);
    });
  6. 为 Node.js 应用程序创建 Docker 镜像。首先创建 Dockerfile

    1
    touch Dockerfile

    添加以下内容:

    • 基础 Docker 镜像:node:18.12.1-bullseye-slim
    • 安装 9.1.3 版本的 npm
    • 从当前目录内添加 package.jsonapp.jscar_records.json 到 Docker 镜像的根目录
    • 复制当前目录的所有文件到 Docker 镜像
    • 让容器监听 3050 端口
    • 将容器启动时要运行的默认命令指定为 node app.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    FROM node:18.12.1-bullseye-slim

    RUN npm install -g [email protected]

    ADD package.json .
    ADD app.js .
    ADD data/car_records.json .
    COPY . .
    RUN npm install

    EXPOSE 3050

    CMD [ "node", "app.js" ]
  7. 为运行两个服务(MongoDB 容器和 Node.js 应用程序)设置 Docker Compose 配置文件。
    创建 Docker Compose 配置 YAML 文件(docker-compose.yml):

    1
    touch docker-compose.yml

    添加内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    version: '3.9'

    services:
    # Mongodb service
    mongo_db:
    container_name: carsInventory_container
    image: mongo:latest
    ports:
    - 27018:27017
    restart: always
    volumes:
    - mongo_data:/data/db

    # Node api service
    api:
    image: nodeapp
    ports:
    - 3050:3050
    depends_on:
    - mongo_db

    volumes:
    mongo_data: {}

    构建 Docker 应用:

    1
    docker build . -t nodeapp

    执行以下命令启动服务器:

    1
    docker-compose up

    启动服务器并验证汽车库存服务的根端点是否显示了 Welcome to the Mongoose API 的消息。

将微服务与 Django 应用程序后端集成,并成功启动后端服务器

  1. 导航至 xrwvm-fullstack_developer_capstone/server 目录。如果 Django 服务器已在运行,则停止该服务器。

  2. .env 文件中插入汽车库存服务端点的 URL(上一节已复制)。

    1
    searchcars_url='your end'

    不要添加尾端的 \

  3. restapis.py 中加入执行以下功能的代码:

    1. .env 文件中获取 URL。

      1
      2
      3
      4
      searchcars_url = os.getenv(
      'searchcars_url',
      default="http://localhost:3050/"
      )
    2. 执行一个名为 searchcars_request 的方法,该方法具有以下功能:

      • 接受端点和变量关键字参数
      • 利用提供的端点、查询参数和基本 URL 构建一个完整的请求 URL
      • 执行 GET 请求并返回响应的 JSON 内容
      • 处理错误(包括网络异常)并提供成功完成消息

      代码结构将与您之前在 Capstone 主项目中创建的 get_request 方法非常相似:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      def searchcars_request(endpoint, **kwargs):
      params = ""
      if (kwargs):
      for key, value in kwargs.items():
      params = params+key + "=" + value + "&"

      request_url = searchcars_url+endpoint+"?"+params

      print("GET from {} ".format(request_url))
      try:
      # Call get method of requests library with URL and parameters
      response = requests.get(request_url)
      return response.json()
      except:
      # If any error occurs
      print("Network exception occurred")
      finally:
      print("GET request call complete!")
  4. djangoapp/views.py 文件中添加名为 get_inventory 的视图,以获取汽车库存。

    • 该视图应处理 HTTP 请求,提取查询参数和经销商 ID。
    • 它应使用提供的参数构建一个 API 端点,并调用 restapis.py 中定义的 searchcars_request 函数来检索汽车数据。确保为 searchcars_request 函数加入模块导入。
    • 它应返回状态为 200 的 JSON 响应,如果提供了经销商 ID,则返回获取的汽车。
    • 如果没有经销商 ID,则应返回状态为 400 的 JSON 响应和 Bad Request 消息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # Module import
    from .restapis import get_request, analyze_review_sentiments, post_review, searchcars_request

    # Code for the view
    def get_inventory(request, dealer_id):
    data = request.GET
    if (dealer_id):
    if 'year' in data:
    endpoint = "/carsbyyear/"+str(dealer_id)+"/"+data['year']
    elif 'make' in data:
    endpoint = "/carsbymake/"+str(dealer_id)+"/"+data['make']
    elif 'model' in data:
    endpoint = "/carsbymodel/"+str(dealer_id)+"/"+data['model']
    elif 'mileage' in data:
    endpoint = "/carsbymaxmileage/"+str(dealer_id)+"/"+data['mileage']
    elif 'price' in data:
    endpoint = "/carsbyprice/"+str(dealer_id)+"/"+data['price']
    else:
    endpoint = "/cars/"+str(dealer_id)

    cars = searchcars_request(endpoint)
    return JsonResponse({"status": 200, "cars": cars})
    else:
    return JsonResponse({"status": 400, "message": "Bad Request"})
    return JsonResponse({"status": 400, "message": "Bad Request"})
  5. djangoapp/urls.py 文件中包含该视图的路由。

    1
    path(route='get_inventory/<int:dealer_id>', view=views.get_inventory, name='get_inventory'),
  6. 导航到 xrwvm-fullstack_developer_capstone/server 目录。
    确保 Docker Compose 服务器正在运行。如果没有,请使用 docker-compose up 命令启动它。

  7. 执行模型迁移并启动服务器。

    1
    2
    3
    python3 manage.py makemigrations
    python3 manage.py migrate
    python3 manage.py runserver
  8. 确保服务器启动成功且无错误。如果出现任何错误日志,请检查并根据需要处理您的代码。

汽车库存服务的前端开发

开发并集成与后端汽车库存微服务相对应的前端服务

这需要创建一个 React 组件,专门用于搜索和显示汽车信息,并为该组件整合相应的路线。

  1. 创建一个用于搜索和显示汽车信息的 React 组件。

    1. 导航至 xrwvm-fullstack_developer_capstone/server/frontend/src/components/Dealers 目录,并添加名为 SearchCars.jsx 的新文件。
    2. 在该文件中加入以下内容:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      import React, { useState, useEffect } from 'react';
      import { useParams } from 'react-router-dom';
      import Header from '../Header/Header';

      const SearchCars = () => {
      const [cars, setCars] = useState([]);
      const [makes, setMakes] = useState([]);
      const [models, setModels] = useState([]);
      const [dealer, setDealer] = useState({"full_name":""});
      const [message, setMessage] = useState("Loading Cars....");
      const { id } = useParams();


      let dealer_url = `/djangoapp/get_inventory/${id}`;

      let fetch_url = `/djangoapp/dealer/${id}`;

      const fetchDealer = async ()=>{
      const res = await fetch(fetch_url, {
      method: "GET"
      });
      const retobj = await res.json();
      if(retobj.status === 200) {
      let dealer = retobj.dealer;
      setDealer({"full_name":dealer[0].full_name})
      }
      }

      const populateMakesAndModels = (cars)=>{
      let tmpmakes = []
      let tmpmodels = []
      cars.forEach((car)=>{
      tmpmakes.push(car.make)
      tmpmodels.push(car.model)
      })
      setMakes(Array.from(new Set(tmpmakes)));
      setModels(Array.from(new Set(tmpmodels)));
      }


      const fetchCars = async ()=>{
      const res = await fetch(dealer_url, {
      method: "GET"
      });
      const retobj = await res.json();

      if(retobj.status === 200) {
      let cars = Array.from(retobj.cars)
      setCars(cars);
      populateMakesAndModels(cars);
      }
      }

      const setCarsmatchingCriteria = async(matching_cars)=>{
      let cars = Array.from(matching_cars)
      console.log("Number of matching cars "+cars.length);

      let makeIdx = document.getElementById('make').selectedIndex;
      let modelIdx = document.getElementById('model').selectedIndex;
      let yearIdx = document.getElementById('year').selectedIndex;
      let mileageIdx = document.getElementById('mileage').selectedIndex;
      let priceIdx = document.getElementById('price').selectedIndex;

      if(makeIdx !== 0) {
      let currmake = document.getElementById('make').value;
      cars = cars.filter(car => car.make === currmake);
      }
      if(modelIdx !== 0) {
      let currmodel = document.getElementById('model').value;
      cars = cars.filter(car => car.model === currmodel);
      if(cars.length !== 0) {
      document.getElementById('make').value = cars[0].make;
      }
      }

      if(yearIdx !== 0) {
      let curryear = document.getElementById('year').value;
      cars = cars.filter(car => car.year >= curryear);
      if(cars.length !== 0) {
      document.getElementById('make').value = cars[0].make;
      }
      }

      if(mileageIdx !== 0) {
      let currmileage = parseInt(document.getElementById('mileage').value);
      if(currmileage === 50000) {
      cars = cars.filter(car => car.mileage <= currmileage);
      } else if (currmileage === 100000){
      cars = cars.filter(car => car.mileage <= currmileage && car.mileage > 50000);
      } else if (currmileage === 150000){
      cars = cars.filter(car => car.mileage <= currmileage && car.mileage > 100000);
      } else if (currmileage === 200000){
      cars = cars.filter(car => car.mileage <= currmileage && car.mileage > 150000);
      } else {
      cars = cars.filter(car => car.mileage > 200000);
      }
      }

      if(priceIdx !== 0) {
      let currprice = parseInt(document.getElementById('price').value);
      if(currprice === 20000) {
      cars = cars.filter(car => car.price <= currprice);
      } else if (currprice === 40000){
      cars = cars.filter(car => car.price <= currprice && car.price > 20000);
      } else if (currprice === 60000){
      cars = cars.filter(car => car.price <= currprice && car.price > 40000);
      } else if (currprice === 80000){
      cars = cars.filter(car => car.price <= currprice && car.price > 60000);
      } else {
      cars = cars.filter(car => car.price > 80000);
      }
      }

      if(cars.length === 0) {
      setMessage("No cars found matching criteria");
      }
      setCars(cars);
      }


      let SearchCarsByMake = async ()=> {
      let make = document.getElementById("make").value;
      dealer_url = dealer_url + "?make="+make;

      const res = await fetch(dealer_url, {
      method: 'GET',
      headers: {
      'Content-Type': 'application/json',
      }})

      const retobj = await res.json();

      if(retobj.status === 200) {
      setCarsmatchingCriteria(retobj.cars);
      }
      }

      let SearchCarsByModel = async ()=> {
      let model = document.getElementById("model").value;
      dealer_url = dealer_url + "?model="+model;

      const res = await fetch(dealer_url, {
      method: 'GET',
      headers: {
      'Content-Type': 'application/json',
      }})

      const retobj = await res.json();

      if(retobj.status === 200) {
      setCarsmatchingCriteria(retobj.cars);
      }
      }

      let SearchCarsByYear = async ()=> {
      let year = document.getElementById("year").value;
      if (year !== "all") {
      dealer_url = dealer_url + "?year="+year;
      }

      const res = await fetch(dealer_url, {
      method: 'GET',
      headers: {
      'Content-Type': 'application/json',
      }})

      const retobj = await res.json();

      if(retobj.status === 200) {
      setCarsmatchingCriteria(retobj.cars);
      }
      }

      let SearchCarsByMileage = async ()=> {

      let mileage = document.getElementById("mileage").value;
      if (mileage !== "all") {
      dealer_url = dealer_url + "?mileage="+mileage;
      }

      const res = await fetch(dealer_url, {
      method: 'GET',
      headers: {
      'Content-Type': 'application/json',
      }})

      const retobj = await res.json();

      if(retobj.status === 200) {
      setCarsmatchingCriteria(retobj.cars);
      }
      }


      let SearchCarsByPrice = async ()=> {
      let price = document.getElementById("price").value;
      if(price !== "all") {
      dealer_url = dealer_url + "?price="+price;
      }

      const res = await fetch(dealer_url, {
      method: 'GET',
      headers: {
      'Content-Type': 'application/json',
      }})

      const retobj = await res.json();

      if(retobj.status === 200) {
      setCarsmatchingCriteria(retobj.cars);
      }
      }

      const reset = ()=>{
      const selectElements = document.querySelectorAll('select');

      selectElements.forEach((select) => {
      select.selectedIndex = 0;
      });
      fetchCars();
      }


      useEffect(() => {
      fetchCars();
      fetchDealer();
      },[]);

      return (
      <div>
      <Header />
      <h1 style={{ marginBottom: '20px'}}>Cars at {dealer.full_name}</h1>
      <div>
      <span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Make</span>
      <select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="make" id="make" onChange={SearchCarsByMake}>
      {makes.length === 0 ? (
      <option value=''>No data found</option>
      ):(
      <>
      <option disabled defaultValue> -- All -- </option>
      {makes.map((make, index) => (
      <option key={index} value={make}>
      {make}
      </option>
      ))}
      </>
      )
      }
      </select>
      <span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Model</span>
      <select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="model" id="model" onChange={SearchCarsByModel}>
      {models.length === 0 ? (
      <option value=''>No data found</option>
      ) : (
      <>
      <option disabled defaultValue> -- All -- </option>
      {models.map((model, index) => (
      <option key={index} value={model}>
      {model}
      </option>
      ))}
      </>
      )}
      </select>
      <span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Year</span>
      <select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="year" id="year" onChange={SearchCarsByYear}>
      <option selected value='all'> -- All -- </option>
      <option value='2024'>2024 or newer</option>
      <option value='2023'>2023 or newer</option>
      <option value='2022'>2022 or newer</option>
      <option value='2021'>2021 or newer</option>
      <option value='2020'>2020 or newer</option>
      </select>
      <span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Mileage</span>
      <select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="mileage" id="mileage" onChange={SearchCarsByMileage}>
      <option selected value='all'> -- All -- </option>
      <option value='50000'>Under 50000</option>
      <option value='100000'>50000 - 100000</option>
      <option value='150000'>100000 - 150000</option>
      <option value='200000'>150000 - 200000</option>
      <option value='200001'>Over 200000</option>
      </select>
      <span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Price</span>
      <select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="price" id="price" onChange={SearchCarsByPrice}>
      <option selected value='all'> -- All -- </option>
      <option value='20000'>Under 20000</option>
      <option value='40000'>20000 - 40000</option>
      <option value='60000'>40000 - 60000</option>
      <option value='80000'>60000 - 80000</option>
      <option value='80001'>Over 80000</option>
      </select>

      <button style={{marginLeft: '10px', paddingLeft: '10px'}} onClick={reset}>Reset</button>

      </div>


      <div style={{ marginLeft: '10px', marginRight: '10px' , marginTop: '20px'}} >
      {cars.length === 0 ? (
      <p style={{ marginLeft: '10px', marginRight: '10px', marginTop: '20px' }}>{message}</p>
      ) : (
      <div>
      <hr/>
      {cars.map((car) => (
      <div>
      <div key={car._id}>
      <h3>{car.make} {car.model}</h3>
      <p>Year: {car.year}</p>
      <p>Mileage: {car.mileage}</p>
      <p>Price: {car.price}</p>
      </div>
      <hr/>
      </div>
      ))}
      </div>
      )}
      </div>
      </div>
      );
      };

      export default SearchCars;
  2. 为该组件添加路由并构建前端。

    1. frontend/src/App.js 中为该组件添加导入语句和路由,将搜索路径设为 /searchcars/:id
      1
      import SearchCars from "./components/Dealers/SearchCars";	
      1
      <Route path="/searchcars/:id" element={<SearchCars />} />
    2. 整合锚链接,将用户从特定经销商的评论页面重定向到 Search Cars 页面。
      • 转到 frontend/src/components/Dealers/Dealer.jsx,插入一个锚元素。
      • 将其标记为 Search Cars,并设置为点击后导航至 Search Cars 页面。
      1
      <a href={`/searchcars/${id}`}>SearchCars</a>	
    3. 构建应用程序的前端,以便部署。
      1
      npm run build
    4. djangoproj/urls.py 中添加路径为 searchcars/<int:dealer_id> 的动态 URL 模式。
      1
      path('searchcars/<int:dealer_id>',TemplateView.as_view(template_name="index.html")),