当我们在购物车种选择好商品时,会选择右下角的结算按钮去下单:
会跳转到订单结算页,并不是直接去付款:
因此此处页面需要渲染的内容主要包含3部分:
这部分渲染已经在前端实现了
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyouartifactId>
<groupId>com.leyou.parentgroupId>
<version>1.0.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.leyou.servicegroupId>
<artifactId>ly-orderartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.leyou.servicegroupId>
<artifactId>ly-item-interfaceartifactId>
<version>${leyou.latest.version}version>
dependency>
<dependency>
<groupId>com.leyou.commongroupId>
<artifactId>ly-commonartifactId>
<version>${leyou.latest.version}version>
dependency>
<dependency>
<groupId>com.leyou.servicegroupId>
<artifactId>ly-auth-commonartifactId>
<version>${leyou.latest.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency><dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.github.wxpaygroupId>
<artifactId>wxpay-sdkartifactId>
<version>0.0.3version>
dependency>
dependencies>
project>
server:
port: 8089
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://localhost:3306/yun6
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
jackson:
default-property-inclusion: non_null
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
mybatis:
type-aliases-package: com.leyou.order.pojo
ly:
jwt:
pubKeyPath: H:/javacode/idea/rsa/rsa.pub # 公钥地址
cookieName: LY_TOKEN
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.leyou.order.mapper")
public class LyOrderApplication {
public static void main(String[] args) {
SpringApplication.run(LyOrderApplication.class);
}
}
订单微服务和购物车微服务有共同特征,都需要解析token,进行鉴权,知道当前登陆的用户是谁,所以应该把购物车微服务中的JwtProperties
,UserInterceptor
,MvcConfig
在订单微服务中准备一份,结构如下:
注:以上代码其实可以抽取到ly-common
中,减少代码重复,但是不用担心拦截器会无条件生效,只有配置了MvcConfig
拦截器才会生效
@Data
@Table(name = "tb_order")
public class Order {
@Id
private Long orderId;// id
private Long totalPay;// 总金额
private Long actualPay;// 实付金额
private Integer paymentType; // 支付类型,1、在线支付,2、货到付款
private String promotionIds; // 参与促销活动的id
private Long postFee = 0L;// 邮费
private Date createTime;// 创建时间
private String shippingName;// 物流名称
private String shippingCode;// 物流单号
private Long userId;// 用户id
private String buyerMessage;// 买家留言
private String buyerNick;// 买家昵称
private Boolean buyerRate;// 买家是否已经评价
private String receiver; // 收货人全名
private String receiverMobile; // 移动电话
private String receiverState; // 省份
private String receiverCity; // 城市
private String receiverDistrict; // 区/县
private String receiverAddress; // 收货地址,如:xx路xx号
private String receiverZip; // 邮政编码,如:310001
private Integer invoiceType = 0;// 发票类型,0无发票,1普通发票,2电子发票,3增值税发票
private Integer sourceType = 1;// 订单来源 1:app端,2:pc端,3:M端,4:微信端,5:手机qq端
@Transient
private OrderStatus orderStatus;
@Transient
private List<OrderDetail> orderDetails;
}
@Data
@Table(name = "tb_order_detail")
public class OrderDetail {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private Long orderId;// 订单id
private Long skuId;// 商品id
private Integer num;// 商品购买数量
private String title;// 商品标题
private Long price;// 商品单价
private String ownSpec;// 商品规格数据
private String image;// 图片
}
@Data
@Table(name = "tb_order_status")
public class OrderStatus {
@Id
private Long orderId;
private Integer status;
private Date createTime;// 创建时间
private Date paymentTime;// 付款时间
private Date consignTime;// 发货时间
private Date endTime;// 交易结束时间
private Date closeTime;// 交易关闭时间
private Date commentTime;// 评价时间
}
当我们在订单详情页确认好地址商品等信息无误后,点击右下角的提交订单:
发现报错了,不过没关系,因为这个接口我们没有实现,现在我们去实现
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDTO {
@NotNull
private Long addressId; // 收获人地址id
private Integer paymentType;// 付款类型
private List<CartDTO> carts;// 订单详情,carts又是一个集合 包含了商品信息,所以又定义一个cartDTO
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CartDTO {
private Long skuId; // 商品skuId
private Integer num; // 购买数量
}
之前订单详情页上我们发现商品的价格标题参数等信息都有但是为什么这里我们只传递ID这一个参数呢?这是因为安全问题,因为我们的URL是对外暴露的,防止有人利用insomnia等工具修改价格,之后再进行订单的提交,造成损失
OrderMapper:
public interface OrderMapper extends BaseMapper<Order> {
}
OrderDetailMapper:
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
}
OrderStatusMapper:
public interface OrderStatusMapper extends Mapper<OrderStatus>{
}
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<Long> createOrder(@RequestBody OrderDTO orderDTO){
return ResponseEntity.ok(orderService.createOrder(orderDTO));
}
}
@RequestBody
:
参考资料:@requestBody注解的使用
生成ID的方式——雪花算法
订单id的特殊性
订单数据非常庞大,将来一定会做分库分表。那么这种情况下, 要保证id的唯一,就不能靠数据库自增,而是自己来实现算法,生成唯一id。
雪花算法
这里的订单id是通过一个工具类生成的:
而工具类所采用的生成id算法,是由Twitter公司开源的snowflake(雪花)算法。
简单原理
雪花算法会生成一个64位的二进制数据,为一个Long型。(转换成字符串后长度最多19) ,其基本结构:
第一位:为未使用
第二部分:41位为毫秒级时间(41位的长度可以使用69年)
第三部分:5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
第四部分:最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高。经测试snowflake每秒能够产生26万个ID。
配置
为了保证不重复,我们给每个部署的节点都配置机器id:
ly:
worker:
workerId: 1
datacenterId: 1
加载属性:
@Data
@ConfigurationProperties(prefix = "ly.worker")
public class IdWorkerProperties {
private long workerId;// 当前机器id
private long dataCenterId;// 序列号
}
编写配置类:
@Configuration
@EnableConfigurationProperties(IdWorkerProperties.class)
public class IdWorkerConfig {
@Bean
public IdWorker idWorker(IdWorkerProperties prop) {
return new IdWorker(prop.getWorkerId(), prop.getDataCenterId());
}
}
使用:
直接@Autowired
注入,然后使用
准备物流、收货人信息:
前端页面传递过来的是addressId,我们需要根据这个id查询物流信息,但是因为没有做物流地址管理,所以我们准备一些假数据:
@Data
public class AddressDTO {
private Long id;
private String name;
private String phone;
private String state;
private String city;
private String district;
private String address;
private String zipCode;
private Boolean isDefault;
}
然后准备一个常量类:
public abstract class AddressClient {
public static final List<AddressDTO> addressList = new ArrayList<AddressDTO>(){
{
AddressDTO address = new AddressDTO();
address.setId(1L);
address.setAddress("太白南路");
address.setCity("西安");
address.setDistrict("雁塔区");
address.setName("max");
address.setPhone("15656789999");
address.setState("陕西");
address.setZipCode("7100710");
address.setIsDefault(true);
add(address);
AddressDTO address2 = new AddressDTO();
address2.setId(2L);
address2.setAddress("学院路三号");
address2.setCity("太原");
address2.setDistrict("尖草坪区");
address2.setName("su");
address2.setPhone("15656781314");
address2.setState("山西");
address2.setZipCode("03500150");
address2.setIsDefault(false);
add(address2);
}
};
public static AddressDTO findById(Long id){
for (AddressDTO addressDTO : addressList) {
if(addressDTO.getId() == id){
return addressDTO;
}
}
return null;
}
}
结构:
然后我们就可以在service中调用,根据id查到收货人信息
订单状态:
订单状态一般以数字表示状态,但是不容易理解,所以我们创建一个枚举:
public enum OrderStatusEnum {
UN_PAY(1, "初始化,未付款"),
PAYED(2, "已付款,未发货"),
DELIVERED(3, "已发货,未确认"),
SUCCESS(4, "已确认,未评价"),
CLOSED(5, "已关闭,交易失败"),
RATED(6, "已评价,交易结束")
;
private int code;
private String msg;
OrderStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int value(){
return this.code;
}
public String msg(){
return msg;
}
}
减库存:
减库存应该是在属于商品微服务的业务,因此我们写一个接口,在商品微服务那边实现:
GoodsClient:
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi{
}
GoodsApi:
@PostMapping("stock/decrease")
void decreaseStock(@RequestBody List<CartDTO> cartDTOS);
GoodsController:
@PostMapping("stock/decrease")
public ResponseEntity<Void> decreaseStock(@RequestBody List<CartDTO> cartDTOS){
goodsService.decreaseStock(cartDTOS);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
分析:减库存的业务实现
减库存可以采用同步调用(Feign的方式),也可以采用异步调用(RabbitMQ传递消息),我们这里采用同步调用,接下来我们分析为什么
如果我们采用异步调用的方式,减库存的这条消息发送到MQ就不管了,那么到底库存减成功了没有呢?这我们并不知道,如果库存不足,那么我们减库存失败,但是service的业务不会回滚,这个问题就是分布式事务问题,即跨服务的事务。减库存这个业务从订单微服务跨越到了商品微服务,而事务是由Spring来管理的,两套tomcat两套Spring,本身没有任何关联,但是却是一个事务,如果采用异步,这边的微服务执行失败另一边的微服务并不知道,破坏了事务的一致性,我们解决的方案是什么呢?
变异步调用为同步调用,如果一个微服务执行失败就会抛出异常,事务自然回滚(减库存的操作只能放在创建订单业务的最后,因为减库存执行失败事务自然回滚订单也不会创建成功,但是如果上来就先减库存,那玩意订单创建失败库存无法回滚),但是这种方案也不是最优的,因为我们没做优惠券功能,当我们做了优惠券功能,那计算优惠和减库存哪个放在最后呢?哪个放在最后都不可行,这时候就必须解决分布式事务问题了
解决分布式事务问题:
综上,在电商行业中适用的还是TCC,虽然业务变得复杂了,但是行之有效;如果是转账业务,适合异步确保,转账业务只需要消息可靠就可以,执行时间晚一点也无妨,所以异步确保的关键点是消息的可靠
但是在我们这个小项目中,无需把业务变得这么复杂,接下来讨论我们采用的同步调用的解决方案。
同步调用中加锁实现方式:
但是这里不推荐加锁实现,因为用了锁,就变成单线程了,相当于一执行这段代码就把数据库锁死,同一时刻只能有一个人来操作,这样的实现类似于悲观锁,默认线程安全问题一定会发生,在面对高并发时,往往性能很差。
那既然不推荐悲观锁,是不是可以采用乐观锁呢?乐观锁是默认线程安全问题不会发生,不加锁,但是不加锁会有线程安全问题,那怎么处理这件事情呢?
——我们不做查询不做判断,业务执行到减库存代码这里之后直接开始减库存,唉?这不是会超卖吗?不要紧,我们的sql内部可以加条件来判断,失败则事务回滚,所有人不论怎么操作,最后都会来操作数据库,但是数据库写了判断语句来判断库存,每个人来执行都会被判断,本质上还是乐观锁。如果执行失败会反馈失败信息,而不像是悲观锁那样线程阻塞,导致一直等待,性能上来将,这种处理方式优于加锁,我们的sql语句如下:
"UPDATE tb_stock SET stock = stock - #{num} WHERE sku_id = #{id} AND stock >= #{num}"
StockMapper:
public interface StockMapper extends BaseMapper<Stock> {
@Update("UPDATE tb_stock SET stock = stock - #{num} WHERE sku_id = #{id} AND stock >= #{num}")
int decreaseStock(@Param("id") Long id, @Param("num") Integer num);
}
GoodsService实现减库存:
@Transactional
public void decreaseStock(List<CartDTO> cartDTOS) {
for (CartDTO cartDTO : cartDTOS) {
int count = stockMapper.decreaseStock(cartDTO.getSkuId(), cartDTO.getNum());
if(count != 1){
throw new LyException(ExceptionEnum.STOCK_NOT_ENOUGH);
}
}
}
我们可以写一个测试类来测试:
可以看到库存只有1的时候执行失败,抛出异常,事务回滚,查询数据库,商品的库存还是1,不会发生超卖现象
最终的service:
@Transactional
public Long createOrder(OrderDTO orderDTO) {
// 1 新增订单
Order order = new Order();
// 1.1 订单编号,基本信息 -- 订单ID,雪花算法(snowflake)生成全局唯一的ID
long orderId = idWorker.nextId();
order.setOrderId(orderId);
order.setCreateTime(new Date());
order.setPaymentType(orderDTO.getPaymentType());
// 1.2 用户信息
UserInfo user = UserInterceptor.getUser();
order.setUserId(user.getId());
order.setBuyerNick(user.getUsername());
order.setBuyerRate(false);
// 1.3 收货人地址信息 -- orderDTO中只有地址ID(addressID),要根据地址ID去数据库中查询(假数据)
AddressDTO addr = AddressClient.findById(orderDTO.getAddressId());
order.setReceiver(addr.getName());//收货人
order.setReceiverMobile(addr.getPhone());//收货人手机号码
order.setReceiverAddress(addr.getAddress());//收货所在街道
order.setReceiverState(addr.getState());//收货人所在省
order.setReceiverCity(addr.getCity());//收货人所在城市
order.setReceiverDistrict(addr.getDistrict());//收货人所在区
order.setReceiverZip(addr.getZipCode());//收货人邮编
// 1.4 金额
Map<Long, Integer> numMap = orderDTO.getCarts()
.stream().collect(Collectors.toMap(CartDTO::getSkuId,CartDTO::getNum));
Set<Long> ids = numMap.keySet();
List<Sku> skus = goodsClient.querySkuByIds(new ArrayList<>(ids));
// 准备orderDetail集合
List<OrderDetail> details = new ArrayList<>();
Long totalPrice = 0L;
for (Sku sku : skus) {
totalPrice += sku.getPrice() * numMap.get(sku.getId());
//封装orderDetail
OrderDetail detail = new OrderDetail();
detail.setImage(StringUtils.substringBefore(sku.getImages(),","));
detail.setNum(numMap.get(sku.getId()));
detail.setOrderId(orderId);
detail.setOwnSpec(sku.getOwnSpec());
detail.setPrice(sku.getPrice());
detail.setSkuId(sku.getId());
detail.setTitle(sku.getTitle());
details.add(detail);
}
order.setTotalPay(totalPrice);
order.setActualPay(totalPrice + order.getPostFee() - 0 );// 实付金额= 总金额 + 邮费 - 优惠金额
// 1.5 写入数据库
int count = orderMapper.insertSelective(order);
if(count != 1){
log.error("[创建订单] 创建订单失败,orderID:{}", orderId);
throw new LyException(ExceptionEnum.CREATE_ORDER_ERROR);
}
// 2 新增订单详情
count = orderDetailMapper.insertList(details);
if(count != details.size()){
log.error("[创建订单] 创建订单失败,orderID:{}", orderId);
throw new LyException(ExceptionEnum.CREATE_ORDER_ERROR);
}
// 3 新增订单状态
OrderStatus orderStatus = new OrderStatus();
orderStatus.setOrderId(orderId);
orderStatus.setCreateTime(order.getCreateTime());
orderStatus.setStatus(OrderStatusEnum.UN_PAY.value());
count = orderStatusMapper.insertSelective(orderStatus);
if(count != 1){
log.error("[创建订单] 创建订单失败,orderID:{}", orderId);
throw new LyException(ExceptionEnum.CREATE_ORDER_ERROR);
}
// 4 减库存 -- 需要调用商品微服务,传递商品id和数量两个参数
List<CartDTO> cartDTOS = orderDTO.getCarts();
goodsClient.decreaseStock(cartDTOS);
return orderId;
}
点击提交订单跳转到订单支付页面
当写完订单业务时,跳转到支付页面,可以看到没有订单编号、二维码、支付金额这三项,同时前台页面发起了一个请求:
@GetMapping("{id}")
public ResponseEntity<Order> queryOrderById(@PathVariable("id") Long id){
return ResponseEntity.ok(orderService.queryOrderById(id));
}
我们实际需求中会有 订单状态 这一选项供用户查看,因此查询的时候不单单要实现订单的查询,还要查询订单的金额和订单的状态:
public Order queryOrderById(Long id) {
Order order = orderMapper.selectByPrimaryKey(id);
if (order == null) {
throw new LyException(ExceptionEnum.ORDER_NOT_FOUND);
}
// 查询订单详情
OrderDetail detail = new OrderDetail();
detail.setOrderId(id);
List<OrderDetail> orderDetails = orderDetailMapper.select(detail);
if(CollectionUtils.isEmpty(orderDetails)){
throw new LyException(ExceptionEnum.ORDER_DETAIL_NOT_FOUNT);
}
order.setOrderDetails(orderDetails);
// 查询订单状态
OrderStatus orderStatus = orderStatusMapper.selectByPrimaryKey(id);
if(orderStatus == null){
throw new LyException(ExceptionEnum.ORDER_STATUS_NOT_FOUND);
}
order.setOrderStatus(orderStatus);
return order;
}
现在刷新页面,发现又发起了两个请求,同时二维码还没有生成:
可以看到二维码是根据URL生成的,接下来,我们去实现微信支付,见下一篇博客:笔记十六