// 静态页面的引入,静态资源上传nginx等
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com
192.168.56.10 cart.gulimall.com
192.168.56.10 order.gulimall.com
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.com
需要引入springSession,异步线程池,在引入的页面中显示session登录信息,这些在之前都配置过,就不赘述了。
订单基本概念
电商系统涉及到 3 流,分别是信息流(商品信息,优惠信息等一些数据的查询),资金流(付款,退款等),物流,而订单系统作为中枢将三者有机的集合起来。
订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。
订单状态
- 待付款
锁定库存,配置支付超时时间,超时后将自动取消订单,订单变更为关闭状态,并解锁库存。 - 已付款/待发货
订单系统记录支付时间,支付流水单号;仓储系统配货,出库等 - 待收货/已发货
订单系统同步物流信息 - 已完成
用户确认收货,订单交易完成。 - 已取消
付款之前取消订单。包括超时未付款或者用户商户取消订单。 - 售后中
订单流程
订单生成–>支付订单–>卖家发货–>确认收货–>交易成功
订单创建与支付
- 订单创建前需要预览订单,选择收货信息等
- 订单创建需要锁定库存,库存有才可创建,否则不能创建
- 订单创建后超时未支付需要解锁库存
- 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
- 支付的每笔流水都需要记录,以待查账
- 订单创建,支付成功等状态都需要给 MQ 发送消息,方便其他系统感知订阅
订单服务登录拦截
package com.atlinxi.gulimall.order.interceptor;
import com.atlinxi.common.constant.AuthServerConstant;
import com.atlinxi.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//
//
/**
* 在库存服务远程调用订单服务时,请求是不携带session的,进入的时候会因为没有获取到登录信息而被拦截
* 而我们在库存服务的时候已经判断了是否登录,所以这里是不需要判断登录的,放行就是
*
* uri 就是类似于上面那个路径,url是完整的请求地址
*
* order/order/status/{orderSn} orderSn是动态的
*
*
*/
boolean match = new AntPathMatcher().match("/order/order/status/**", request.getRequestURI());
if (match){
return true;
}
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
}else {
// 没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
package com.atlinxi.gulimall.order.config;
import com.atlinxi.gulimall.order.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
vo
// 订单确认页
package com.atlinxi.gulimall.order.vo;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
// 订单确认页需要用的数据
public class OrderConfirmVo {
// 收货地址,ums_member_receive_address 表
@Getter @Setter
private List<MemberAddressVo> addresses;
// 购物清单,根据购物车页面传递过来的 skuIds 查询
// 所有选中的购物项
@Getter @Setter
private List<OrderItemVo> orderItems;
// 可用积分,ums_member 表中的 integration 字段
@Getter @Setter
private Integer bounds;
// 订单令牌,防止重复提交
@Getter @Setter
private String orderToken;
// 积分
@Getter @Setter
private Integer integration;
@Getter @Setter
Map<Long,Boolean> stocks;
// 有get方法就相当于有count属性
public Integer getCount(){
Integer i = 0;
if (orderItems!=null){
for (OrderItemVo item: orderItems){
i+= item.getCount();
}
}
return i;
}
// 订单总额
// BigDecimal total;
// 应付价格
// BigDecimal payPrice;
// 订单总额
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if (orderItems!=null){
for (OrderItemVo item: orderItems){
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount() + ""));
sum = sum.add(multiply);
}
}
return sum;
}
// 应付价格
// 暂时没有优惠信息
public BigDecimal getPayPrice() {
return getTotal();
}
// 发票记录。。。
//优惠券信息。。。
}
package com.atlinxi.gulimall.order.vo;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
// 会员的收货地址列表
@Data
public class MemberAddressVo {
/**
* id
*/
@TableId
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
package com.atlinxi.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 购物项内容
*/
@Data
public class OrderItemVo {
private Long skuId;
private String title;
private String image;
// 内存 cpu
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
private BigDecimal weight;
}
feign远程调用丢失数据
我们在远程获取会员的地址信息是传的会员id,远程查询购物车信息没传任何参数,由购物车自己判断用户是否登录,登录的话返回当前用户的购物车信息。
此时购物车的拦截器没有从session中获取到用户信息
,但是我们确实是登录了的。
浏览器直接访问的话,会携带cookie,cookie携带我们的jsessionId。
feign远程调用源码简单剖析
feign在远程调用之前要构造请求,调用很多的拦截器RequestInterceptor
,我们没有设置拦截器,相当于feign认为当前请求不需要增强,所以将默认构造的请求传递过来。
浏览器发起提交订单请求,进入controller,service,肯定是携带了请求头的,但是feign在远程调用的过程中给我们创建了一个新的请求模板,而里面什么都没有。
// 如果是公共方法equals,hashcode,toString等就不需要远程调用
// 否则就调用,dispatch.get(method)).invoke(args),获取方法并执行
// public class ReflectiveFeign extends Feign
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!"equals".equals(method.getName())) {
if ("hashCode".equals(method.getName())) {
return this.hashCode();
} else {
return "toString".equals(method.getName()) ? this.toString() : ((MethodHandler)this.dispatch.get(method)).invoke(args);
}
} else {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return this.equals(otherHandler);
} catch (IllegalArgumentException var5) {
return false;
}
}
}
// final class SynchronousMethodHandler implements MethodHandler
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
// 模板指定了我们请求的url地址,得到当前请求,利用客户端执行
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
long start = System.nanoTime();
Response response;
try {
response = this.client.execute(request, options);
// 拦截所有的请求拦截器并返回
Request targetRequest(RequestTemplate template) {
Iterator var2 = this.requestInterceptors.iterator();
while(var2.hasNext()) {
RequestInterceptor interceptor = (RequestInterceptor)var2.next();
interceptor.apply(template);
}
return this.target.apply(template);
}
feign远程调用丢失请求头
package com.atlinxi.gulimall.order.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
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;
@Configuration
public class GuliFeignConfig {
/**
*
* 浏览器发起提交订单的请求,肯定是携带请求头的,包含cookie,这些订单服务肯定是可以获取到的
* 订单服务远程调用购物车服务获取当前用户的购物车信息,在调用过程中因为feign创建了新的请求模板,但是并没有携带旧的请求头
* 导致购物车服务不能获取session,错误认为是用户没有登录
*
* feign在发起请求之前,会拦截所有RequestInterceptor,对当前请求进行增强
* Cookie:user-key=93827f16-f88a-480a-9552-a784fa80b372;GULISESSION=MGI2ZWJkYzktOTlhNC00MTY4LTk4ZWQtZmI0MzhiYWM1NDRk
*
*
*
* 解决feign远程调用丢失请求头的问题
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
/**
*
* 把之前的请求头设置到feign的requestTemplate中
*
* 原生的办法
*
* 浏览器发送 /toTrade 请求,来到service,service远程调用feign,
* feign创建新的请求对象来发请求,在创建对象的时候会调用拦截器
*
* 拦截器,controller,service其实都是同一个线程,
* 所以在拦截器中想获取到之前的请求,
* 在controller中使用HttpServletRequest获取到请求头并使用ThreadLocal共享给拦截器
*
*/
// 我们不用原生的,spring为我们提供了RequestContextHolder(上下文环境的保持器)
// RequestContextHolder拿到刚进来的这个请求,这个实现其实也是上面原生的方法
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
/**
* 因为异步问题的原因,我们需要在service层使用RequestContextHolder来获取RequestAttributes设置到每一个异步任务中,
* getRequestAttributes()在这儿才能被获取到
*
* 共有三个异步任务 会员服务查询收获地址,购物车服务查询当前用户的购物项,库存服务查询各购物项是否有库存
*
* 库存服务我们没有设置RequestAttributes(无需共享数据),所以我们在这儿获取不到,需要判断null
*
*
*
*
* 主线程获取requestAttributes,设置到每一个副线程中
* 虽然都是RequestContextHolder,但是是两个线程,
* RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
*
* CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
* // 每一个线程都来共享之前的请求数据
* RequestContextHolder.setRequestAttributes(requestAttributes);
* // 1. 远程查询所有的收获地址列表
* List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
* confirmVo.setAddresses(address);
* }, executor);
*
*
*/
if (attributes!=null){
HttpServletRequest request = attributes.getRequest(); // 老请求
// 老师这块儿request是判断null的,对RequestContextHolder不太熟,但是凭感觉这儿不应该request为null
// 出现过一次,但再没复现过
requestTemplate.header("Cookie",request.getHeader("Cookie"));
}
}
};
}
}
feign异步远程调用丢失上下文
使用异步远程调用购物车服务和会员服务,发现在RequestInterceptor
并不能获取到当前请求属性。
简单讲就是spring提供的RequestContextHolder
底层的实现是ThreadLocal
,开启异步的时候,oderService和远程调用address、cart都是不同的线程,自然不能从ThreadLocal获取到上下文。
如果没有开启异步,在同一线程的话,肯定是可以获取到的。
接口幂等性
订单被提交以后,数据库里就有一份订单了,接下来就是进入我们的支付流程,
假设网速很慢,用户多次提交订单,有可能导致同一份订单在数据库中被保存多份,订单的防重复提交是非常重要的,专业的术语叫提交订单的幂等性
。
幂等性在这里的意思是提交订单一次和多次的结果是一样的,数据库只会有一次订单。
在分布式系统中无论是我们给页面提交数据,表单的提交,或是分布式系统中的互相调用,有可能同一个方法都会执行多次,比如用户点击了多次提交,或者调用一次不成功多次调用,无论怎么调用都要保证接口的幂等性。
什么是幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的
,不会因为多次点击而产生了副作用;
比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。
哪些情况需要防止
- 用户多次点击按钮
- 用户页面回退再次提交
- 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
- 其他业务情况
什么情况下需要幂等
以 SQL 为例,有些操作是天然幂等的。
- SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
- UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
- delete from user where userid=1,多次操作,结果一样,具备幂等性
- insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性。
不是幂等的
- UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
- insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
我们在数据库设计层面,以提交订单为场景,可以把订单号设置为unique
,这样在插入的时候就不会被多次提交。
幂等解决方案
token 机制
- 服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
- 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
- 服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
- 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
危险性:
先删除 token 还是后删除 token;
我们假设用户快速点击了两次提交订单
-
先删除token可能导致,
假设两次点击很快,同时从redis中获取到token,同时删除了token,并执行了两次业务逻辑。 -
后删除token可能导致,
第一个请求进来,获取到了token,正在创建订单(此时redis中的token还没有删除),这时第二个请求也进来获取到了token,就执行了两次业务逻辑。
Token 获取、比较和删除必须是原子性
-
redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行
-
可以在 redis 使用 lua 脚本完成这个操作
if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end
各种锁机制
-
数据库悲观锁
select * from xxxx where id = 1 for update;(查询的时候就锁定了这条记录,第二个请求进来就需要验证)
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。 -
数据库乐观锁
这种方法适合在更新的场景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题
-
业务层分布式锁
如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。
各种唯一约束
-
数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。 -
redis set 防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。 -
防重表
使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。
这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
之前说的 redis 防重也算 -
全局请求唯一 id
调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。
可以使用 nginx 设置每一个请求的唯一 id;
proxy_set_header X-Request-Id $request_id;
使用token机制解决提交订单的幂等性问题
- 点击去结算进入到订单页面创建token,给redis保存一份,给前端返回一份
- 点击提交订单的时候携带token,与redis中的token比较,成功则提交,不存在则为重复提交
创建订单
提交订单在下一篇文章中
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取之前的请求,
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
// 每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
// 1. 远程查询所有的收获地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddresses(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 2. 远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setOrderItems(items);
}, executor).thenRunAsync(()->{
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = confirmVo.getOrderItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wmsFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data!=null){
Map<Long, Boolean> map = data.stream()
.collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
// 3. 查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4. 其他数据自动计算
// 5. 防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
mvn
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atlinxi.gulimall</groupId>
<artifactId>gulimall-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-order</name>
<description>谷粒商城-订单服务</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 由于SpringCloud Feign高版本不使用Ribbon而是使用spring-cloud-loadbalancer,
所以需要引用spring-cloud-loadbalancer或者降版本-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlinxi.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 整合springSession解决session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
软的像奶母的手心。鹌鹑蛋的手心。诗眼的手心。也许走对了不一定。
房思琪的初恋乐园
林奕含