From 84c6e1df4b6bd48ee0517a33778b514008022875 Mon Sep 17 00:00:00 2001 From: seatonwan9 Date: 星期二, 19 八月 2025 18:10:44 +0800 Subject: [PATCH] 产品订购 --- src/main/java/com/webmanage/service/OrderInfoService.java | 2 src/main/java/com/webmanage/WebManageApplication.java | 2 src/main/java/com/webmanage/mapper/CartMapper.java | 2 src/main/java/com/webmanage/service/impl/ProductPricingServiceImpl.java | 10 src/main/resources/mapper/CartMapper.xml | 3 src/main/java/com/webmanage/service/impl/CartServiceImpl.java | 165 ++++++++++-------- src/main/java/com/webmanage/config/AsyncExecutorProperties.java | 40 ++++ src/main/java/com/webmanage/service/TokenService.java | 22 ++ src/main/java/com/webmanage/config/CartProperties.java | 31 +++ src/main/java/com/webmanage/controller/OrderController.java | 27 ++ src/main/java/com/webmanage/entity/Cart.java | 1 src/main/java/com/webmanage/service/impl/TokenServiceImpl.java | 45 +++++ src/main/java/com/webmanage/config/RedisConfig.java | 5 src/main/java/com/webmanage/service/impl/CartPersistenceServiceImpl.java | 64 +++++++ src/main/java/com/webmanage/service/impl/OrderInfoServiceImpl.java | 4 src/main/java/com/webmanage/config/AsyncConfig.java | 52 +++++ src/main/java/com/webmanage/vo/CartItemVO.java | 3 src/main/resources/application.yml | 17 + src/main/java/com/webmanage/service/CartPersistenceService.java | 11 + 19 files changed, 417 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/webmanage/WebManageApplication.java b/src/main/java/com/webmanage/WebManageApplication.java index 4af56a8..e54698e 100644 --- a/src/main/java/com/webmanage/WebManageApplication.java +++ b/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 { diff --git a/src/main/java/com/webmanage/config/AsyncConfig.java b/src/main/java/com/webmanage/config/AsyncConfig.java new file mode 100644 index 0000000..d712fac --- /dev/null +++ b/src/main/java/com/webmanage/config/AsyncConfig.java @@ -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); + } + }; + } +} + + diff --git a/src/main/java/com/webmanage/config/AsyncExecutorProperties.java b/src/main/java/com/webmanage/config/AsyncExecutorProperties.java new file mode 100644 index 0000000..4b06d45 --- /dev/null +++ b/src/main/java/com/webmanage/config/AsyncExecutorProperties.java @@ -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; } +} + + diff --git a/src/main/java/com/webmanage/config/CartProperties.java b/src/main/java/com/webmanage/config/CartProperties.java new file mode 100644 index 0000000..d4fd690 --- /dev/null +++ b/src/main/java/com/webmanage/config/CartProperties.java @@ -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; + /** 鍚屾绛栫暐锛歳ealtime|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; } +} + + diff --git a/src/main/java/com/webmanage/config/RedisConfig.java b/src/main/java/com/webmanage/config/RedisConfig.java index 65383b7..803da7b 100644 --- a/src/main/java/com/webmanage/config/RedisConfig.java +++ b/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鏉ュ簭鍒楀寲鍜屽弽搴忓垪鍖杛edis鐨刱ey鍊� diff --git a/src/main/java/com/webmanage/controller/OrderController.java b/src/main/java/com/webmanage/controller/OrderController.java index 6680838..d2d30e7 100644 --- a/src/main/java/com/webmanage/controller/OrderController.java +++ b/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() { diff --git a/src/main/java/com/webmanage/entity/Cart.java b/src/main/java/com/webmanage/entity/Cart.java index 2504dd7..5b5c741 100644 --- a/src/main/java/com/webmanage/entity/Cart.java +++ b/src/main/java/com/webmanage/entity/Cart.java @@ -106,7 +106,6 @@ private LocalDateTime updateTime; @ApiModelProperty("閫昏緫鍒犻櫎锛�1-宸插垹闄わ紝0-鏈垹闄�") - @TableLogic @TableField("deleted") private Integer deleted; } diff --git a/src/main/java/com/webmanage/mapper/CartMapper.java b/src/main/java/com/webmanage/mapper/CartMapper.java index 76abe93..5cc4d07 100644 --- a/src/main/java/com/webmanage/mapper/CartMapper.java +++ b/src/main/java/com/webmanage/mapper/CartMapper.java @@ -32,4 +32,6 @@ * 鏍规嵁鐢ㄦ埛ID鍜屽崟浣岻D璁$畻璐墿杞︽�婚噾棰� */ java.math.BigDecimal sumTotalAmountByUserIdAndUnitId(@Param("userId") Long userId, @Param("unitId") Long unitId); + + Integer deleteByCustomerCondition(@Param("id") Long id); } diff --git a/src/main/java/com/webmanage/service/CartPersistenceService.java b/src/main/java/com/webmanage/service/CartPersistenceService.java new file mode 100644 index 0000000..bd7cb3d --- /dev/null +++ b/src/main/java/com/webmanage/service/CartPersistenceService.java @@ -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); +} + + diff --git a/src/main/java/com/webmanage/service/OrderInfoService.java b/src/main/java/com/webmanage/service/OrderInfoService.java index 8e85fc7..d50282a 100644 --- a/src/main/java/com/webmanage/service/OrderInfoService.java +++ b/src/main/java/com/webmanage/service/OrderInfoService.java @@ -35,7 +35,7 @@ /** * 鍒涘缓璁㈠崟锛堝寘鍚鍗曞ご涓庢槑缁嗘彃鍏ワ級锛岃繑鍥炶鍗曠紪鍙� */ - String createOrder(CreateOrderDTO createOrderDTO); + OrderInfo createOrder(CreateOrderDTO createOrderDTO); /** * 涓婁紶璁㈠崟闄勪欢 diff --git a/src/main/java/com/webmanage/service/TokenService.java b/src/main/java/com/webmanage/service/TokenService.java new file mode 100644 index 0000000..3312ff8 --- /dev/null +++ b/src/main/java/com/webmanage/service/TokenService.java @@ -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); +} + + diff --git a/src/main/java/com/webmanage/service/impl/CartPersistenceServiceImpl.java b/src/main/java/com/webmanage/service/impl/CartPersistenceServiceImpl.java new file mode 100644 index 0000000..cdbe2c2 --- /dev/null +++ b/src/main/java/com/webmanage/service/impl/CartPersistenceServiceImpl.java @@ -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) {} + } +} + + diff --git a/src/main/java/com/webmanage/service/impl/CartServiceImpl.java b/src/main/java/com/webmanage/service/impl/CartServiceImpl.java index 304a9fe..7072069 100644 --- a/src/main/java/com/webmanage/service/impl/CartServiceImpl.java +++ b/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("鍟嗗搧瀹氫环涓嶅瓨鍦�"); } - + // 鏋勫缓璐墿杞ey 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()))); } } - + // 淇濆瓨鍒癛edis - 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); - + // 浠嶳edis涓垹闄ゅ晢鍝侀」 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()); - + // 鏇存柊鍒癛edis - 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()); - + // 淇濆瓨鍒癛edis 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("鎴愬姛浠庢暟鎹簱鍔犺浇鐢ㄦ埛{}鐨勮喘鐗╄溅鏁版嵁鍒癛edis", 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("浠嶳edis鑾峰彇璐墿杞﹀晢鍝佸垪琛ㄥけ璐�", 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); diff --git a/src/main/java/com/webmanage/service/impl/OrderInfoServiceImpl.java b/src/main/java/com/webmanage/service/impl/OrderInfoServiceImpl.java index 92a9b54..c981a09 100644 --- a/src/main/java/com/webmanage/service/impl/OrderInfoServiceImpl.java +++ b/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 diff --git a/src/main/java/com/webmanage/service/impl/ProductPricingServiceImpl.java b/src/main/java/com/webmanage/service/impl/ProductPricingServiceImpl.java index 25076bf..0d59412 100644 --- a/src/main/java/com/webmanage/service/impl/ProductPricingServiceImpl.java +++ b/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) { diff --git a/src/main/java/com/webmanage/service/impl/TokenServiceImpl.java b/src/main/java/com/webmanage/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..964e8b4 --- /dev/null +++ b/src/main/java/com/webmanage/service/impl/TokenServiceImpl.java @@ -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; + } +} + + diff --git a/src/main/java/com/webmanage/vo/CartItemVO.java b/src/main/java/com/webmanage/vo/CartItemVO.java index b41078d..ab0275a 100644 --- a/src/main/java/com/webmanage/vo/CartItemVO.java +++ b/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; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3e402c4..52bb679 100644 --- a/src/main/resources/application.yml +++ b/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 # 鍚屾绛栫暐锛歳ealtime锛堝疄鏃跺悓姝ワ級銆乥atch锛堟壒閲忓悓姝ワ級銆乵anual锛堟墜鍔ㄥ悓姝ワ級 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 diff --git a/src/main/resources/mapper/CartMapper.xml b/src/main/resources/mapper/CartMapper.xml index 086a599..3d56c30 100644 --- a/src/main/resources/mapper/CartMapper.xml +++ b/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> -- Gitblit v1.8.0