Skip to content

Latest commit

 

History

History
1590 lines (1199 loc) · 59.1 KB

File metadata and controls

1590 lines (1199 loc) · 59.1 KB

七、为市场扩展订单和支付

在客户下订单时处理他们的付款并允许卖家管理这些订单是电子商务应用的关键方面。在本章中,我们将通过引入以下功能来扩展上一章中构建的在线市场:

  • 购物车
  • 带条纹的付款处理
  • 订单管理

带有购物车、付款和订单的 MERN 市场

第 6 章中开发的 MERN Marketplace 应用将通过在线市场练习新的 MERN 技能,该应用将被扩展,包括购物车功能、处理信用卡付款的条带集成以及基本的订单管理流程。下面的实现保持简单,以作为开发这些功能的更复杂版本的起点。

下面的组件树图显示了构成 MERN Marketplace 前端的所有自定义组件。本章讨论的功能修改了一些现有组件,如ProfileMyShopsProductsSuggestions,并添加了新组件,如AddToCartMyOrdersCartShopOrders

The code for the complete MERN Marketplace application is available on GitHub github.com/shamahoque/mern-marketplace. You can clone this code and run the application as you go through the code explanations in the rest of this chapter. To get the code for Stripe payments working, you will need to create your own Stripe account and update the config/config.js file with your testing values for the Stripe API key, secret key, and Stripe Connect client ID.

购物车

MERN Marketplace 的访问者可以通过单击每个产品上的add to cart按钮将他们想要购买的产品添加到购物车中。当用户继续浏览市场时,菜单中的购物车图标将指示已添加到购物车中的产品数量。他们还可以通过打开购物车视图来更新购物车内容并开始结账。但要完成结账并下订单,用户需要登录。

购物车主要是一个前端功能,因此购物车的详细信息将存储在客户端本地,直到用户在结帐时下单。为了实现购物车功能,我们将在client/cart/cart-helper.js中设置助手方法,以帮助使用相关组件操作购物车详细信息。

添加到购物车

client/Cart/AddToCart.js中的AddToCart组件将product对象和 CSS 样式对象作为其添加到的父组件的道具。例如,在 MERN Marketplace 中,它被添加到产品视图中,如下所示:

<AddToCart cartStyle={classes.addCart} item={this.state.product}/>

AddToCart组件本身显示一个购物车图标按钮,具体取决于传递的商品是否有库存:

例如,如果项目数量大于0,则显示AddCartIcon,否则显示DisabledCartIcon

mern-marketplace/client/cart/AddToCart.js

{this.props.item.quantity >= 0 ? 
    <IconButton color="accent" dense="dense" onClick={this.addToCart}>
      <AddCartIcon className={this.props.cartStyle || 
     classes.iconButton}/>
    </IconButton> : 
    <IconButton disabled={true} color="accent" dense="dense"
      <DisabledCartIcon className={this.props.cartStyle || 
     classes.disabledIconButton}/>
    </IconButton>}

点击AddCartIcon按钮时调用addToCart方法。

mern-marketplace/client/cart/AddToCart.js

addToCart = () => {
    cart.addItem(this.props.item, () => {
      this.setState({redirect:true})
    })
}

cart-helper.js中定义的addItem助手方法,以product项和状态更新callback函数为参数,将更新后的购物车明细存储在localStorage中,并执行传递的回调。

mern-marketplace/client/cart/cart-helper.js

addItem(item, cb) {
    let cart = []
    if (typeof window !== "undefined") {
      if (localStorage.getItem('cart')) {
        cart = JSON.parse(localStorage.getItem('cart'))
      }
      cart.push({
        product: item,
        quantity: 1,
        shop: item.shop._id
      })
      localStorage.setItem('cart', JSON.stringify(cart))
      cb()
    }
}

localStorage中存储的购物车数据包含一个购物车项目对象数组,每个对象包含产品详细信息、添加到购物车的产品数量(默认设置为1)以及产品所属商店的 ID。

菜单上的购物车图标

在菜单中,我们将添加到购物车视图的链接,并添加一个显示localStorage中存储的购物车数组长度的徽章,以便直观地告知用户当前购物车中有多少物品:

购物车的链接与菜单中的其他链接类似,但显示购物车长度的物料界面Badge组件除外。

mern-marketplace/client/core/Menu.js

<Link to="/cart">
    <Button color={isActive(history, "/cart")}>
       Cart
       <Badge color="accent" badgeContent={cart.itemTotal()} >
           <CartIcon />
       </Badge>
    </Button>
</Link>

购物车长度由cart-helper.js中的itemTotal助手方法返回,该方法读取localStorage中存储的购物车数组并返回数组的长度。

mern-marketplace/client/cart/cart-helper.js

itemTotal() {
    if (typeof window !== "undefined") {
      if (localStorage.getItem('cart')) {
        return JSON.parse(localStorage.getItem('cart')).length
      }
    }
    return 0
}

购物车视图

购物车视图将包含购物车项目和结帐详细信息,但在用户准备结帐之前,最初仅显示购物车详细信息。

mern-marketplace/client/cart/Cart.js

<Grid container spacing={24}>
      <Grid item xs={6} sm={6}>
            <CartItems checkout={this.state.checkout}
 setCheckout={this.setCheckout}/>
      </Grid>
 {this.state.checkout && 
      <Grid item xs={6} sm={6}>
        <Checkout/>
      </Grid>}
</Grid>

CartItems组件被传递一个checkout布尔值,以及该签出值的状态更新方法,以便Checkout组件和选项可以基于用户交互进行呈现。

mern-marketplace/client/cart/Cart.js

setCheckout = val =>{
    this.setState({checkout: val})
}

Cart组件将在/cart路径访问,因此我们需要在MainRouter组件中添加一个Route,如下所示。

mern-marketplace/client/MainRouter.js

<Route path="/cart" component={Cart}/>

CartItems 组件

CartItems组件将允许用户查看和更新购物车中当前的物品。如果他们已登录,还可以选择启动签出过程:

如果购物车包含商品,CartItems组件将迭代商品并呈现购物车中的商品。如果没有添加任何项目,则购物车视图仅显示购物车为空的消息。

mern-marketplace/client/cart/CartItems.js

{this.state.cartItems.length > 0 ? <span>
      {this.state.cartItems.map((item, i) => {
          ...          
             Product details
               Edit quantity
               Remove product option
          ...
        })
      }
     … Show total price and Checkout options … 
    </span> : 
    <Typography type="subheading" component="h3" color="primary">
        No items added to your cart.    
    </Typography>
}

每个产品项显示产品的详细信息和可编辑的数量文本字段,以及删除项选项。最后,它显示购物车中物品的总价和开始结账的选项。

检索购物车详细信息

cart-helper.js中的getCart助手方法从localStorage检索并返回购物车详细信息。

mern-marketplace/client/cart/cart-helper.js

getCart() {
    if (typeof window !== "undefined") {
      if (localStorage.getItem('cart')) {
        return JSON.parse(localStorage.getItem('cart'))
      }
    }
    return []
}

CartItems组件中,我们将使用componentDidMount中的getCart助手方法检索购物车项目,并将其设置为状态。

mern-marketplace/client/cart/CartItems.js

componentDidMount = () => {
    this.setState({cartItems: cart.getCart()})
}

然后使用map函数对从localStorage检索到的cartItems数组进行迭代,以呈现每个项目的细节。

mern-marketplace/client/cart/CartItems.js

<span key={i}>
  <Card>
    <CardMedia image={'/api/product/image/'+item.product._id}
         title={item.product.name}/>
         <CardContent>
                <Link to={'/product/'+item.product._id}>
                    <Typography type="title" component="h3" 
                    color="primary">
                      {item.product.name}</Typography>
                </Link>
                <Typography type="subheading" component="h3" 
               color="primary">
                      $ {item.product.price}
                </Typography>
                <span>${item.product.price * item.quantity}</span>
                <span>Shop: {item.product.shop.name}</span>
         </CardContent>
         <div>
          … Editable quantity …
          … Remove item option ...
         </div>
  </Card>
  <Divider/>
</span> 

修改量

为每个购物车项目呈现的可编辑数量TextField允许用户更新他们正在购买的每个产品的数量,并设置允许的最小值1

mern-marketplace/client/cart/CartItems.js

Quantity: <TextField
          value={item.quantity}
          onChange={this.handleChange(i)}
          type="number"
          inputProps={{ min:1 }}
          InputLabelProps={{
            shrink: true,
          }}
        />

当用户更新此值时,调用handleChange方法来执行最小值验证,更新cartItems处于状态,并使用 helper 方法更新localStorage中的购物车。

mern-marketplace/client/cart/CartItems.js

handleChange = index => event => {
    let cartItems = this.state.cartItems 
    if(event.target.value == 0){
      cartItems[index].quantity = 1 
    }else{
      cartItems[index].quantity = event.target.value 
    }
    this.setState({cartItems: cartItems}) 
    cart.updateCart(index, event.target.value) 
  } 

updateCarthelper 方法以 cart 数组中正在更新的产品的索引和新的数量值为参数,更新localStorage中存储的详细信息。

mern-marketplace/client/cart/cart-helper.js

updateCart(itemIndex, quantity) {
    let cart = []
    if (typeof window !== "undefined") {
      if (localStorage.getItem('cart')) {
        cart = JSON.parse(localStorage.getItem('cart'))
      }
      cart[itemIndex].quantity = quantity
      localStorage.setItem('cart', JSON.stringify(cart))
    }
}

删除项目

为购物车中的每个项目呈现的移除项目选项是一个按钮,单击该按钮时,会将项目的数组索引传递给removeItem方法,以便将其从数组中移除。

mern-marketplace/client/cart/CartItems.js

<Button color="primary" onClick={this.removeItem(i)}>x Remove</Button>

removeItem点击处理程序方法使用removeItem助手方法从localStorage中的购物车中移除该项目,然后更新cartItems的状态。此方法还检查购物车是否已清空,因此可以使用作为道具从Cart组件传递的setCheckout函数隐藏签出。

mern-marketplace/client/cart/CartItems.js

removeItem = index => event =>{
    let cartItems = cart.removeItem(index)
    if(cartItems.length == 0){
      this.props.setCheckout(false)
    }
    this.setState({cartItems: cartItems})
}

cart-helper.js中的removeItem助手方法将要从数组中移除的产品的索引进行拼接,并在返回更新后的cart数组之前更新localStorage

mern-marketplace/client/cart/cart-helper.js

removeItem(itemIndex) {
    let cart = []
    if (typeof window !== "undefined") {
      if (localStorage.getItem('cart')) {
        cart = JSON.parse(localStorage.getItem('cart'))
      }
      cart.splice(itemIndex, 1)
      localStorage.setItem('cart', JSON.stringify(cart))
    }
    return cart
}

显示总价

CartItems组件的底部,我们将显示购物车中物品的总价。

mern-marketplace/client/cart/CartItems.js

<span className={classes.total}>Total: ${this.getTotal()}</span>

getTotal方法将考虑cartItems数组中每个项目的单价和数量来计算总价。

mern-marketplace/client/cart/CartItems.js

getTotal(){
    return this.state.cartItems.reduce( function(a, b){
        return a + (b.quantity*b.product.price)
    }, 0)
}

选择退房

用户将看到执行签出的选项,具体取决于他们是否已登录以及是否已打开签出。

mern-marketplace/client/cart/CartItems.js

{!this.props.checkout && (auth.isAuthenticated() ? 
    <Button onClick={this.openCheckout}>
        Checkout
    </Button> : 
    <Link to="/signin">
        <Button>Sign in to checkout</Button>
    </Link>)
}

当点击 checkout 按钮时,openCheckout方法将使用作为道具传递的setCheckout方法将Cart组件中的 checkout 值设置为true

openCheckout = () => {
    this.props.setCheckout(true)
}

一旦在购物车视图中将结帐值设置为true,将呈现Checkout组件,以允许用户输入结帐详细信息并下订单。

使用条带进行支付

付款处理需要跨结帐、订单创建和订单管理流程的实现。它还涉及更新买方和卖方的用户数据。在深入研究签出和订单功能的实现之前,我们将简要讨论使用 Stripe 的支付处理选项和注意事项,并了解如何将其集成到 MERN Marketplace 中。

条纹

Stripe 提供了在任何 web 应用中集成支付所需的广泛工具集。根据应用的特定类型和正在实施的支付用例,可以以不同的方式选择和使用这些工具。

在 MERN Marketplace 设置的情况下,应用本身将在 Stripe 上有一个平台,并且期望卖家在平台上有连接的 Stripe 帐户,因此应用可以向代表卖家在结账时输入信用卡详细信息的用户收费。在 MERN Marketplace 中,用户可以将来自不同商店的产品添加到他们的购物车中,因此他们的卡上的费用将仅由应用在卖家处理订购的特定产品时创建。此外,卖家将完全控制他们自己的条纹仪表板上代表他们创建的费用。我们将演示如何使用 Stripe 提供的工具使此支付设置正常工作。

Stripe 为每个工具提供了一套完整的文档和指南,还公开了在 Stripe 上设置的帐户和平台的测试数据。为了在 MERN Marketplace 中实现支付,我们将使用测试密钥,并让您自行扩展实时支付的实现。

每个卖家的条带连接帐户

为了代表卖家创建费用,应用将允许卖家用户将其 Stripe 帐户连接到其 MERN Marketplace 用户帐户。

更新用户模型

要在用户的条带帐户成功连接后存储条带 OAuth 凭据,我们将使用以下字段更新用户模型。

mern-marketplace/server/models/user.model.js

stripe_seller: {}

stripe_seller字段将存储卖家的 Stripe 账户凭证,当需要通过 Stripe 为他们从商店出售的产品处理费用时,将使用该凭证。

用于连接条带的按钮

在卖家的用户配置文件页面中,如果用户尚未连接其条带帐户,我们将显示一个按钮,该按钮将引导用户到条带进行身份验证并连接其条带帐户:

如果用户已成功连接其条带帐户,我们将显示禁用的条带连接按钮:

添加到Profile组件的代码将首先检查用户是否是卖家,然后再呈现任何STRIPE CONNECTED按钮。然后,第二次检查将确认给定用户的stripe_seller字段中是否已经存在条带凭据。如果用户的条带凭据已经存在,则会显示禁用的STRIPE CONNECTED按钮,否则会显示使用 OAuth 链接连接到条带的链接。

mern-marketplace/client/user/Profile.js

{this.state.user.seller &&
   (this.state.user.stripe_seller ?
       (<Button variant="raised" disabled>
            Stripe connected
        </Button>) :
       (<a href={"https://connect.stripe.com/oauth/authorize?response_type=code&client_id="+config.stripe_connect_test_client_id+"&scope=read_write"}}>
           <img src={stripeButton}/>
        </a>)
)}

OAuth 链接使用平台的客户端 ID(我们将在config变量中设置)和其他选项值作为查询参数。此链接将用户带到条带,并允许用户连接现有条带帐户或创建新的条带帐户。然后,一旦 Stripe 的身份验证过程完成,它将使用 Stripe 上仪表板中平台连接设置中设置的重定向 URL 返回到我们的应用。条带将身份验证代码或错误消息作为查询参数附加到重定向 URL。

MERN Marketplace 重定向 URI 设置为/seller/stripe/connect,将呈现StripeConnect组件。

mern-marketplace/client/MainRouter.js

<Route path="/seller/stripe/connect" component={StripeConnect}/>

StripeConnect 组件

StripeConnect组件将使用条带基本完成剩余的身份验证过程步骤,并根据条带连接是否成功呈现相关消息:

StripeConnect组件加载时,在componentDidMount中,我们将首先解析从条带重定向附加到 URL 的查询参数。对于解析,我们使用与之前用于产品搜索相同的query-stringnpm 模块。然后,如果 URLquery参数包含身份验证代码,我们将进行必要的 API 调用,以从服务器完成条带 OAuth。

mern-marketplace/client/user/StripeConnect.js

  componentDidMount = () => {
    const parsed = queryString.parse(this.props.location.search)
    if(parsed.error){
      this.setState({error: true})
    }
    if(parsed.code){
      this.setState({connecting: true, error: false})
      const jwt = auth.isAuthenticated()
      stripeUpdate({
        userId: jwt.user._id
      }, {
        t: jwt.token
      }, parsed.code).then((data) => {
        if (data.error) {
          this.setState({error: true, connected: false,
          connecting:false})
        } else {
          this.setState({connected: true, connecting: false, 
          error:false})
        }
      })
    }
 }

stripeUpdate获取方法在api-user.js中定义,它将从 Stripe 中检索到的身份验证代码传递给我们将在'/api/stripe_auth/:userId'服务器中设置的 API。

mern-marketplace/client/user/api-user.js

const stripeUpdate = (params, credentials, auth_code) => {
  return fetch('/api/stripe_auth/'+params.userId, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    },
    body: JSON.stringify({stripe: auth_code})
  }).then((response)=> {
    return response.json()
  }).catch((err) => {
    console.log(err)
  })
}

条带身份验证更新 API

一旦 Stripe 帐户连接成功,为了完成 OAuth 进程,我们需要使用检索到的身份验证代码从服务器对 Stripe OAuth 进行 POST API 调用,并检索要存储在卖方用户帐户中的凭据以支付处理费用。Stripe auth update API 在/api/stripe_auth/:userId接收到一个请求,并启动 POST API 调用以从 Stripe 检索凭据。

此条带身份验证更新 API 的路由将在服务器上的用户路由中声明,如下所示。

mern-marketplace/server/routes/user.routes.js

router.route('/api/stripe_auth/:userId')
   .put(authCtrl.requireSignin, authCtrl.hasAuthorization,   
    userCtrl.stripe_auth, userCtrl.update)

对该路由的请求使用stripe_auth控制器方法从条带中检索凭据,并将其传递给现有的用户更新方法以存储在数据库中。

为了从我们的服务器向条带 API 发出 POST 请求,我们将使用requestnpm 模块:

npm install request --save

用户控制器中的stripe_auth控制器方法如下。

mern-marketplace/server/controllers/user.controller.js

const stripe_auth = (req, res, next) => {
  request({
    url: "https://connect.stripe.com/oauth/token",
    method: "POST",
    json: true,
    body:  
  {client_secret:config.stripe_test_secret_key,code:req.body.stripe, 
  grant_type:'authorization_code'}
  }, (error, response, body) => {
    if(body.error){
      return res.status('400').json({
        error: body.error_description
      })
    }
    req.body.stripe_seller = body
    next()
  })
}

对 Stripe 的 POST API 调用使用平台的密钥和检索到的身份验证代码来完成授权,并返回连接帐户的凭据,然后将其附加到请求正文中,以便用户可以通过next()方法进行更新。

使用这些凭据,应用可以代表卖家在客户信用卡上创建费用。

用于结帐的条纹卡元素

在结账过程中,为了向用户收集信用卡详细信息,我们将使用 Stripe 的Card``Elements在结账表单中添加信用卡字段。为了将Card``Elements与我们的 React 接口集成,我们将使用react-stripe-elementsnpm 模块:

npm install --save react-stripe-elements

我们还需要在template.js中注入Stripe.js代码来访问前端代码中的条带:

<script id="stripe-js" src="https://js.stripe.com/v3/" async></script>

对于 MERN Marketplace,Checkout组件需要在购物车视图中显示Card``Elements和流程卡详细信息输入,只需要在购物车视图中显示条带。因此,我们将在Cart组件装入其componentDidMount后,使用应用的条带 API 密钥初始化条带实例。

mern-marketplace/client/cart/Cart.js

componentDidMount = () => {
    if (window.Stripe) {
      this.setState({stripe: 
     window.Stripe(config.stripe_test_api_key)})
    } else {
      document.querySelector('#stripe-js')
     .addEventListener('load', () 
     => {
        this.setState({stripe: 
     window.Stripe(config.stripe_test_api_key)})
      })
    }
 }

Cart.js中增加的Checkout组件需要用react-stripe-elements中的StripeProvider组件进行包装,这样Checkout中的Elements就可以访问 Stripe 实例。

mern-marketplace/client/cart/Cart.js

<StripeProvider stripe={this.state.stripe}> 
     <Checkout/>
</StripeProvider>

然后,在Checkout组件中,我们将使用 Stripe 的Elements组件。使用 Stripe 的Card Elements将使应用能够收集用户的信用卡详细信息,并使用 Stripe 实例标记卡信息,而不是在我们自己的服务器上处理。在结账创建新订单部分将讨论在结账过程中收集卡详细信息和生成卡令牌这一部分的实现。

为客户记录卡的详细信息

在结账过程结束时下订单时,生成的卡令牌将用于创建或更新条带客户(https://stripe.com/docs/api#customers 代表我们的用户,这是存储信用卡信息的好方法(https://stripe.com/docs/saving-cards 使用 Stripe 进一步使用,例如仅当卖家从其店铺处理订购的产品时,才为购物车中的特定产品创建费用。这消除了必须在您自己的服务器上安全存储用户信用卡详细信息的复杂性。

更新用户模型

为了跟踪数据库中用户的相应条带Customer信息,我们将使用以下字段更新用户模型:

stripe_customer: {},

更新用户控制器

当用户在输入信用卡详细信息后下订单时,我们将创建一个新的或更新现有的条带客户。为了实现这一点,我们将使用stripeCustomer方法更新用户控制器,当我们的服务器接收到对创建订单 API 的请求时,将在创建订单之前调用该方法(在创建新订单一节中讨论)。

stripeCustomer控制器方法中,我们需要使用stripenpm 模块:

npm install stripe --save

安装stripe模块后,需要导入到用户控制器文件中,并使用应用的条带密钥初始化stripe实例。

mern-marketplace/server/controllers/user.controller.js

import stripe from 'stripe'
const myStripe = stripe(config.stripe_test_secret_key)

stripeCustomer控制器方法将首先检查当前用户是否已在数据库中存储了相应的条带客户,然后使用从前端接收的卡令牌创建新的条带客户或更新现有条带客户。

创建新客户

如果当前用户没有对应的条带Customer,换句话说,stripe_customer字段没有存储值,我们将使用创建客户 API(https://stripe.com/docs/api#create_customer )来自条纹。

mern-marketplace/server/controllers/user.controller.js

myStripe.customers.create({
            email: req.profile.email,
            source: req.body.token
      }).then((customer) => {
          User.update({'_id':req.profile._id},
            {'$set': { 'stripe_customer': customer.id }},
            (err, order) => {
              if (err) {
                return res.status(400).send({
                  error: errorHandler.getErrorMessage(err)
                })
              }
              req.body.order.payment_id = customer.id
              next()
        })
})

如果成功创建条带客户,我们将通过在stripe_customer字段中存储条带客户 ID 引用来更新当前用户的数据。我们还将把这个客户 ID 添加到下订单中,因此创建与订单相关的费用更简单。

更新现有客户

对于现有的条带客户,换句话说,当前用户在stripe_customer字段中存储了一个值,我们将使用条带 API 来更新条带客户。

mern-marketplace/server/controllers/user.controller.js

 myStripe.customers.update(req.profile.stripe_customer, {
       source: req.body.token
     }, 
       (err, customer) => {
         if(err){
           return res.status(400).send({
             error: "Could not update charge details"
           })
         }
         req.body.order.payment_id = customer.id
         next()
       })

成功更新条带客户后,我们将向next()调用中创建的订单添加客户 ID。

虽然这里没有介绍,但 Stripe Customer 功能可以进一步用于允许用户从应用存储和更新其信用卡信息。

为处理的每个产品创建费用

当卖家通过处理在其店铺订购的产品来更新订单时,应用将代表卖家在客户的信用卡上为订购的产品的成本创建费用。为了实现这一点,我们将使用createCharge控制器方法更新user.controller.js文件,该方法将使用 Stripe 创建收费 API,并且需要卖方的 Stripe 帐户 ID 和买方的 Stripe 客户 ID。

mern-marketplace/server/controllers/user.controller.js

const createCharge = (req, res, next) => {
  if(!req.profile.stripe_seller){
    return res.status('400').json({
      error: "Please connect your Stripe account"
    })
  }
  myStripe.tokens.create({
    customer: req.order.payment_id,
  }, {
    stripe_account: req.profile.stripe_seller.stripe_user_id,
  }).then((token) => {
      myStripe.charges.create({
        amount: req.body.amount * 100, //amount in cents
        currency: "usd",
        source: token.id,
      }, {
        stripe_account: req.profile.stripe_seller.stripe_user_id,
      }).then((charge) => {
        next()
      })
  })
}

如果卖方尚未连接其条带帐户,createCharge方法将返回 400 错误响应,表明需要连接条带帐户。

为了能够代表卖方的条带帐户向条带客户收费,我们首先需要使用客户 ID 和卖方的条带帐户 ID 生成条带令牌,然后使用该令牌创建收费。

当服务器接收到更新订单的请求,且产品状态更改为处理时,将调用createCharge控制器方法(此订单更新请求的 API 实现将在车间订单部分讨论)。

这涵盖了与 MERN Marketplace 特定用例的支付处理实现相关的所有条带相关概念。现在我们将继续讨论允许用户完成结账并下订单。

结账

已登录并已将项目添加到购物车的用户将能够启动签出过程。结帐表单将收集客户详细信息、送货地址信息和信用卡信息:

正在初始化签出详细信息

Checkout组件中,我们将在从表单收集详细信息之前初始化状态为的checkoutDetails对象。

mern-marketplace/client/cart/Checkout.js

state = {
    checkoutDetails: {customer_name: '', customer_email:'', 
                      delivery_address: {street: '', city: '', state: 
                        '', zipcode: '', country:''}},
  }

安装组件后,我们将根据当前用户的详细信息预填充客户详细信息,并将当前购物车项目添加到checkoutDetails

mern-marketplace/client/cart/Checkout.js

componentDidMount = () => {
    let user = auth.isAuthenticated().user
    let checkoutDetails = this.state.checkoutDetails
    checkoutDetails.products = cart.getCart()
    checkoutDetails.customer_name = user.name
    checkoutDetails.customer_email = user.email
    this.setState({checkoutDetails: checkoutDetails})
}

客户信息

在结帐表单中,我们将添加文本字段以收集客户姓名和电子邮件。

mern-marketplace/client/cart/Checkout.js

<TextField id="name" label="Name" value={this.state.checkoutDetails.customer_name} onChange={this.handleCustomerChange('customer_name')}/>
<TextField id="email" type="email" label="Email" value={this.state.checkoutDetails.customer_email} onChange={this.handleCustomerChange('customer_email')}/><br/>

当用户更新值时,handleCustomerChange方法将更新状态中的相关细节:

handleCustomerChange = name => event => {
    let checkoutDetails = this.state.checkoutDetails
    checkoutDetails[name] = event.target.value || undefined
    this.setState({checkoutDetails: checkoutDetails})
}

送货地址

要从用户处收集送货地址,我们将在结帐表单中添加以下文本字段,以收集街道地址、城市、邮政编码、州和国家。

mern-marketplace/client/cart/Checkout.js

<TextField id="street" label="Street Address" value={this.state.checkoutDetails.delivery_address.street} onChange={this.handleAddressChange('street')}/>
<TextField id="city" label="City" value={this.state.checkoutDetails.delivery_address.city} onChange={this.handleAddressChange('city')}/>
<TextField id="state" label="State" value={this.state.checkoutDetails.delivery_address.state} onChange={this.handleAddressChange('state')}/>
<TextField id="zipcode" label="Zip Code" value={this.state.checkoutDetails.delivery_address.zipcode} onChange={this.handleAddressChange('zipcode')}/>
<TextField id="country" label="Country" value={this.state.checkoutDetails.delivery_address.country} onChange={this.handleAddressChange('country')}/>

当用户更新这些地址字段时,handleAddressChange方法将更新状态中的相关详细信息。

mern-marketplace/client/cart/Checkout.js

handleAddressChange = name => event => {
    let checkoutDetails = this.state.checkoutDetails
    checkoutDetails.delivery_address[name] = event.target.value || 
    undefined
    this.setState({checkoutDetails: checkoutDetails})
}

PlaceOrder 组件

信用卡字段将使用react-stripe-elements中的 StripeCardElement组件添加到结帐表单中

CardElement组件必须是使用injectStripe高阶组件HOC构建并用Elements组件包装的支付表单组件的一部分。因此,我们将创建一个名为PlaceOrder的组件,其中包含injectStripe,它将包含 Stripe 的CardElementPlaceOrder按钮。

mern-marketplace/client/cart/PlaceOrder.js

class PlaceOrder extends Component {  } 
export default injectStripe(withStyles(styles)(PlaceOrder))

然后我们将这个PlaceOrder组件添加到签出表单中,将checkoutDetails对象作为道具传递给它,并用react-stripe-elements中的Elements组件包裹它。

mern-marketplace/client/cart/Checkout.js

<Elements> <PlaceOrder checkoutDetails={this.state.checkoutDetails} /> </Elements>

injectStripeHOC 提供管理Elements组的this.props.stripe属性。这将允许我们在PlaceOrder内致电this.props.stripe.createToken向 Stripe 提交卡详细信息并取回卡令牌。

条纹元件

Stripe 的CardElement是独立的,所以我们只需将其添加到PlaceOrder组件中,然后根据需要添加样式,就可以完成卡片细节的输入。

mern-marketplace/client/cart/PlaceOrder.js

<CardElement className={classes.StripeElement}
      {...{style: {
      base: {
        color: '#424770',
        letterSpacing: '0.025em',
        '::placeholder': {
          color: '#aab7c4',
        },
      },
      invalid: {
        color: '#9e2146',
      },
    }}}/>

下订单

下订单按钮也放置在CardElement之后的PlaceOrder组件中。

mern-marketplace/client/cart/PlaceOrder.js

<Button color="secondary" variant="raised" onClick={this.placeOrder}>Place Order</Button>

点击 Place Order 按钮将调用placeOrder方法,该方法将尝试使用stripe.createToken标记卡的详细信息。如果失败,将通知用户错误,但如果成功,则签出详细信息和生成的卡令牌将发送到服务器的创建订单 API(将在下一节中介绍)。

mern-marketplace/client/cart/PlaceOrder.js

placeOrder = ()=>{
  this.props.stripe.createToken().then(payload => {
      if(payload.error){
        this.setState({error: payload.error.message})
      }else{
        const jwt = auth.isAuthenticated()
        create({userId:jwt.user._id}, {
          t: jwt.token
        }, this.props.checkoutDetails, payload.token.id).then((data) => 
        {
          if (data.error) {
            this.setState({error: data.error})
          } else {
            cart.emptyCart(()=> {
              this.setState({'orderId':data._id,'redirect': true})
            })
          }
        })
      }
  })
}

client/order/api-order.js中定义了向后端的创建订单 API 发出 POST 请求的create获取方法。它将签出详细信息、卡令牌和用户凭据作为参数,并将其发送到位于/api/orders/:userId的 API。

mern-marketplace/client/order/api-order.js

const create = (params, credentials, order, token) => {
  return fetch('/api/orders/'+params.userId, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + credentials.t
      },
      body: JSON.stringify({order: order, token:token})
    })
    .then((response) => {
      return response.json()
    }).catch((err) => console.log(err))
}

空车

如果创建订单 API 成功,我们将使用cart-helper.js中的emptyCart助手方法清空购物车。

mern-marketplace/client/cart/cart-helper.js

emptyCart(cb) {
  if(typeof window !== "undefined"){
     localStorage.removeItem('cart')
     cb()
  }
}

emptyCart方法从localStorage中删除 cart 对象,并通过执行传递的回调更新视图的状态。

重定向到订单视图

在下单且购物车清空后,用户将被重定向到订单视图,该视图将向他们显示刚刚下单的详细信息。

mern-marketplace/client/cart/PlaceOrder.js

if (this.state.redirect) {
      return (<Redirect to={'/order/' + this.state.orderId}/>)
}

这将表明签出过程已经完成,成功调用了 createorderapi,我们将在服务器中设置该 API,以便在数据库中创建和存储订单。

创建新秩序

当用户下订单时,结帐时确认的订单的详细信息将用于在数据库中创建新的订单记录,更新或为用户创建条带客户,以及减少订购产品的库存量。

订单模型

为了存储订单,我们将为订单模型定义一个 Mongoose 模式,该模式将记录客户详细信息以及用户帐户引用、交货地址信息、付款引用、在时间戳创建和更新,以及一个订购产品数组,其中每个产品的结构将在一个单独的子模式中定义,该子模式称为CartItemSchema

由客户和为客户订购

为了记录订单所针对的客户的详细信息,我们将在Order模式中添加customer_namecustomer_email字段。

mern-marketplace/server/models/order.model.js

customer_name: { type: String,  trim: true, required: 'Name is required' },
customer_email: { type: String, trim: true,
    match: [/.+\@.+\..+/, 'Please fill a valid email address'],
    required: 'Email is required' }

为了引用下订单的登录用户,我们将添加一个ordered_by字段。

mern-marketplace/server/models/order.model.js

ordered_by: {type: mongoose.Schema.ObjectId, ref: 'User'}

送货地址

订单的交货地址信息将存储在交货地址子文档中,包含streetcitystatezipcodecountry字段。

mern-marketplace/server/models/order.model.js

delivery_address: {
    street: {type: String, required: 'Street is required'},
    city: {type: String, required: 'City is required'},
    state: {type: String},
    zipcode: {type: String, required: 'Zip Code is required'},
    country: {type: String, required: 'Country is required'}
  },

付款参考

当订单更新时,付款信息将是相关的,并且在卖方处理订单产品后需要创建费用。我们将在Order模式的payment_id字段中记录与信用卡详细信息相关的客户 ID。

mern-marketplace/server/models/order.model.js

payment_id: {},

订购的产品

订单的主要内容将是订购的产品清单以及详细信息,如每种产品的数量。我们将在Order模式中名为products的字段中记录此列表。每个产品的结构将在CartItemSchema中单独定义。

mern-marketplace/server/models/order.model.js

products: [CartItemSchema],

CartItem 模式

CartItem模式将表示订购的每个产品。它将包含对产品的引用、用户订购的产品数量、对产品所属商店的引用以及状态。

mern-marketplace/server/models/order.model.js

const CartItemSchema = new mongoose.Schema({
  product: {type: mongoose.Schema.ObjectId, ref: 'Product'},
  quantity: Number,
  shop: {type: mongoose.Schema.ObjectId, ref: 'Shop'},
  status: {type: String,
    default: 'Not processed',
    enum: ['Not processed' , 'Processing', 'Shipped', 'Delivered', 
   'Cancelled']}
}) 
const CartItem = mongoose.model('CartItem', CartItemSchema)

产品的status只能具有枚举中定义的值,表示卖方更新的订购产品的当前状态。

此处定义的Order模式将记录客户和卖方完成订购产品的采购步骤所需的详细信息。

创建订单 API

创建订单 API 路由在server/routes/order.routes.js中声明。订单路线与用户路线非常相似。要在 Express 应用中加载订单路由,我们需要在express.js中装载路由,就像我们对身份验证和用户路由所做的那样。

mern-marketplace/server/express.js

app.use('/', orderRoutes)

当创建订单 API 在/api/orders/:userId接收到 POST 请求时,将按以下顺序执行许多操作:

  • 确保用户已登录
  • 使用前面讨论的stripeCustomer用户控制器方法创建或更新条带Customer
  • 使用decreaseQuanity产品控制器方法更新所有订购产品的库存量
  • 订单是通过create订单控制器方法在订单集合中创建的

路线定义如下。

mern-marketplace/server/routes/order.routes.js

router.route('/api/orders/:userId') 
    .post(authCtrl.requireSignin, userCtrl.stripeCustomer, 
          productCtrl.decreaseQuantity, orderCtrl.create)

为了检索路由中与:userId参数相关联的用户,我们将使用userByID用户控制器方法,该方法从用户集合中获取用户,并将其附加到下一个方法访问的请求对象。我们将添加它与订单路线如下。

mern-marketplace/server/routes/order.routes.js

router.param('userId', userCtrl.userByID)

减少产品库存量

我们将更新产品控制器文件以添加decreaseQuantity控制器方法,该方法将更新新订单中购买的所有产品的库存数量。

mern-marketplace/server/controllers/product.controller.js

const decreaseQuantity = (req, res, next) => {
  let bulkOps = req.body.order.products.map((item) => {
    return {
        "updateOne": {
            "filter": { "_id": item.product._id } ,
            "update": { "$inc": {"quantity": -item.quantity} }
        }
    }
   })
   Product.bulkWrite(bulkOps, {}, (err, products) => {
     if(err){
       return res.status(400).json({
         error: "Could not update product"
       })
     }
     next()
   })
}

由于本例中的更新操作涉及到集合中的多个产品在与订购的产品数组匹配后的批量更新,我们将使用 MongoDB 中的bulkWrite方法,通过一条命令向 MongoDB 服务器发送多个updateOne操作。首先使用map功能在bulkOps中列出所需的多个updateOne操作。这将比发送多个独立的保存或更新操作更快,因为使用bulkWrite()到 MongoDB 只有一次往返。

创建订单控制器方法

订单控制器中定义的create控制器方法获取订单详细信息,创建新订单,并将其保存到 MongoDB 中的订单集合。

mern-marketplace/server/controllers/order.controller.js

const create = (req, res) => {
  req.body.order.user = req.profile
  const order = new Order(req.body.order)
  order.save((err, result) => {
    if (err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.status(200).json(result)
  })
}

通过实现此功能,MERN Marketplace 上的任何登录用户都可以在后端创建和存储订单。现在,我们可以设置 API 来按用户、按商店获取订单列表,或者读取单个订单并将获取的数据显示在前端的视图中。

按店铺订购

市场的一个重要功能是允许卖家查看和更新他们在商店收到的产品订单的状态。为了实现这一点,我们将首先设置 API 以按店铺列出订单,然后在卖家更改购买产品的状态时更新订单。

按店铺空气污染指数列出

我们将实现一个 API 来获取特定店铺的订单,这样经过身份验证的卖家就可以查看他们每个店铺的订单。此 API 的请求将在'/api/orders/shop/:shopId接收,路径在order.routes.js中定义如下。

mern-marketplace/server/routes/order.routes.js

router.route('/api/orders/shop/:shopId') 
    .get(authCtrl.requireSignin, shopCtrl.isOwner, orderCtrl.listByShop)
router.param('shopId', shopCtrl.shopByID)

要检索与路由中的:shopId参数关联的店铺,我们将使用shopByID店铺控制器方法,该方法从店铺集合中获取店铺,并将其附加到下一个方法访问的请求对象

listByShop控制器方法将检索购买了具有匹配店铺 ID 的产品的订单,然后填充每个产品的 ID、名称和价格字段,订单按日期从最近到最早排序。

mern-marketplace/server/controllers/order.controller.js

const listByShop = (req, res) => {
  Order.find({"products.shop": req.shop._id})
  .populate({path: 'products.product', select: '_id name price'})
  .sort('-created')
  .exec((err, orders) => {
    if (err) {
      return res.status(400).json({
        error: errorHandler.getErrorMessage(err)
      })
    }
    res.json(orders)
  })
}

为了在前端获取此 API,我们将在api-order.js中添加相应的listByShop方法,用于ShopOrders组件中,以显示每个店铺的订单。

mern-marketplace/client/order/api-order.js

const listByShop = (params, credentials) => {
  return fetch('/api/orders/shop/'+params.shopId, {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    }
  }).then((response) => {
    return response.json()
  }).catch((err) => {
    console.log(err)
  })
}

ShopOrders 组件

卖家将在ShopOrders组件中查看其订单列表,每个订单仅显示与店铺相关的已购买产品,并允许卖家通过可能的状态值下拉菜单更改产品状态:

我们将用一个PrivateRoute更新MainRouter,在/seller/orders/:shop/:shopId路线加载ShopOrders组件。

mern-marketplace/client/MainRouter.js

<PrivateRoute path="/seller/orders/:shop/:shopId" component={ShopOrders}/>

列出订单

ShopOrders组件挂载时,我们将使用listByShopfetch 方法加载相关订单,并将检索到的订单设置为 state。

mern-marketplace/client/order/ShopOrders.js

 loadOrders = () => {
    const jwt = auth.isAuthenticated()
    listByShop({
      shopId: this.match.params.shopId
    }, {t: jwt.token}).then((data) => {
      if (data.error) {
        console.log(data)
      } else {
        this.setState({orders: data})
      }
    })
 }

在视图中,我们将遍历订单列表,并从Material-UI将每个订单呈现在可折叠列表中,该列表将在单击时展开。

mern-marketplace/client/order/ShopOrders.js

<Typography type="title"> Orders in {this.match.params.shop} </Typography>
<List dense> {this.state.orders.map((order, index) => { return 
    <span key={index}>
        <ListItem button onClick={this.handleClick(index)}>
           <ListItemText primary={'Order # '+order._id} 
                 secondary={(new Date(order.created)).toDateString()}/>
           {this.state.open == index ? <ExpandLess /> : <ExpandMore />}
        </ListItem>
        <Collapse component="li" in={this.state.open == index} 
       timeout="auto" unmountOnExit>
           <ProductOrderEdit shopId={this.match.params.shopId} 
           order={order} orderIndex={index} 
           updateOrders={this.updateOrders}/>
           <Typography type="subheading"> Deliver to:</Typography>
           <Typography type="subheading" color="primary">
               {order.customer_name} ({order.customer_email})
          </Typography>
           <Typography type="subheading" color="primary">
               {order.delivery_address.street}</Typography>
           <Typography type="subheading" color="primary">
               {order.delivery_address.city}, 
           {order.delivery_address.state}
               {order.delivery_address.zipcode}</Typography>
           <Typography type="subheading" color="primary">
               {order.delivery_address.country}</Typography>
        </Collapse>
    </span>})}
</List>

每个扩展订单将显示订单详细信息和ProductOrderEdit组件。ProductOrderEdit组件将显示购买的产品,并允许卖方编辑每个产品的状态。updateOrders方法作为道具传递给ProductOrderEdit组件,以便在产品状态更改时更新状态。

mern-marketplace/client/order/ShopOrders.js

updateOrders = (index, updatedOrder) => {
    let orders = this.state.orders 
    orders[index] = updatedOrder 
    this.setState({orders: orders}) 
}

ProductOrderEdit 组件

ProductOrderEdit组件将订单对象作为道具,并遍历订单的 products 数组,以仅显示从当前商店购买的产品,同时使用下拉菜单更改每个产品的状态值。

mern-marketplace/client/order/ProductOrderEdit.js

{this.props.order.products.map((item, index) => { return <span key={index}> 
     { item.shop == this.props.shopId && 
          <ListItem button>
              <ListItemText primary={ <div>
                     <img src=
                    {'/api/product/image/'+item.product._id}/> 
                     {item.product.name}
                     <p>{"Quantity: "+item.quantity}</p>
              </div>}/>
              <TextField id="select-status" select
                   label="Update Status" value={item.status}
                   onChange={this.handleStatusChange(index)}
                   SelectProps={{
                       MenuProps: { className: classes.menu },
                   }}>
                      {this.state.statusValues.map(option => (
                          <MenuItem key={option} value={option}>
                            {option}
                          </MenuItem>
                      ))}
              </TextField>
          </ListItem>}

ProductOrderEdit组件加载并设置为statusValues中的状态以在下拉列表中呈现为MenuItem时,从服务器获取可能的状态值列表。

mern-marketplace/client/order/ProductOrderEdit.js

loadStatusValues = () => {
    getStatusValues().then((data) => {
      if (data.error) {
        this.setState({error: "Could not get status"})
      } else {
        this.setState({statusValues: data, error: ''})
      }
    })
}

当从可能的状态值中选择一个选项时,将调用handleStatusChange方法更新处于状态的订单,并根据所选状态值向相应的后端 API 发送请求。

mern-marketplace/client/order/ProductOrderEdit.js

handleStatusChange = productIndex => event => {
    let order = this.props.order 
    order.products[productIndex].status = event.target.value 
    let product = order.products[productIndex] 
    const jwt = auth.isAuthenticated() 
    if(event.target.value == "Cancelled"){
       cancelProduct({ shopId: this.props.shopId, 
       productId: product.product._id }, 
       {t: jwt.token}, 
       {cartItemId: product._id, status: 
       event.target.value, 
       quantity: product.quantity
       }).then((data) => { 
       if (data.error) {
       this.setState({error: "Status not updated, 
       try again"})
       } else {
 this.props.updateOrders(this.props.orderIndex, order)      this.setState(error: '') 
       } 
       }) 
       } else if(event.target.value == "Processing"){
       processCharge({ userId: jwt.user._id, shopId: 
       this.props.shopId, orderId: order._id }, 
       { t: jwt.token}, 
       { cartItemId: product._id, 
       amount: (product.quantity *
       product.product.price)
       status: event.target.value }).then((data) => { ... 
       })
       } else {
       update({ shopId: this.props.shopId }, {t: 
       jwt.token}, 
       { cartItemId: product._id, 
       status: event.target.value}).then((data) => { ... })
      }
}

api-order.js中定义了cancelProductprocessChargeupdate取数方法,用于在后端调用相应的 API 来更新已取消产品的库存量,在处理产品时在客户的信用卡上创建费用,以及在产品状态发生变化时更新订单。

订购产品的原料药

允许卖家更新产品的状态需要设置四种不同的 API,包括检索可能的状态值的 API。然后,实际状态更新将需要 API 来处理状态更改时订单本身的更新,启动相关操作,例如增加取消产品的库存量,以及在处理产品时在客户的信用卡上创建费用。

获取状态值

订购产品的可能状态值在CartItem模式中设置为枚举,为了在下拉视图中将这些值显示为选项,我们将在/api/order/status_values设置获取这些值的 GET API 路由。

mern-marketplace/server/routes/order.routes.js

router.route('/api/order/status_values')
    .get(orderCtrl.getStatusValues)

getStatusValues控制器方法将从CartItem模式返回status字段的枚举值。

mern-marketplace/server/controllers/order.controller.js

const getStatusValues = (req, res) => {
  res.json(CartItem.schema.path('status').enumValues)
}

我们还将在api-order.js中设置一个fetch方法,用于视图中向 API 路由发出请求。

mern-marketplace/client/order/api-order.js

const getStatusValues = () => {
  return fetch('/api/order/status_values', {
    method: 'GET'
  }).then((response) => {
    return response.json()
  }).catch((err) => console.log(err))
}

更新订单状态

当产品状态更改为除处理取消之外的任何值时,如果当前用户是订购产品的店铺的已验证所有者,则对'/api/order/status/:shopId'的 PUT 请求将直接更新数据库中的订单。

mern-marketplace/server/routes/order.routes.js

router.route('/api/order/status/:shopId')
    .put(authCtrl.requireSignin, shopCtrl.isOwner, orderCtrl.update)

update控制器方法将查询订单集合,找到与更新产品匹配的CartItem对象的订单,并在订单的products数组中设置该匹配的CartItemstatus值。

mern-marketplace/server/controllers/order.controller.js

const update = (req, res) => {
  Order.update({'products._id':req.body.cartItemId}, {'$set': {
        'products.$.status': req.body.status
    }}, (err, order) => {
      if (err) {
        return res.status(400).send({
          error: errorHandler.getErrorMessage(err)
        })
      }
      res.json(order)
    })
}

api-order.js中,我们将添加一个updatefetch 方法,使用从视图传递的所需参数调用此更新 API。

mern-marketplace/client/order/api-order.js

const update = (params, credentials, product) => {
  return fetch('/api/order/status/' + params.shopId, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    },
    body: JSON.stringify(product)
  }).then((response) => {
    return response.json()
  }).catch((err) => {
    console.log(err)
  }) 
}

取消产品订单

当卖家决定取消某个产品的订单时,会向/api/order/:shopId/cancel/:productId发送 PUT 请求,以便增加产品库存量,并在数据库中更新订单。

mern-marketplace/server/routes/order.routes.js

router.route('/api/order/:shopId/cancel/:productId')
       .put(authCtrl.requireSignin, shopCtrl.isOwner,
       productCtrl.increaseQuantity, orderCtrl.update)
       router.param('productId', productCtrl.productByID)

要检索路由中与productId参数关联的产品,我们将使用productByID产品控制器方法

product.controller.js增加了increaseQuantity控制器方法。它根据产品集合中的匹配 ID 查找产品,并根据客户订购的数量增加数量值,因为此产品的订单已取消。

mern-marketplace/server/controllers/product.controller.js

const increaseQuantity = (req, res, next) => {
  Product.findByIdAndUpdate(req.product._id, {$inc: 
  {"quantity": req.body.quantity}}, {new: true})
    .exec((err, result) => {
      if (err) {
        return res.status(400).json({
          error: errorHandler.getErrorMessage(err)
        })
      }
      next()
    })
}

从这个角度来看,我们将使用api-order.js中增加的相应的取数方法来调用取消产品订单 API。

mern-marketplace/client/order/api-order.js

const cancelProduct = (params, credentials, product) => {
  return fetch('/api/order/'+params.shopId+'/cancel/'+params.productId, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    },
    body: JSON.stringify(product)
  }).then((response) => {
    return response.json()
  }).catch((err) => {
    console.log(err)
  })
}

产品加工费

当卖家将产品状态更改为处理时,我们将设置一个后端 API,不仅更新订单,还将在客户的信用卡上为产品价格乘以订购数量创建费用。

mern-marketplace/server/routes/order.routes.js

router.route('/api/order/:orderId/charge/:userId/:shopId')
            .put(authCtrl.requireSignin, shopCtrl.isOwner,     
            userCtrl.createCharge, orderCtrl.update)
router.param('orderId', orderCtrl.orderByID)

为了检索路由中与orderId参数关联的订单,我们将使用orderByID订单控制器方法,该方法从订单集合中获取订单,并将其附加到next方法访问的请求对象,如下所示。

mern-marketplace/server/controllers/order.controller.js:

const orderByID = (req, res, next, id) => {
  Order.findById(id).populate('products.product', 'name price')
       .populate('products.shop', 'name')
       .exec((err, order) => {
          if (err || !order)
            return res.status('400').json({
              error: "Order not found"
            })
          req.order = order
          next()
       })
}

此过程费用 API 将在/api/order/:orderId/charge/:userId/:shopId接收 PUT 请求,在成功验证后,用户将通过调用createCharge用户控制器创建费用,如前面的使用条带支付部分所述,然后最终使用update方法更新订单。

从这个角度来看,我们将使用api-order.js中的processCharge取数方式,并提供所需的路由参数值、凭证和产品详细信息,包括收费金额。

mern-marketplace/client/order/api-order.js

const processCharge = (params, credentials, product) => {
  return fetch('/api/order/'+params.orderId+'/charge/'+params.userId+'/'
    +params.shopId, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + credentials.t
    },
    body: JSON.stringify(product)
  }).then((response) => {
    return response.json()
  }).catch((err) => {
    console.log(err)
  })
}

卖家可以查看在每个店铺收到的产品订单,他们可以轻松地更新每个订购产品的状态,同时应用负责其他任务,例如更新库存数量和启动付款。这涵盖了 MERN Marketplace 应用的基本订单管理功能,可以根据需要进一步扩展。

查看订单详细信息

在订单收集和数据库访问都已设置的情况下,向前看,很容易添加为每个用户列出订单的功能,并在单独的视图中显示单个订单的详细信息,用户可以在其中跟踪每个订购产品的状态:

按照本书中重复的步骤,为了设置后端 API 来检索数据,并在前端使用它来构建前端视图,您可以根据需要开发订单相关视图,灵感来自 MERN Marketplace 应用代码中这些示例视图的快照:

在本章第 6 章中开发的 MERN Marketplace 应用通过构建 MERN 骨架应用,运用在线市场的新 MERN 技能,涵盖了标准在线市场应用的关键功能。这反过来又说明了如何扩展 MERN 堆栈以合并复杂的功能。

总结

在本章中,我们扩展了 MERN Marketplace 应用,并探讨了如何在在线市场应用中为买家添加购物车、使用信用卡付款的结账流程以及为卖家添加订单管理。

我们发现了 MERN stack 技术如何与第三方集成很好地协同工作,因为我们实现了购物车结帐流程,并使用 Stripe 提供的用于管理在线支付的工具处理订购产品的信用卡费用

我们还解锁了 MERN 的更多功能,例如 MongoDB 中优化的批量写入操作,用于响应单个 API 调用更新多个文档。这允许我们一次性减少多个产品的库存量,例如当用户从不同的商店订购多个产品时。

MERN marketplace 应用中开发的 marketplace 功能揭示了如何通过添加简单或更复杂的功能,利用此堆栈和结构来设计和构建不断增长的应用。

在下一章中,我们将学习本书到目前为止所学到的经验教训,并通过扩展 MERN 框架构建媒体流应用,探索 MERN 更高级的可能性。