业务流程
第一部分是信息的收集
主要是参数类型数据的完整性验证,计算以及转换
第二部分是数据库操作
减少库存,删除购物车,新增订单,和新增订单项
第三部分是收集需要的返回值
我们新增订单成功后,要返回给前端一些信息,例如订单号,实际支付金额等
持久层
开发新增order_item持久层
order_item表中保存每张订单包含什么商品的信息, 我们新增这个表,要包含订单号,商品id和相关信息, mapper下创建OmsOrderItemMapper, 为了提高效率, 执行批量新增
@Repository
public interface OmsOrderItemMapper {
// 新增订单项(order_item)的方法
// 一个订单可能包含多个订单项,如果循环遍历新增每一个订单项,连库次数多,效率降低
// 我们采用一次连库新增多条订单项的方法,完成这个业务,来提高数据库操作效率
// 也就是进行批量新增,这个方法的参数就是一个List集合了
int insertOrderItemList(List<OmsOrderItem> omsOrderItems);
}
OmsOrderItemMapper.xml文件添加内容
<!-- 新增订单项(order_item)的方法 -->
<insert id="insertOrderItemList">
insert into oms_order_item(
id,
order_id,
sku_id,
title,
bar_code,
data,
main_picture,
price,
quantity
) values
<foreach collection="list" item="ooi" separator=",">
(
#{ooi.id},
#{ooi.orderId},
#{ooi.skuId},
#{ooi.title},
#{ooi.barCode},
#{ooi.data},
#{ooi.mainPicture},
#{ooi.price},
#{ooi.quantity}
)
</foreach>
</insert>
开发新增order的持久层
mapper包下再创建OmsOrderMapper, 添加新增Order的方法
@Repository
public interface OmsOrderMapper {
// 新增订单的方法
int insertOrder(OmsOrder order);
}
OmsOrderMapper.xml中添加方法
<!-- 新增订单的mapper -->
<insert id="insertOrder" >
insert into oms_order(
id,
sn,
user_id,
contact_name,
mobile_phone,
telephone,
province_code,
province_name,
city_code,
city_name,
district_code,
district_name,
street_code,
street_name,
detailed_address,
tag,
payment_type,
state,
reward_point,
amount_of_original_price,
amount_of_freight,
amount_of_discount,
amount_of_actual_pay,
gmt_pay,
gmt_order,
gmt_create,
gmt_modified
) values (
#{id},
#{sn},
#{userId},
#{contactName},
#{mobilePhone},
#{telephone},
#{provinceCode},
#{provinceName},
#{cityCode},
#{cityName},
#{districtCode},
#{districtName},
#{streetCode},
#{streetName},
#{detailedAddress},
#{tag},
#{paymentType},
#{state},
#{rewardPoint},
#{amountOfOriginalPrice},
#{amountOfFreight},
#{amountOfDiscount},
#{amountOfActualPay},
#{gmtPay},
#{gmtOrder},
#{gmtCreate},
#{gmtModified}
)
</insert>
关于Leaf
Leaf是美团公司开源的一个分布式序列号(id)生成系统, 我们可以在Github网站上下载直接使用
为什么需要Leaf
上面的图片中, 是一个实际开发中常见的读写分离的数据库部署格式, 专门进行数据更新(写)的有两个数据库节点, 它们同时新增数据可能产生相同的id, 一旦生成相同的id,数据同步就会有问题, 会产生id冲突,甚至引发异常, 我们为了在这种多数据库节点的环境下能够产生唯一id, 可以使用Leaf来生成
Leaf的工作原理
Leaf底层支持通过"雪花算法"生成不同id, 我们使用的是单纯的序列
要想使用,需要事先设置好leaf的起始值和缓存id数
举例,从1000开始缓存500, 也就是从id1000~1499这些值,都会保存在Leaf的内存中,当有服务需要时,直接取出下一个值, 取出过的值不会再次生成
leaf要想设置起始值和缓存数, 需要给leaf创建一个指定格式的数据库表, 运行过程中会从数据库表获取信息, 我们当前的信息保存在leafdb.leaf_alloc表中
业务层
创建OmsOrderServiceImpl类
// 后期秒杀业务也需要生成订单,可以直接调用当前类中的方法
@DubboService
@Service
@Slf4j
public class OmsOrderServiceImpl implements IOmsOrderService {
@Autowired
private OmsOrderMapper omsOrderMapper;
@Autowired
private OmsOrderItemMapper omsOrderItemMapper;
@Autowired
private IOmsCartService omsCartService;
@DubboReference
private IForOrderSkuService dubboSkuService;
// 新增订单的方法
// 这个方法调用了product模块的数据库操作功能,
// 运行发送异常时,必须依靠分布式事务组件(seata)进行回滚,以保证事务的原子性
// 我们要利用注解激活Seata的分布式事务功能
@GlobalTransactional
@Override
public OrderAddVO addOrder(OrderAddDTO orderAddDTO) {
// 第一部分:收集信息,准备数据
// 先实例化OmsOrder对象
OmsOrder order=new OmsOrder();
// 将当前方法参数OrderAddDTO类型对象的同名属性赋值给OmsOrder对象
BeanUtils.copyProperties(orderAddDTO,order);
// orderAddDTO中的属性较少,order对象还有一些属性不能被赋值,需要我们手动计算或赋值
// 我们可以专门编写一个方法,在这个方法中处理
loadOrder(order);
// 运行完上面的方法,order的赋值就完成了
// 下面开始为当前订单包含的订单项OmsOrderItem赋值
// orderAddDTO中包含了一个OrderItemAddDTO类型的集合
// 我们需要将这个集合转换为OmsOrderItem类型
// 首先如果OrderItemAddDTO集合是空,是要抛出异常的
List<OrderItemAddDTO> itemAddDTOs=orderAddDTO.getOrderItems();
if(itemAddDTOs==null || itemAddDTOs.isEmpty()){
// 如果订单参数中一件商品都没有,就无法继续生成订单了
throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,
"订单中必须至少包含一件商品");
}
// 先将要获得的最终结果的集合实例化
List<OmsOrderItem> omsOrderItems=new ArrayList<>();
// 遍历DTO的集合
for(OrderItemAddDTO addDTO : itemAddDTOs){
// 还是先将当前遍历的addDTO对象转化为OmsOrderItem类型对象
OmsOrderItem orderItem=new OmsOrderItem();
BeanUtils.copyProperties(addDTO,orderItem);
// 上面的赋值操作后,仍然有个别属性没有被赋值,下面进行赋值
// 赋值id
Long itemId=IdGeneratorUtils.getDistributeId("order_item");
orderItem.setId(itemId);
// 赋值orderId
orderItem.setOrderId(order.getId());
// 将赋好值的orderItem对象保存到集合中
omsOrderItems.add(orderItem);
// 第二部分:执行数据库操作
// 我们减少库存和删除选中的购物车信息都是有skuId作为依据的
// 所以上述两个操作在当前循环中继续编写即可
// 1.减少库存
// 获得skuId
Long skuId=orderItem.getSkuId();
// 执行减少库存的方法(dubbo调用product模块写好的功能)
int rows=dubboSkuService.reduceStockNum(skuId,orderItem.getQuantity());
// 判断执行修改影响的行数
if(rows==0){
log.warn("商品skuId:{},库存不足",skuId);
//库存不足不能继续运行,抛出异常,seata会自动终止事务
throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,
"库存不足!");
}
// 2.删除购物车信息
OmsCart omsCart=new OmsCart();
omsCart.setUserId(order.getUserId());
omsCart.setSkuId(skuId);
// 执行删除操作
omsCartService.removeUserCarts(omsCart);
}
// 3.执行新增订单
// OmsOrderMapper直接调用新增订单的方法即可
omsOrderMapper.insertOrder(order);
// 4.新增订单项
// OmsOrderItemMapper直接调用批量新增订单项的方法即可
omsOrderItemMapper.insertOrderItemList(omsOrderItems);
// 第三部分:返回订单信息给前端
// 当前业务逻辑层方法返回值为OrderAddVO,这是业务要求我们的返回类型
// 我们需要做的就是实例化这个对象,然后为它赋所有值
OrderAddVO addVO=new OrderAddVO();
// 给addVO各个属性赋值
addVO.setId(order.getId());
addVO.setSn(order.getSn());
addVO.setCreateTime(order.getGmtOrder());
addVO.setPayAmount(order.getAmountOfActualPay());
// 最后千万别忘了返回
// 返回addVO!!!!
return addVO;
}
private void loadOrder(OmsOrder order) {
// 本方法针对order对象未被赋值的属性,进行手动赋值
// 给订单id赋值,我们使用Leaf分布式方式来赋值
Long id= IdGeneratorUtils.getDistributeId("order");
order.setId(id);
// 赋值用户id
// 以后做秒杀时,用户id会被赋值,所以这里要判断一下,没有用户id再为其赋值
if(order.getUserId()==null){
// getUserId方法是从SpringSecurity上下文中获得的用户id
order.setUserId(getUserId());
}
// 赋值订单号
// 使用随机的UUID做订单号即可
order.setSn(UUID.randomUUID().toString());
// 为订单状态赋值
// 如果订单状态为null,默认赋值为0
if(order.getState()==null){
order.setState(0);
}
// 为了保证下单时间\数据创建时间\最后修改时间一致
// 我们在这里为它们赋相同的值
LocalDateTime now=LocalDateTime.now();
order.setGmtOrder(now);
order.setGmtCreate(now);
order.setGmtModified(now);
// 计算实际支付金额
// 计算公式: 实际支付金额=原价-优惠+运费
// 数据类型使用BigDecimal,是没有浮点偏移的精确计算
BigDecimal price=order.getAmountOfOriginalPrice();
BigDecimal freight=order.getAmountOfFreight();
BigDecimal discount=order.getAmountOfDiscount();
BigDecimal actualPay=price.subtract(discount).add(freight);
// 最后将计算完成的实际支付金额赋值给order
order.setAmountOfActualPay(actualPay);
}
@Override
public void updateOrderState(OrderStateUpdateDTO orderStateUpdateDTO) {
}
@Override
public JsonPage<OrderListVO> listOrdersBetweenTimes(OrderListTimeDTO orderListTimeDTO) {
return null;
}
@Override
public OrderDetailVO getOrderDetail(Long id) {
return null;
}
public CsmallAuthenticationInfo getUserInfo(){
// 编码获得SpringSecurity上下文中保存的权限
UsernamePasswordAuthenticationToken authenticationToken=
(UsernamePasswordAuthenticationToken)
SecurityContextHolder.getContext().getAuthentication();
// 为了保险起见,判断一下从SpringSecurity中获得的信息是不是null
if(authenticationToken == null){
throw new CoolSharkServiceException(ResponseCode.UNAUTHORIZED,
"请您先登录!");
}
// 上下文信息确定存在后,获取其中的用户信息
// 这个信息就是有JWT解析获得的
CsmallAuthenticationInfo csmallAuthenticationInfo=
(CsmallAuthenticationInfo) authenticationToken.getCredentials();
// 返回登录信息
return csmallAuthenticationInfo;
}
// 业务逻辑层大多数方法需要用户的信息实际上就是用户的ID,编写一个只返回用户ID的方法方便调用
public Long getUserId(){
return getUserInfo().getId();
}
}
控制层
新建OmsOrderController
@RestController
@RequestMapping("/oms/order")
@Api(tags = "订单模块")
public class OmsOrderController {
@Autowired
private IOmsOrderService omsOrderService;
@PostMapping("/add")
@ApiOperation("执行新增订单的方法")
@PreAuthorize("hasRole('user')")
public JsonResult<OrderAddVO> addOrder(@Validated OrderAddDTO orderAddDTO){
OrderAddVO orderAddVO=omsOrderService.addOrder(orderAddDTO);
return JsonResult.ok(orderAddVO);
}
}
启动Nacos\seata
依次启动服务Leaf\product\[passport]\order
访问10005执行新增
Seata使用常见错误
Seata在开始工作时,会将方法相关对象序列化后保存在对应数据库的undo_log表中, 但是Seata我们序列化的方式支持很多中,常见的jackson格式序列化的情况下,不支持java对象LocalDataTime类型的序列化,序列化运行时会发送错误:
com.fasterxml.iackson.databind.exc.InvalidDefinitionException Create breakpoint : Tvpe id
如果见到这样的错误, 就是因为jackson不能序列化LocalDataTime导致的, 要想解决,两方面思路
1.将序列化过程中LocalDataTime类型转换为Date
2.将Seata序列化转换为kryo类型,但是需要在pom文件中添加依赖
<!--解决seata序列化问题-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-serializer-kryo</artifactId>
</dependency>
yml文件使用kryo序列化对象的配置
#seata服务端
seata:
tx-service-group: csmall_group
service:
vgroup-mapping:
csmall_group: default
grouplist:
default: ${my.server.addr}:8091
client:
undo:
log-serialization: kryo
订单查询功能
在新增订单成功之后,用户会看到订单列表, 可以按时间查询一段时间范围内的订单列表, 我们默认查询当前时间一个月以内的所有订单信息, 订单信息要包括oms_order和oms_order_item两个表的信息, 所以是一个联表查询
持久层
mapper xml的语句, 注意, 由于两个表中有相同的字段名,因此相同字段名需要起别名ooi_id
SELECT
oo.id,
oo.sn,
oo.user_id,
oo.contact_name,
oo.state,
oo.amount_of_actual_pay,
oo.gmt_order,
oo.gmt_pay,
oo.gmt_create,
oo.gmt_modified,
ooi.id ooi_id,
ooi.order_id,
ooi.sku_id,
ooi.title,
ooi.price,
ooi.quantity
FROM oms_order oo
INNER JOIN oms_order_item ooi ON oo.id=ooi.order_id
WHERE
oo.user_id=1
AND
oo.gmt_create > '2022-10-01 08:00:00'
AND
oo.gmt_create < NOW()
ORDER BY oo.gmt_modified DESC
业务层
// 分页查询当前登录用户,指定时间范围内的所有订单
// 默认查询最近一个月内的订单,查询返回值OrderListVO,是包含订单信息和订单中商品信息的对象
// 持久层已经编写好OrderListVO类和order和order_item表的映射关系(xml文件中关联查询)
@Override
public JsonPage<OrderListVO> listOrdersBetweenTimes(OrderListTimeDTO orderListTimeDTO) {
// 业务逻辑层要判断用户指定的时间信息,必须保证它们合理才能进行后面的操作
// 编写一个方法,来判断时间的可用
validateTimeAndLoadTimes(orderListTimeDTO);
// 获得用户Id
Long userId=getUserId();
// 将用户Id赋值到参数中
orderListTimeDTO.setUserId(userId);
// 设置分页条件
PageHelper.startPage(orderListTimeDTO.getPage(),
orderListTimeDTO.getPageSize());
// 调用mapper编写的关联查询方法
List<OrderListVO> list=omsOrderMapper.
selectOrdersBetweenTimes(orderListTimeDTO);
// 别忘了返回!!!
return JsonPage.restPage(new PageInfo<>(list));
}
private void validateTimeAndLoadTimes(OrderListTimeDTO orderListTimeDTO) {
// 获取参数中的开始和结束时间
LocalDateTime start=orderListTimeDTO.getStartTime();
LocalDateTime end=orderListTimeDTO.getEndTime();
// 为了不在增加业务的复杂度,我们设计当start和end中有一个为null时就查询最近一个月的订单
if(start==null || end ==null){
// 设置开始时间为当前时间的前一个月
start=LocalDateTime.now().minusMonths(1);
end=LocalDateTime.now();
// 赋值到参数中
orderListTimeDTO.setStartTime(start);
orderListTimeDTO.setEndTime(end);
}else{
// 如果start和end都非空
// 要判断start是否小于end,如果end小于start要抛异常
// 如果编写支持国际不同时区的时间判断,在比较时要添加时区的修正
if(end.toInstant(ZoneOffset.of("+8")).toEpochMilli()<
start.toInstant(ZoneOffset.of("+8")).toEpochMilli()){
// 如果判断结果表示结束时间小于开始时间,抛出异常,终止方法
throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST,
"结束时间应大于起始时间");
}
}
}
控制层
@GetMapping("/list")
@ApiOperation("分页查询当前用户指定时间的订单")
@PreAuthorize("hasRole('user')")
public JsonResult<JsonPage<OrderListVO>> listUserOrders(
OrderListTimeDTO orderListTimeDTO){
JsonPage<OrderListVO> jsonPage=
omsOrderService.listOrdersBetweenTimes(orderListTimeDTO);
return JsonResult.ok(jsonPage);
}
订单修改功能
订单的状态修改是非常普通的业务
随着商品的购买流程,订单的状态有
状态:
0=未支付
1=已关闭(超时未支付)
2=已取消
3=已支付
4=已签收
5=已拒收
6=退款处理中
7=已退款
持久层
修改订单状态就是根据订单id修改订单的state
我们随着业务的发展,订单可能需要更多修改的需求
订单的列(字段)比较多,如果每个字段修改,都需要编写一个方法的话,那么方法的数量会非常多
如果我们编写一个方法,能够接收订单对象的实体类参数(OmsOrder)
我们要实现可以根据OmsOrder对象的实际数据来实现动态的修改要修改的字段
Mybatis中可以通过编写动态修改sql语句完成这个需求
OmsOrderMapper接口添加方法
<!--
动态修改订单,参数是omsOrder对象
参数对象中必须有id,id不可修改,其他属性不为空就修改为当前属性值
-->
<!--
OmsOrder对象动态修改数据库中数据的sql语句
Mybatis框架动态生成修改sql语句需要设置一个<set>标签
1.在<set>标签的位置生成一个set关键字
2.在<set></set>标签之间的范围内,动态生成sql语句,如果生成的内容最后是","就将它删除
-->
<update id="updateOrderById">
update oms_order
<set>
<if test="contactName!=null">
contact_name=#{contactName},
</if>
<if test="mobilePhone!=null">
mobile_phone=#{mobilePhone},
</if>
<if test="telephone!=null">
telephone=#{telephone},
</if>
<if test="streetCode!=null">
street_code=#{streetCode},
</if>
<if test="streetName!=null">
street_name=#{streetName},
</if>
<if test="detailedAddress!=null">
detailed_address=#{detailedAddress},
</if>
<if test="tag!=null">
tag=#{tag},
</if>
<if test="paymentType!=null">
payment_type=#{paymentType},
</if>
<if test="state!=null">
state=#{state},
</if>
<if test="rewardPoint!=null">
reward_point=#{rewardPoint},
</if>
<if test="amountOfOriginalPrice!=null">
amount_of_original_price=#{amountOfOriginalPrice},
</if>
<if test="amountOfFreight!=null">
amount_of_freight=#{amountOfFreight},
</if>
<if test="amountOfDiscount!=null">
amount_of_discount=#{amountOfDiscount},
</if>
<if test="amountOfActualPay!=null">
amount_of_actual_pay=#{amountOfActualPay},
</if>
<if test="gmtPay!=null">
gmt_pay=#{gmtPay},
</if>
</set>
where
id=#{id}
</update>
业务逻辑层
// 根据订单id,修改订单状态的业务逻辑层方法
@Override
public void updateOrderState(OrderStateUpdateDTO orderStateUpdateDTO) {
// 参数OrderStateUpdateDTO包含了订单id和要修改的订单状态
// 可以使用我们编写的动态修改订单信息的方法,需要创建OmsOrder对象
OmsOrder order=new OmsOrder();
BeanUtils.copyProperties(orderStateUpdateDTO,order);
// 调用持久层方法修改即可
omsOrderMapper.updateOrderById(order);
}
控制层
@PostMapping("/update/state")
@ApiOperation("修改订单状态的方法")
@PreAuthorize("hasRole('user')")
public JsonResult updateOrderState(
@Validated OrderStateUpdateDTO orderStateUpdateDTO){
omsOrderService.updateOrderState(orderStateUpdateDTO);
return JsonResult.ok("修改完成!");
}