微服务之开发订单功能

业务流程

第一部分是信息的收集

主要是参数类型数据的完整性验证,计算以及转换

第二部分是数据库操作

减少库存,删除购物车,新增订单,和新增订单项

第三部分是收集需要的返回值

我们新增订单成功后,要返回给前端一些信息,例如订单号,实际支付金额等

持久层

开发新增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("修改完成!");
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值