RocketMQ學習筆記 2
1. 案例介紹
1.1 業務分析
模擬電商網站購物場景中的【下單】和【支付】業務
1)下單
- 使用者請求訂單系統下單
- 訂單系統通過RPC呼叫訂單服務下單
- 訂單服務呼叫優惠券服務,扣減優惠券
- 訂單服務呼叫呼叫庫存服務,校驗並扣減庫存
- 訂單服務呼叫使用者服務,扣減使用者餘額
- 訂單服務完成確認訂單
2)支付
- 使用者請求支付系統
- 支付系統呼叫第三方支付平臺API進行發起支付流程
- 使用者通過第三方支付平臺支付成功後,第三方支付平臺回撥通知支付系統
- 支付系統呼叫訂單服務修改訂單狀態
- 支付系統呼叫積分服務新增積分
- 支付系統呼叫日誌服務記錄日誌
1.2 問題分析
問題1
使用者提交訂單後,扣減庫存成功、扣減優惠券成功、使用餘額成功,但是在確認訂單操作失敗,需要對庫存、庫存、餘額進行回退。
如何保證資料的完整性?
使用MQ保證在下單失敗後系統資料的完整性
問題2
使用者通過第三方支付平臺(支付寶、微信)支付成功後,第三方支付平臺要通過回撥API非同步通知商家支付系統使用者支付結果,支付系統根據支付結果修改訂單狀態、記錄支付日誌和給使用者增加積分。
商家支付系統如何保證在收到第三方支付平臺的非同步通知時,如何快速給第三方支付憑條做出回應?
通過MQ進行資料分發,提高系統處理效能
2. 技術分析
2.1 技術選型
- SpringBoot
- Dubbo
- Zookeeper
- RocketMQ
- Mysql
2.2 SpringBoot整合RocketMQ
下載rocketmq-spring專案
將rocketmq-spring安裝到本地倉庫
mvn install -Dmaven.skip.test=true
2.2.1 訊息生產者
1)新增依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<properties>
<rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot-starter-version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot-starter-version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2)配置檔案
# application.properties
rocketmq.name-server=192.168.25.135:9876;192.168.25.138:9876
rocketmq.producer.group=my-group
3)啟動類
@SpringBootApplication
public class MQProducerApplication {
public static void main(String[] args) {
SpringApplication.run(MQSpringBootApplication.class);
}
}
4)測試類
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MQSpringBootApplication.class})
public class ProducerTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void test1(){
rocketMQTemplate.convertAndSend("springboot-mq","hello springboot rocketmq");
}
}
2.2.2 訊息消費者
1)新增依賴
同訊息生產者
2)配置檔案
同訊息生產者
3)啟動類
@SpringBootApplication
public class MQConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(MQSpringBootApplication.class);
}
}
4)訊息監聽器
@Slf4j
@Component
@RocketMQMessageListener(topic = "springboot-mq",consumerGroup = "springboot-mq-consumer-1")
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
log.info("Receive message:"+message);
}
}
2.3 SpringBoot整合Dubbo
下載dubbo-spring-boot-starter依賴包
將dubbo-spring-boot-starter
安裝到本地倉庫
mvn install -Dmaven.skip.test=true
2.3.1 搭建Zookeeper叢集
1)準備工作
- 安裝JDK
- 將Zookeeper上傳到伺服器
- 解壓Zookeeper,並建立data目錄,將conf下的zoo_sample.cfg檔案改名為zoo.cfg
- 建立
/user/local/zookeeper-cluster
,將解壓後的Zookeeper複製到以下三個目錄
/usr/local/zookeeper-cluster/zookeeper-1
/usr/local/zookeeper-cluster/zookeeper-2
/usr/local/zookeeper-cluster/zookeeper-3
-
配置每一個 Zookeeper 的 dataDir(zoo.cfg) clientPort 分別為 2181 2182 2183
修改
/usr/local/zookeeper-cluster/zookeeper-1/conf/zoo.cfg
clientPort=2181
dataDir=/usr/local/zookeeper-cluster/zookeeper-1/data
修改/usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
clientPort=2182
dataDir=/usr/local/zookeeper-cluster/zookeeper-2/data
修改/usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
clientPort=2183
dataDir=/usr/local/zookeeper-cluster/zookeeper-3/data
2)配置叢集
-
在每個 zookeeper 的 data 目錄下建立一個 myid 檔案,內容分別是 1、2、3 。這個檔案就是記錄每個伺服器的 ID
-
在每一個 zookeeper 的 zoo.cfg 配置客戶端訪問埠(clientPort)和叢集伺服器 IP 列表。
叢集伺服器 IP 列表如下
server.1=192.168.25.140:2881:3881
server.2=192.168.25.140:2882:3882
server.3=192.168.25.140:2883:3883
解釋:server.伺服器 ID=伺服器 IP 地址:伺服器之間通訊埠:伺服器之間投票選舉埠
3)啟動叢集
啟動叢集就是分別啟動每個例項。
2.3.2 RPC服務介面
public interface IUserService {
public String sayHello(String name);
}
2.3.3 服務提供者
1)新增依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<!--dubbo-->
<dependency>
<groupId>com.alibaba.spring.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!--spring-boot-stater-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<artifactId>log4j-to-slf4j</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--zookeeper-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--API-->
<dependency>
<groupId>com.itheima.demo</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2)配置檔案
# application.properties
spring.application.name=dubbo-demo-provider
spring.dubbo.application.id=dubbo-demo-provider
spring.dubbo.application.name=dubbo-demo-provider
spring.dubbo.registry.address=zookeeper://192.168.25.140:2181;zookeeper://192.168.25.140:2182;zookeeper://192.168.25.140:2183
spring.dubbo.server=true
spring.dubbo.protocol.name=dubbo
spring.dubbo.protocol.port=20880
3)啟動類
@EnableDubboConfiguration
@SpringBootApplication
public class ProviderBootstrap {
public static void main(String[] args) throws IOException {
SpringApplication.run(ProviderBootstrap.class,args);
}
}
4)服務實現
@Component
@Service(interfaceClass = IUserService.class)
public class UserServiceImpl implements IUserService{
@Override
public String sayHello(String name) {
return "hello:"+name;
}
}
2.3.4 服務消費者
1)新增依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--dubbo-->
<dependency>
<groupId>com.alibaba.spring.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<artifactId>log4j-to-slf4j</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--zookeeper-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--API-->
<dependency>
<groupId>com.itheima.demo</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2)配置檔案
# application.properties
spring.application.name=dubbo-demo-consumer
spring.dubbo.application.name=dubbo-demo-consumer
spring.dubbo.application.id=dubbo-demo-consumer
spring.dubbo.registry.address=zookeeper://192.168.25.140:2181;zookeeper://192.168.25.140:2182;zookeeper://192.168.25.140:2183
3)啟動類
@EnableDubboConfiguration
@SpringBootApplication
public class ConsumerBootstrap {
public static void main(String[] args) {
SpringApplication.run(ConsumerBootstrap.class);
}
}
4)Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Reference
private IUserService userService;
@RequestMapping("/sayHello")
public String sayHello(String name){
return userService.sayHello(name);
}
}
3. 環境搭建
3.1 資料庫
1)優惠券表
Field | Type | Comment |
---|---|---|
coupon_id | bigint(50) NOT NULL | 優惠券ID |
coupon_price | decimal(10,2) NULL | 優惠券金額 |
user_id | bigint(50) NULL | 使用者ID |
order_id | bigint(32) NULL | 訂單ID |
is_used | int(1) NULL | 是否使用 0未使用 1已使用 |
used_time | timestamp NULL | 使用時間 |
2)商品表
Field | Type | Comment |
---|---|---|
goods_id | bigint(50) NOT NULL | 主鍵 |
goods_name | varchar(255) NULL | 商品名稱 |
goods_number | int(11) NULL | 商品庫存 |
goods_price | decimal(10,2) NULL | 商品價格 |
goods_desc | varchar(255) NULL | 商品描述 |
add_time | timestamp NULL | 新增時間 |
3)訂單表
Field | Type | Comment |
---|---|---|
order_id | bigint(50) NOT NULL | 訂單ID |
user_id | bigint(50) NULL | 使用者ID |
order_status | int(1) NULL | 訂單狀態 0未確認 1已確認 2已取消 3無效 4退款 |
pay_status | int(1) NULL | 支付狀態 0未支付 1支付中 2已支付 |
shipping_status | int(1) NULL | 發貨狀態 0未發貨 1已發貨 2已退貨 |
address | varchar(255) NULL | 收貨地址 |
consignee | varchar(255) NULL | 收貨人 |
goods_id | bigint(50) NULL | 商品ID |
goods_number | int(11) NULL | 商品數量 |
goods_price | decimal(10,2) NULL | 商品價格 |
goods_amount | decimal(10,0) NULL | 商品總價 |
shipping_fee | decimal(10,2) NULL | 運費 |
order_amount | decimal(10,2) NULL | 訂單價格 |
coupon_id | bigint(50) NULL | 優惠券ID |
coupon_paid | decimal(10,2) NULL | 優惠券 |
money_paid | decimal(10,2) NULL | 已付金額 |
pay_amount | decimal(10,2) NULL | 支付金額 |
add_time | timestamp NULL | 建立時間 |
confirm_time | timestamp NULL | 訂單確認時間 |
pay_time | timestamp NULL | 支付時間 |
4)訂單商品日誌表
Field | Type | Comment |
---|---|---|
goods_id | int(11) NOT NULL | 商品ID |
order_id | varchar(32) NOT NULL | 訂單ID |
goods_number | int(11) NULL | 庫存數量 |
log_time | datetime NULL | 記錄時間 |
5)使用者表
Field | Type | Comment |
---|---|---|
user_id | bigint(50) NOT NULL | 使用者ID |
user_name | varchar(255) NULL | 使用者姓名 |
user_password | varchar(255) NULL | 使用者密碼 |
user_mobile | varchar(255) NULL | 手機號 |
user_score | int(11) NULL | 積分 |
user_reg_time | timestamp NULL | 註冊時間 |
user_money | decimal(10,0) NULL | 使用者餘額 |
6)使用者餘額日誌表
Field | Type | Comment |
---|---|---|
user_id | bigint(50) NOT NULL | 使用者ID |
order_id | bigint(50) NOT NULL | 訂單ID |
money_log_type | int(1) NOT NULL | 日誌型別 1訂單付款 2 訂單退款 |
use_money | decimal(10,2) NULL | 操作金額 |
create_time | timestamp NULL | 日誌時間 |
7)訂單支付表
Field | Type | Comment |
---|---|---|
pay_id | bigint(50) NOT NULL | 支付編號 |
order_id | bigint(50) NULL | 訂單編號 |
pay_amount | decimal(10,2) NULL | 支付金額 |
is_paid | int(1) NULL | 是否已支付 1否 2是 |
8)MQ訊息生產表
Field | Type | Comment |
---|---|---|
id | varchar(100) NOT NULL | 主鍵 |
group_name | varchar(100) NULL | 生產者組名 |
msg_topic | varchar(100) NULL | 訊息主題 |
msg_tag | varchar(100) NULL | Tag |
msg_key | varchar(100) NULL | Key |
msg_body | varchar(500) NULL | 訊息內容 |
msg_status | int(1) NULL | 0:未處理;1:已經處理 |
create_time | timestamp NOT NULL | 記錄時間 |
###9)MQ訊息消費表
Field | Type | Comment |
---|---|---|
msg_id | varchar(50) NULL | 訊息ID |
group_name | varchar(100) NOT NULL | 消費者組名 |
msg_tag | varchar(100) NOT NULL | Tag |
msg_key | varchar(100) NOT NULL | Key |
msg_body | varchar(500) NULL | 訊息體 |
consumer_status | int(1) NULL | 0:正在處理;1:處理成功;2:處理失敗 |
consumer_times | int(1) NULL | 消費次數 |
consumer_timestamp | timestamp NULL | 消費時間 |
remark | varchar(500) NULL | 備註 |
3.2 專案初始化
shop系統基於Maven進行專案管理
3.1.1 工程瀏覽
- 父工程:shop-parent
- 訂單系統:shop-order-web
- 支付系統:shop-pay-web
- 優惠券服務:shop-coupon-service
- 訂單服務:shop-order-service
- 支付服務:shop-pay-service
- 商品服務:shop-goods-service
- 使用者服務:shop-user-service
- 實體類:shop-pojo
- 持久層:shop-dao
- 介面層:shop-api
- 工具工程:shop-common
共12個系統
3.1.2 工程關係
3.3 Mybatis逆向工程使用
1)程式碼生成
使用Mybatis逆向工程針對資料表生成CURD持久層程式碼
2)程式碼匯入
- 將實體類匯入到shop-pojo工程
- 在服務層工程中匯入對應的Mapper類和對應配置檔案
3.4 公共類介紹
-
ID生成器
IDWorker:Twitter雪花演算法
-
異常處理類
CustomerException:自定義異常類
CastException:異常丟擲類
-
常量類
ShopCode:系統狀態類
-
響應實體類
Result:封裝響應狀態和響應資訊
4. 下單業務
4.1 下單基本流程
1)介面定義
- IOrderService
public interface IOrderService {
/**
* 確認訂單
* @param order
* @return Result
*/
Result confirmOrder(TradeOrder order);
}
###2)業務類實現
@Slf4j
@Component
@Service(interfaceClass = IOrderService.class)
public class OrderServiceImpl implements IOrderService {
@Override
public Result confirmOrder(TradeOrder order) {
//1.校驗訂單
//2.生成預訂單
try {
//3.扣減庫存
//4.扣減優惠券
//5.使用餘額
//6.確認訂單
//7.返回成功狀態
} catch (Exception e) {
//1.確認訂單失敗,傳送訊息
//2.返回失敗狀態
}
}
}
3)校驗訂單
private void checkOrder(TradeOrder order) {
//1.校驗訂單是否存在
if(order==null){
CastException.cast(ShopCode.SHOP_ORDER_INVALID);
}
//2.校驗訂單中的商品是否存在
TradeGoods goods = goodsService.findOne(order.getGoodsId());
if(goods==null){
CastException.cast(ShopCode.SHOP_GOODS_NO_EXIST);
}
//3.校驗下單使用者是否存在
TradeUser user = userService.findOne(order.getUserId());
if(user==null){
CastException.cast(ShopCode.SHOP_USER_NO_EXIST);
}
//4.校驗商品單價是否合法
if(order.getGoodsPrice().compareTo(goods.getGoodsPrice())!=0){
CastException.cast(ShopCode.SHOP_GOODS_PRICE_INVALID);
}
//5.校驗訂單商品數量是否合法
if(order.getGoodsNumber()>=goods.getGoodsNumber()){
CastException.cast(ShopCode.SHOP_GOODS_NUM_NOT_ENOUGH);
}
log.info("校驗訂單通過");
}
4)生成預訂單
private Long savePreOrder(TradeOrder order) {
//1.設定訂單狀態為不可見
order.setOrderStatus(ShopCode.SHOP_ORDER_NO_CONFIRM.getCode());
//2.訂單ID
order.setOrderId(idWorker.nextId());
//核算運費是否正確
BigDecimal shippingFee = calculateShippingFee(order.getOrderAmount());
if (order.getShippingFee().compareTo(shippingFee) != 0) {
CastException.cast(ShopCode.SHOP_ORDER_SHIPPINGFEE_INVALID);
}
//3.計算訂單總價格是否正確
BigDecimal orderAmount = order.getGoodsPrice().multiply(new BigDecimal(order.getGoodsNumber()));
orderAmount.add(shippingFee);
if (orderAmount.compareTo(order.getOrderAmount()) != 0) {
CastException.cast(ShopCode.SHOP_ORDERAMOUNT_INVALID);
}
//4.判斷優惠券資訊是否合法
Long couponId = order.getCouponId();
if (couponId != null) {
TradeCoupon coupon = couponService.findOne(couponId);
//優惠券不存在
if (coupon == null) {
CastException.cast(ShopCode.SHOP_COUPON_NO_EXIST);
}
//優惠券已經使用
if ((ShopCode.SHOP_COUPON_ISUSED.getCode().toString())
.equals(coupon.getIsUsed().toString())) {
CastException.cast(ShopCode.SHOP_COUPON_INVALIED);
}
order.setCouponPaid(coupon.getCouponPrice());
} else {
order.setCouponPaid(BigDecimal.ZERO);
}
//5.判斷餘額是否正確
BigDecimal moneyPaid = order.getMoneyPaid();
if (moneyPaid != null) {
//比較餘額是否大於0
int r = order.getMoneyPaid().compareTo(BigDecimal.ZERO);
//餘額小於0
if (r == -1) {
CastException.cast(ShopCode.SHOP_MONEY_PAID_LESS_ZERO);
}
//餘額大於0
if (r == 1) {
//查詢使用者資訊
TradeUser user = userService.findOne(order.getUserId());
if (user == null) {
CastException.cast(ShopCode.SHOP_USER_NO_EXIST);
}
//比較餘額是否大於使用者賬戶餘額
if (user.getUserMoney().compareTo(order.getMoneyPaid().longValue()) == -1) {
CastException.cast(ShopCode.SHOP_MONEY_PAID_INVALID);
}
order.setMoneyPaid(order.getMoneyPaid());
}
} else {
order.setMoneyPaid(BigDecimal.ZERO);
}
//計算訂單支付總價
order.setPayAmount(orderAmount.subtract(order.getCouponPaid())
.subtract(order.getMoneyPaid()));
//設定訂單新增時間
order.setAddTime(new Date());
//儲存預訂單
int r = orderMapper.insert(order);
if (ShopCode.SHOP_SUCCESS.getCode() != r) {
CastException.cast(ShopCode.SHOP_ORDER_SAVE_ERROR);
}
log.info("訂單:["+order.getOrderId()+"]預訂單生成成功");
return order.getOrderId();
}
5)扣減庫存
- 通過dubbo呼叫商品服務完成扣減庫存
private void reduceGoodsNum(TradeOrder order) {
TradeGoodsNumberLog goodsNumberLog = new TradeGoodsNumberLog();
goodsNumberLog.setGoodsId(order.getGoodsId());
goodsNumberLog.setOrderId(order.getOrderId());
goodsNumberLog.setGoodsNumber(order.getGoodsNumber());
Result result = goodsService.reduceGoodsNum(goodsNumberLog);
if (result.getSuccess().equals(ShopCode.SHOP_FAIL.getSuccess())) {
CastException.cast(ShopCode.SHOP_REDUCE_GOODS_NUM_FAIL);
}
log.info("訂單:["+order.getOrderId()+"]扣減庫存["+order.getGoodsNumber()+"個]成功");
}
- 商品服務GoodsService扣減庫存
@Override
public Result reduceGoodsNum(TradeGoodsNumberLog goodsNumberLog) {
if (goodsNumberLog == null ||
goodsNumberLog.getGoodsNumber() == null ||
goodsNumberLog.getOrderId() == null ||
goodsNumberLog.getGoodsNumber() == null ||
goodsNumberLog.getGoodsNumber().intValue() <= 0) {
CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID);
}
TradeGoods goods = goodsMapper.selectByPrimaryKey(goodsNumberLog.getGoodsId());
if(goods.getGoodsNumber()<goodsNumberLog.getGoodsNumber()){
//庫存不足
CastException.cast(ShopCode.SHOP_GOODS_NUM_NOT_ENOUGH);
}
//減庫存
goods.setGoodsNumber(goods.getGoodsNumber()-goodsNumberLog.getGoodsNumber());
goodsMapper.updateByPrimaryKey(goods);
//記錄庫存操作日誌
goodsNumberLog.setGoodsNumber(-(goodsNumberLog.getGoodsNumber()));
goodsNumberLog.setLogTime(new Date());
goodsNumberLogMapper.insert(goodsNumberLog);
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage());
}
6)扣減優惠券
- 通過dubbo完成扣減優惠券
private void changeCoponStatus(TradeOrder order) {
//判斷使用者是否使用優惠券
if (!StringUtils.isEmpty(order.getCouponId())) {
//封裝優惠券物件
TradeCoupon coupon = couponService.findOne(order.getCouponId());
coupon.setIsUsed(ShopCode.SHOP_COUPON_ISUSED.getCode());
coupon.setUsedTime(new Date());
coupon.setOrderId(order.getOrderId());
Result result = couponService.changeCouponStatus(coupon);
//判斷執行結果
if (result.getSuccess().equals(ShopCode.SHOP_FAIL.getSuccess())) {
//優惠券使用失敗
CastException.cast(ShopCode.SHOP_COUPON_USE_FAIL);
}
log.info("訂單:["+order.getOrderId()+"]使用扣減優惠券["+coupon.getCouponPrice()+"元]成功");
}
}
- 優惠券服務CouponService更改優惠券狀態
@Override
public Result changeCouponStatus(TradeCoupon coupon) {
try {
//判斷請求引數是否合法
if (coupon == null || StringUtils.isEmpty(coupon.getCouponId())) {
CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID);
}
//更新優惠券狀態為已使用
couponMapper.updateByPrimaryKey(coupon);
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage());
} catch (Exception e) {
return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage());
}
}
7)扣減使用者餘額
- 通過使用者服務完成扣減餘額
private void reduceMoneyPaid(TradeOrder order) {
//判斷訂單中使用的餘額是否合法
if (order.getMoneyPaid() != null && order.getMoneyPaid().compareTo(BigDecimal.ZERO) == 1) {
TradeUserMoneyLog userMoneyLog = new TradeUserMoneyLog();
userMoneyLog.setOrderId(order.getOrderId());
userMoneyLog.setUserId(order.getUserId());
userMoneyLog.setUseMoney(order.getMoneyPaid());
userMoneyLog.setMoneyLogType(ShopCode.SHOP_USER_MONEY_PAID.getCode());
//扣減餘額
Result result = userService.changeUserMoney(userMoneyLog);
if (result.getSuccess().equals(ShopCode.SHOP_FAIL.getSuccess())) {
CastException.cast(ShopCode.SHOP_USER_MONEY_REDUCE_FAIL);
}
log.info("訂單:["+order.getOrderId()+"扣減餘額["+order.getMoneyPaid()+"元]成功]");
}
}
- 使用者服務UserService,更新餘額
@Override
public Result changeUserMoney(TradeUserMoneyLog userMoneyLog) {
//判斷請求引數是否合法
if (userMoneyLog == null
|| userMoneyLog.getUserId() == null
|| userMoneyLog.getUseMoney() == null
|| userMoneyLog.getOrderId() == null
|| userMoneyLog.getUseMoney().compareTo(BigDecimal.ZERO) <= 0) {
CastException.cast(ShopCode.SHOP_REQUEST_PARAMETER_VALID);
}
//查詢該訂單是否存在付款記錄
TradeUserMoneyLogExample userMoneyLogExample = new TradeUserMoneyLogExample();
userMoneyLogExample.createCriteria()
.andUserIdEqualTo(userMoneyLog.getUserId())
.andOrderIdEqualTo(userMoneyLog.getOrderId());
int count = userMoneyLogMapper.countByExample(userMoneyLogExample);
TradeUser tradeUser = new TradeUser();
tradeUser.setUserId(userMoneyLog.getUserId());
tradeUser.setUserMoney(userMoneyLog.getUseMoney().longValue());
//判斷餘額操作行為
//【付款操作】
if (userMoneyLog.getMoneyLogType().equals(ShopCode.SHOP_USER_MONEY_PAID.getCode())) {
//訂單已經付款,則拋異常
if (count > 0) {
CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY);
}
//使用者賬戶扣減餘額
userMapper.reduceUserMoney(tradeUser);
}
//【退款操作】
if (userMoneyLog.getMoneyLogType().equals(ShopCode.SHOP_USER_MONEY_REFUND.getCode())) {
//如果訂單未付款,則不能退款,拋異常
if (count == 0) {
CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY);
}
//防止多次退款
userMoneyLogExample = new TradeUserMoneyLogExample();
userMoneyLogExample.createCriteria()
.andUserIdEqualTo(userMoneyLog.getUserId())
.andOrderIdEqualTo(userMoneyLog.getOrderId())
.andMoneyLogTypeEqualTo(ShopCode.SHOP_USER_MONEY_REFUND.getCode());
count = userMoneyLogMapper.countByExample(userMoneyLogExample);
if (count > 0) {
CastException.cast(ShopCode.SHOP_USER_MONEY_REFUND_ALREADY);
}
//使用者賬戶新增餘額
userMapper.addUserMoney(tradeUser);
}
//記錄使用者使用餘額日誌
userMoneyLog.setCreateTime(new Date());
userMoneyLogMapper.insert(userMoneyLog);
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage());
}
###8)確認訂單
private void updateOrderStatus(TradeOrder order) {
order.setOrderStatus(ShopCode.SHOP_ORDER_CONFIRM.getCode());
order.setPayStatus(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY.getCode());
order.setConfirmTime(new Date());
int r = orderMapper.updateByPrimaryKey(order);
if (r <= 0) {
CastException.cast(ShopCode.SHOP_ORDER_CONFIRM_FAIL);
}
log.info("訂單:["+order.getOrderId()+"]狀態修改成功");
}
9)小結
@Override
public Result confirmOrder(TradeOrder order) {
//1.校驗訂單
checkOrder(order);
//2.生成預訂單
Long orderId = savePreOrder(order);
order.setOrderId(orderId);
try {
//3.扣減庫存
reduceGoodsNum(order);
//4.扣減優惠券
changeCoponStatus(order);
//5.使用餘額
reduceMoneyPaid(order);
//6.確認訂單
updateOrderStatus(order);
log.info("訂單:["+orderId+"]確認成功");
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage());
} catch (Exception e) {
//確認訂單失敗,傳送訊息
...
return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage());
}
}
4.2 失敗補償機制
4.2.1 訊息傳送方
- 配置RocketMQ屬性值
rocketmq.name-server=192.168.25.135:9876;192.168.25.138:9876
rocketmq.producer.group=orderProducerGroup
mq.order.consumer.group.name=order_orderTopic_cancel_group
mq.order.topic=orderTopic
mq.order.tag.confirm=order_confirm
mq.order.tag.cancel=order_cancel
- 注入模板類和屬性值資訊
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Value("${mq.order.topic}")
private String topic;
@Value("${mq.order.tag.cancel}")
private String cancelTag;
- 傳送下單失敗訊息
@Override
public Result confirmOrder(TradeOrder order) {
//1.校驗訂單
//2.生成預訂
try {
//3.扣減庫存
//4.扣減優惠券
//5.使用餘額
//6.確認訂單
} catch (Exception e) {
//確認訂單失敗,傳送訊息
CancelOrderMQ cancelOrderMQ = new CancelOrderMQ();
cancelOrderMQ.setOrderId(order.getOrderId());
cancelOrderMQ.setCouponId(order.getCouponId());
cancelOrderMQ.setGoodsId(order.getGoodsId());
cancelOrderMQ.setGoodsNumber(order.getGoodsNumber());
cancelOrderMQ.setUserId(order.getUserId());
cancelOrderMQ.setUserMoney(order.getMoneyPaid());
try {
sendMessage(topic,
cancelTag,
cancelOrderMQ.getOrderId().toString(),
JSON.toJSONString(cancelOrderMQ));
} catch (Exception e1) {
e1.printStackTrace();
CastException.cast(ShopCode.SHOP_MQ_SEND_MESSAGE_FAIL);
}
return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage());
}
}
private void sendMessage(String topic, String tags, String keys, String body) throws Exception {
//判斷Topic是否為空
if (StringUtils.isEmpty(topic)) {
CastException.cast(ShopCode.SHOP_MQ_TOPIC_IS_EMPTY);
}
//判斷訊息內容是否為空
if (StringUtils.isEmpty(body)) {
CastException.cast(ShopCode.SHOP_MQ_MESSAGE_BODY_IS_EMPTY);
}
//訊息體
Message message = new Message(topic, tags, keys, body.getBytes());
//傳送訊息
rocketMQTemplate.getProducer().send(message);
}
4.2.2 消費接收方
- 配置RocketMQ屬性值
rocketmq.name-server=192.168.25.135:9876;192.168.25.138:9876
mq.order.consumer.group.name=order_orderTopic_cancel_group
mq.order.topic=orderTopic
- 建立監聽類,消費訊息
@Slf4j
@Component
@RocketMQMessageListener(topic = "${mq.order.topic}",
consumerGroup = "${mq.order.consumer.group.name}",
messageModel = MessageModel.BROADCASTING)
public class CancelOrderConsumer implements RocketMQListener<MessageExt>{
@Override
public void onMessage(MessageExt messageExt) {
...
}
}
1)回退庫存
- 流程分析
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Qt7ySDlb-1602929081740)(img/回退庫存.png)]
- 訊息消費者
@Slf4j
@Component
@RocketMQMessageListener(topic = "${mq.order.topic}",consumerGroup = "${mq.order.consumer.group.name}",messageModel = MessageModel.BROADCASTING )
public class CancelMQListener implements RocketMQListener<MessageExt>{
@Value("${mq.order.consumer.group.name}")
private String groupName;
@Autowired
private TradeGoodsMapper goodsMapper;
@Autowired
private TradeMqConsumerLogMapper mqConsumerLogMapper;
@Autowired
private TradeGoodsNumberLogMapper goodsNumberLogMapper;
@Override
public void onMessage(MessageExt messageExt) {
String msgId=null;
String tags=null;
String keys=null;
String body=null;
try {
//1. 解析訊息內容
msgId = messageExt.getMsgId();
tags= messageExt.getTags();
keys= messageExt.getKeys();
body= new String(messageExt.getBody(),"UTF-8");
log.info("接受訊息成功");
//2. 查詢訊息消費記錄
TradeMqConsumerLogKey primaryKey = new TradeMqConsumerLogKey();
primaryKey.setMsgTag(tags);
primaryKey.setMsgKey(keys);
primaryKey.setGroupName(groupName);
TradeMqConsumerLog mqConsumerLog = mqConsumerLogMapper.selectByPrimaryKey(primaryKey);
if(mqConsumerLog!=null){
//3. 判斷如果消費過...
//3.1 獲得訊息處理狀態
Integer status = mqConsumerLog.getConsumerStatus();
//處理過...返回
if(ShopCode.SHOP_MQ_MESSAGE_STATUS_SUCCESS.getCode().intValue()==status.intValue()){
log.info("訊息:"+msgId+",已經處理過");
return;
}
//正在處理...返回
if(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode().intValue()==status.intValue()){
log.info("訊息:"+msgId+",正在處理");
return;
}
//處理失敗
if(ShopCode.SHOP_MQ_MESSAGE_STATUS_FAIL.getCode().intValue()==status.intValue()){
//獲得訊息處理次數
Integer times = mqConsumerLog.getConsumerTimes();
if(times>3){
log.info("訊息:"+msgId+",訊息處理超過3次,不能再進行處理了");
return;
}
mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode());
//使用資料庫樂觀鎖更新
TradeMqConsumerLogExample example = new TradeMqConsumerLogExample();
TradeMqConsumerLogExample.Criteria criteria = example.createCriteria();
criteria.andMsgTagEqualTo(mqConsumerLog.getMsgTag());
criteria.andMsgKeyEqualTo(mqConsumerLog.getMsgKey());
criteria.andGroupNameEqualTo(groupName);
criteria.andConsumerTimesEqualTo(mqConsumerLog.getConsumerTimes());
int r = mqConsumerLogMapper.updateByExampleSelective(mqConsumerLog, example);
if(r<=0){
//未修改成功,其他執行緒併發修改
log.info("併發修改,稍後處理");
}
}
}else{
//4. 判斷如果沒有消費過...
mqConsumerLog = new TradeMqConsumerLog();
mqConsumerLog.setMsgTag(tags);
mqConsumerLog.setMsgKey(keys);
mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_PROCESSING.getCode());
mqConsumerLog.setMsgBody(body);
mqConsumerLog.setMsgId(msgId);
mqConsumerLog.setConsumerTimes(0);
//將訊息處理資訊新增到資料庫
mqConsumerLogMapper.insert(mqConsumerLog);
}
//5. 回退庫存
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
Long goodsId = mqEntity.getGoodsId();
TradeGoods goods = goodsMapper.selectByPrimaryKey(goodsId);
goods.setGoodsNumber(goods.getGoodsNumber()+mqEntity.getGoodsNum());
goodsMapper.updateByPrimaryKey(goods);
//記錄庫存操作日誌
TradeGoodsNumberLog goodsNumberLog = new TradeGoodsNumberLog();
goodsNumberLog.setOrderId(mqEntity.getOrderId());
goodsNumberLog.setGoodsId(goodsId);
goodsNumberLog.setGoodsNumber(mqEntity.getGoodsNum());
goodsNumberLog.setLogTime(new Date());
goodsNumberLogMapper.insert(goodsNumberLog);
//6. 將訊息的處理狀態改為成功
mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_SUCCESS.getCode());
mqConsumerLog.setConsumerTimestamp(new Date());
mqConsumerLogMapper.updateByPrimaryKey(mqConsumerLog);
log.info("回退庫存成功");
} catch (Exception e) {
e.printStackTrace();
TradeMqConsumerLogKey primaryKey = new TradeMqConsumerLogKey();
primaryKey.setMsgTag(tags);
primaryKey.setMsgKey(keys);
primaryKey.setGroupName(groupName);
TradeMqConsumerLog mqConsumerLog = mqConsumerLogMapper.selectByPrimaryKey(primaryKey);
if(mqConsumerLog==null){
//資料庫未有記錄
mqConsumerLog = new TradeMqConsumerLog();
mqConsumerLog.setMsgTag(tags);
mqConsumerLog.setMsgKey(keys);
mqConsumerLog.setConsumerStatus(ShopCode.SHOP_MQ_MESSAGE_STATUS_FAIL.getCode());
mqConsumerLog.setMsgBody(body);
mqConsumerLog.setMsgId(msgId);
mqConsumerLog.setConsumerTimes(1);
mqConsumerLogMapper.insert(mqConsumerLog);
}else{
mqConsumerLog.setConsumerTimes(mqConsumerLog.getConsumerTimes()+1);
mqConsumerLogMapper.updateByPrimaryKeySelective(mqConsumerLog);
}
}
}
}
2)回退優惠券
@Slf4j
@Component
@RocketMQMessageListener(topic = "${mq.order.topic}",consumerGroup = "${mq.order.consumer.group.name}",messageModel = MessageModel.BROADCASTING )
public class CancelMQListener implements RocketMQListener<MessageExt>{
@Autowired
private TradeCouponMapper couponMapper;
@Override
public void onMessage(MessageExt message) {
try {
//1. 解析訊息內容
String body = new String(message.getBody(), "UTF-8");
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
log.info("接收到訊息");
//2. 查詢優惠券資訊
TradeCoupon coupon = couponMapper.selectByPrimaryKey(mqEntity.getCouponId());
//3.更改優惠券狀態
coupon.setUsedTime(null);
coupon.setIsUsed(ShopCode.SHOP_COUPON_UNUSED.getCode());
coupon.setOrderId(null);
couponMapper.updateByPrimaryKey(coupon);
log.info("回退優惠券成功");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("回退優惠券失敗");
}
}
}
3)回退餘額
@Slf4j
@Component
@RocketMQMessageListener(topic = "${mq.order.topic}",consumerGroup = "${mq.order.consumer.group.name}",messageModel = MessageModel.BROADCASTING )
public class CancelMQListener implements RocketMQListener<MessageExt>{
@Autowired
private IUserService userService;
@Override
public void onMessage(MessageExt messageExt) {
try {
//1.解析訊息
String body = new String(messageExt.getBody(), "UTF-8");
MQEntity mqEntity = JSON.parseObject(body, MQEntity.class);
log.info("接收到訊息");
if(mqEntity.getUserMoney()!=null && mqEntity.getUserMoney().compareTo(BigDecimal.ZERO)>0){
//2.呼叫業務層,進行餘額修改
TradeUserMoneyLog userMoneyLog = new TradeUserMoneyLog();
userMoneyLog.setUseMoney(mqEntity.getUserMoney());
userMoneyLog.setMoneyLogType(ShopCode.SHOP_USER_MONEY_REFUND.getCode());
userMoneyLog.setUserId(mqEntity.getUserId());
userMoneyLog.setOrderId(mqEntity.getOrderId());
userService.updateMoneyPaid(userMoneyLog);
log.info("餘額回退成功");
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("餘額回退失敗");
}
}
}
4)取消訂單
@Override
public void onMessage(MessageExt messageExt) {
String body = new String(messageExt.getBody(), "UTF-8");
String msgId = messageExt.getMsgId();
String tags = messageExt.getTags();
String keys = messageExt.getKeys();
log.info("CancelOrderProcessor receive message:"+messageExt);
CancelOrderMQ cancelOrderMQ = JSON.parseObject(body, CancelOrderMQ.class);
TradeOrder order = orderService.findOne(cancelOrderMQ.getOrderId());
order.setOrderStatus(ShopCode.SHOP_ORDER_CANCEL.getCode());
orderService.changeOrderStatus(order);
log.info("訂單:["+order.getOrderId()+"]狀態設定為取消");
return order;
}
4.3 測試
1)準備測試環境
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShopOrderServiceApplication.class)
public class OrderTest {
@Autowired
private IOrderService orderService;
}
1)準備測試資料
- 使用者資料
- 商品資料
- 優惠券資料
2)測試下單成功流程
@Test
public void add(){
Long goodsId=XXXL;
Long userId=XXXL;
Long couponId=XXXL;
TradeOrder order = new TradeOrder();
order.setGoodsId(goodsId);
order.setUserId(userId);
order.setGoodsNumber(1);
order.setAddress("北京");
order.setGoodsPrice(new BigDecimal("5000"));
order.setOrderAmount(new BigDecimal("5000"));
order.setMoneyPaid(new BigDecimal("100"));
order.setCouponId(couponId);
order.setShippingFee(new BigDecimal(0));
orderService.confirmOrder(order);
}
執行完畢後,檢視資料庫中使用者的餘額、優惠券資料,及訂單的狀態資料
3)測試下單失敗流程
程式碼同上。
執行完畢後,檢視使用者的餘額、優惠券資料是否發生更改,訂單的狀態是否為取消。
5. 支付業務
5.1 建立支付訂單
public Result createPayment(TradePay tradePay) {
//查詢訂單支付狀態
try {
TradePayExample payExample = new TradePayExample();
TradePayExample.Criteria criteria = payExample.createCriteria();
criteria.andOrderIdEqualTo(tradePay.getOrderId());
criteria.andIsPaidEqualTo(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode());
int count = tradePayMapper.countByExample(payExample);
if (count > 0) {
CastException.cast(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY);
}
long payId = idWorker.nextId();
tradePay.setPayId(payId);
tradePay.setIsPaid(ShopCode.SHOP_ORDER_PAY_STATUS_NO_PAY.getCode());
tradePayMapper.insert(tradePay);
log.info("建立支付訂單成功:" + payId);
} catch (Exception e) {
return new Result(ShopCode.SHOP_FAIL.getSuccess(), ShopCode.SHOP_FAIL.getMessage());
}
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage());
}
5.2 支付回撥
5.2.1 流程分析
5.2.2 程式碼實現
public Result callbackPayment(TradePay tradePay) {
if (tradePay.getIsPaid().equals(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode())) {
tradePay = tradePayMapper.selectByPrimaryKey(tradePay.getPayId());
if (tradePay == null) {
CastException.cast(ShopCode.SHOP_PAYMENT_NOT_FOUND);
}
tradePay.setIsPaid(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode());
int i = tradePayMapper.updateByPrimaryKeySelective(tradePay);
//更新成功代表支付成功
if (i == 1) {
TradeMqProducerTemp mqProducerTemp = new TradeMqProducerTemp();
mqProducerTemp.setId(String.valueOf(idWorker.nextId()));
mqProducerTemp.setGroupName("payProducerGroup");
mqProducerTemp.setMsgKey(String.valueOf(tradePay.getPayId()));
mqProducerTemp.setMsgTag(topic);
mqProducerTemp.setMsgBody(JSON.toJSONString(tradePay));
mqProducerTemp.setCreateTime(new Date());
mqProducerTempMapper.insert(mqProducerTemp);
TradePay finalTradePay = tradePay;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
SendResult sendResult = sendMessage(topic,
tag,
finalTradePay.getPayId(),
JSON.toJSONString(finalTradePay));
log.info(JSON.toJSONString(sendResult));
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
mqProducerTempMapper.deleteByPrimaryKey(mqProducerTemp.getId());
System.out.println("刪除訊息表成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
CastException.cast(ShopCode.SHOP_PAYMENT_IS_PAID);
}
}
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(), ShopCode.SHOP_SUCCESS.getMessage());
}
執行緒池優化訊息傳送邏輯
- 建立執行緒池物件
@Bean
public ThreadPoolTaskExecutor getThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("Pool-A");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
- 使用執行緒池
@Autowired
private ThreadPoolTaskExecutor executorService;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
SendResult sendResult = sendMessage(topic, tag, finalTradePay.getPayId(), JSON.toJSONString(finalTradePay));
log.info(JSON.toJSONString(sendResult));
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
mqProducerTempMapper.deleteByPrimaryKey(mqProducerTemp.getId());
System.out.println("刪除訊息表成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
5.2.3
處理訊息
支付成功後,支付服務payService傳送MQ訊息,訂單服務、使用者服務、日誌服務需要訂閱訊息進行處理
- 訂單服務修改訂單狀態為已支付
- 日誌服務記錄支付日誌
- 使用者服務負責給使用者增加積分
以下用訂單服務為例說明訊息的處理情況
1)配置RocketMQ屬性值
mq.pay.topic=payTopic
mq.pay.consumer.group.name=pay_payTopic_group
2)消費訊息
- 在訂單服務中,配置公共的訊息處理類
public class BaseConsumer {
public TradeOrder handleMessage(IOrderService
orderService,
MessageExt messageExt,Integer code) throws Exception {
//解析訊息內容
String body = new String(messageExt.getBody(), "UTF-8");
String msgId = messageExt.getMsgId();
String tags = messageExt.getTags();
String keys = messageExt.getKeys();
OrderMQ orderMq = JSON.parseObject(body, OrderMQ.class);
//查詢
TradeOrder order = orderService.findOne(orderMq.getOrderId());
if(ShopCode.SHOP_ORDER_MESSAGE_STATUS_CANCEL.getCode().equals(code)){
order.setOrderStatus(ShopCode.SHOP_ORDER_CANCEL.getCode());
}
if(ShopCode.SHOP_ORDER_MESSAGE_STATUS_ISPAID.getCode().equals(code)){
order.setPayStatus(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode());
}
orderService.changeOrderStatus(order);
return order;
}
}
- 接受訂單支付成功訊息
@Slf4j
@Component
@RocketMQMessageListener(topic = "${mq.pay.topic}",
consumerGroup = "${mq.pay.consumer.group.name}")
public class PayConsumer extends BaseConsumer implements RocketMQListener<MessageExt> {
@Autowired
private IOrderService orderService;
@Override
public void onMessage(MessageExt messageExt) {
try {
log.info("CancelOrderProcessor receive message:"+messageExt);
TradeOrder order = handleMessage(orderService,
messageExt,
ShopCode.SHOP_ORDER_MESSAGE_STATUS_ISPAID.getCode());
log.info("訂單:["+order.getOrderId()+"]支付成功");
} catch (Exception e) {
e.printStackTrace();
log.error("訂單支付失敗");
}
}
}
6. 整體聯調
通過Rest客戶端請求shop-order-web和shop-pay-web完成下單和支付操作
6.1 準備工作
1)配置RestTemplate類
@Configuration
public class RestTemplateConfig {
@Bean
@ConditionalOnMissingBean({ RestOperations.class, RestTemplate.class })
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
// 使用 utf-8 編碼集的 conver 替換預設的 conver(預設的 string conver 的編碼集為"ISO-8859-1")
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
Iterator<HttpMessageConverter<?>> iterator = messageConverters.iterator();
while (iterator.hasNext()) {
HttpMessageConverter<?> converter = iterator.next();
if (converter instanceof StringHttpMessageConverter) {
iterator.remove();
}
}
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
@Bean
@ConditionalOnMissingBean({ClientHttpRequestFactory.class})
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// ms
factory.setReadTimeout(15000);
// ms
factory.setConnectTimeout(15000);
return factory;
}
}
2)配置請求地址
- 訂單系統
server.host=http://localhost
server.servlet.path=/order-web
server.port=8080
shop.order.baseURI=${server.host}:${server.port}${server.servlet.path}
shop.order.confirm=/order/confirm
- 支付系統
server.host=http://localhost
server.servlet.path=/pay-web
server.port=9090
shop.pay.baseURI=${server.host}:${server.port}${server.servlet.path}
shop.pay.createPayment=/pay/createPayment
shop.pay.callbackPayment=/pay/callbackPayment
6.2 下單測試
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ShopOrderWebApplication.class)
@TestPropertySource("classpath:application.properties")
public class OrderTest {
@Autowired
private RestTemplate restTemplate;
@Value("${shop.order.baseURI}")
private String baseURI;
@Value("${shop.order.confirm}")
private String confirmOrderPath;
@Autowired
private IDWorker idWorker;
/**
* 下單
*/
@Test
public void confirmOrder(){
Long goodsId=XXXL;
Long userId=XXXL;
Long couponId=XXXL;
TradeOrder order = new TradeOrder();
order.setGoodsId(goodsId);
order.setUserId(userId);
order.setGoodsNumber(1);
order.setAddress("北京");
order.setGoodsPrice(new BigDecimal("5000"));
order.setOrderAmount(new BigDecimal("5000"));
order.setMoneyPaid(new BigDecimal("100"));
order.setCouponId(couponId);
order.setShippingFee(new BigDecimal(0));
Result result = restTemplate.postForEntity(baseURI + confirmOrderPath, order, Result.class).getBody();
System.out.println(result);
}
}
6.3 支付測試
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ShopPayWebApplication.class)
@TestPropertySource("classpath:application.properties")
public class PayTest {
@Autowired
private RestTemplate restTemplate;
@Value("${shop.pay.baseURI}")
private String baseURI;
@Value("${shop.pay.createPayment}")
private String createPaymentPath;
@Value("${shop.pay.callbackPayment}")
private String callbackPaymentPath;
@Autowired
private IDWorker idWorker;
/**
* 建立支付訂單
*/
@Test
public void createPayment(){
Long orderId = 346321587315814400L;
TradePay pay = new TradePay();
pay.setOrderId(orderId);
pay.setPayAmount(new BigDecimal(4800));
Result result = restTemplate.postForEntity(baseURI + createPaymentPath, pay, Result.class).getBody();
System.out.println(result);
}
/**
* 支付回撥
*/
@Test
public void callbackPayment(){
Long payId = 346321891507720192L;
TradePay pay = new TradePay();
pay.setPayId(payId);
pay.setIsPaid(ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.getCode());
Result result = restTemplate.postForEntity(baseURI + callbackPaymentPath, pay, Result.class).getBody();
System.out.println(result);
}
}
相關文章
- rocketMQ學習筆記——nameServerMQ筆記Server
- rocketmq學習雜記MQ
- Vue學習筆記2Vue筆記
- MySQL學習筆記2MySql筆記
- Oracle學習筆記2Oracle筆記
- react學習筆記2React筆記
- mysql學習筆記-2MySql筆記
- jQuery學習筆記(2)jQuery筆記
- Scala學習筆記2筆記
- TestNG—學習筆記2筆記
- autolayout學習筆記_2筆記
- vue學習筆記-2Vue筆記
- 學習筆記2(下)筆記
- koa2學習筆記筆記
- hibernate學習筆記(2)筆記
- Python學習筆記(2)Python筆記
- Android學習筆記(2)Android筆記
- koa@2學習筆記筆記
- C#學習筆記2C#筆記
- db2學習筆記DB2筆記
- 藍芽學習筆記2藍芽筆記
- PL/SQL學習筆記-2SQL筆記
- iproute2學習筆記筆記
- Flex學習筆記(Day 2)Flex筆記
- Grub2 學習筆記筆記
- 2-SAT 學習筆記筆記
- 強化學習-學習筆記2 | 價值學習強化學習筆記
- ASP.NET學習筆記2ASP.NET筆記
- Ext2.x學習筆記筆記
- 學習筆記(2)IPC機制筆記
- LevelDB 學習筆記2:合併筆記
- G01學習筆記-2筆記
- CryptoZombies學習筆記——Lesson2筆記
- <node.js學習筆記(2)>Node.js筆記
- docker學習筆記(2)- 倉庫Docker筆記
- 人工智慧學習筆記(2)人工智慧筆記
- angular學習筆記(十四)-$watch(2)Angular筆記
- angular學習筆記(七)-迭代2Angular筆記