src/main/java/com/webmanage/WebManageApplication.java
@@ -1,6 +1,7 @@ package com.webmanage; import org.mybatis.spring.annotation.MapperScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -11,6 +12,7 @@ * @date 2024-08-07 */ @SpringBootApplication @EnableAsync @MapperScan("com.webmanage.mapper") public class WebManageApplication { src/main/java/com/webmanage/config/AsyncConfig.java
New file @@ -0,0 +1,52 @@ package com.webmanage.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import javax.annotation.Resource; import java.lang.reflect.Method; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; @Configuration public class AsyncConfig implements AsyncConfigurer { private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class); @Resource private AsyncExecutorProperties properties; @Override @Bean("asyncExecutor") public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(properties.getCorePoolSize()); executor.setMaxPoolSize(properties.getMaxPoolSize()); executor.setQueueCapacity(properties.getQueueCapacity()); executor.setKeepAliveSeconds(properties.getKeepAliveSeconds()); executor.setThreadNamePrefix(properties.getThreadNamePrefix()); executor.setWaitForTasksToCompleteOnShutdown(properties.isWaitForTasksToCompleteOnShutdown()); executor.setAwaitTerminationSeconds(properties.getAwaitTerminationSeconds()); // 饱和策略:调用方线程执行,避免任务被丢弃 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncUncaughtExceptionHandler() { @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { log.error("异步任务执行异常, method={}, params={}", method, params, ex); } }; } } src/main/java/com/webmanage/config/AsyncExecutorProperties.java
New file @@ -0,0 +1,40 @@ package com.webmanage.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "async.executor") public class AsyncExecutorProperties { /** 核心线程数 */ private int corePoolSize = 4; /** 最大线程数 */ private int maxPoolSize = 8; /** 队列容量 */ private int queueCapacity = 200; /** 线程存活时间(秒) */ private int keepAliveSeconds = 60; /** 线程名前缀 */ private String threadNamePrefix = "async-exec-"; /** 关闭时是否等待任务完成 */ private boolean waitForTasksToCompleteOnShutdown = true; /** 关闭时最大等待秒数 */ private int awaitTerminationSeconds = 30; public int getCorePoolSize() { return corePoolSize; } public void setCorePoolSize(int corePoolSize) { this.corePoolSize = corePoolSize; } public int getMaxPoolSize() { return maxPoolSize; } public void setMaxPoolSize(int maxPoolSize) { this.maxPoolSize = maxPoolSize; } public int getQueueCapacity() { return queueCapacity; } public void setQueueCapacity(int queueCapacity) { this.queueCapacity = queueCapacity; } public int getKeepAliveSeconds() { return keepAliveSeconds; } public void setKeepAliveSeconds(int keepAliveSeconds) { this.keepAliveSeconds = keepAliveSeconds; } public String getThreadNamePrefix() { return threadNamePrefix; } public void setThreadNamePrefix(String threadNamePrefix) { this.threadNamePrefix = threadNamePrefix; } public boolean isWaitForTasksToCompleteOnShutdown() { return waitForTasksToCompleteOnShutdown; } public void setWaitForTasksToCompleteOnShutdown(boolean waitForTasksToCompleteOnShutdown) { this.waitForTasksToCompleteOnShutdown = waitForTasksToCompleteOnShutdown; } public int getAwaitTerminationSeconds() { return awaitTerminationSeconds; } public void setAwaitTerminationSeconds(int awaitTerminationSeconds) { this.awaitTerminationSeconds = awaitTerminationSeconds; } } src/main/java/com/webmanage/config/CartProperties.java
New file @@ -0,0 +1,31 @@ package com.webmanage.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "cart") public class CartProperties { /** Redis缓存过期天数 */ private Integer expireDays = 30; /** 是否启用数据库持久化 */ private Boolean enablePersistence = true; /** 是否启用一致性检查 */ private Boolean enableConsistencyCheck = true; /** 同步策略:realtime|batch|manual */ private String syncStrategy = "realtime"; public Integer getExpireDays() { return expireDays; } public void setExpireDays(Integer expireDays) { this.expireDays = expireDays; } public Boolean getEnablePersistence() { return enablePersistence; } public void setEnablePersistence(Boolean enablePersistence) { this.enablePersistence = enablePersistence; } public Boolean getEnableConsistencyCheck() { return enableConsistencyCheck; } public void setEnableConsistencyCheck(Boolean enableConsistencyCheck) { this.enableConsistencyCheck = enableConsistencyCheck; } public String getSyncStrategy() { return syncStrategy; } public void setSyncStrategy(String syncStrategy) { this.syncStrategy = syncStrategy; } } src/main/java/com/webmanage/config/RedisConfig.java
@@ -3,7 +3,9 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -27,6 +29,9 @@ ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); objectMapper.registerModule(new JavaTimeModule()); // 禁用日期时间作为时间戳 objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 使用StringRedisSerializer来序列化和反序列化redis的key值 src/main/java/com/webmanage/controller/OrderController.java
@@ -3,7 +3,9 @@ import com.webmanage.common.Result; import com.webmanage.dto.CreateOrderDTO; import com.webmanage.dto.OrderQueryDTO; import com.webmanage.entity.OrderInfo; import com.webmanage.service.OrderInfoService; import com.webmanage.service.TokenService; import com.webmanage.service.OrderNoService; import com.webmanage.vo.OrderDetailVO; import io.swagger.annotations.Api; @@ -29,6 +31,7 @@ public class OrderController { @Resource private OrderInfoService orderInfoService; @Resource private OrderNoService orderNoService; @Resource private TokenService tokenService; @PostMapping("/buyer/page") @ApiOperation("分页查询买家订单列表") @@ -38,17 +41,33 @@ } @PostMapping("/create") @ApiOperation("创建订单(包含订单详情)") public Result<Object> createOrder(@Valid @RequestBody CreateOrderDTO createOrderDTO) { @ApiOperation("创建订单(包含订单详情),需在 Header 携带 Idempotency-Token 防重复提交") public Result<OrderInfo> createOrder(@RequestHeader(value = "Idempotency-Token", required = false) String token, @Valid @RequestBody CreateOrderDTO createOrderDTO) { try { String orderId = orderInfoService.createOrder(createOrderDTO); return Result.success(orderId); if (!tokenService.verifyAndConsume(token)) { return Result.error("请求无效或重复提交,请刷新页面后重试"); } OrderInfo orderInfo = orderInfoService.createOrder(createOrderDTO); return Result.success(orderInfo); } catch (Exception e) { log.error("创建订单失败", e); return Result.error("创建订单失败:" + e.getMessage()); } } @GetMapping("/idempotency/token") @ApiOperation("获取一次性防重复提交 Token") public Result<Object> getIdempotencyToken(@RequestParam(required = false) Long userId) { try { String token = tokenService.generateToken(userId); return Result.success("token生成",token); } catch (Exception e) { log.error("生成防重复提交 Token 失败", e); return Result.error("生成防重复提交 Token 失败:" + e.getMessage()); } } @GetMapping("/no/new") @ApiOperation("生成唯一订单号") public Result<Object> generateOrderNo() { src/main/java/com/webmanage/entity/Cart.java
@@ -106,7 +106,6 @@ private LocalDateTime updateTime; @ApiModelProperty("逻辑删除:1-已删除,0-未删除") @TableLogic @TableField("deleted") private Integer deleted; } src/main/java/com/webmanage/mapper/CartMapper.java
@@ -32,4 +32,6 @@ * 根据用户ID和单位ID计算购物车总金额 */ java.math.BigDecimal sumTotalAmountByUserIdAndUnitId(@Param("userId") Long userId, @Param("unitId") Long unitId); Integer deleteByCustomerCondition(@Param("id") Long id); } src/main/java/com/webmanage/service/CartPersistenceService.java
New file @@ -0,0 +1,11 @@ package com.webmanage.service; import com.webmanage.vo.CartItemVO; public interface CartPersistenceService { void saveOrUpdate(Long userId, Long unitId, CartItemVO item); void remove(Long userId, Long unitId, Long pricingId); void clear(Long userId, Long unitId); } src/main/java/com/webmanage/service/OrderInfoService.java
@@ -35,7 +35,7 @@ /** * 创建订单(包含订单头与明细插入),返回订单编号 */ String createOrder(CreateOrderDTO createOrderDTO); OrderInfo createOrder(CreateOrderDTO createOrderDTO); /** * 上传订单附件 src/main/java/com/webmanage/service/TokenService.java
New file @@ -0,0 +1,22 @@ package com.webmanage.service; /** * 防重复提交 Token 服务 */ public interface TokenService { /** * 生成一次性防重复提交 Token(默认有效期短时间) * @param userId 可选的用户ID,仅用于追踪 * @return token 字符串 */ String generateToken(Long userId); /** * 校验并消费 Token(一次性)。成功返回 true,失败/不存在/过期返回 false。 * @param token header 中传递的 token * @return 校验并删除成功返回 true,否则 false */ boolean verifyAndConsume(String token); } src/main/java/com/webmanage/service/impl/CartPersistenceServiceImpl.java
New file @@ -0,0 +1,64 @@ package com.webmanage.service.impl; import com.webmanage.entity.Cart; import com.webmanage.mapper.CartMapper; import com.webmanage.service.CartPersistenceService; import com.webmanage.vo.CartItemVO; import org.springframework.beans.BeanUtils; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; @Service public class CartPersistenceServiceImpl implements CartPersistenceService { @Resource private CartMapper cartMapper; @Override @Async("asyncExecutor") public void saveOrUpdate(Long userId, Long unitId, CartItemVO item) { try { Cart cart = new Cart(); BeanUtils.copyProperties(item, cart); cart.setUserId(userId); cart.setUnitId(unitId); cart.setUpdateTime(LocalDateTime.now()); Cart existing = cartMapper.selectByUserIdUnitIdAndPricingId(userId, unitId, item.getPricingId()); if (existing != null) { cart.setId(existing.getId()); cartMapper.updateById(cart); } else { cart.setAddTime(LocalDateTime.now()); cartMapper.insert(cart); } } catch (Exception ignored) {} } @Override @Async("asyncExecutor") public void remove(Long userId, Long unitId, Long pricingId) { try { Cart existing = cartMapper.selectByUserIdUnitIdAndPricingId(userId, unitId, pricingId); if (existing != null) { cartMapper.deleteById(existing.getId()); } } catch (Exception ignored) {} } @Override @Async("asyncExecutor") public void clear(Long userId, Long unitId) { try { java.util.List<Cart> cartItems = cartMapper.selectByUserIdAndUnitId(userId, unitId); for (Cart item : cartItems) { cartMapper.deleteById(item.getId()); } } catch (Exception ignored) {} } } src/main/java/com/webmanage/service/impl/CartServiceImpl.java
@@ -7,11 +7,14 @@ import com.webmanage.mapper.CartMapper; import com.webmanage.mapper.ProductPricingMapper; import com.webmanage.service.CartService; import com.webmanage.service.CartPersistenceService; import com.webmanage.vo.CartItemVO; import com.webmanage.vo.CartVO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import com.webmanage.config.CartProperties; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.format.datetime.DateFormatter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -39,11 +42,17 @@ @Resource private ProductPricingMapper productPricingMapper; @Resource private CartPersistenceService cartPersistenceService; @Resource private CartProperties cartProperties; // Redis key前缀 private static final String CART_KEY_PREFIX = "cart:"; private static final String CART_ITEM_KEY_PREFIX = "cart_item:"; private static final int CART_EXPIRE_DAYS = 30; // 购物车过期时间30天 @Override @Transactional(rollbackFor = Exception.class) public boolean addToCart(Long userId, Long unitId, CartItemDTO cartItemDTO) { @@ -53,11 +62,11 @@ if (pricing == null) { throw new BusinessException("商品定价不存在"); } // 构建购物车key String cartKey = buildCartKey(userId, unitId); String cartItemKey = buildCartItemKey(userId, unitId, cartItemDTO.getPricingId()); // 检查商品是否已在购物车中 CartItemVO existingItem = (CartItemVO) redisTemplate.opsForValue().get(cartItemKey); if (existingItem != null) { @@ -76,19 +85,21 @@ existingItem.setTotalPrice(existingItem.getUnitPrice().multiply(BigDecimal.valueOf(existingItem.getQuantity()))); } } // 保存到Redis redisTemplate.opsForValue().set(cartItemKey, existingItem, CART_EXPIRE_DAYS, TimeUnit.DAYS); redisTemplate.opsForValue().set(cartItemKey, existingItem, (cartProperties.getExpireDays() != null ? cartProperties.getExpireDays() : CART_EXPIRE_DAYS), TimeUnit.DAYS); // 更新购物车商品列表 updateCartItemList(userId, unitId, cartItemDTO.getPricingId(), true); // 设置购物车过期时间 redisTemplate.expire(cartKey, CART_EXPIRE_DAYS, TimeUnit.DAYS); // 同步到数据库 syncCartItemToDatabase(userId, unitId, existingItem); redisTemplate.expire(cartKey, (cartProperties.getExpireDays() != null ? cartProperties.getExpireDays() : CART_EXPIRE_DAYS), TimeUnit.DAYS); // 异步持久化到数据库(根据配置) if (Boolean.TRUE.equals(cartProperties.getEnablePersistence()) && "realtime".equalsIgnoreCase(cartProperties.getSyncStrategy())) { cartPersistenceService.saveOrUpdate(userId, unitId, existingItem); } log.info("用户{}成功添加商品{}到购物车", userId, cartItemDTO.getProductName()); return true; } catch (Exception e) { @@ -96,22 +107,24 @@ throw new BusinessException("添加商品到购物车失败:" + e.getMessage()); } } @Override @Transactional(rollbackFor = Exception.class) public boolean removeFromCart(Long userId, Long unitId, Long pricingId) { try { String cartItemKey = buildCartItemKey(userId, unitId, pricingId); // 从Redis中删除商品项 Boolean removed = redisTemplate.delete(cartItemKey); if (Boolean.TRUE.equals(removed)) { // 更新购物车商品列表 updateCartItemList(userId, unitId, pricingId, false); // 从数据库中删除 removeCartItemFromDatabase(userId, unitId, pricingId); // 异步从数据库中删除(根据配置) if (Boolean.TRUE.equals(cartProperties.getEnablePersistence()) && "realtime".equalsIgnoreCase(cartProperties.getSyncStrategy())) { cartPersistenceService.remove(userId, unitId, pricingId); } log.info("用户{}成功从购物车移除商品{}", userId, pricingId); return true; } @@ -121,7 +134,7 @@ throw new BusinessException("从购物车移除商品失败:" + e.getMessage()); } } @Override @Transactional(rollbackFor = Exception.class) public boolean updateCartItemQuantity(Long userId, Long unitId, Long pricingId, Integer quantity) { @@ -129,23 +142,25 @@ if (quantity <= 0) { return removeFromCart(userId, unitId, pricingId); } String cartItemKey = buildCartItemKey(userId, unitId, pricingId); CartItemVO cartItem = (CartItemVO) redisTemplate.opsForValue().get(cartItemKey); if (cartItem == null) { throw new BusinessException("购物车商品不存在"); } cartItem.setQuantity(quantity); cartItem.setTotalPrice(cartItem.getUnitPrice().multiply(BigDecimal.valueOf(quantity))); cartItem.setUpdateTime(LocalDateTime.now()); // 更新到Redis redisTemplate.opsForValue().set(cartItemKey, cartItem, CART_EXPIRE_DAYS, TimeUnit.DAYS); // 同步到数据库 syncCartItemToDatabase(userId, unitId, cartItem); redisTemplate.opsForValue().set(cartItemKey, cartItem, (cartProperties.getExpireDays() != null ? cartProperties.getExpireDays() : CART_EXPIRE_DAYS), TimeUnit.DAYS); // 异步持久化到数据库(根据配置) if (Boolean.TRUE.equals(cartProperties.getEnablePersistence()) && "realtime".equalsIgnoreCase(cartProperties.getSyncStrategy())) { cartPersistenceService.saveOrUpdate(userId, unitId, cartItem); } log.info("用户{}成功更新购物车商品{}数量为{}", userId, pricingId, quantity); return true; } catch (Exception e) { @@ -153,26 +168,28 @@ throw new BusinessException("更新购物车商品数量失败:" + e.getMessage()); } } @Override @Transactional(rollbackFor = Exception.class) public boolean clearCart(Long userId, Long unitId) { try { String cartKey = buildCartKey(userId, unitId); List<Long> pricingIds = getCartItemPricingIds(userId, unitId); // 删除所有商品项 for (Long pricingId : pricingIds) { String cartItemKey = buildCartItemKey(userId, unitId, pricingId); redisTemplate.delete(cartItemKey); } // 删除购物车列表 redisTemplate.delete(cartKey); // 清空数据库中的购物车数据 clearCartFromDatabase(userId, unitId); // 异步清空数据库中的购物车数据(根据配置) if (Boolean.TRUE.equals(cartProperties.getEnablePersistence()) && "realtime".equalsIgnoreCase(cartProperties.getSyncStrategy())) { cartPersistenceService.clear(userId, unitId); } log.info("用户{}成功清空购物车", userId); return true; } catch (Exception e) { @@ -180,34 +197,34 @@ throw new BusinessException("清空购物车失败:" + e.getMessage()); } } @Override public CartVO getCart(Long userId, Long unitId) { try { CartVO cartVO = new CartVO(); cartVO.setUserId(userId); cartVO.setUnitId(unitId); List<CartItemVO> items = getCartItems(userId, unitId); cartVO.setItems(items); // 计算总数量和总金额 int totalQuantity = items.stream().mapToInt(item -> item.getQuantity()).sum(); BigDecimal totalAmount = items.stream() .map(item -> item.getTotalPrice()) .reduce(BigDecimal.ZERO, BigDecimal::add); cartVO.setTotalQuantity(totalQuantity); cartVO.setTotalAmount(totalAmount); cartVO.setLastUpdateTime(LocalDateTime.now()); return cartVO; } catch (Exception e) { log.error("获取购物车信息失败", e); throw new BusinessException("获取购物车信息失败:" + e.getMessage()); } } @Override public List<CartItemVO> getCartItems(Long userId, Long unitId) { try { @@ -216,7 +233,7 @@ if (items != null && !items.isEmpty()) { return items; } // Redis中没有数据,从数据库加载 log.info("Redis中无购物车数据,从数据库加载用户{}的购物车", userId); return loadCartFromDatabase(userId, unitId) ? getCartItemsFromRedis(userId, unitId) : new ArrayList<>(); @@ -226,13 +243,13 @@ return getCartItemsFromDatabase(userId, unitId); } } @Override public boolean checkCartItemStock(Long userId, Long unitId, Long pricingId) { // TODO: 实现库存检查逻辑 return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean batchRemoveFromCart(Long userId, Long unitId, List<Long> pricingIds) { @@ -240,18 +257,18 @@ if (CollectionUtils.isEmpty(pricingIds)) { return true; } for (Long pricingId : pricingIds) { removeFromCart(userId, unitId, pricingId); } return true; } catch (Exception e) { log.error("批量删除购物车商品失败", e); throw new BusinessException("批量删除购物车商品失败:" + e.getMessage()); } } @Override public Integer getCartItemCount(Long userId, Long unitId) { try { @@ -261,7 +278,7 @@ if (pricingIds != null) { return pricingIds.size(); } // 从数据库获取 return cartMapper.countByUserIdAndUnitId(userId, unitId); } catch (Exception e) { @@ -269,7 +286,7 @@ return 0; } } @Override public boolean loadCartFromDatabase(Long userId, Long unitId) { try { @@ -277,22 +294,22 @@ if (CollectionUtils.isEmpty(cartItems)) { return false; } String cartKey = buildCartKey(userId, unitId); List<Long> pricingIds = new ArrayList<>(); for (Cart cartItem : cartItems) { CartItemVO itemVO = convertCartToCartItemVO(cartItem); String cartItemKey = buildCartItemKey(userId, unitId, cartItem.getPricingId()); // 保存到Redis redisTemplate.opsForValue().set(cartItemKey, itemVO, CART_EXPIRE_DAYS, TimeUnit.DAYS); pricingIds.add(cartItem.getPricingId()); } // 保存购物车列表到Redis redisTemplate.opsForValue().set(cartKey, pricingIds, CART_EXPIRE_DAYS, TimeUnit.DAYS); log.info("成功从数据库加载用户{}的购物车数据到Redis", userId); return true; } catch (Exception e) { @@ -300,7 +317,7 @@ return false; } } @Override public boolean syncCartToDatabase(Long userId, Long unitId) { try { @@ -308,15 +325,15 @@ if (CollectionUtils.isEmpty(redisItems)) { return true; } // 清空数据库中的购物车数据 clearCartFromDatabase(userId, unitId); // 同步Redis数据到数据库 for (CartItemVO item : redisItems) { syncCartItemToDatabase(userId, unitId, item); } log.info("成功同步Redis购物车数据到数据库,用户{}", userId); return true; } catch (Exception e) { @@ -324,18 +341,18 @@ return false; } } @Override public boolean checkCartConsistency(Long userId, Long unitId) { try { List<CartItemVO> redisItems = getCartItemsFromRedis(userId, unitId); List<Cart> dbItems = cartMapper.selectByUserIdAndUnitId(userId, unitId); if (redisItems.size() != dbItems.size()) { log.warn("购物车数据不一致:Redis数量{},数据库数量{}", redisItems.size(), dbItems.size()); return false; } // 检查每个商品项是否一致 for (CartItemVO redisItem : redisItems) { boolean found = false; @@ -351,56 +368,56 @@ return false; } } return true; } catch (Exception e) { log.error("检查购物车数据一致性失败", e); return false; } } // ==================== 私有方法 ==================== private String buildCartKey(Long userId, Long unitId) { return CART_KEY_PREFIX + userId + ":" + unitId; } private String buildCartItemKey(Long userId, Long unitId, Long pricingId) { return CART_ITEM_KEY_PREFIX + userId + ":" + unitId + ":" + pricingId; } private void updateCartItemList(Long userId, Long unitId, Long pricingId, boolean add) { String cartKey = buildCartKey(userId, unitId); List<Long> pricingIds = (List<Long>) redisTemplate.opsForValue().get(cartKey); if (pricingIds == null) { pricingIds = new ArrayList<>(); } if (add && !pricingIds.contains(pricingId)) { pricingIds.add(pricingId); } else if (!add) { pricingIds.remove(pricingId); } redisTemplate.opsForValue().set(cartKey, pricingIds, CART_EXPIRE_DAYS, TimeUnit.DAYS); } private List<Long> getCartItemPricingIds(Long userId, Long unitId) { String cartKey = buildCartKey(userId, unitId); List<Long> pricingIds = (List<Long>) redisTemplate.opsForValue().get(cartKey); return pricingIds != null ? pricingIds : new ArrayList<>(); } private List<CartItemVO> getCartItemsFromRedis(Long userId, Long unitId) { try { String cartKey = buildCartKey(userId, unitId); List<Long> pricingIds = (List<Long>) redisTemplate.opsForValue().get(cartKey); if (CollectionUtils.isEmpty(pricingIds)) { return new ArrayList<>(); } List<CartItemVO> items = new ArrayList<>(); for (Long pricingId : pricingIds) { String cartItemKey = buildCartItemKey(userId, unitId, pricingId); @@ -409,7 +426,7 @@ items.add(item); } } return items; } catch (Exception e) { log.error("从Redis获取购物车商品列表失败", e); @@ -467,7 +484,7 @@ try { Cart existingCart = cartMapper.selectByUserIdUnitIdAndPricingId(userId, unitId, pricingId); if (existingCart != null) { cartMapper.deleteById(existingCart.getId()); cartMapper.deleteByCustomerCondition(existingCart.getId()); } } catch (Exception e) { log.error("从数据库删除购物车商品失败", e); src/main/java/com/webmanage/service/impl/OrderInfoServiceImpl.java
@@ -190,7 +190,7 @@ @Override @Transactional(rollbackFor = Exception.class) public String createOrder(CreateOrderDTO createOrderDTO) { public OrderInfo createOrder(CreateOrderDTO createOrderDTO) { if (createOrderDTO == null || CollectionUtils.isEmpty(createOrderDTO.getItems())) { throw new BusinessException("订单信息不完整"); } @@ -258,7 +258,7 @@ orderDetailMapper.insert(detail); } return orderId; return orderInfo; } @Override src/main/java/com/webmanage/service/impl/ProductPricingServiceImpl.java
@@ -48,10 +48,16 @@ if (!StringUtils.hasText(productPricing.getPriceType())) { throw new BusinessException("价格设置不能为空"); } if (productPricing.getPointsPrice() == null || productPricing.getPointsPrice().doubleValue() < 0) { if (productPricing.getPriceType().indexOf(PriceTypeEnum.POINTS.getName()) > -1 && productPricing.getPointsPrice() == null || productPricing.getPriceType().indexOf(PriceTypeEnum.POINTS.getName()) > -1 &&productPricing.getPointsPrice().doubleValue() < 0) { throw new BusinessException("积分价格值不能为空且不能为负数"); } if (productPricing.getCurrencyPrice()== null || productPricing.getCurrencyPrice().doubleValue() < 0) { if (productPricing.getPriceType().indexOf(PriceTypeEnum.CURRENCY.getName()) > -1 && productPricing.getCurrencyPrice()== null || productPricing.getPriceType().indexOf(PriceTypeEnum.CURRENCY.getName()) > -1 && productPricing.getCurrencyPrice().doubleValue() < 0){ throw new BusinessException("货币价格值不能为空且不能为负数"); } if (productPricing.getProductId() == null) { src/main/java/com/webmanage/service/impl/TokenServiceImpl.java
New file @@ -0,0 +1,45 @@ package com.webmanage.service.impl; import com.webmanage.service.TokenService; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.UUID; import java.util.concurrent.TimeUnit; @Service public class TokenServiceImpl implements TokenService { private static final String IDEMPOTENCY_TOKEN_PREFIX = "idempotency:token:"; private static final long DEFAULT_EXPIRE_SECONDS = 60 * 5; // 5分钟 @Resource private RedisTemplate<String, Object> redisTemplate; @Override public String generateToken(Long userId) { String token = UUID.randomUUID().toString().replace("-", ""); String key = IDEMPOTENCY_TOKEN_PREFIX + token; // 值不重要,设置一个标记即可 redisTemplate.opsForValue().set(key, userId == null ? 0L : userId, DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS); return token; } @Override public boolean verifyAndConsume(String token) { if (token == null || token.isEmpty()) { return false; } String key = IDEMPOTENCY_TOKEN_PREFIX + token; Boolean existed = redisTemplate.hasKey(key); if (Boolean.TRUE.equals(existed)) { // 消费后删除,确保一次性 redisTemplate.delete(key); return true; } return false; } } src/main/java/com/webmanage/vo/CartItemVO.java
@@ -1,5 +1,6 @@ package com.webmanage.vo; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -66,8 +67,10 @@ private String remarks; @ApiModelProperty("添加时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime addTime; @ApiModelProperty("最后更新时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; } src/main/resources/application.yml
@@ -7,11 +7,12 @@ application: name: web-manage-back profiles: active: test active: dev jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 serialization: write-dates-as-timestamps: false #mcv配置 mvc: pathmatch: @@ -29,9 +30,6 @@ # 主键类型 id-type: auto # 逻辑删除配置 logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml # 日志配置 @@ -63,3 +61,12 @@ enable-consistency-check: true # 同步策略:realtime(实时同步)、batch(批量同步)、manual(手动同步) sync-strategy: realtime async: executor: core-pool-size: 4 max-pool-size: 16 queue-capacity: 500 keep-alive-seconds: 60 thread-name-prefix: async-cart- wait-for-tasks-to-complete-on-shutdown: true await-termination-seconds: 30 src/main/resources/mapper/CartMapper.xml
@@ -69,5 +69,8 @@ AND user_id = #{userId} AND unit_id = #{unitId} </select> <delete id="deleteByCustomerCondition"> delete from cart where id = #{id} </delete> </mapper>