parent
87e2af259a
commit
79b4170705
@ -0,0 +1,65 @@
|
||||
## transaction log store, only used in seata-server
|
||||
store {
|
||||
## store mode: file、db、redis
|
||||
mode = "db"
|
||||
|
||||
## file store property
|
||||
file {
|
||||
## store location dir
|
||||
dir = "sessionStore"
|
||||
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
|
||||
maxBranchSessionSize = 16384
|
||||
# globe session size , if exceeded throws exceptions
|
||||
maxGlobalSessionSize = 512
|
||||
# file buffer size , if exceeded allocate new buffer
|
||||
fileWriteBufferCacheSize = 16384
|
||||
# when recover batch read size
|
||||
sessionReloadReadSize = 100
|
||||
# async, sync
|
||||
flushDiskMode = async
|
||||
}
|
||||
|
||||
## database store property
|
||||
db {
|
||||
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
|
||||
datasource = "druid"
|
||||
## mysql/oracle/postgresql/h2/oceanbase etc.
|
||||
dbType = "mysql"
|
||||
driverClassName = "com.mysql.jdbc.Driver"
|
||||
url = "jdbc:mysql://127.0.0.1:3306/seata?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false"
|
||||
user = "root"
|
||||
password = "root"
|
||||
minConn = 5
|
||||
maxConn = 100
|
||||
globalTable = "global_table"
|
||||
branchTable = "branch_table"
|
||||
lockTable = "lock_table"
|
||||
queryLimit = 100
|
||||
maxWait = 5000
|
||||
}
|
||||
|
||||
## redis store property
|
||||
redis {
|
||||
host = "127.0.0.1"
|
||||
port = "6379"
|
||||
password = ""
|
||||
database = "0"
|
||||
minConn = 1
|
||||
maxConn = 10
|
||||
maxTotal = 100
|
||||
queryLimit = 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
service {
|
||||
vgroupMapping.dev-protocol = "default"
|
||||
default.grouplist = "127.0.0.1:8091"
|
||||
}
|
||||
client {
|
||||
async.commit.buffer.limit = 10000
|
||||
lock {
|
||||
retry.internal = 10
|
||||
retry.times = 30
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
registry {
|
||||
# file、nacos、eureka、redis、zk、consul
|
||||
type = "file"
|
||||
|
||||
file {
|
||||
name = "file.conf"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
config {
|
||||
type = "file"
|
||||
|
||||
file {
|
||||
name = "file.conf"
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
## transaction log store, only used in seata-server
|
||||
store {
|
||||
## store mode: file、db、redis
|
||||
mode = "db"
|
||||
|
||||
## file store property
|
||||
file {
|
||||
## store location dir
|
||||
dir = "sessionStore"
|
||||
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
|
||||
maxBranchSessionSize = 16384
|
||||
# globe session size , if exceeded throws exceptions
|
||||
maxGlobalSessionSize = 512
|
||||
# file buffer size , if exceeded allocate new buffer
|
||||
fileWriteBufferCacheSize = 16384
|
||||
# when recover batch read size
|
||||
sessionReloadReadSize = 100
|
||||
# async, sync
|
||||
flushDiskMode = async
|
||||
}
|
||||
|
||||
## database store property
|
||||
db {
|
||||
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
|
||||
datasource = "druid"
|
||||
## mysql/oracle/postgresql/h2/oceanbase etc.
|
||||
dbType = "mysql"
|
||||
driverClassName = "com.mysql.jdbc.Driver"
|
||||
url = "jdbc:mysql://127.0.0.1:3306/seata?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false"
|
||||
user = "root"
|
||||
password = "root"
|
||||
minConn = 5
|
||||
maxConn = 100
|
||||
globalTable = "global_table"
|
||||
branchTable = "branch_table"
|
||||
lockTable = "lock_table"
|
||||
queryLimit = 100
|
||||
maxWait = 5000
|
||||
}
|
||||
|
||||
## redis store property
|
||||
redis {
|
||||
host = "127.0.0.1"
|
||||
port = "6379"
|
||||
password = ""
|
||||
database = "0"
|
||||
minConn = 1
|
||||
maxConn = 10
|
||||
maxTotal = 100
|
||||
queryLimit = 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
service {
|
||||
vgroupMapping.dev-protocol = "default"
|
||||
default.grouplist = "127.0.0.1:8091"
|
||||
}
|
||||
client {
|
||||
async.commit.buffer.limit = 10000
|
||||
lock {
|
||||
retry.internal = 10
|
||||
retry.times = 30
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
registry {
|
||||
# file、nacos、eureka、redis、zk、consul
|
||||
type = "file"
|
||||
|
||||
file {
|
||||
name = "file.conf"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
config {
|
||||
type = "file"
|
||||
|
||||
file {
|
||||
name = "file.conf"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
# 订单微服务
|
||||
|
||||
|
||||
##
|
@ -0,0 +1,52 @@
|
||||
package org.example.controller;
|
||||
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.common.TableId;
|
||||
import org.example.order.OrderInfo;
|
||||
import org.example.service.IOrderService;
|
||||
import org.example.vo.PageSimpleOrderDetail;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <h1>订单服务对外 HTTP 接口</h1>
|
||||
* */
|
||||
@Api(tags = "订单服务")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/order")
|
||||
public class OrderController {
|
||||
|
||||
private final IOrderService orderService;
|
||||
|
||||
public OrderController(IOrderService orderService) {
|
||||
this.orderService = orderService;
|
||||
}
|
||||
|
||||
@ApiOperation(
|
||||
value = "创建",
|
||||
notes = "购买(分布式事务): 创建订单 -> 扣减库存 -> 扣减余额 -> 发送物流消息",
|
||||
httpMethod = "POST"
|
||||
)
|
||||
@PostMapping("/create-order")
|
||||
public TableId createOrder(@RequestBody OrderInfo orderInfo) {
|
||||
return orderService.createOrder(orderInfo);
|
||||
}
|
||||
|
||||
@ApiOperation(
|
||||
value = "订单信息",
|
||||
notes = "获取当前用户的订单信息: 带有分页",
|
||||
httpMethod = "GET"
|
||||
)
|
||||
@GetMapping("/order-detail")
|
||||
public PageSimpleOrderDetail getSimpleOrderDetailByPage(
|
||||
@RequestParam(required = false, defaultValue = "1") int page) {
|
||||
return orderService.getSimpleOrderDetailByPage(page);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.example.dao;
|
||||
|
||||
import org.example.entity.EcommerceOrder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
/**
|
||||
* <h1>EcommerceOrder Dao 接口定义</h1>
|
||||
* 支持分页
|
||||
* */
|
||||
public interface EcommerceOrderDao extends PagingAndSortingRepository<EcommerceOrder, Long> {
|
||||
|
||||
/**
|
||||
* <h2>根据 userId 查询分页订单</h2>
|
||||
* select * from t_dev_protocol_cloud_order where user_id = ?
|
||||
* order by ... desc/asc limit x offset y
|
||||
* */
|
||||
Page<EcommerceOrder> findAllByUserId(Long userId, Pageable pageable);
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package org.example.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EntityListeners;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <h1>用户订单表实体类定义</h1>
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Entity
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Table(name = "t_dev_protocol_cloud_order")
|
||||
public class EcommerceOrder {
|
||||
|
||||
/** 自增主键 */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
/** 用户 id */
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
/** 用户地址 id */
|
||||
@Column(name = "address_id", nullable = false)
|
||||
private Long addressId;
|
||||
|
||||
/** 订单详情(json 存储) */
|
||||
@Column(name = "order_detail", nullable = false)
|
||||
private String orderDetail;
|
||||
|
||||
/** 创建时间 */
|
||||
@CreatedDate
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private Date createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@LastModifiedDate
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private Date updateTime;
|
||||
|
||||
public EcommerceOrder(Long userId, Long addressId, String orderDetail) {
|
||||
|
||||
this.userId = userId;
|
||||
this.addressId = addressId;
|
||||
this.orderDetail = orderDetail;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.example.feign;
|
||||
|
||||
import org.example.account.AddressInfo;
|
||||
import org.example.common.TableId;
|
||||
import org.example.feign.hystrix.AddressClientHystrix;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
/**
|
||||
* <h1>用户账户服务 Feign 接口(安全的)</h1>
|
||||
* */
|
||||
@FeignClient(
|
||||
contextId = "AddressClient",
|
||||
value = "dev-protocol-springcloud-project-account-service", // 调用账号微服务
|
||||
fallback = AddressClientHystrix.class // 兜底策略
|
||||
)
|
||||
public interface AddressClient {
|
||||
|
||||
/**
|
||||
* <h2>根据 id 查询地址信息</h2>
|
||||
* */
|
||||
@RequestMapping(
|
||||
value = "/dev-protocol-springcloud-project-account-service/address/address-info-by-table-id",
|
||||
method = RequestMethod.POST
|
||||
)
|
||||
CommonResponse<AddressInfo> getAddressInfoByTablesId(@RequestBody TableId tableId);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package org.example.feign;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* <h1>Feign 调用时, 把 Header 也传递到服务提供方</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class FeignConfig {
|
||||
|
||||
/**
|
||||
* <h2>给 Feign 配置请求拦截器</h2>
|
||||
* RequestInterceptor 是我们提供给 open-feign 的请求拦截器, 把 Header 信息传递
|
||||
* */
|
||||
@Bean
|
||||
public RequestInterceptor headerInterceptor() {
|
||||
|
||||
return template -> {
|
||||
// 获取请求的信息
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (null != attributes) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
if (null != headerNames) {
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String name = headerNames.nextElement();
|
||||
String values = request.getHeader(name);
|
||||
// 不能把当前请求的 content-length 传递到下游的服务提供方, 这明显是不对的
|
||||
// 请求可能一直返回不了, 或者是请求响应数据被截断
|
||||
if (!name.equalsIgnoreCase("content-length")) {
|
||||
// 这里的 template 就是 RestTemplate
|
||||
template.header(name, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.example.feign;
|
||||
|
||||
import org.example.account.BalanceInfo;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
/**
|
||||
* <h1>用户账户服务 Feign 接口</h1>
|
||||
* 无兜底策略
|
||||
* */
|
||||
@FeignClient(
|
||||
contextId = "NotSecuredBalanceClient",
|
||||
value = "dev-protocol-springcloud-project-account-service" // 调用账号微服务
|
||||
)
|
||||
public interface NotSecuredBalanceClient {
|
||||
|
||||
@RequestMapping(
|
||||
value = "/dev-protocol-springcloud-project-account-service/balance/deduct-balance",
|
||||
method = RequestMethod.PUT
|
||||
)
|
||||
CommonResponse<BalanceInfo> deductBalance(@RequestBody BalanceInfo balanceInfo);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package org.example.feign;
|
||||
|
||||
import org.example.common.TableId;
|
||||
import org.example.goods.DeductGoodsInventory;
|
||||
import org.example.goods.SimpleGoodsInfo;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <h1>不安全的商品服务 Feign 接口</h1>
|
||||
* */
|
||||
@FeignClient(
|
||||
contextId = "NotSecuredGoodsClient",
|
||||
value = "dev-protocol-springcloud-project-goods-service" // 调用商品微服务接口
|
||||
)
|
||||
public interface NotSecuredGoodsClient {
|
||||
|
||||
/**
|
||||
* <h2>根据 ids 查询简单的商品信息</h2>
|
||||
* */
|
||||
@RequestMapping(
|
||||
value = "/dev-protocol-springcloud-project-goods-service/goods/deduct-goods-inventory",
|
||||
method = RequestMethod.PUT
|
||||
)
|
||||
CommonResponse<Boolean> deductGoodsInventory(
|
||||
@RequestBody List<DeductGoodsInventory> deductGoodsInventories);
|
||||
|
||||
/**
|
||||
* <h2>根据 ids 查询简单的商品信息</h2>
|
||||
* */
|
||||
@RequestMapping(
|
||||
value = "/dev-protocol-springcloud-project-goods-service/goods/simple-goods-info",
|
||||
method = RequestMethod.POST
|
||||
)
|
||||
CommonResponse<List<SimpleGoodsInfo>> getSimpleGoodsInfoByTableId(
|
||||
@RequestBody TableId tableId);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.example.feign;
|
||||
|
||||
import org.example.common.TableId;
|
||||
import org.example.feign.hystrix.GoodsClientHystrix;
|
||||
import org.example.goods.SimpleGoodsInfo;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <h1>商品服务 Feign 接口(安全的)</h1>
|
||||
* */
|
||||
@FeignClient(
|
||||
contextId = "SecuredGoodsClient",
|
||||
value = "dev-protocol-springcloud-project-goods-service",
|
||||
fallback = GoodsClientHystrix.class
|
||||
)
|
||||
public interface SecuredGoodsClient {
|
||||
|
||||
/**
|
||||
* <h2>根据 ids 查询简单的商品信息</h2>
|
||||
* */
|
||||
@RequestMapping(
|
||||
value = "/dev-protocol-springcloud-project-goods-service/goods/simple-goods-info",
|
||||
method = RequestMethod.POST
|
||||
)
|
||||
CommonResponse<List<SimpleGoodsInfo>> getSimpleGoodsInfoByTableId(
|
||||
@RequestBody TableId tableId);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.example.feign.hystrix;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.account.AddressInfo;
|
||||
import org.example.common.TableId;
|
||||
import org.example.feign.AddressClient;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* <h1>账户服务熔断降级兜底策略</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AddressClientHystrix implements AddressClient {
|
||||
|
||||
@Override
|
||||
public CommonResponse<AddressInfo> getAddressInfoByTablesId(TableId tableId) {
|
||||
|
||||
log.error("[account client feign request error in order service] get address info" +
|
||||
"error: [{}]", JSON.toJSONString(tableId));
|
||||
return new CommonResponse<>(
|
||||
-1,
|
||||
"[account client feign request error in order service]",
|
||||
new AddressInfo(-1L, Collections.emptyList()) // 返回用户Id为-1, 兜底返回一个空的List
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.example.feign.hystrix;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.common.TableId;
|
||||
import org.example.feign.SecuredGoodsClient;
|
||||
import org.example.goods.SimpleGoodsInfo;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <h1>商品服务熔断降级兜底</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GoodsClientHystrix implements SecuredGoodsClient {
|
||||
|
||||
@Override
|
||||
public CommonResponse<List<SimpleGoodsInfo>> getSimpleGoodsInfoByTableId(TableId tableId) {
|
||||
|
||||
log.error("[goods client feign request error in order service] get simple goods" +
|
||||
"error: [{}]", JSON.toJSONString(tableId));
|
||||
return new CommonResponse<>(
|
||||
-1,
|
||||
"[goods client feign request error in order service]",
|
||||
Collections.emptyList() // 兜底返回一个空的List
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.example.service;
|
||||
|
||||
|
||||
import org.example.common.TableId;
|
||||
import org.example.order.OrderInfo;
|
||||
import org.example.vo.PageSimpleOrderDetail;
|
||||
|
||||
/**
|
||||
* <h1>订单相关服务接口定义</h1>
|
||||
* */
|
||||
public interface IOrderService {
|
||||
|
||||
/**
|
||||
* <h2>下单(分布式事务): 创建订单 -> 扣减库存 -> 扣减余额 -> 创建物流信息(Stream + Kafka)</h2>
|
||||
* */
|
||||
TableId createOrder(OrderInfo orderInfo);
|
||||
|
||||
/**
|
||||
* <h2>获取当前用户的订单信息: 带有分页</h2>
|
||||
* */
|
||||
PageSimpleOrderDetail getSimpleOrderDetailByPage(int page);
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
package org.example.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import io.seata.spring.annotation.GlobalTransactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.example.account.AddressInfo;
|
||||
import org.example.account.BalanceInfo;
|
||||
import org.example.common.TableId;
|
||||
import org.example.dao.EcommerceOrderDao;
|
||||
import org.example.entity.EcommerceOrder;
|
||||
import org.example.feign.AddressClient;
|
||||
import org.example.feign.NotSecuredBalanceClient;
|
||||
import org.example.feign.NotSecuredGoodsClient;
|
||||
import org.example.feign.SecuredGoodsClient;
|
||||
import org.example.filter.AccessContext;
|
||||
import org.example.goods.DeductGoodsInventory;
|
||||
import org.example.goods.SimpleGoodsInfo;
|
||||
import org.example.order.LogisticsMessage;
|
||||
import org.example.order.OrderInfo;
|
||||
import org.example.service.IOrderService;
|
||||
import org.example.source.LogisticsSource;
|
||||
import org.example.vo.PageSimpleOrderDetail;
|
||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <h1>订单相关服务接口实现</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableBinding(LogisticsSource.class)
|
||||
public class OrderServiceImpl implements IOrderService {
|
||||
|
||||
/** 表的 dao 接口 */
|
||||
private final EcommerceOrderDao orderDao;
|
||||
|
||||
/** Feign 客户端 */
|
||||
private final AddressClient addressClient;
|
||||
private final SecuredGoodsClient securedGoodsClient;
|
||||
private final NotSecuredGoodsClient notSecuredGoodsClient;
|
||||
private final NotSecuredBalanceClient notSecuredBalanceClient;
|
||||
|
||||
/** SpringCloud Stream 的发射器 */
|
||||
private final LogisticsSource logisticsSource;
|
||||
|
||||
public OrderServiceImpl(EcommerceOrderDao orderDao,
|
||||
// fixme 这里如果报红的话, 可以不用理会, 去 Idea 工具中进行配置忽略 设置-> 编辑器(Editor) -> 检查(Inspections) ->
|
||||
// Spring | Spring Core | 代码 | Spring Bean 组件中不正确的自动装配(Autowiring for bean class)
|
||||
AddressClient addressClient,
|
||||
SecuredGoodsClient securedGoodsClient,
|
||||
NotSecuredGoodsClient notSecuredGoodsClient,
|
||||
NotSecuredBalanceClient notSecuredBalanceClient,
|
||||
LogisticsSource logisticsSource) {
|
||||
this.orderDao = orderDao;
|
||||
this.addressClient = addressClient;
|
||||
this.securedGoodsClient = securedGoodsClient;
|
||||
this.notSecuredGoodsClient = notSecuredGoodsClient;
|
||||
this.notSecuredBalanceClient = notSecuredBalanceClient;
|
||||
this.logisticsSource = logisticsSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>创建订单: 这里会涉及到分布式事务</h2>
|
||||
* 创建订单会涉及到多个步骤和校验, 当不满足情况时直接抛出异常;
|
||||
* 1. 校验请求对象是否合法
|
||||
* 2. 创建订单 - 本地事务
|
||||
* 3. 扣减商品库存 - 本地事务
|
||||
* 4. 扣减用户余额 - 本地事务
|
||||
* 5. 发送订单物流消息 SpringCloud Stream + Kafka
|
||||
* */
|
||||
@Override
|
||||
@GlobalTransactional(rollbackFor = Exception.class) // 分布式事务注解
|
||||
public TableId createOrder(OrderInfo orderInfo) {
|
||||
|
||||
// 获取地址信息
|
||||
AddressInfo addressInfo = addressClient.getAddressInfoByTablesId(
|
||||
new TableId(Collections.singletonList(
|
||||
new TableId.Id(orderInfo.getUserAddress())))).getData();
|
||||
|
||||
// 1. 校验请求对象是否合法(商品信息不需要校验, 扣减库存会做校验)
|
||||
if (CollectionUtils.isEmpty(addressInfo.getAddressItems())) {
|
||||
throw new RuntimeException("user address is not exist: "
|
||||
+ orderInfo.getUserAddress());
|
||||
}
|
||||
|
||||
// 2. 创建订单
|
||||
EcommerceOrder newOrder = orderDao.save(
|
||||
new EcommerceOrder(
|
||||
AccessContext.getLoginUserInfo().getId(),
|
||||
orderInfo.getUserAddress(),
|
||||
JSON.toJSONString(orderInfo.getOrderItems())
|
||||
)
|
||||
);
|
||||
log.info("create order success: [{}], [{}]",
|
||||
AccessContext.getLoginUserInfo().getId(), newOrder.getId());
|
||||
|
||||
// 3. 扣减商品库存
|
||||
if (
|
||||
!notSecuredGoodsClient.deductGoodsInventory(
|
||||
orderInfo.getOrderItems()
|
||||
.stream()
|
||||
.map(OrderInfo.OrderItem::toDeductGoodsInventory)
|
||||
.collect(Collectors.toList())
|
||||
).getData()
|
||||
) {
|
||||
throw new RuntimeException("deduct goods inventory failure");
|
||||
}
|
||||
|
||||
// 4. 扣减用户账户余额
|
||||
// 4.1 获取商品信息, 计算总价格
|
||||
List<SimpleGoodsInfo> goodsInfos = notSecuredGoodsClient.getSimpleGoodsInfoByTableId(
|
||||
new TableId(
|
||||
orderInfo.getOrderItems()
|
||||
.stream()
|
||||
.map(o -> new TableId.Id(o.getGoodsId()))
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
).getData();
|
||||
|
||||
Map<Long, SimpleGoodsInfo> goodsId2GoodsInfo = goodsInfos.stream()
|
||||
.collect(Collectors.toMap(SimpleGoodsInfo::getId, Function.identity()));
|
||||
long balance = 0;
|
||||
for (OrderInfo.OrderItem orderItem : orderInfo.getOrderItems()) {
|
||||
balance += goodsId2GoodsInfo.get(orderItem.getGoodsId()).getPrice()
|
||||
* orderItem.getCount();
|
||||
}
|
||||
assert balance > 0;
|
||||
|
||||
// 4.2 填写总价格, 扣减账户余额
|
||||
BalanceInfo balanceInfo = notSecuredBalanceClient.deductBalance(
|
||||
new BalanceInfo(AccessContext.getLoginUserInfo().getId(), balance)
|
||||
).getData();
|
||||
if (null == balanceInfo) {
|
||||
throw new RuntimeException("deduct user balance failure");
|
||||
}
|
||||
log.info("deduct user balance: [{}], [{}]", newOrder.getId(),
|
||||
JSON.toJSONString(balanceInfo));
|
||||
|
||||
// 5. 发送订单物流消息 SpringCloud Stream + Kafka
|
||||
LogisticsMessage logisticsMessage = new LogisticsMessage(
|
||||
AccessContext.getLoginUserInfo().getId(),
|
||||
newOrder.getId(),
|
||||
orderInfo.getUserAddress(),
|
||||
null // 没有备注信息
|
||||
);
|
||||
if (!logisticsSource.logisticsOutput().send(
|
||||
MessageBuilder.withPayload(JSON.toJSONString(logisticsMessage)).build()
|
||||
)) {
|
||||
throw new RuntimeException("send logistics message failure");
|
||||
}
|
||||
log.info("send create order message to kafka with stream: [{}]",
|
||||
JSON.toJSONString(logisticsMessage));
|
||||
|
||||
// 返回订单 id
|
||||
return new TableId(Collections.singletonList(new TableId.Id(newOrder.getId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageSimpleOrderDetail getSimpleOrderDetailByPage(int page) {
|
||||
|
||||
if (page <= 0) {
|
||||
page = 1; // 默认是第一页
|
||||
}
|
||||
|
||||
// 这里分页的规则是: 1页10条数据, 按照 id 倒序排列
|
||||
Pageable pageable = PageRequest.of(page - 1, 10,
|
||||
Sort.by("id").descending());
|
||||
Page<EcommerceOrder> orderPage = orderDao.findAllByUserId(
|
||||
AccessContext.getLoginUserInfo().getId(), pageable
|
||||
);
|
||||
List<EcommerceOrder> orders = orderPage.getContent();
|
||||
|
||||
// 如果是空, 直接返回空数组
|
||||
if (CollectionUtils.isEmpty(orders)) {
|
||||
return new PageSimpleOrderDetail(Collections.emptyList(), false);
|
||||
}
|
||||
|
||||
// 获取当前订单中所有的 goodsId, 这个 set 不可能为空或者是 null, 否则, 代码一定有 bug
|
||||
Set<Long> goodsIdsInOrders = new HashSet<>();
|
||||
orders.forEach(o -> {
|
||||
List<DeductGoodsInventory> goodsAndCount = JSON.parseArray(
|
||||
o.getOrderDetail(), DeductGoodsInventory.class
|
||||
);
|
||||
goodsIdsInOrders.addAll(goodsAndCount.stream()
|
||||
.map(DeductGoodsInventory::getGoodsId)
|
||||
.collect(Collectors.toSet()));
|
||||
});
|
||||
|
||||
assert CollectionUtils.isNotEmpty(goodsIdsInOrders);
|
||||
|
||||
// 是否还有更多页: 总页数是否大于当前给定的页
|
||||
boolean hasMore = orderPage.getTotalPages() > page;
|
||||
|
||||
// 获取商品信息
|
||||
List<SimpleGoodsInfo> goodsInfos = securedGoodsClient.getSimpleGoodsInfoByTableId(
|
||||
new TableId(goodsIdsInOrders.stream()
|
||||
.map(TableId.Id::new).collect(Collectors.toList()))
|
||||
).getData();
|
||||
|
||||
// 获取地址信息
|
||||
AddressInfo addressInfo = addressClient.getAddressInfoByTablesId(
|
||||
new TableId(orders.stream()
|
||||
.map(o -> new TableId.Id(o.getAddressId()))
|
||||
.distinct().collect(Collectors.toList()))
|
||||
).getData();
|
||||
|
||||
// 组装订单中的商品, 地址信息 -> 订单信息
|
||||
return new PageSimpleOrderDetail(
|
||||
assembleSimpleOrderDetail(orders, goodsInfos, addressInfo),
|
||||
hasMore
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>组装订单详情</h2>
|
||||
* */
|
||||
private List<PageSimpleOrderDetail.SingleOrderItem> assembleSimpleOrderDetail(
|
||||
List<EcommerceOrder> orders, List<SimpleGoodsInfo> goodsInfos,
|
||||
AddressInfo addressInfo
|
||||
) {
|
||||
// goodsId -> SimpleGoodsInfo
|
||||
Map<Long, SimpleGoodsInfo> id2GoodsInfo = goodsInfos.stream()
|
||||
.collect(Collectors.toMap(SimpleGoodsInfo::getId, Function.identity()));
|
||||
// addressId -> AddressInfo.AddressItem
|
||||
Map<Long, AddressInfo.AddressItem> id2AddressItem = addressInfo.getAddressItems()
|
||||
.stream().collect(
|
||||
Collectors.toMap(AddressInfo.AddressItem::getId, Function.identity())
|
||||
);
|
||||
|
||||
List<PageSimpleOrderDetail.SingleOrderItem> result = new ArrayList<>(orders.size());
|
||||
orders.forEach(o -> {
|
||||
|
||||
PageSimpleOrderDetail.SingleOrderItem orderItem =
|
||||
new PageSimpleOrderDetail.SingleOrderItem();
|
||||
orderItem.setId(o.getId());
|
||||
orderItem.setUserAddress(id2AddressItem.getOrDefault(o.getAddressId(),
|
||||
new AddressInfo.AddressItem(-1L)).toUserAddress());
|
||||
orderItem.setGoodsItems(buildOrderGoodsItem(o, id2GoodsInfo));
|
||||
|
||||
result.add(orderItem);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>构造订单中的商品信息</h2>
|
||||
* */
|
||||
private List<PageSimpleOrderDetail.SingleOrderGoodsItem> buildOrderGoodsItem(
|
||||
EcommerceOrder order, Map<Long, SimpleGoodsInfo> id2GoodsInfo
|
||||
) {
|
||||
|
||||
List<PageSimpleOrderDetail.SingleOrderGoodsItem> goodsItems = new ArrayList<>();
|
||||
List<DeductGoodsInventory> goodsAndCount = JSON.parseArray(
|
||||
order.getOrderDetail(), DeductGoodsInventory.class
|
||||
);
|
||||
|
||||
goodsAndCount.forEach(gc -> {
|
||||
|
||||
PageSimpleOrderDetail.SingleOrderGoodsItem goodsItem =
|
||||
new PageSimpleOrderDetail.SingleOrderGoodsItem();
|
||||
goodsItem.setCount(gc.getCount());
|
||||
goodsItem.setSimpleGoodsInfo(id2GoodsInfo.getOrDefault(gc.getGoodsId(),
|
||||
new SimpleGoodsInfo(-1L)));
|
||||
|
||||
goodsItems.add(goodsItem);
|
||||
});
|
||||
|
||||
return goodsItems;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.example.source;
|
||||
|
||||
import org.springframework.cloud.stream.annotation.Output;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
/**
|
||||
* <h1>自定义物流消息通信信道(Source)</h1>
|
||||
* */
|
||||
public interface LogisticsSource {
|
||||
|
||||
/** 输出信道名称 */
|
||||
String OUTPUT = "logisticsOutput";
|
||||
|
||||
/**
|
||||
* <h2>物流 Source -> logisticsOutput</h2>
|
||||
* 通信信道的名称是 logisticsOutput, 对应到 yml 文件里的配置
|
||||
* */
|
||||
@Output(LogisticsSource.OUTPUT)
|
||||
MessageChannel logisticsOutput();
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package org.example.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.example.account.UserAddress;
|
||||
import org.example.goods.SimpleGoodsInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <h1>订单详情</h1> - 分页, 仅用于订单服务
|
||||
* */
|
||||
@ApiModel(description = "分页订单详情对象")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PageSimpleOrderDetail {
|
||||
|
||||
@ApiModelProperty(value = "订单详情")
|
||||
private List<SingleOrderItem> orderItems;
|
||||
|
||||
@ApiModelProperty(value = "是否有更多的订单(分页)")
|
||||
private Boolean hasMore;
|
||||
|
||||
/**
|
||||
* <h2>单个订单信息</h2>
|
||||
* */
|
||||
@ApiModel(description = "单个订单信息对象")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SingleOrderItem {
|
||||
|
||||
@ApiModelProperty(value = "订单表主键 id")
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "用户地址信息")
|
||||
private UserAddress userAddress;
|
||||
|
||||
@ApiModelProperty(value = "订单商品信息")
|
||||
private List<SingleOrderGoodsItem> goodsItems;
|
||||
}
|
||||
|
||||
@ApiModel(description = "单个订单中的单项商品信息")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SingleOrderGoodsItem {
|
||||
|
||||
@ApiModelProperty(value = "简单商品信息")
|
||||
private SimpleGoodsInfo simpleGoodsInfo;
|
||||
|
||||
@ApiModelProperty(value = "商品个数")
|
||||
private Integer count;
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package org.example.conf;
|
||||
|
||||
//import com.alibaba.druid.support.http.StatViewServlet;
|
||||
//import com.alibaba.druid.support.http.WebStatFilter;
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
//import com.zaxxer.hikari.HikariDataSource;
|
||||
import io.seata.rm.datasource.DataSourceProxy;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.sql.DataSource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2022/12/28/ 16:13:00
|
||||
* @Description Seata 所需要的数据源代理配置类
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class DataSourceProxyAutoConfiguration {
|
||||
|
||||
@Resource
|
||||
private DataSourceProperties dataSourceProperties;
|
||||
|
||||
/**
|
||||
* 配置数据源代理, 用于 Seata 全局事务回滚
|
||||
* before image + after image -> undo_log
|
||||
*/
|
||||
@Primary
|
||||
@Bean("dataSource")
|
||||
public DataSource dataSource() {
|
||||
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
log.info("dataSource properties:[{}]", JSON.toJSONString(
|
||||
dataSourceProperties
|
||||
));
|
||||
dataSource.setJdbcUrl(dataSourceProperties.getUrl());
|
||||
dataSource.setUsername(dataSourceProperties.getUsername());
|
||||
dataSource.setPassword(dataSourceProperties.getPassword());
|
||||
dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
|
||||
return new DataSourceProxy(dataSource);
|
||||
}
|
||||
|
||||
|
||||
// @ConfigurationProperties(prefix = "spring.datasource")
|
||||
// @Bean
|
||||
// public DataSource dataSource(){
|
||||
// return new DruidDataSource();
|
||||
// }
|
||||
|
||||
// @Bean
|
||||
// @ConfigurationProperties(prefix = "spring.datasource")
|
||||
// public DataSource druidDataSource(){
|
||||
// return new DruidDataSource();
|
||||
// }
|
||||
//
|
||||
// @Bean
|
||||
// public DataSourceProxy dataSourceProxy(DataSource dataSource) {
|
||||
// return new DataSourceProxy(dataSource);
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* @Description: 后台监控
|
||||
* @Author: J.Flying
|
||||
* @Date: 2020/10/20
|
||||
*/
|
||||
// @Bean
|
||||
// public ServletRegistrationBean registrationBean(){
|
||||
// ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");
|
||||
// Map<String, String> initParameters=new HashMap<>();
|
||||
// // 登录名
|
||||
// initParameters.put("loginUsername","admin");
|
||||
// initParameters.put("loginPassword","1234");
|
||||
//
|
||||
// //准许访问
|
||||
// initParameters.put("allow","");
|
||||
//
|
||||
// bean.setInitParameters(initParameters);
|
||||
// return bean;
|
||||
// }
|
||||
|
||||
// @Bean
|
||||
// public FilterRegistrationBean druidStatFilter() {
|
||||
// FilterRegistrationBean filterRegistrationBean =
|
||||
// new FilterRegistrationBean(new WebStatFilter());
|
||||
// //添加过滤规则
|
||||
// filterRegistrationBean.addUrlPatterns("/*");
|
||||
// //添加需要忽略的格式信息
|
||||
// filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif," +
|
||||
// "*.jpg,*.png, *.css,*.ico,/druid/*");
|
||||
// return filterRegistrationBean;
|
||||
// }
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.example.order;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2022/12/29/ 17:23:00
|
||||
* @Description 创建订单时发送的物流消息
|
||||
*/
|
||||
@ApiModel(description = "Stream 物流消息对象")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LogisticsMessage {
|
||||
|
||||
@ApiModelProperty(value = "用户表主键 id")
|
||||
private Long userId;
|
||||
|
||||
@ApiModelProperty(value = "订单表主键 id")
|
||||
private Long orderId;
|
||||
|
||||
@ApiModelProperty(value = "用户地址表主键 id")
|
||||
private Long addressId;
|
||||
|
||||
@ApiModelProperty(value = "备注信息(json 存储)")
|
||||
private String extraInfo;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package org.example.order;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.example.goods.DeductGoodsInventory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author danny
|
||||
* @createTime 2022/12/29/ 17:21:00
|
||||
* @Description 订单信息
|
||||
*/
|
||||
@ApiModel(description = "用户发起购买订单")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OrderInfo {
|
||||
|
||||
@ApiModelProperty(value = "用户地址表主键 id")
|
||||
private Long userAddress;
|
||||
|
||||
@ApiModelProperty(value = "订单中的商品信息")
|
||||
private List<OrderItem> orderItems;
|
||||
|
||||
/**
|
||||
* 订单中的商品信息
|
||||
*/
|
||||
@ApiModel(description = "订单中的单项商品信息")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class OrderItem {
|
||||
|
||||
@ApiModelProperty(value = "商品表主键 id")
|
||||
private Long goodsId;
|
||||
|
||||
@ApiModelProperty(value = "购买商品个数")
|
||||
private Integer count;
|
||||
|
||||
/**
|
||||
* 扣减库存
|
||||
*/
|
||||
public DeductGoodsInventory toDeductGoodsInventory() {
|
||||
return new DeductGoodsInventory(this.goodsId, this.count);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue