feat(master):订单微服务

代码完成
master
土豆兄弟 15 hours ago
parent 87e2af259a
commit 79b4170705

@ -1,8 +1,10 @@
package org.example;
import org.example.conf.DataSourceProxyAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
@ -13,6 +15,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
@EnableDiscoveryClient
@Import(DataSourceProxyAutoConfiguration.class) // 显示的指定定义的数据源
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);

@ -16,6 +16,9 @@ spring:
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
alibaba:
seata:
tx-service-group: dev-protocol # seata 全局事务分组, 对应 file.conf 里面的配置名一样
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:

@ -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"
}
}

@ -1,8 +1,10 @@
package org.example;
import org.example.conf.DataSourceProxyAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
@ -13,6 +15,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@EnableDiscoveryClient
@SpringBootApplication
@Import(DataSourceProxyAutoConfiguration.class) // 显示的指定定义的数据源
public class GoodsApplication {
public static void main(String[] args) {

@ -16,6 +16,9 @@ spring:
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
alibaba:
seata:
tx-service-group: dev-protocol # seata 全局事务分组, 对应 file.conf 里面的配置名一样
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:

@ -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"
}
}

@ -1,10 +1,12 @@
package org.example;
import org.example.conf.DataSourceProxyAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
@ -12,9 +14,10 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
* */
@EnableJpaAuditing
@SpringBootApplication
@EnableCircuitBreaker
@EnableCircuitBreaker // 保证 OpenFeign 开启 Hystrix
@EnableFeignClients
@EnableDiscoveryClient
@Import(DataSourceProxyAutoConfiguration.class) // 显示的指定定义的数据源
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);

@ -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;
}
}

@ -20,7 +20,7 @@ spring:
content-type: text/plain
alibaba:
seata:
tx-service-group: dev-protocol # seata 全局事务分组
tx-service-group: dev-protocol # seata 全局事务分组, 对应 file.conf 里面的配置名一样
nacos:
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可

@ -53,7 +53,7 @@ store {
}
service {
vgroupMapping.imooc-ecommerce = "default"
vgroupMapping.dev-protocol = "default"
default.grouplist = "127.0.0.1:8091"
}
client {

@ -10,4 +10,4 @@ CREATE TABLE `undo_log` (
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

@ -52,7 +52,7 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- seata -->
<!-- seata 引入分布式事务 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>

@ -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;
// }
}

@ -1,11 +1,16 @@
package org.example.conf;
import com.alibaba.cloud.seata.web.SeataHandlerInterceptor;
import org.example.filter.LoginUserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
/**
* <h1>Web Mvc </h1>
* */
@ -22,6 +27,27 @@ public class DevProtocolWebMvcConfig extends WebMvcConfigurationSupport {
// 添加用户身份统一登录拦截的拦截器
registry.addInterceptor(new LoginUserInfoInterceptor())
.addPathPatterns("/**").order(0);
// Seata 传递 xid 事务 id 给其他的微服务
// 只有这样, 其他的服务才会写 undo_log, 才能够实现回滚
registry.addInterceptor(new SeataHandlerInterceptor()).addPathPatterns("/**");
//通用拦截器排除swagger设置所有拦截器都会自动加swagger相关的资源排除信息
// try {
// Field registrationsField = FieldUtils.getField(InterceptorRegistry.class, "registrations", true);
// List<InterceptorRegistration> registrations = (List<InterceptorRegistration>) ReflectionUtils.getField(registrationsField, registry);
// if (registrations != null) {
// for (InterceptorRegistration interceptorRegistration : registrations) {
// interceptorRegistration
// .excludePathPatterns("/swagger**/**")
// .excludePathPatterns("/webjars/**")
// .excludePathPatterns("/v3/**")
// .excludePathPatterns("/doc.html");
// }
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
}
/**

@ -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…
Cancel
Save