1. 事業紹介
オーダービジネスは電子商取引プラットフォーム全体の中核であり、比較的複雑なビジネスでもあります。「モノ」を「お金」に変えるための乗換駅です。
注文モジュール全体は 4 つの部分で構成されます。
- チェックアウトページ
ショッピングカート一覧ページには、チェックアウトボタンがあり、ユーザーがこのボタンをクリックすると、チェックアウトページにジャンプします。チェックアウトページには、ユーザーがショッピングカート内で選択した商品データ(一覧)が表示されるほか、ユーザーのすべての品目 ユーザーが選択できる荷受人情報。
ユーザーが注文送信後にブラウザのロールバック機能を使用し、更新せずに繰り返し注文を送信することを防ぐために、チェックアウト ページの生成時にチェックアウト ページにシリアル番号 (注文番号) を入力します。
このシリアル番号は UUID によってバックグラウンドで生成され、redis および決済ページに保存されます。
- 注文する
ユーザーがチェックアウト ページの注文送信ボタンをクリックして注文を開始します (注文情報は保存されます)。
- シリアル番号の確認は、決済ページのシリアル番号とredis上のシリアル番号を比較し、同じであれば注文可能、異なる場合は注文できません。
- 在庫を確認し、在庫インターフェースを呼び出して在庫を照会し、在庫がない場合は、在庫が不足していることを通知します。
- ご注文時に商品価格が変更されないように、商品価格をご確認ください。製品サービスに電話して製品の最新の価格を照会し、以前の価格と比較します。
- Orders テーブルと OrderDetails テーブルにデータを追加します。
- redisのシリアル番号を削除すると、注文は1件のみとなり、redisのシリアル番号は消えます ブラウザから決済に戻っても、決済ページにはシリアル番号が残っていますが、 redis がなくなったため、比較は機能しません。
- 注文を保存した後、rabbitMQを使用して遅延メッセージを送信し、期限切れの注文を閉じます。
- ドッキング決済サービス
注文が正常に完了すると、ユーザーは支払いページにリダイレクトされ、支払い方法を選択して支払いを行うことができます。
支払いが成功すると、支払いサービスは RabbitMQ を使用して、注文ステータスを未払いから支払い済みに変更する必要があることを注文サービスに通知します。
- ドッキング在庫管理システム
注文時に在庫を確認します。支払いが成功した後、注文サービスは RabbitMQ を使用して在庫サービスに通知し、在庫を梱包するよう通知します。在庫がロックされ、在庫が在庫から差し引かれると、差し引き後の在庫がロックされます。成功すると、在庫サービスは RabbitMQ を使用して注文サービスに通知し、ステータスを「支払い済み」から「出荷待ち」に変更します。その後、在庫が梱包されて出荷されると、注文のステータスを出荷保留中から出荷済みに変更する必要があり、物流情報がユーザーに表示されます。
2.決済ページ
入口:ショッピングカート内の「計算する」ボタンをクリックしてください。
分析する
分析ページに必要なデータ:
- ユーザー情報を取得する
- カート内で選択した商品一覧
- 受信者情報
3. 注文する
1. データ構造
orderInfo : オーダーフォーム
orderDetail : 注文の詳細
ID |
主キー。自動生成 |
荷受人 |
荷受人の名前。ページフェッチ |
荷受人の電話番号 |
荷受人の電話番号。ページフェッチ |
配達先住所 |
お届け先の住所。ページフェッチ |
合計金額 |
一時金。計算する |
注文の状況 |
注文ステータス。ユーザーに表示するために使用されます。初期値を設定します。 |
ユーザーID |
ユーザーID。スレーブ インターセプタはリクエスト属性に組み込まれています。 |
お支払い方法 |
お支払い方法(オンライン決済、代金引換)。ページフェッチ |
注文コメント |
注文の状況。ページフェッチ |
out_trade_no |
サードパーティの支払い番号。ルールによって生成される |
作成時間 |
作成時間。現在時刻を設定する |
有効期限 |
デフォルトの現在時刻 + 1 日 |
プロセスステータス |
注文の進行状況、プログラム制御、バックグラウンド管理ビュー。初期値を設定し、 |
追跡番号なし |
物流番号は最初は空ですが、納品後に追加されます |
親注文ID |
注文を分割するときに生成され、デフォルトは空です |
ID |
主キー、自動生成 |
注文ID |
注文番号。メインテーブルが保存された後、スレーブテーブルに付与されます。 |
sku_id |
製品IDページパス |
sku_name |
製品名、背景に追加 |
img_url |
バックグラウンドで追加された画像パス |
注文価格 |
商品の単価をページから取得して確認します。 |
sku_num |
ページから取得したアイテムの数 |
2. 分析して注文します。
- 文書を保存する前に確認してください: 在庫を確認し、価格を確認してください。
- ドキュメントを保存します: orderInfo orderDetail。
- 保存後、ショッピングカート内の商品を削除してください。
- 支払いページにリダイレクトします。
3. ブラウザの更新とロールバックを使用してユーザーが繰り返し注文を送信する問題を解決するにはどうすればよいですか?
決済ページに入るときにシリアル番号を生成し、決済ページの隠し要素にコピーを 1 つ保存し、Redis にコピーを 1 つ保存しました。ユーザーが注文を送信するたびに、reids のシリアル番号が一致するかどうかを確認します。ページ上で送信されたもので、それらが等しい場合は送信でき、注文が保存された後に背景のシリアル番号は削除されます。その後、ユーザーが同じページで 2 回目に送信すると、シリアル番号が一致しなくなり、注文を繰り返し保存できなくなります。
3.1 シリアル番号の生成を増やすために決済ページを修正
実装クラス @Override
public String getTradeNo(String userId) {
//キーを定義String tradeNoKey = "user:" + userId + ":tradeCode" ; //シリアル番号を定義String tradeNo = UUID.randomUUID ( ).toString().replace( "-" , "" ); redisTemplate .opsForValue().set(tradeNoKey, tradeNo); return tradeNo;
} @Override public boolean checkTradeCode(String userId, String tradeCodeNo) { //キーを定義String tradeNoKey = "user:" +ユーザーID + ":トレードコード" ;
String redisTradeNo = (String) redisTemplate.opsForValue().get(tradeNoKey);
return tradeCodeNo.equals(redisTradeNo);
}
//删除流水号
@Override
public void deleteTradeNo(String userId) {
// 定义key
String tradeNoKey = "user:" + userId + ":tradeCode";
// 删除数据
redisTemplate.delete(tradeNoKey);
} |
3.2 OrderController中的submitOrder方法中
/**
* 提交订单
* @param orderInfo
* @param request
* @return
*/
@PostMapping("auth/submitOrder")
public Result submitOrder(@RequestBody OrderInfo orderInfo, HttpServletRequest request) {
// 获取到用户Id
String userId = AuthContextHolder.getUserId(request);
orderInfo.setUserId(Long.parseLong(userId));
// 获取前台页面的流水号
String tradeNo = request.getParameter("tradeNo");
// 调用服务层的比较方法
boolean flag = orderService.checkTradeCode(userId, tradeNo);
if (!flag) {
// 比较失败!
return Result.fail().message("不能重复提交订单!");
}
// 删除流水号
orderService.deleteTradeNo(userId);
// 验证库存:
List<OrderDetail> orderDetailList = orderInfo.getOrderDetailList();
for (OrderDetail orderDetail : orderDetailList) {
// 验证库存:
boolean result = orderService.checkStock(orderDetail.getSkuId(), orderDetail.getSkuNum());
if (!result) {
return Result.fail().message(orderDetail.getSkuName() + "库存不足!");
}
// 验证价格:
BigDecimal skuPrice = productFeignClient.getSkuPrice(orderDetail.getSkuId());
if (orderDetail.getOrderPrice().compareTo(skuPrice) != 0) {
// 重新查询价格!
cartFeignClient.loadCartCache(userId);
return Result.fail().message(orderDetail.getSkuName() + "价格有变动!");
}
}
// 验证通过,保存订单!
Long orderId = orderService.saveOrderInfo(orderInfo);
return Result.ok(orderId);
}
4、验库存
通过restful接口查询商品是否有库存
一般电商系统的商品库存,都不由电商系统本身来管理,由另外一套仓库管理系统,或者进销存系统来管理,电商系统通过第三方接口调用该系统。
由于库管系统可能是异构的系统,所以不在微服务体系之内。只支持restful风格的webservice调用和消息队列的调用。
详见《库存管理系统手册》
根据手册中的接口文档,编写调用代码。
查询库存接口
四、业务流程和话术
1、订单业务流程图
2、业务话术(自己总结)
当用户点击结算的时候,这块我们用网关全局过滤器先判断用户是否登录,如果用户没有登录,则跳转到登陆页面,让用户去登录,登录成功之后,跳转到订单结算页面(这块在跳转到登录页面的时候,把之前的请求地址保存下来,作为参数进行跳转,在登录成功之后,查看是否有请求参数,如果有就跳转到对应的url,如果值为null,跳转到首页);如果用户登录了,则跳转到订单结算页面;
当跳转到订单结算页面的时候,首先对收货人地址进行管理,其次选择支付方式,一期的时候只提供了支付宝支付(微信、支付宝),确认订单信息,然后提交数据到后台,生成对应的订单表、订单详情表和订单物流表(当订单生成的时候,我们要调用对应的库存系统针对订单的商品数量进行验库存,还要进行验价格)。当订单创建成功之后,自动跳转到成功页面(将订单数据和到期时间传递过去)。
这块我们设置的订单的有效时间为24小时(这个时间可以自己定,只要合理就行),因为我们利用延时队列实现定时消息发送,消费者到时间后监听到消息,进行订单校验,如果订单是未支付状态,把订单状态修改为关闭订单。
3、常见面试问题:
- 订单表结构(存了什么信息)?
谁负责订单的开发,谁创建订单表。
订单表:
人+流程+金额+时间
收货人信息(收货人电话、地址、名称)、用户的id、总金额、订单状态、订单交易编号(全局唯一不重复,采用字符串+时间戳+随机数)、物流单编号、创建时间、失效时间。
订单明细表:
商品有关的数据
Sku的id,数量、购买的价格、默认图片
- 订单业务流程是什么样的?(订单你是怎么做的?)
看上面的文字描述,把文字描述 变成你的话 面试的时候 说出来了。
- 订单有效期是多久,怎么取消订单?
有效期 只要合理就行 30分钟、45分、2小时 24小时
10秒钟,365一天 这种的就是不合理了。
咱们使用的是 rabbitMQ的延时消息。下订单时候 发送 了一个消息,这个消息时间不到不能被消费,只有时间到了,消费者才能去消费这个消息,进行订单的关闭,关闭之前需要判断 订单是否支付,只有没支付的订单才能关闭。
- 怎么防止订单重复提交?
- 用户通过浏览器回退功能,回退到结算页,并且没有刷新页面的情况下,再次提交订单。 解决是通过 生成结算的时候 生成流水号,这个流水号存redis一份,在结算页也存了一份,当提交订单的时候,结算页中的流水号会传给提交订单接口,拿到页面传过的流水号和redis的流水号比较,相同,可以提交订单,不同不能提交订单。当订单提交完成后,删除redis的流水号,redis流水号没了,再回退提交,比较流水号就会不通过。
- 用户通过浏览器回退功能,回退到结算页,刷新页面了,重新请求了一次结算页,结算页会重新查询购物车中选中的商品数据。当用户下单成功后,可以去删除购物车中所有选中的商品。
- 订单超卖问题怎么解决的?
这个商品在库存中就一个了,有多个人同时下单,这种问题 你们怎么解决的?
咱们做的:
这个问题没解决,都可以下单成功。
为了多卖货,减少库存积压,增加流动资金。
用户都可以下单成功 也都可以付钱成功,没货了,再去进。
拼多多:2天内发货。
下单的时候 就去做库存扣减,防止超卖:
- 防止并发的情况 库存扣减变成负数。
加锁了
使用分布式锁。
数据库层面的锁。
悲观锁:
Select 库存量 from 库存表 where skuid=? for update;
Update 更新 去减库存。
事务的提交或者回滚的时候 锁释放。
乐观锁
表中加一个version字段
Select 库存量,version from 库存表 where skuid =?
Update 库存表 set (库存量-1,version+1) where skuid =? And version=之前查出的值。
- 订单状态都有什么