目录
- 1 预约下单
- 1.1 需求分析
- 1.1.1 业务流程
- 1.1.2 订单状态
- 1.2 系统设计
- 1.2.1 订单表设计
- 1.2.2 表结构的设置
- 1.3 开发远程调用接口
- 1.3.0 复习下远程调用的开发
- 1.3.1 查询地址簿远程接口
- jzo2o-api工程定义接口
- Customer服务实现接口
- 1.3.2 查询服务&服务项远程接口
- jzo2o-api工程定义接口
- foundations服务实现接口
- Mapper开发
- Service层开发
- Controller开发
- 1.4 熔断降级
- 1.4.1 复习
- 1.4.2 使用sentinel实现熔断降级
- 1.4.3 客户端集成sentinel
- 1.4.4 sentinel实现熔断降级代码
- 1.4.5 测试
- 1.5 接口设计
- 1.5.1 接口分析
- 1.5.1 接口开发
- Controller层开发
- Service层开发
1 预约下单
1.1 需求分析
1.1.1 业务流程
首先明确本模块在核心业务流程的位置,下图是项目的核心业务流程:
大概界面原型如下:
1.1.2 订单状态
本项目订单状态共有7种,如下图:
待支付:订单的初始状态。
派单中:用户支付成功后订单的状态由待支付变为派单中。
待服务:服务人员或机构抢单成功订单的状态由派单中变为待服务。
服务中:服务人员开始服务,订单状态变为服务中。
订单完成:服务人员完成服务订单状态变为订单完成。
已取消:订单是待支付状态时用户取消订单,订单状态变为已取消。
已关闭:订单已支付状态下取消订单后订单状态变为已关闭。
1.2 系统设计
1.2.1 订单表设计
在设计订单表时通常采用的结构是订单主表与订单明细表一对多关系结构,比如:在电商系统中,一个订单购买的多件不同的商品,设计订单表和订单明细表:
订单表:记录订单号、订单金额、下单人、订单状态等信息。
订单明细表:记录该订单购买商品的信息,包括:商品名称、商品价格、交易价格、购买商品数量等。
如下图:
1.2.2 表结构的设置
除了订单号、订单金额、订单状态、下单人ID等字段外,订单表还存储哪些信息?
根据需求梳理预约下单提交的数据如下:
通过分析,订单表包括以下几部分:
订单基础信息:订单号、订单状态、排序字段、是否显示标记等。
价格信息:单价、购买数量、优惠金额、订单总金额等。
下单人信息:下单人ID、联系方式、位置信息(相当于收货地址)等。
服务(商品)相关信息:服务类型名称、服务项名称、服务单价、价格单位、购买数量等。
服务信息相当于商品,如果有订单明细表要在订单明细表中存储,本项目将服务相关信息存储在订单表。
Mysql表结构如下:
create table `jzo2o-orders`.orders
(id bigint not null comment '订单id'constraint `PRIMARY`primary key,user_id bigint not null comment '订单所属人',serve_type_id bigint null comment '服务类型id',serve_type_name varchar(50) null comment '服务类型名称',serve_item_id bigint not null comment '服务项id',serve_item_name varchar(50) null comment '服务项名称',serve_item_img varchar(255) null comment '服务项图片',unit int null comment '服务单位',serve_id bigint not null comment '服务id',orders_status int not null comment '订单状态,0:待支付,100:派单中,200:待服务,300:服务中,400:待评价,500:订单完成,600:已取消,700:已关闭',pay_status int null comment '支付状态,2:待支付,4:支付成功',refund_status int null comment '退款状态 1退款中 2退款成功 3退款失败',price decimal(10, 2) not null comment '单价',pur_num int default 1 not null comment '购买数量',total_amount decimal(10, 2) not null comment '订单总金额',real_pay_amount decimal(10, 2) not null comment '实际支付金额',discount_amount decimal(10, 2) not null comment '优惠金额',city_code varchar(20) not null comment '城市编码',serve_address varchar(255) not null comment '服务详细地址',contacts_phone varchar(20) not null comment '联系人手机号',contacts_name varchar(255) not null comment '联系人姓名',serve_start_time datetime not null comment '服务开始时间',lon double(10, 5) null comment '经度',lat double(10, 5) null comment '纬度',pay_time datetime null comment '支付时间',evaluation_time datetime null comment '评价时间',trading_order_no bigint null comment '支付服务交易单号',transaction_id varchar(50) null comment '第三方支付的交易号',refund_no bigint null comment '支付服务退款单号',refund_id varchar(50) null comment '第三方支付的退款单号',trading_channel varchar(50) null comment '支付渠道',display int default 1 null comment '用户端是否展示,1:展示,0:隐藏',sort_by bigint null comment '排序字段,serve_start_time毫秒级时间戳+订单id后六位',create_time datetime default CURRENT_TIMESTAMP not null,update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP
)
数据来源分析:
其中serve_id,pur_num ,serve_start_time 是前端传过来的
1.3 开发远程调用接口
下单接口保存的数据较多,有一些数据需要远程调用来获取:
- 根据地址簿Id远程调用客户中心,查询我的地址簿信息。
- 根据服务Id远程调用运营基础服务,查询服务相关的信息。
1.3.0 复习下远程调用的开发
由于远程调用接口会被大量微服务所用,因此可以把接口抽取复用到一个API工程
之后,例如查询用户的远程调用,我们需要在API工程写一个接口,而在Customer微服务(远程调用中的服务端)进行实现接口即可,因此在开发中,一般先开发服务端,再开发远程调用的客户端,最后把API工程打包放入依赖仓库即可使用。
1.3.1 查询地址簿远程接口
微服务之间远程调用的接口统一定义在jzo2o-api工程。
查询地址簿远程接口是根据地址簿ID查询地址簿信息,接口定义如下:
接口路径:GET/customer/inner/address-book/{id}
请求数据类型 application/x-www-form-urlencoded
jzo2o-api工程定义接口
/*** 地址薄相关的远程调用接口*/
//contextId 指定FeignClient实例的上下文id,如果不设置默认为类名,value指定微服务的名称,path:指定接口地址
@FeignClient(contextId = "jzo2o-customer", value = "jzo2o-customer", path = "/customer/inner/address-book")
public interface AddressBookApi {@GetMapping("/{id}")AddressBookResDTO detail(@PathVariable("id") Long id);
}
Customer服务实现接口
/*** 地址薄远程调用*/
@RestController
@RequestMapping("inner/address-book")
@Api(tags = "内部接口 - 地址薄相关接口")
public class InnerAddressBookController implements AddressBookApi {private IAddressBookService addressBookService;@Override@GetMapping("/{id}")@ApiOperation("地址薄详情")@ApiImplicitParams({@ApiImplicitParam(name = "id", value = "地址薄id", required = true, dataTypeClass = Long.class)})public AddressBookResDTO detail(@PathVariable("id") Long id) {AddressBook addressBook = addressBookService.getById(id);return BeanUtil.toBean(addressBook, AddressBookResDTO.class);}
1.3.2 查询服务&服务项远程接口
jzo2o-api工程定义接口
@FeignClient(contextId = "jzo2o-foundations", value = "jzo2o-foundations", path = "/foundations/inner/serve")
public interface ServeApi {@GetMapping("/{id}")ServeAggregationResDTO findById(@PathVariable("id") Long id);}
foundations服务实现接口
很明显这是个多表关联查询,不能用MP,因此先开发Mapper
Mapper开发
/*** 根据id查询详情** @param id 服务id* @return 服务详情*/ServeAggregationResDTO findServeDetailById(@Param("id") Long id);
<select id="findServeDetailById" resultType="com.jzo2o.api.foundations.dto.response.ServeAggregationResDTO">SELECTserve.id,serve.city_code,serve.price,serve.is_hot,serve.hot_time_stamp,serve.sale_status,item.id AS serve_item_id,item.`name` AS serve_item_name,item.img AS serve_item_img,item.detail_img,item.serve_item_icon,item.unit,item.sort_num AS serve_item_sort_num,item.serve_type_id AS serve_type_id,type.`name` AS serve_type_name,type.img AS serve_type_img,type.serve_type_icon,type.sort_num AS serve_type_sort_numFROMserveinner JOIN serve_item AS item ON item.id = serve.serve_item_idinner JOIN serve_type AS type ON type.id = item.serve_type_idWHEREserve.id = #{id}</select>
Service层开发
/*** 根据id查询详情** @param id 服务id* @return 服务详情*/@Overridepublic ServeAggregationResDTO findServeDetailById(Long id) {return baseMapper.findServeDetailById(id);}
Controller开发
@RestController
@RequestMapping("/inner/serve")
@Api(tags = "内部接口 - 服务相关接口")
public class InnerServeController implements ServeApi {@Resourceprivate IServeService serveService;@Override@GetMapping("/{id}")@ApiOperation("根据id查询服务")@ApiImplicitParams({@ApiImplicitParam(name = "id", value = "服务项id", required = true, dataTypeClass = Long.class)})public ServeAggregationResDTO findById(@NotNull(message = "id不能为空") @PathVariable("id") Long id) {return serveService.findServeDetailById(id);}
}
1.4 熔断降级
1.4.1 复习
什么是熔断降级?
在微服务架构一定要去预防微服务雪崩问题,微服务雪崩问题是指在微服务架构中,当一个服务出现故障时,由于服务之间的依赖关系,故障可能会传播到其他服务,导致大规模的服务失败,系统无法正常运行。这种情况就像雪崩一样,最初一个小问题最终引发了整个系统的崩溃。简单理解微服务雪崩就是微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。
常用的预防微服务雪崩的的方法:
超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
熔断降级:当服务的异常数或异常比例超过了预设的阈值时,熔断器会进入开启状态,暂时中断对该服务的请求,此时走降级方法,能够快速响应,确保系统的基本功能能够继续运行。
限流:限制对服务的请求速率,避免短时间内大量的请求导致系统崩溃。
线程池隔离:给要请求的资源分配一个线程池,线程池去控制请求数量
1.4.2 使用sentinel实现熔断降级
本项目使用Sentinel实现限流、熔断等机制预防微服务雪崩。
熔断降级是微服务保护的一种方法,当使用Feign进行远程调用,在客户端通过熔断降级措施进行微服务保护。
如下图:
orders-manager订单服务请求customer查询地址簿,在进行feign远程调用过程出现异常将走降级方法,当异常比例或异常数达到一定的阈值将触发熔断,熔断期间将直接走降级逻辑快速响应。
当customer服务恢复后,熔断时间结束此时会再次尝试请求customer,如果成功请求将关闭熔断,恢复原来的链路。
根据上图可知,熔断、降级发生在客户端,下边在订单管理服务(调用customer的客户端)定义CustomerClient类用于请求customer服务。
1.4.3 客户端集成sentinel
这里是以服务提供者为单独定义远程调用Client类,如果要远程调用jzo2o-foundations服务则定义CustomerClient 类。
添加nacos配置文件shared-sentinel.yaml,如下:
在order项目中引入shared-sentinel.yaml配置文件:
cloud:nacos:config:file-extension: yamlshared-configs: # 共享配置- data-id: shared-redis-cluster.yaml # 共享redis集群配置refresh: false- data-id: shared-xxl-job.yaml # xxl-job配置refresh: false- data-id: shared-rabbitmq.yaml # rabbitmq配置refresh: false- data-id: shared-es.yaml # esrefresh: false- data-id: shared-mysql.yaml # mysql配置refresh: false- data-id: shared-sentinel.yaml # msentinel配置refresh: false
项目代码中添加依赖
<dependency><groupId>com.jzo2o</groupId><artifactId>jzo2o-sentinel</artifactId>
</dependency>
问:为什么这样呢?
答:我在fremwork中定义了全部依赖,只需要在需要这个sentinel的地方导入这一个依赖就可以全部导入所有相关依赖
1.4.4 sentinel实现熔断降级代码
问:为什么不在api定义这个熔断处理器
答:因为每个客户端的业务需求不一样
@SentinelResource注解的属性说明:
value: 用于定义资源的名称,即 Sentinel 会对该资源进行流量控制和熔断降级。
fallback :非限流、熔断等导致的异常执行的降级方法
blockHandler :触发限流、熔断时执行的降级方法
测试:
/*** 调用customer的客户端类*/
@Component
@Slf4j
public class CustomerClient {@Resourceprivate AddressBookApi addressBookApi;/*** 客户端定义自己的降级逻辑* @param id* @return*///value 资源名称 将来在sentinel可以查到//fallback 定义降级逻辑//blockHandler 定义降级逻辑@SentinelResource(value = "getAddressBookDetail", fallback = "detailFallback", blockHandler = "detailBlockHandler")public AddressBookResDTO getDetail(Long id){AddressBookResDTO detail = addressBookApi.detail(id);return detail;}//getDetail执行异常走这个方法public AddressBookResDTO detailFallback(Long id, Throwable throwable) {log.error("非限流、熔断等导致的异常执行的降级方法,id:{},throwable:", id, throwable);return null;}//熔断后的降级逻辑public AddressBookResDTO detailBlockHandler(Long id, BlockException blockException) {log.error("触发限流、熔断时执行的降级方法,id:{},blockException:", id, blockException);return null;}
}
下边在下单方法中通过CustomerClient 调用customer:
@Slf4j
@Service
public class OrdersCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersCreateService {@Resourceprivate CustomerClient customerClient;/*** 下单服务* @param placeOrderReqDTO* @return*/@Overridepublic PlaceOrderResDTO placeOrder(PlaceOrderReqDTO placeOrderReqDTO) {//地址簿idLong addressBookId = placeOrderReqDTO.getAddressBookId();//下单人信息,获取地址簿,调用jzo2o-customer服务获取AddressBookResDTO detail = customerClient.getDetail(addressBookId);//服务相关信息,调用jzo2o-foundations获取//生成订单号//计算价格//组装订单信息,插入数据库订单表return null;}}
使用项目使用@EnableFeignClients扫描Feign接口,生成代理对象。
具体代码在jzo2o-api工程:
@Slf4j
@Configuration
@EnableFeignClients(basePackages = "com.jzo2o.api")
@Import({com.jzo2o.utils.MyQueryMapEncoder.class})
@ConditionalOnProperty(prefix = "feign", name = "enable", havingValue = "true")
public class ClientScanConfiguration {........
在CustomerClient 中注入了Feign接口的代理对象,通过Feign进行远程调用。
1.4.5 测试
1、通过接口文档测试下单接口,触发customerClient.getDetail(addressBookId);
五秒内俩异常就测完了,关闭customer服务发两次请求就行了
2、在sentinel中配置熔断规则
5秒以内最少请求2次,有1次异常则进行熔断。熔断时长为30秒。
3、测试:
一次异常后熔断:
后面再发请求就是熔断降级方法:
1.5 接口设计
整个订单模块包括:订单管理、抢单、派单、历史订单四个小模块,对应的工程如下:
1.5.1 接口分析
除了serve_id、pur_num、serve_start_time 由前端传入以外还需要传入以下参数:
优惠券ID:用户选择优惠券,系统根据优惠券的信息计算优惠金额,需要前端传入优惠券的Id。
我的地址簿ID:用户从我的地址簿中选择地址,前端传入我的地址簿Id,系统从我的地址簿中查询服务地址及具体的经纬度坐标。
其中服务和服务项的信息需要远程调用Foundations服务,而关于客户的信息需要远程调用Costumer服务
接口定义如下:
接口名称:下单接口
接口功能:普通用户创建订单
接口路径:POST/orders-manager/consumer/orders/place
请求数据类型 application/json
1.5.1 接口开发
Controller层开发
@ApiOperation("下单接口")@PostMapping("/place")public PlaceOrderResDTO place(@RequestBody PlaceOrderReqDTO placeOrderReqDTO) {return ordersCreateService.placeOrder(placeOrderReqDTO);}