目录
引读
在上一章我们创建了订单服务模块,生成了订单服务模块的代码,本章将模拟客户下订单。
本系列源代码存放在github和gitee中,可直接clone
https://gitee.com/kutasms/saas-demo 或 https://github.com/kutasms/saas-demo
在开始之前有几个上一章的BUG需要修复一下:
- 订单模块增加文件 V1.0.2__alter_order_item_attr.ddl.sql
ALTER TABLE `saas_order_item_attr`
ADD COLUMN `attr_id` BIGINT NOT NULL COMMENT '订单项属性编号' FIRST,
CHANGE COLUMN `tenant_id` `tenant_id` BIGINT NOT NULL COMMENT '租户编号' ,
DROP PRIMARY KEY,
ADD PRIMARY KEY (`attr_id`);
然后重新生成下代码
定义枚举和参数
在商城系统中下单模式可能会有多种,包括普通订单、拼团订单、助力购订单等,所以在这里我们将借助SPI特性来实现下单策略。
OrderType 枚举
package org.example.saas.core.domain.enums;
import com.chia.multienty.core.domain.enums.HttpResultEnum;
import com.chia.multienty.core.exception.KutaRuntimeException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum OrderType {
NORMAL_ORDER(1, "普通订单"),
GROUP_BUYING_LAUNCH(2, "发起拼团"),
GROUP_BUYING_JOIN(3, "参与拼团");
@JsonValue
private Integer value;
private String describe;
@JsonCreator
public static OrderType valueOf(Integer value) {
if(value == null) {
return null;
}
for (OrderType type : values()) {
if(type.getValue().equals(value)) {
return type;
}
}
throw new KutaRuntimeException(HttpResultEnum.ENUM_ITEM_NOT_FOUND);
}
}
ActivityType 活动类型枚举
package org.example.saas.core.domain.enums;
import com.chia.multienty.core.domain.enums.HttpResultEnum;
import com.chia.multienty.core.exception.KutaRuntimeException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ActivityType {
DISCOUNT(1, "限时折扣", false),
GROUP_BUYING(2, "团购", true),
;
@JsonValue
private Integer id;
private String describe;
private Boolean standaloneStock;
@JsonCreator
public static ActivityType valueOf(Integer id) {
if(id == null) {
return null;
}
for (ActivityType type : values()) {
if(type.getId().equals(id)) {
return type;
}
}
throw new KutaRuntimeException(HttpResultEnum.ENUM_ITEM_NOT_FOUND);
}
}
OrderSubmitMode 下单模式枚举
package org.example.saas.core.domain.enums;
import com.chia.multienty.core.domain.enums.HttpResultEnum;
import com.chia.multienty.core.exception.KutaRuntimeException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum OrderSubmitMode {
SHOPPING_CART(1, "购物车"),
IMMEDIATE(2, "直接购买");
@JsonValue
private Integer value;
private String describe;
@JsonCreator
public static OrderSubmitMode valueOf(Integer value) {
if(value == null) {
return null;
}
for (OrderSubmitMode mode : values()) {
if(mode.getValue().equals(value)) {
return mode;
}
}
throw new KutaRuntimeException(HttpResultEnum.ENUM_ITEM_NOT_FOUND);
}
}
OrderPlaceParameter 下单参数
package org.example.saas.core.parameter.order;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.example.saas.core.domain.enums.ActivityType;
import org.example.saas.core.domain.enums.OrderSubmitMode;
import org.example.saas.core.domain.enums.OrderType;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Data
public class OrderPlaceParameter implements Serializable {
/**
* 订单提交模式
*/
@ApiModelProperty("订单提交模式")
private OrderSubmitMode submitMode;
/**
* 订单类型
*/
@ApiModelProperty("订单类型")
private OrderType orderType;
/**
* 购物车编号列表
*/
@ApiModelProperty("购物车编号列表")
private List<Long> cartIds;
/**
* 商品规格编号列表
*/
@ApiModelProperty("商品规格编号列表")
private Map<Long, Integer> skuMap;
/**
* 使用优惠券
*/
@ApiModelProperty("使用优惠券")
private Long couponId;
/**
* 活动编号
*/
@ApiModelProperty("活动编号")
private Long activityId;
/**
* 活动类型
*/
@ApiModelProperty("活动类型")
private ActivityType activityType;
}
OrderPlaceResultDTO 下单结果DTO对象
package org.example.saas.core.domain.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import org.example.saas.core.pojo.Trade;
import java.util.List;
@Data
@Accessors(chain = true)
public class OrderPlaceResultDTO {
private String payUrl;
private Trade trade;
private List<OrderDTO> orders;
}
创建CustomerContext用于存储线程安全的客户信息存储单元
package org.example.saas.core.tools;
import org.example.saas.core.domain.dto.CustomerDTO;
public class CustomerContext {
private static ThreadLocal<CustomerDTO> cachedCustomer = new ThreadLocal<>();
public static void setCustomer(CustomerDTO customer) {
cachedCustomer.set(customer);
}
public static CustomerDTO getCustomer() {
return cachedCustomer.get();
}
}
修改OrderDTO
package org.example.saas.core.domain.dto;
import org.example.saas.core.pojo.Order;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.example.saas.core.pojo.OrderDetail;
import org.example.saas.core.pojo.OrderItem;
import java.util.List;
/**
* <p>
* 订单 DTO对象
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-02-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value="OrderDTO", description="订单DTO对象")
public class OrderDTO extends Order {
private OrderDetail detail;
private List<OrderItem> items;
/**
* 是否包含订单详情
*/
@ApiModelProperty("是否包含订单详情")
private Boolean containsDetail;
/**
* 是否包含订单项
*/
@ApiModelProperty("是否包含订单项")
private Boolean containsItems;
}
订单策略
下单SPI接口定义与实现
接下来我们需要定义一个下单的接口,后面将通过SPI的方式加载
package org.example.saas.core.service.order.strategy;
import org.example.saas.core.domain.dto.OrderPlaceResultDTO;
import org.example.saas.core.domain.enums.OrderType;
import org.example.saas.core.parameter.order.OrderPlaceParameter;
public interface IOrderPlaceProvider {
OrderType getType();
OrderPlaceResultDTO placeOrder(OrderPlaceParameter parameter);
}
添加一个抽象提供者对象AbstractOrderPlaceProvider,实现基础方法
package org.example.saas.core.service.order.strategy;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.chia.multienty.core.domain.constants.MultiTenantConstants;
import com.chia.multienty.core.domain.enums.StatusEnum;
import com.chia.multienty.core.tools.IdWorkerProvider;
import com.chia.multienty.core.tools.MultiTenantContext;
import io.seata.spring.annotation.GlobalTransactional;
import org.example.saas.core.domain.dto.OrderDTO;
import org.example.saas.core.domain.dto.OrderPlaceResultDTO;
import org.example.saas.core.parameter.order.OrderPlaceParameter;
import org.example.saas.core.pojo.Trade;
import org.example.saas.core.service.order.OrderService;
import org.example.saas.core.service.order.TradeService;
import org.example.saas.core.tools.CustomerContext;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public abstract class AbstractOrderPlaceProvider implements IOrderPlaceProvider {
@Autowired
protected OrderService orderService;
@Autowired
protected TradeService tradeService;
@Override
@GlobalTransactional
public OrderPlaceResultDTO placeOrder(OrderPlaceParameter parameter) {
DynamicDataSourceContextHolder.push(MultiTenantConstants.DS_SHARDING);
Trade trade = createTrade(parameter);
List<OrderDTO> orders = createOrders(parameter, trade);
OrderPlaceResultDTO result = new OrderPlaceResultDTO()
.setOrders(orders)
.setTrade(trade);
return result;
}
/**
* 创建交易
* @param parameter
* @return
*/
@DS(MultiTenantConstants.DS_SHARDING)
protected Trade createTrade(OrderPlaceParameter parameter) {
Trade trade = new Trade();
trade.setTradeId(IdWorkerProvider.next());
trade.setPaid(false);
trade.setStatus(StatusEnum.NORMAL.getCode());
trade.setCustomerId(CustomerContext.getCustomer().getCustomerId());
trade.setTenantId(MultiTenantContext.getTenant().getTenantId());
trade.setCreateTime(LocalDateTime.now());
tradeService.saveTE(trade);
return trade;
}
protected abstract List<OrderDTO> createOrders(OrderPlaceParameter parameter, Trade trade);
}
模拟商品相关类
因为我们并未创建商品相关模块,所以我们模拟创建商品类和Sku类
package org.example.saas.core.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
* 商品
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-02-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("saas_product")
@ApiModel(value="Product对象", description="商品")
public class Product implements Serializable {
/**
* 商品编号
*/
@ApiModelProperty(value = "商品编号")
@TableId(value = "product_id", type = IdType.AUTO)
private Long productId;
/**
* 租户编号
*/
@ApiModelProperty(value = "租户编号")
@TableField("`tenant_id`")
private Long tenantId;
/**
* 商品名称
*/
@ApiModelProperty(value = "商品名称")
@TableField("`name`")
private String name;
/**
* 副标题
*/
@ApiModelProperty(value = "副标题")
@TableField("`subtitle`")
private String subtitle;
/**
* 简述
*/
@ApiModelProperty(value = "简述")
@TableField("`sketch`")
private String sketch;
/**
* 目录
*/
@ApiModelProperty(value = "目录")
@TableField("`category_id`")
private Long categoryId;
/**
* 详情
*/
@ApiModelProperty(value = "详情")
@TableField("`detail`")
private String detail;
/**
* 状态
*/
@ApiModelProperty(value = "状态")
@TableField(value = "`status`", fill = FieldFill.INSERT)
private String status;
/**
* 价格区间-开始
*/
@ApiModelProperty(value = "价格区间-开始")
@TableField("`price_start`")
private BigDecimal priceStart;
/**
* 价格区间-结束
*/
@ApiModelProperty(value = "价格区间-结束")
@TableField("`price_end`")
private BigDecimal priceEnd;
/**
* 是否已上架
*/
@ApiModelProperty(value = "是否已上架")
@TableField("`listed`")
private Boolean listed;
/**
* 是否已删除
*/
@ApiModelProperty(value = "是否已删除")
@TableField(value = "`deleted`", fill = FieldFill.INSERT)
@TableLogic
private Boolean deleted;
/**
* 乐观锁版本号
*/
@ApiModelProperty(value = "乐观锁版本号")
@TableField(value = "`version`", fill = FieldFill.INSERT)
private Long version;
/**
* 创建日期
*/
@ApiModelProperty(value = "创建日期")
@TableField(value = "`create_time`", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新日期
*/
@ApiModelProperty(value = "更新日期")
@TableField(value = "`update_time`", fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
/**
* 卖点
*/
@ApiModelProperty(value = "卖点")
@TableField("`brief`")
private String brief;
/**
* 允许使用优惠券
*/
@ApiModelProperty(value = "允许使用优惠券")
@TableField("`allow_coupon`")
private Boolean allowCoupon;
}
ProductSku类
package org.example.saas.core.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
* 商品规格
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-02-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("saas_product_sku")
@ApiModel(value="ProductSku对象", description="商品规格")
public class ProductSku implements Serializable {
/**
* sku编号
*/
@ApiModelProperty(value = "sku编号")
@TableId(value = "sku_id", type = IdType.AUTO)
private Long skuId;
/**
* 货物编号
*/
@ApiModelProperty(value = "货物编号")
@TableField("`product_id`")
private Long productId;
/**
* 租户编号
*/
@ApiModelProperty(value = "租户编号")
@TableField("`tenant_id`")
private Long tenantId;
/**
* 现价
*/
@ApiModelProperty(value = "现价")
@TableField("`price`")
private BigDecimal price;
/**
* 原价
*/
@ApiModelProperty(value = "原价")
@TableField("`original_Price`")
private BigDecimal originalPrice;
/**
* 库存
*/
@ApiModelProperty(value = "库存")
@TableField("`stock`")
private Integer stock;
/**
* 图片地址
*/
@ApiModelProperty(value = "图片地址")
@TableField("`image_url`")
private String imageUrl;
/**
* 文件编号
*/
@ApiModelProperty(value = "文件编号")
@TableField("`image_file_id`")
private Long imageFileId;
/**
* 模式
*/
@ApiModelProperty(value = "模式")
@TableField("`mode`")
private Byte mode;
/**
* 是否可用
*/
@ApiModelProperty(value = "是否可用")
@TableField("`enable`")
private Boolean enable;
/**
* 乐观锁版本号
*/
@ApiModelProperty(value = "乐观锁版本号")
@TableField(value = "`version`", fill = FieldFill.INSERT)
private Long version;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
@TableField(value = "`create_time`", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间")
@TableField(value = "`update_time`", fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
/**
* 是否已删除
*/
@ApiModelProperty(value = "是否已删除")
@TableField(value = "`deleted`", fill = FieldFill.INSERT)
@TableLogic
private Boolean deleted;
}
SkuAttribute类
package org.example.saas.core.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <p>
* 商品规格属性
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-02-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("saas_sku_attribute")
@ApiModel(value="SkuAttribute对象", description="商品规格属性")
public class SkuAttribute implements Serializable {
/**
* 规格属性编号
*/
@ApiModelProperty(value = "规格属性编号")
@TableId(value = "attr_id", type = IdType.AUTO)
private Long attrId;
/**
* 规格编号
*/
@ApiModelProperty(value = "规格编号")
@TableField("`sku_id`")
private Long skuId;
/**
* 租户编号
*/
@ApiModelProperty(value = "租户编号")
@TableField("`tenant_id`")
private Long tenantId;
/**
* 规格名称
*/
@ApiModelProperty(value = "规格名称")
@TableField("`name`")
private String name;
/**
* 规格值
*/
@ApiModelProperty(value = "规格值")
@TableField("`value`")
private String value;
/**
* 规格代码名称
*/
@ApiModelProperty(value = "规格代码名称")
@TableField("`code_name`")
private String codeName;
/**
* 规格代码值
*/
@ApiModelProperty(value = "规格代码值")
@TableField("`code_value`")
private String codeValue;
/**
* 规格图片
*/
@ApiModelProperty(value = "规格图片")
@TableField("`image`")
private String image;
/**
* 排序索引
*/
@ApiModelProperty(value = "排序索引")
@TableField("`index`")
private Byte index;
}
ProductSkuDTO类
package org.example.saas.core.domain.dto;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.example.saas.core.pojo.ProductSku;
import org.example.saas.core.pojo.SkuAttribute;
import java.util.List;
/**
* <p>
* 商品规格 DTO对象
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-01-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value="ProductSkuDTO", description="商品规格DTO对象")
public class ProductSkuDTO extends ProductSku {
/**
* SKU特性
*/
private List<SkuAttribute> attributes;
}
ProductDTO类
package org.example.saas.core.domain.dto;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.example.saas.core.pojo.Product;
import java.util.List;
/**
* <p>
* 商品 DTO对象
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-01-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value="ProductDTO", description="商品DTO对象")
public class ProductDTO extends Product {
private List<ProductSkuDTO> skus;
private String mainImageUrl;
}
接下来我们创建NormalOrderPlaceProvider,正常下单模式
package org.example.saas.core.service.order.strategy;
import com.chia.multienty.core.domain.basic.KeyValuePair;
import com.chia.multienty.core.domain.enums.StatusEnum;
import com.chia.multienty.core.tools.IdWorkerProvider;
import com.chia.multienty.core.tools.MultiTenantContext;
import org.example.saas.core.domain.dto.OrderDTO;
import org.example.saas.core.domain.dto.ProductDTO;
import org.example.saas.core.domain.dto.ProductSkuDTO;
import org.example.saas.core.domain.enums.OrderType;
import org.example.saas.core.parameter.order.OrderPlaceParameter;
import org.example.saas.core.parameter.order.TradeUpdateParameter;
import org.example.saas.core.pojo.*;
import org.example.saas.core.service.order.OrderDetailService;
import org.example.saas.core.service.order.OrderItemAttrService;
import org.example.saas.core.service.order.OrderItemService;
import org.example.saas.core.tools.CustomerContext;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class NormalOrderPlaceProvider extends AbstractOrderPlaceProvider{
@Autowired
private OrderDetailService orderDetailService;
@Autowired
private OrderItemService orderItemService;
@Autowired
private OrderItemAttrService orderItemAttrService;
@Override
protected List<OrderDTO> createOrders(OrderPlaceParameter parameter, Trade trade) {
List<OrderDTO> orders = new ArrayList<>();
BigDecimal total = BigDecimal.ZERO;
BigDecimal discountAmount = BigDecimal.ZERO;
if(parameter.getCouponId() != null) {
//模拟2.5元优惠券
discountAmount = BigDecimal.valueOf(2.5);
}
OrderDTO order = new OrderDTO();
order.setOrderId(IdWorkerProvider.next());
order.setOrderType(parameter.getOrderType().getValue().shortValue());
order.setStatus(StatusEnum.WORKING.getCode());
order.setPaid(false);
order.setTenantId(trade.getTenantId());
order.setRefunded(false);
order.setCreateTime(trade.getCreateTime());
order.setCustomerId(CustomerContext.getCustomer().getCustomerId());
order.setDiscountAmount(discountAmount);
order.setPaidAmount(total.subtract(discountAmount));
order.setTotalAmount(total);
orders.add(order);
orderService.batchSaveDTOTE(orders);
// 保存订单详情
OrderDetail orderDetail = new OrderDetail()
.setOrderId(order.getOrderId())
.setTradeId(trade.getTradeId())
.setTenantId(trade.getTenantId())
.setCouponId(parameter.getCouponId())
.setActivityId(parameter.getActivityId())
.setActivityType(parameter.getActivityType().getId().shortValue())
.setCustomerNick(CustomerContext.getCustomer().getNickName())
.setCustomerPhone(CustomerContext.getCustomer().getPhoneNumber())
.setCreateTime(trade.getCreateTime())
.setReceived(false);
orderDetailService.saveTE(orderDetail);
order.setDetail(orderDetail);
List<ProductDTO> products = mockProducts();
// 保存订单子项
saveOrderItems(parameter, trade, order, products);
// 修改交易金额
trade.setDiscountAmount(orders.stream().map(m->m.getDiscountAmount()).reduce((a,b) -> a.add(b)).get());
trade.setTotalAmount(orders.stream().map(m->m.getTotalAmount()).reduce((a,b)-> a.add(b)).get());
trade.setPaidAmount(trade.getTotalAmount().subtract(trade.getDiscountAmount()));
TradeUpdateParameter tradeUpdateParameter = new TradeUpdateParameter();
BeanUtils.copyProperties(trade, tradeUpdateParameter);
tradeService.update(tradeUpdateParameter);
return orders;
}
public void saveOrderItems(OrderPlaceParameter parameter, Trade trade, OrderDTO order, List<ProductDTO> products) {
List<OrderItem> orderItems = new ArrayList<>();
List<OrderItemAttr> orderItemAttrs = new ArrayList<>();
for (ProductDTO product : products) {
for (ProductSkuDTO sku : product.getSkus()) {
OrderItem orderItem = new OrderItem()
.setItemId(IdWorkerProvider.next())
.setOrderId(order.getOrderId())
.setImage(sku.getImageUrl() == null ? product.getMainImageUrl() : sku.getImageUrl())
.setProductId(product.getProductId())
.setPrice(sku.getPrice())
.setDiffAmount(BigDecimal.ZERO)
.setCreateTime(trade.getCreateTime())
.setQuantity(parameter.getSkuMap().get(sku.getSkuId()))
.setProductName(product.getName())
.setSkuName(sku.getAttributes().stream().map(m->m.getValue()).collect(Collectors.joining(",")))
.setSkuId(sku.getSkuId())
.setTenantId(trade.getTenantId())
;
orderItem.setTotalAmount(BigDecimal.valueOf(orderItem.getQuantity()).multiply(sku.getPrice()));
orderItems.add(orderItem);
for (SkuAttribute attribute : sku.getAttributes()) {
OrderItemAttr attr = new OrderItemAttr();
attr.setItemId(orderItem.getItemId());
attr.setName(attribute.getName());
attr.setValue(attribute.getValue());
attr.setTenantId(trade.getTenantId());
attr.setCodeValue(attribute.getCodeValue());
attr.setCodeName(attribute.getCodeName());
attr.setCreateTime(trade.getCreateTime());
orderItemAttrs.add(attr);
}
}
}
orderItemService.saveBatchTE(orderItems);
orderItemAttrService.saveBatchTE(orderItemAttrs);
}
private List<ProductDTO> mockProducts() {
List<ProductDTO> products = new ArrayList<>();
ProductDTO product1 = new ProductDTO();
product1.setBrief("商品1");
product1.setName("商品1");
product1.setDeleted(false);
product1.setAllowCoupon(true);
product1.setDetail("商品1的详情");
product1.setListed(true);
product1.setProductId(123L);
product1.setCategoryId(111L);
product1.setSkus(mockSkus(product1.getProductId()));
return products;
}
private List<ProductSkuDTO> mockSkus(Long productId) {
List<ProductSkuDTO> skus = new ArrayList<>();
if(productId.equals(123L)) {
ProductSkuDTO sku = new ProductSkuDTO();
sku.setSkuId(1L);
sku.setAttributes(mockSkuAttr(sku.getSkuId()));
sku.setDeleted(false);
sku.setPrice(BigDecimal.valueOf(12.5));
sku.setEnable(true);
sku.setMode((byte) 1);
sku.setCreateTime(LocalDateTime.now());
sku.setImageFileId(333L);
sku.setOriginalPrice(BigDecimal.valueOf(16.5));
sku.setProductId(productId);
sku.setStock(500);
sku.setTenantId(MultiTenantContext.getTenant().getTenantId());
sku.setImageUrl("https://image.baidu.com?t=281643");
sku.setVersion(1L);
skus.add(sku);
} else {
ProductSkuDTO sku = new ProductSkuDTO();
sku.setSkuId(2L);
sku.setAttributes(mockSkuAttr(sku.getSkuId()));
sku.setDeleted(false);
sku.setPrice(BigDecimal.valueOf(19.8));
sku.setEnable(true);
sku.setMode((byte) 1);
sku.setCreateTime(LocalDateTime.now());
sku.setImageFileId(222L);
sku.setOriginalPrice(BigDecimal.valueOf(26.5));
sku.setProductId(productId);
sku.setStock(300);
sku.setTenantId(MultiTenantContext.getTenant().getTenantId());
sku.setImageUrl("https://image.baidu.com?t=153917B2382");
sku.setVersion(2L);
skus.add(sku);
}
return skus;
}
private List<SkuAttribute> mockSkuAttr(Long skuId) {
List<SkuAttribute> attributes = new ArrayList<>();
if(skuId.equals(1L)) {
SkuAttribute attribute = new SkuAttribute();
attribute.setName("颜色");
attribute.setIndex((byte) 1);
attribute.setImage("https://image.baidu.com?t=1531233082");
attribute.setAttrId(39178702L);
attribute.setValue("红色");
attribute.setCodeName("attr_1");
attribute.setCodeValue("attr_v_0");
attribute.setSkuId(skuId);
attribute.setTenantId(MultiTenantContext.getTenant().getTenantId());
attributes.add(attribute);
SkuAttribute attribute2 = new SkuAttribute();
attribute2.setName("尺码");
attribute2.setIndex((byte) 2);
attribute2.setImage("https://image.baidu.com?t=2321354534");
attribute2.setAttrId(53243112L);
attribute2.setValue("XL");
attribute2.setCodeName("attr_2");
attribute2.setCodeValue("attr_v_1");
attribute2.setSkuId(skuId);
attribute2.setTenantId(MultiTenantContext.getTenant().getTenantId());
attributes.add(attribute2);
} else {
SkuAttribute attribute = new SkuAttribute();
attribute.setName("颜色");
attribute.setIndex((byte) 1);
attribute.setImage("https://image.baidu.com?t=1533212482");
attribute.setAttrId(339288702L);
attribute.setValue("蓝色");
attribute.setCodeName("attr_1");
attribute.setCodeValue("attr_v_0");
attribute.setSkuId(skuId);
attribute.setTenantId(MultiTenantContext.getTenant().getTenantId());
attributes.add(attribute);
SkuAttribute attribute2 = new SkuAttribute();
attribute2.setName("尺码");
attribute2.setIndex((byte) 2);
attribute2.setImage("https://image.baidu.com?t=2329874534");
attribute2.setAttrId(5374652912L);
attribute2.setValue("XXL");
attribute2.setCodeName("attr_2");
attribute2.setCodeValue("attr_v_1");
attribute2.setSkuId(skuId);
attribute2.setTenantId(MultiTenantContext.getTenant().getTenantId());
attributes.add(attribute2);
}
return attributes;
}
@Override
public OrderType getType() {
return OrderType.NORMAL_ORDER;
}
}
新增META-INF.services
在resources资源目录下创建META-INF/services
在后期如果要加入其它的实现方式,我们只需要在这里添加即可。
然后添加一个全局定位类
package org.example.saas.core.domain.constants;
public class SaasConstants {
public static final String ORDER_PLACE_PROVIDER_PREFIX = "ORDER_PLACE";
public static final String MOCK_CUSTOMER_ID = "X-MOCK-ID";
/**
* http头 客户编号
*/
public static final String HEADER_CUSTOMER_ID = "X-CUSTOMER-ID";
}
然后增加下单提供者注册器
package org.example.saas.core.config;
import com.chia.multienty.core.tools.MultiTenantServiceLoader;
import org.example.saas.core.domain.constants.SaasConstants;
import org.example.saas.core.service.order.strategy.IOrderPlaceProvider;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import java.util.Collection;
public class OrderPlaceProviderRegistry implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
MultiTenantServiceLoader.register(IOrderPlaceProvider.class);
Collection<IOrderPlaceProvider> providers = MultiTenantServiceLoader.getInstances(IOrderPlaceProvider.class);
for (IOrderPlaceProvider provider : providers) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(provider.getClass());
registry.registerBeanDefinition(SaasConstants.ORDER_PLACE_PROVIDER_PREFIX + provider.getType().name(), builder.getBeanDefinition());
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
对接下单接口
package org.example.saas.core.service.order.strategy;
import lombok.RequiredArgsConstructor;
import org.example.saas.core.domain.constants.SaasConstants;
import org.example.saas.core.domain.dto.OrderPlaceResultDTO;
import org.example.saas.core.parameter.order.OrderPlaceParameter;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 下单策略
*/
@Component
@RequiredArgsConstructor
public class OrderPlaceStrategy {
private final ObjectProvider<Map<String, IOrderPlaceProvider>> provider;
public OrderPlaceResultDTO placeOrder(OrderPlaceParameter parameter) {
Map<String, IOrderPlaceProvider> map = provider.getIfAvailable();
if(map != null) {
IOrderPlaceProvider service = map.get(SaasConstants.ORDER_PLACE_PROVIDER_PREFIX + parameter.getOrderType().name());
return service.placeOrder(parameter);
}
return null;
}
}
新增SaasDemoConfiguration
package org.example.saas.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaasDemoConfiguration {
@Bean
public static OrderPlaceProviderRegistry orderPlaceProviderRegistry() {
return new OrderPlaceProviderRegistry();
}
}
修改saasdemo-order模块的TradeController,增加下单接口
package org.example.saas.order.controller;
import org.example.saas.core.domain.dto.OrderPlaceResultDTO;
import org.example.saas.core.parameter.order.*;
import org.example.saas.core.service.order.strategy.OrderPlaceStrategy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.example.saas.core.service.order.TradeService;
import org.example.saas.core.domain.dto.TradeDTO;
import org.example.saas.core.pojo.Trade;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.chia.multienty.core.annotation.WebLog;
import com.chia.multienty.core.domain.basic.Result;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* <p>
* 交易 服务
* </p>
*
* @author Multi Tenant Auto Generator
* @since 2024-02-25
*/
@RestController
@Validated
@RequestMapping("/trade")
@RequiredArgsConstructor
@Api(tags = "交易")
public class TradeController {
private final TradeService tradeService;
private final OrderPlaceStrategy placeStrategy;
@PostMapping("/detail")
@ApiOperation("获取交易详情")
public Result<TradeDTO> getDetail(@Validated @RequestBody TradeDetailGetParameter parameter) {
TradeDTO detail = tradeService.getDetail(parameter);
return new Result<>(detail);
}
@PostMapping("/page")
@ApiOperation("获取交易分页列表")
public Result<IPage<TradeDTO>> getPage(@Validated @RequestBody TradePageGetParameter parameter) {
IPage<TradeDTO> page = tradeService.getPage(parameter);
return new Result<>(page);
}
@PostMapping("/update")
@ApiOperation("更新交易")
@WebLog
public Result<Boolean> update(@Validated @RequestBody TradeUpdateParameter parameter) {
tradeService.update(parameter);
return new Result<>(true);
}
@PostMapping("/save")
@ApiOperation("保存交易")
@WebLog
public Result<Boolean> save(@Validated @RequestBody TradeSaveParameter parameter) {
tradeService.save(parameter);
return new Result<>(true);
}
@PostMapping("/enable")
@ApiOperation("启用交易")
@WebLog
public Result<Boolean> enable(@Validated @RequestBody TradeEnableParameter parameter) {
tradeService.enable(parameter);
return new Result<>(true);
}
@PostMapping("/disable")
@ApiOperation("禁用交易")
@WebLog
public Result<Boolean> save(@Validated @RequestBody TradeDisableParameter parameter) {
tradeService.disable(parameter);
return new Result<>(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除交易")
@WebLog
public Result<Boolean> delete(@Validated @RequestBody TradeDeleteParameter parameter) {
tradeService.delete(parameter);
return new Result<>(true);
}
@PostMapping("/place-order")
@ApiOperation("下单")
@WebLog
public Result<OrderPlaceResultDTO> placeOrder(@Validated @RequestBody OrderPlaceParameter parameter) {
OrderPlaceResultDTO dto = placeStrategy.placeOrder(parameter);
return new Result<>(dto);
}
}
拦截器和配置
添加Dubbo接口
DubboUserService新增getCustomer接口
package org.example.saas.core.dubbo.service;
import org.example.saas.core.domain.dto.CustomerDTO;
import org.example.saas.core.parameter.user.CustomerDetailGetParameter;
public interface DubboUserService {
/**
* 获取客户信息
* @param parameter
* @return
*/
CustomerDTO getCustomer(CustomerDetailGetParameter parameter);
}
用户模块新增DubboUserServiceImpl
package org.example.saas.user.dubbo.service.impl;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboService;
import org.example.saas.core.domain.dto.CustomerDTO;
import org.example.saas.core.dubbo.service.DubboUserService;
import org.example.saas.core.parameter.user.CustomerDetailGetParameter;
import org.example.saas.core.service.user.CustomerService;
@DubboService
@RequiredArgsConstructor
public class DubboUserServiceImpl implements DubboUserService {
private final CustomerService customerService;
@Override
public CustomerDTO getCustomer(CustomerDetailGetParameter parameter) {
return customerService.getDetail(parameter);
}
}
实现拦截器
saasdemo-core新增拦截器
package org.example.saas.core.interceptor;
import com.chia.multienty.core.domain.basic.Result;
import com.chia.multienty.core.domain.constants.MultiTenantHeaderConstants;
import com.chia.multienty.core.properties.yaml.YamlMultiTenantProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.example.saas.core.domain.constants.SaasConstants;
import org.example.saas.core.domain.dto.CustomerDTO;
import org.example.saas.core.dubbo.service.DubboUserService;
import org.example.saas.core.parameter.user.CustomerDetailGetParameter;
import org.example.saas.core.tools.CustomerContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class SaasInterceptor implements HandlerInterceptor {
@Autowired
private DubboUserService dubboUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.setHeader("Content-Type", "json/application; charset=utf-8");
Result result = null;
String strTenantId = request.getHeader(MultiTenantHeaderConstants.TENANT_ID_KEY);
Long tenantId = Long.parseLong(strTenantId);
String mockCustomerId = request.getHeader(SaasConstants.MOCK_CUSTOMER_ID);
if(Strings.isNotEmpty(mockCustomerId)) {
CustomerDTO customer = dubboUserService.getCustomer(new CustomerDetailGetParameter()
.setTenantId(tenantId).setCustomerId(Long.parseLong(mockCustomerId)));
CustomerContext.setCustomer(customer);
}
return true;
}
}
saasdemo-order模块新增配置
WebMvcConfig配置
package org.example.saas.order.config;
import org.example.saas.core.interceptor.SaasInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public SaasInterceptor kutaShopInterceptor() {
return new SaasInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(kutaShopInterceptor()).addPathPatterns("/**");
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// config.addAllowedOrigin("*");
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
DubboReferenceConfiguration配置
package org.example.saas.order.config;
import com.chia.multienty.core.dubbo.service.DubboWechatService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.spring.ReferenceBean;
import org.example.saas.core.dubbo.service.DubboMasterService;
import org.example.saas.core.dubbo.service.DubboUserService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(prefix = "dubbo", name = "enabled", havingValue = "true")
public class DubboReferenceConfiguration {
@Bean
@DubboReference(timeout = 5000)
public ReferenceBean<DubboMasterService> dubboMasterServiceReferenceBean() {
return new ReferenceBean<>();
}
@Bean
public DubboMasterService dubboMasterService(ReferenceBean<DubboMasterService> dubboMasterServiceReferenceBean) {
DubboMasterService dubboMasterService = dubboMasterServiceReferenceBean.getObject();
return dubboMasterService;
}
@Bean
@DubboReference(timeout = 5000)
public ReferenceBean<DubboUserService> dubboUserServiceReferenceBean() {
return new ReferenceBean<>();
}
@Bean
public DubboUserService dubboUserService(ReferenceBean<DubboUserService> dubboUserServiceReferenceBean) {
DubboUserService dubboUserService = dubboUserServiceReferenceBean.getObject();
return dubboUserService;
}
}
修复历史问题
1.修改CustomerDetailGetParameter implements Serializable
2.重新拉取multienty源码并执行 mvn clean install
3.修改在上一章节中我们在nacos中shardingsphere-saasdemo-order.yml配置
com.chia.multienty.core.sharding.tools.ShardingAlgorithmTool.tableNames中逻辑表名称ks_开头修改为saas_开头
4.修改nacos中shardingsphere-saasdemo-order.yml配置如下,下面的是示例, 如果你有多个数据源都要加上druid的配置。
ds_order_1:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx:3301/saas_order_1?autoReconnect=true&useUnicode=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: saas_order_1
password: xxxx
initialSize: 1
minIdle: 3
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 10000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
# 验证的SQL
validationQuery: select 'x'
# 验证SQL超时时间
validationQueryTimeout: 5000
# 每隔timeBetweenEvictionRunsMillis时长使用validationQuery去验证非活动的连接
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;
# 合并多个DruidDataSource的监控数据
useGlobalDataSourceStat: true
测试下单
在postman执行订单生成
最后我们查看数据库中已生成数据。
OK, 通过shardingsphere的分区分库,我们将数据锁定到saas_order_1数据库中,下一章我们将讲解如何关联查询。