Resource Integration
Update the hosts file and copy static resources to nginx. Place dynamic template files in the order service's templates directory and adjust resource paths. Configure routing in the gateway similar to creating a new service.
SpringSession Integration
Import required dependencies according to official documentation and configure Redis settings, Cookie configurations including primary and secondary domain settings, which have been covered previously.
Order Management Module
Key modules involved in the order system:
Order states:
- Pending Payment: Order placement and pre-order processing
- Paid/Pending Shipment: Payment completion, record payment time and transaction ID for reconciliation, dispatch order to WMS for warehouse operations
- Pending Receipt/Shipped: Warehouse shipping triggers logistics process
- Completed: Customer confirms receipt, trasnaction finalized, settlement follows, potential return handling
- Cancelled: Order cancellation before payment
- In Return: Post-payment refund requests or post-shipping return/exchange requests
Authentication Middleware
All requests to the order service require authentication. Implement an interceptor to validate login status:
package com.sherrymall.mall.order.interceptor;
import com.sherrymall.common.constant.AuthConstant;
import com.sherrymall.common.vo.MemberVO;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AuthInterceptor implements HandlerInterceptor {
public static ThreadLocal threadLocal = new ThreadLocal();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS);
if(attribute != null){
MemberVO memberVO = (MemberVO) attribute;
threadLocal.set(memberVO);
return true;
}
session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"Please log in first");
response.sendRedirect("http://auth.sherrymall.com/login.html");
return false;
}
}
Register the interceptor:
@Configuration
public class MyWebInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**");
}
}
Order Confirmation Page
Data Transfer Object Extraction
public class OrderConfirmVo {
// Shipping address information
@Getter @Setter
List<MemberAddressVo> address;
// Selected cart items
@Getter @Setter
List<OrderItemVo> items;
public Integer getCountNum(){
int count = 0;
if(items != null){
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
public BigDecimal getTotal(){
BigDecimal sum = new BigDecimal(0);
if(items != null ){
for (OrderItemVo item : items) {
BigDecimal totalPrice = item.getPrice().multiply(new BigDecimal(item.getCount()));
sum = sum.add(totalPrice);
}
}
return sum;
}
public BigDecimal getPayTotal(){
return getTotal();
}
}
Data Retrieval Implementation
Use Feign client to fetch member data and cart items:
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo vo = new OrderConfirmVo();
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
Long id = memberVO.getId();
List<MemberAddressVo> addresses = memberFeginService.getAddress(id);
vo.setAddress(addresses);
}, executor);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> userCartItems = cartFeginService.getUserCartItems();
vo.setItems(userCartItems);
}, executor);
try {
CompletableFuture.allOf(future1,future2).get();
} catch (Exception e) {
e.printStackTrace();
}
return vo;
}
Handle missing headers during Feign calls by implementing a RequestInterceptor:
@Configuration
public class MallFeignConfig {
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String cookie = request.getHeader("Cookie");
requestTemplate.header("Cookie",cookie);
}
};
}
}
Idempotency Mechanism
Idempotency ensures repeated API calls produce consistent results as single calls.
Natural Idempotent Operations
SQL examples:
select * from t_user where id = 1;
update t_user set age = 18 where id = 2;
delete from t_user where id = 1;
insert into (userid,username)values(1,'');
Non-idempotent operations:
update t_user set age = age + 1 where id = 1;
insert into (userid,username)values(1,'');
Scenarios Requiring Idempotency
- Frontend duplicate submissions
- Timeout retries
- Message queue duplicate consumption
Solutions
- Token-based approach: Generate unique token stored in Redis, validate and delete on successful use
- Database deduplication: Create deduplication table with unique constraints
- Redis-based approach: Use SETNX operation with expiration time
- State machines, pessimistic locking, optimistic locking
Order Submission
Duplicate Prevention
Implement token validation to ensure idempotent order submisison:
Token Generation
Generate and store token in Redis when fetching confirmation page data:
Order Submission Form
Create form with fields for address ID, payment method, and token:
Validation Logic
Validate token before order creation using atomic Redis operations:
private Lock lock = new ReentrantLock();
@Override
public OrderResponseVo submitOrder(OrderSubmitVo vo) {
OrderResponseVo responseVo = new OrderResponseVo();
MemberVo memberVo = AuthInterceptor.threadLocal.get();
String key = OrderConstant.ORDER_TOKEN_PREFIX+":"+memberVo.getId();
try {
lock.lock();
String redisToken = redisTemplate.opsForValue().get(key);
if (redisToken != null && redisToken.equals(vo.getOrderToken())){
redisTemplate.delete(key);
} else {
// Duplicate submission detected
}
} finally {
lock.unlock();
}
return responseVo;
}
Alternative using Redis Lua script:
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
String key = OrderConstant.ORDER_TOKEN_PREFIX+":"+memberVO.getId();
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), vo.getOrderToken());
if(result == 0){
return responseVO; // Duplicate submission
}
Order Creation Process
Create order and order item entities:
private OrderCreateTO createOrder(OrderSubmitVO vo) {
OrderCreateTO createTO = new OrderCreateTO();
OrderEntity orderEntity = buildOrder(vo);
createTO.setOrderEntity(orderEntity);
List<OrderItemEntity> orderItemEntitys = buildOrderItems(orderEntity.getOrderSn());
createTO.setOrderItemEntitys(orderItemEntitys);
return createTO;
}
private List<OrderItemEntity> buildOrderItems(String orderSN) {
List<OrderItemEntity> orderItemEntitys = new ArrayList<>();
List<OrderItemVo> userCartItems = cartFeignService.getUserCartItems();
if(userCartItems != null && userCartItems.size() > 0){
List<Long> spuIds = userCartItems.stream()
.map(OrderItemVo::getSpuId)
.distinct()
.collect(Collectors.toList());
List<OrderItemSpuInfoVO> spuInfos = productService.getOrderItemSpuInfoBySpuId(
spuIds.toArray(new Long[0]));
Map<Long, OrderItemSpuInfoVO> spuMap = spuInfos.stream()
.collect(Collectors.toMap(OrderItemSpuInfoVO::getId, item -> item));
for (OrderItemVo userCartItem : userCartItems) {
OrderItemSpuInfoVO spuInfo = spuMap.get(userCartItem.getSpuId());
OrderItemEntity orderItemEntity = buildOrderItem(userCartItem, spuInfo);
orderItemEntity.setOrderSn(orderSN);
orderItemEntitys.add(orderItemEntity);
}
}
return orderItemEntitys;
}
private OrderItemEntity buildOrderItem(OrderItemVo userCartItem, OrderItemSpuInfoVO spuInfo) {
OrderItemEntity entity = new OrderItemEntity();
entity.setSkuId(userCartItem.getSkuId());
entity.setSkuName(userCartItem.getTitle());
entity.setSkuPic(userCartItem.getImage());
entity.setSkuQuantity(userCartItem.getCount());
String skuAttrStr = StringUtils.collectionToDelimitedString(userCartItem.getSkuAttr(), ";");
entity.setSkuAttrsVals(skuAttrStr);
entity.setSpuId(spuInfo.getId());
entity.setSpuBrand(spuInfo.getBrandName());
entity.setCategoryId(spuInfo.getCatalogId());
entity.setSpuPic(spuInfo.getImg());
entity.setGiftGrowth(userCartItem.getPrice().intValue());
entity.setGiftIntegration(userCartItem.getPrice().intValue());
return entity;
}
private OrderEntity buildOrder(OrderSubmitVO vo) {
OrderEntity orderEntity = new OrderEntity();
String orderSn = IdWorker.getTimeId();
orderEntity.setOrderSn(orderSn);
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
orderEntity.setMemberId(memberVO.getId());
orderEntity.setMemberUsername(memberVO.getUsername());
MemberAddressVo memberAddressVo = memberFeginService.getAddressById(vo.getAddrId());
orderEntity.setReceiverCity(memberAddressVo.getCity());
orderEntity.setReceiverDetailAddress(memberAddressVo.getDetailAddress());
orderEntity.setReceiverName(memberAddressVo.getName());
orderEntity.setReceiverPhone(memberAddressVo.getPhone());
orderEntity.setReceiverPostCode(memberAddressVo.getPostCode());
orderEntity.setReceiverRegion(memberAddressVo.getRegion());
orderEntity.setReceiverProvince(memberAddressVo.getProvince());
orderEntity.setStatus(OrderConstant.OrderStateEnum.FOR_THE_PAYMENT.getCode());
return orderEntity;
}
Save order and items:
private void saveOrder(OrderCreateTO orderCreateTO) {
OrderEntity orderEntity = orderCreateTO.getOrderEntity();
orderService.save(orderEntity);
List<OrderItemEntity> orderItemEntitys = orderCreateTO.getOrderItemEntitys();
orderItemService.saveBatch(orderItemEntitys);
}
Inventory locking through warehouse service:
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVO vo) {
List<OrderItemVo> items = vo.getItems();
List<SkuWareHasStock> collect = items.stream().map(item -> {
SkuWareHasStock skuWareHasStock = new SkuWareHasStock();
skuWareHasStock.setSkuId(item.getSkuId());
List<WareSkuEntity> wareSkuEntities = this.baseMapper.listHashStock(item.getSkuId());
skuWareHasStock.setWareSkuEntities(wareSkuEntities);
skuWareHasStock.setNum(item.getCount());
return skuWareHasStock;
}).collect(Collectors.toList());
for (SkuWareHasStock skuWareHasStock : collect) {
Long skuId = skuWareHasStock.getSkuId();
List<WareSkuEntity> wareSkuEntities = skuWareHasStock.wareSkuEntities;
if(wareSkuEntities == null || wareSkuEntities.isEmpty()){
throw new NoStockExecption(skuId);
}
Integer count = skuWareHasStock.getNum();
Boolean skuStocked = false;
for (WareSkuEntity wareSkuEntity : wareSkuEntities) {
Integer canStock = wareSkuEntity.getStock() - wareSkuEntity.getStockLocked();
if(count <= canStock){
this.baseMapper.lockSkuStock(skuId, wareSkuEntity.getWareId(), count);
count = 0;
skuStocked = true;
break;
} else {
this.baseMapper.lockSkuStock(skuId, wareSkuEntity.getWareId(), canStock);
count = count - canStock;
}
}
if(count > 0 || !skuStocked) {
throw new NoStockExecption(skuId);
}
}
return true;
}
@Data
class SkuWareHasStock {
private Long skuId;
private Integer num;
private List<WareSkuEntity> wareSkuEntities;
}
Custom exception for inventory issues:
public class NoStockExecption extends RuntimeException {
private Long skuId;
public NoStockExecption(Long skuId) {
super("Product [" + skuId + "] is out of stock");
this.skuId = skuId;
}
public Long getSkuId() { return skuId; }
public void setSkuId(Long skuId) { this.skuId = skuId; }
}
Order submission endpoint:
@PostMapping("/orderSubmit")
public String orderSubmit(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
Integer code = 0;
OrderResponseVo responseVo = null;
try {
responseVo = orderService.submitOrder(vo);
code = responseVo.getCode();
} catch (NoStockExecption noStockExecption) {
code = 2;
}
if (code == 0){
model.addAttribute("orderResponseVO",responseVo);
return "pay";
} else {
String msg = "Order failed";
if (code == 1){
msg = msg + ": Duplicate submission";
} else if (code == 2){
msg = msg + ": Inventory lock failed";
}
redirectAttributes.addFlashAttribute("msg",msg);
return "redirect:http://order.sherrymall.com/toTrade";
}
}
Complete order submission method:
@Override
public OrderResponseVo submitOrder(OrderSubmitVo vo) throws NoStockExecption {
OrderResponseVo responseVo = new OrderResponseVo();
MemberVo memberVo = AuthInterceptor.threadLocal.get();
String key = OrderConstant.ORDER_TOKEN_PREFIX+":"+memberVo.getId();
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), vo.getOrderToken());
if(result == 0){
responseVo.setCode(1);
return responseVo;
}
OrderCreateTO orderCreateTO = createOrder(vo);
responseVo.setOrderEntity(orderCreateTO.getOrderEntity());
saveOrder(orderCreateTO);
WareSkuLockVO wareSkuLockVO = new WareSkuLockVO();
wareSkuLockVO.setOrderSN(orderCreateTO.getOrderEntity().getOrderSn());
List<OrderItemVo> orderItemVos = orderCreateTO.getOrderItemEntitys().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setTitle(item.getSkuName());
itemVo.setCount(item.getSkuQuantity());
return itemVo;
}).collect(Collectors.toList());
wareSkuLockVO.setItems(orderItemVos);
R r = wareFeignService.orderLockStock(wareSkuLockVO);
if (r.getCode() == 0){
responseVo.setCode(0);
} else {
responseVo.setCode(2);
throw new NoStockExecption(1000000L);
}
return responseVo;
}