diff --git a/src/main/java/com/ycwl/basic/biz/OrderBiz.java b/src/main/java/com/ycwl/basic/biz/OrderBiz.java index 83825a1..b6232c3 100644 --- a/src/main/java/com/ycwl/basic/biz/OrderBiz.java +++ b/src/main/java/com/ycwl/basic/biz/OrderBiz.java @@ -88,7 +88,7 @@ public class OrderBiz { ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); priceObj.setScenicAllPrice(scenic.getPrice()); if (scenicConfig != null) { - if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) { + if (Boolean.TRUE.equals(scenicConfig.getAllFree())) { // 景区全免 priceObj.setFree(true); priceObj.setPrice(BigDecimal.ZERO); diff --git a/src/main/java/com/ycwl/basic/biz/PriceBiz.java b/src/main/java/com/ycwl/basic/biz/PriceBiz.java index fc6747d..0acaf83 100644 --- a/src/main/java/com/ycwl/basic/biz/PriceBiz.java +++ b/src/main/java/com/ycwl/basic/biz/PriceBiz.java @@ -50,10 +50,10 @@ public class PriceBiz { }).forEach(goodsList::add); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig != null) { - if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { + if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) { goodsList.add(new GoodsListRespVO(1L, "录像集")); } - if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) { + if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) { goodsList.add(new GoodsListRespVO(2L, "照片集")); } } @@ -92,7 +92,7 @@ public class PriceBiz { } ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig != null) { - if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) { + if (Boolean.TRUE.equals(scenicConfig.getAllFree())) { // 景区全免 respVO.setFree(true); respVO.setSlashPrice(BigDecimal.ZERO); diff --git a/src/main/java/com/ycwl/basic/config/FeignConfig.java b/src/main/java/com/ycwl/basic/config/FeignConfig.java deleted file mode 100644 index 95594e0..0000000 --- a/src/main/java/com/ycwl/basic/config/FeignConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.ycwl.basic.config; - -import feign.Logger; -import feign.RequestInterceptor; -import feign.codec.ErrorDecoder; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import jakarta.servlet.http.HttpServletRequest; - -@Slf4j -@Configuration -public class FeignConfig { - - @Bean - public Logger.Level feignLoggerLevel() { - return Logger.Level.BASIC; - } - - @Bean - public RequestInterceptor requestInterceptor() { - return requestTemplate -> { - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes != null) { - HttpServletRequest request = attributes.getRequest(); - - // 传递认证头 - String authorization = request.getHeader("Authorization"); - if (authorization != null) { - requestTemplate.header("Authorization", authorization); - } - } - }; - } - - @Bean - public ErrorDecoder errorDecoder() { - return new FeignErrorDecoder(); - } - - public static class FeignErrorDecoder implements ErrorDecoder { - private final ErrorDecoder defaultErrorDecoder = new Default(); - - @Override - public Exception decode(String methodKey, feign.Response response) { - log.error("Feign调用失败: method={}, status={}, reason={}", - methodKey, response.status(), response.reason()); - - if (response.status() >= 400 && response.status() < 500) { - // 4xx错误,客户端错误 - return new RuntimeException("客户端请求错误: " + response.reason()); - } else if (response.status() >= 500) { - // 5xx错误,服务器错误 - return new RuntimeException("服务器内部错误: " + response.reason()); - } - - return defaultErrorDecoder.decode(methodKey, response); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java b/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java new file mode 100644 index 0000000..e84ade6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppOrderV2Controller.java @@ -0,0 +1,304 @@ +package com.ycwl.basic.controller.mobile; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.pricing.dto.*; +import com.ycwl.basic.pricing.service.IPriceCalculationService; +import com.ycwl.basic.service.pc.FaceService; +import com.ycwl.basic.service.PriceCacheService; +import com.ycwl.basic.model.pc.face.resp.FaceRespVO; +import com.ycwl.basic.dto.MobileOrderRequest; +import com.ycwl.basic.order.service.IOrderService; +import com.ycwl.basic.order.dto.OrderV2DetailResponse; +import com.ycwl.basic.order.dto.OrderV2ListResponse; +import com.ycwl.basic.order.dto.OrderV2PageRequest; +import com.ycwl.basic.order.dto.PaymentParamsRequest; +import com.ycwl.basic.order.dto.PaymentParamsResponse; +import com.ycwl.basic.order.dto.PaymentCallbackResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.math.BigDecimal; + +/** + * 移动端订单控制器V2 + * 包含价格查询和订单管理功能 + */ +@Slf4j +@RestController +@RequestMapping("/api/mobile/order/v2") +@RequiredArgsConstructor +public class AppOrderV2Controller { + + private final IPriceCalculationService priceCalculationService; + private final FaceService faceService; + private final PriceCacheService priceCacheService; + private final IOrderService orderService; + + /** + * 移动端价格计算 + * 包含权限验证:验证人脸所属景区与当前用户匹配 + * 集成Redis缓存机制,提升查询性能 + */ + @PostMapping("/calculate") + public ApiResponse calculatePrice(@RequestBody MobilePriceCalculationRequest request) { + // 获取当前登录用户ID + String currentUserIdStr = BaseContextHandler.getUserId(); + if (currentUserIdStr == null) { + log.warn("移动端价格计算:用户未登录"); + return ApiResponse.fail("用户未登录"); + } + + Long currentUserId = Long.valueOf(currentUserIdStr); + log.info("移动端价格计算请求: userId={}, faceId={}, products={}", + currentUserId, request.getFaceId(), request.getProducts().size()); + + // 验证faceId参数 + if (request.getFaceId() == null) { + log.warn("移动端价格计算:faceId参数缺失"); + return ApiResponse.fail("faceId参数不能为空"); + } + + // 查询人脸信息进行权限验证 + ApiResponse faceResponse = faceService.getById(request.getFaceId()); + if (!faceResponse.isSuccess() || faceResponse.getData() == null) { + log.warn("移动端价格计算:人脸信息不存在, faceId={}", request.getFaceId()); + return ApiResponse.fail("人脸信息不存在"); + } + + FaceRespVO face = faceResponse.getData(); + Long scenicId = face.getScenicId(); + + // 先尝试从Redis缓存获取价格计算结果 + PriceCalculationResult cachedResult = priceCacheService.getCachedPriceResult( + currentUserId, scenicId, request.getProducts()); + + if (cachedResult != null) { + log.info("命中价格缓存: userId={}, scenicId={}, finalAmount={}", + currentUserId, scenicId, cachedResult.getFinalAmount()); + return ApiResponse.success(cachedResult); + } + + // 转换为标准价格计算请求 + PriceCalculationRequest standardRequest = request.toStandardRequest(currentUserId, scenicId); + + // 执行价格计算 + PriceCalculationResult result = priceCalculationService.calculatePrice(standardRequest); + + // 将计算结果缓存到Redis + String cacheKey = priceCacheService.cachePriceResult(currentUserId, scenicId, request.getProducts(), result); + + log.info("移动端价格计算完成: userId={}, scenicId={}, originalAmount={}, finalAmount={}, cacheKey={}", + currentUserId, scenicId, result.getOriginalAmount(), result.getFinalAmount(), cacheKey); + + return ApiResponse.success(result); + } + + /** + * 移动端下单接口 + * 验证价格缓存有效性,确保5分钟内使用缓存价格下单 + */ + @PostMapping("/add") + public ApiResponse addOrder(@RequestBody MobileOrderRequest request) { + // 获取当前登录用户ID + String currentUserIdStr = BaseContextHandler.getUserId(); + if (currentUserIdStr == null) { + log.warn("移动端下单:用户未登录"); + return ApiResponse.fail("用户未登录"); + } + + Long currentUserId = Long.valueOf(currentUserIdStr); + log.info("移动端下单请求: userId={}, faceId={}, products={}, expectedFinalAmount={}", + currentUserId, request.getFaceId(), request.getProducts().size(), request.getExpectedFinalAmount()); + + // 验证必填参数 + if (request.getFaceId() == null) { + log.warn("移动端下单:faceId参数缺失"); + return ApiResponse.fail("faceId参数不能为空"); + } + + if (request.getProducts() == null || request.getProducts().isEmpty()) { + log.warn("移动端下单:商品列表为空"); + return ApiResponse.fail("商品列表不能为空"); + } + + if (request.getExpectedFinalAmount() == null) { + log.warn("移动端下单:预期价格参数缺失"); + return ApiResponse.fail("预期价格不能为空"); + } + + // 查询人脸信息进行权限验证 + ApiResponse faceResponse = faceService.getById(request.getFaceId()); + if (!faceResponse.isSuccess() || faceResponse.getData() == null) { + log.warn("移动端下单:人脸信息不存在, faceId={}", request.getFaceId()); + return ApiResponse.fail("人脸信息不存在"); + } + + FaceRespVO face = faceResponse.getData(); + Long scenicId = face.getScenicId(); + + // 验证并消费价格缓存(一次性使用) + PriceCalculationResult cachedResult = priceCacheService.validateAndConsumePriceCache( + currentUserId, scenicId, request.getProducts()); + + if (cachedResult == null) { + log.warn("移动端下单:价格缓存已过期或不存在, userId={}, scenicId={}", currentUserId, scenicId); + return ApiResponse.fail("请重新下单!"); + } + + // 验证价格是否匹配 + if (cachedResult.getFinalAmount().compareTo(request.getExpectedFinalAmount()) != 0) { + log.warn("移动端下单:价格不匹配, cached={}, expected={}, userId={}, scenicId={}", + cachedResult.getFinalAmount(), request.getExpectedFinalAmount(), currentUserId, scenicId); + return ApiResponse.fail("请重新下单!"); + } + + // 验证原价是否匹配(可选) + if (request.getExpectedOriginalAmount() != null && + cachedResult.getOriginalAmount().compareTo(request.getExpectedOriginalAmount()) != 0) { + log.warn("移动端下单:原价不匹配, cached={}, expected={}, userId={}, scenicId={}", + cachedResult.getOriginalAmount(), request.getExpectedOriginalAmount(), currentUserId, scenicId); + return ApiResponse.fail("原价信息不匹配,请重新查询价格后再下单"); + } + + log.info("价格缓存验证通过: userId={}, scenicId={}, finalAmount={}", + currentUserId, scenicId, cachedResult.getFinalAmount()); + + // 创建订单 + try { + Long orderId = orderService.createOrder(request, currentUserId, scenicId, cachedResult); + + log.info("移动端订单创建成功: orderId={}, userId={}, scenicId={}, finalAmount={}", + orderId, currentUserId, scenicId, cachedResult.getFinalAmount()); + + return ApiResponse.success(orderId.toString()); + + } catch (Exception e) { + log.error("订单创建失败: userId={}, scenicId={}, error={}", currentUserId, scenicId, e.getMessage(), e); + return ApiResponse.fail("订单创建失败,请稍后重试"); + } + } + + // ====== 新增移动端订单查询功能 ====== + + /** + * 用户分页查询自己的订单列表 + */ + @PostMapping("/page") + public ApiResponse> pageUserOrders(@RequestBody OrderV2PageRequest request) { + String currentUserIdStr = BaseContextHandler.getUserId(); + if (currentUserIdStr == null) { + log.warn("用户未登录"); + return ApiResponse.fail("用户未登录"); + } + + Long currentUserId = Long.valueOf(currentUserIdStr); + request.setMemberId(currentUserId); // 设置当前用户ID,确保只查询自己的订单 + + log.info("用户查询订单列表: userId={}, request={}", currentUserId, request); + + try { + PageInfo pageInfo = orderService.pageOrdersByUser(request); + return ApiResponse.success(pageInfo); + } catch (Exception e) { + log.error("查询用户订单列表失败: userId={}", currentUserId, e); + return ApiResponse.fail("查询失败:" + e.getMessage()); + } + } + + /** + * 用户查询自己的订单详情 + */ + @GetMapping("/detail/{orderId}") + public ApiResponse getUserOrderDetail(@PathVariable("orderId") Long orderId) { + String currentUserIdStr = BaseContextHandler.getUserId(); + if (currentUserIdStr == null) { + log.warn("用户未登录"); + return ApiResponse.fail("用户未登录"); + } + + Long currentUserId = Long.valueOf(currentUserIdStr); + + log.info("用户查询订单详情: userId={}, orderId={}", currentUserId, orderId); + + try { + OrderV2DetailResponse detail = orderService.getOrderDetail(orderId); + if (detail == null) { + return ApiResponse.fail("订单不存在"); + } + + // 验证订单是否属于当前用户 + if (!currentUserId.equals(detail.getMemberId())) { + log.warn("用户尝试访问他人订单: userId={}, orderId={}, orderOwner={}", + currentUserId, orderId, detail.getMemberId()); + return ApiResponse.fail("无权访问该订单"); + } + + return ApiResponse.success(detail); + } catch (Exception e) { + log.error("查询用户订单详情失败: userId={}, orderId={}", currentUserId, orderId, e); + return ApiResponse.fail("查询失败:" + e.getMessage()); + } + } + + // ====== 支付相关接口 ====== + + /** + * 获取订单支付参数 + * 用于小程序调起支付 + */ + @PostMapping("/{orderId}/payment-params") + public ApiResponse getPaymentParams( + @PathVariable("orderId") Long orderId, + @RequestBody PaymentParamsRequest request) { + + String currentUserIdStr = BaseContextHandler.getUserId(); + if (currentUserIdStr == null) { + log.warn("用户未登录"); + return ApiResponse.fail("用户未登录"); + } + + Long currentUserId = Long.valueOf(currentUserIdStr); + + log.info("获取支付参数: userId={}, orderId={}", currentUserId, orderId); + + try { + PaymentParamsResponse response = orderService.getPaymentParams(orderId, currentUserId, request); + return ApiResponse.success(response); + } catch (Exception e) { + log.error("获取支付参数失败: userId={}, orderId={}", currentUserId, orderId, e); + return ApiResponse.fail(e.getMessage()); + } + } + + /** + * 支付回调处理接口 + * 供第三方支付平台回调使用 + */ + @PostMapping("/payment/callback/{scenicId}") + public String handlePaymentCallback( + @PathVariable("scenicId") Long scenicId, + HttpServletRequest request) { + + log.info("接收支付回调: scenicId={}", scenicId); + + try { + PaymentCallbackResponse response = orderService.handlePaymentCallback(scenicId, request); + + if (response.isSuccess()) { + log.info("支付回调处理成功: scenicId={}, orderId={}, statusChangeType={}", + scenicId, response.getOrderId(), response.getStatusChangeType()); + return "SUCCESS"; // 返回给第三方支付平台的成功标识 + } else { + log.error("支付回调处理失败: scenicId={}, message={}", scenicId, response.getMessage()); + return "FAIL"; // 返回给第三方支付平台的失败标识 + } + } catch (Exception e) { + log.error("支付回调异常: scenicId={}", scenicId, e); + return "FAIL"; + } + } +} diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java index 1593618..939708e 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java @@ -3,12 +3,12 @@ package com.ycwl.basic.controller.mobile; import com.github.pagehelper.PageInfo; import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.constant.BaseContextHandler; -import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; +import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; @@ -16,11 +16,14 @@ import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.utils.ApiResponse; -import com.ycwl.basic.utils.JwtTokenUtil; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; @@ -49,7 +52,7 @@ public class AppScenicController { // 分页查询景区列表 @PostMapping("/page") - public ApiResponse> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){ + public ApiResponse> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){ String userId = BaseContextHandler.getUserId(); if (ENABLED_USER_IDs.contains(userId)) { return appScenicService.pageQuery(scenicReqQuery); diff --git a/src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java b/src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java index a032e62..1dff5b6 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java @@ -2,6 +2,7 @@ package com.ycwl.basic.controller.mobile.manage; import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginOldRespVO; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO; @@ -12,6 +13,7 @@ import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.pc.ScenicAccountService; import com.ycwl.basic.service.pc.ScenicService; @@ -25,10 +27,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN; import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; /** @@ -45,6 +47,8 @@ public class AppScenicAccountController { private AppScenicService scenicService; @Autowired private ScenicService adminScenicService; + @Autowired + private ScenicRepository scenicRepository; // 登录 @PostMapping("/login") @@ -73,8 +77,8 @@ public class AppScenicAccountController { } @GetMapping("/myScenicList") - public ApiResponse> myScenicList() { - List list; + public ApiResponse> myScenicList() { + List list = Collections.emptyList(); if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) { String userId = BaseContextHandler.getUserId(); ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId)); @@ -82,10 +86,12 @@ public class AppScenicAccountController { return ApiResponse.fail("景区账号未绑定景区"); } list = account.getScenicId().stream() - .map(id -> scenicService.getDetails(id).getData()) + .map(id -> scenicRepository.getScenicBasic(id)) .toList(); - } else { - list = adminScenicService.list(new ScenicReqQuery()).getData(); + } else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) { + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + list = scenicRepository.list(query); } return ApiResponse.success(list); } diff --git a/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java b/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java new file mode 100644 index 0000000..8ddfe5c --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/pc/DefaultConfigController.java @@ -0,0 +1,100 @@ +package com.ycwl.basic.controller.pc; + +import com.ycwl.basic.integration.scenic.dto.config.DefaultConfigDTO; +import com.ycwl.basic.integration.scenic.service.DefaultConfigIntegrationService; +import com.ycwl.basic.utils.ApiConst; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 默认配置管理控制器 + * 提供默认配置的增删查改功能 + */ +@Slf4j +@RestController +@RequestMapping("/api/default-config") +@RequiredArgsConstructor +public class DefaultConfigController { + + private final DefaultConfigIntegrationService defaultConfigIntegrationService; + + /** + * 获取默认配置列表 + */ + @GetMapping("/") + public ApiResponse> listDefaultConfigs() { + log.info("获取默认配置列表"); + try { + List configs = defaultConfigIntegrationService.listDefaultConfigs(); + return ApiResponse.success(configs); + } catch (Exception e) { + log.error("获取默认配置列表失败", e); + return ApiResponse.fail("获取默认配置列表失败: " + e.getMessage()); + } + } + + /** + * 根据配置键获取默认配置 + */ + @GetMapping("/{configKey}") + public ApiResponse getDefaultConfig(@PathVariable String configKey) { + log.info("获取默认配置, configKey: {}", configKey); + try { + DefaultConfigDTO config = defaultConfigIntegrationService.getDefaultConfig(configKey); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("获取默认配置失败, configKey: {}", configKey, e); + return ApiResponse.fail("获取默认配置失败: " + e.getMessage()); + } + } + + /** + * 创建默认配置 + */ + @PostMapping("/") + public ApiResponse createDefaultConfig(@RequestBody DefaultConfigDTO request) { + log.info("创建默认配置, configKey: {}", request.getConfigKey()); + try { + DefaultConfigDTO config = defaultConfigIntegrationService.createDefaultConfig(request); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("创建默认配置失败, configKey: {}", request.getConfigKey(), e); + return ApiResponse.fail("创建默认配置失败: " + e.getMessage()); + } + } + + /** + * 更新默认配置 + */ + @PutMapping("/{configKey}") + public ApiResponse updateDefaultConfig(@PathVariable String configKey, + @RequestBody DefaultConfigDTO request) { + log.info("更新默认配置, configKey: {}", configKey); + try { + DefaultConfigDTO config = defaultConfigIntegrationService.updateDefaultConfig(configKey, request); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("更新默认配置失败, configKey: {}", configKey, e); + return ApiResponse.fail("更新默认配置失败: " + e.getMessage()); + } + } + + /** + * 删除默认配置 + */ + @DeleteMapping("/{configKey}") + public ApiResponse deleteDefaultConfig(@PathVariable String configKey) { + log.info("删除默认配置, configKey: {}", configKey); + try { + defaultConfigIntegrationService.deleteDefaultConfig(configKey); + return ApiResponse.buildResponse(ApiConst.Code.CODE_SUCCESS.code(), null, "删除成功"); + } catch (Exception e) { + log.error("删除默认配置失败, configKey: {}", configKey, e); + return ApiResponse.fail("删除默认配置失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/controller/pc/ScenicController.java b/src/main/java/com/ycwl/basic/controller/pc/ScenicController.java index 4647b0d..14c595a 100644 --- a/src/main/java/com/ycwl/basic/controller/pc/ScenicController.java +++ b/src/main/java/com/ycwl/basic/controller/pc/ScenicController.java @@ -1,13 +1,12 @@ package com.ycwl.basic.controller.pc; -import com.github.pagehelper.PageInfo; import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq; +import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity; -import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; -import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.mobile.AppStatisticsService; import com.ycwl.basic.service.pc.ScenicAccountService; @@ -17,16 +16,20 @@ import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.enums.StorageAcl; import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.WxMpUtil; -import com.ycwl.basic.repository.ScenicRepository; -import com.ycwl.basic.model.pc.mp.MpConfigEntity; import org.apache.commons.lang3.Strings; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.util.Collections; import java.util.List; +import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN; import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; /** @@ -50,68 +53,6 @@ public class ScenicController { @Autowired private ScenicAccountService accountService; - // 分页查询景区 - @PostMapping("/page") - public ApiResponse> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery) { - return scenicService.pageQuery(scenicReqQuery); - } - // 查询景区列表 - @PostMapping("/list") - public ApiResponse> list(@RequestBody ScenicReqQuery scenicReqQuery) { - return scenicService.list(scenicReqQuery); - } - // 查询景区详情 - @GetMapping("/getDetail/{id}") - public ApiResponse getDetail(@PathVariable Long id) { - return scenicService.getById(id); - } - // 新增景区 - @PostMapping("/add") - public ApiResponse add(@RequestBody ScenicAddOrUpdateReq scenicAddReq) { - return scenicService.add(scenicAddReq); - } - // 删除景区 - @GetMapping("/delete/{id}") - public ApiResponse delete(@PathVariable Long id) { - return scenicService.deleteById(id); - } - // 修改景区 - @PostMapping("/update") - public ApiResponse update(@RequestBody ScenicAddOrUpdateReq scenicAddReq) { - return scenicService.update(scenicAddReq); - } - // 修改景区状态 - @GetMapping("/updateStatus/{id}") - public ApiResponse updateStatus(@PathVariable Long id) { - return scenicService.updateStatus(id); - } - // 新增景区配置 - @PostMapping("/addConfig") - public ApiResponse addConfig(@RequestBody ScenicConfigEntity scenicConfig) { - return scenicService.addConfig(scenicConfig); - } - // 修改景区配置 - @PostMapping("/updateConfig") - public ApiResponse updateConfig(@RequestBody ScenicConfigEntity scenicConfig) { - return scenicService.updateConfigById(scenicConfig); - } - - // 查询景区配置 - @GetMapping("/config/{id}") - public ApiResponse getConfig(@PathVariable("id") Long id) { - return ApiResponse.success(scenicService.getConfig(id)); - } - @PostMapping("/saveConfig/{id}") - public ApiResponse saveConfig(@PathVariable("id") Long id, @RequestBody ScenicConfigEntity config) { - scenicService.saveConfig(id, config); - return ApiResponse.success(null); - } - @PostMapping("/saveConfig/undefined") - public ApiResponse saveConfig(@RequestBody ScenicConfigEntity config) { - scenicService.addConfig(config); - return ApiResponse.success(null); - } - // 根据景区ID下载小程序二维码 @GetMapping("/{id}/QRCode") public ApiResponse downloadQrCode(@PathVariable Long id) { @@ -167,19 +108,19 @@ public class ScenicController { } @GetMapping("/myScenicList") - public ApiResponse> myScenicList() { - List list = Collections.emptyList(); + public ApiResponse> myScenicList() { + List list = Collections.emptyList(); if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) { String userId = BaseContextHandler.getUserId(); ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId)); if (account == null || account.getScenicId().isEmpty()) { return ApiResponse.fail("景区账号未绑定景区"); } - list = account.getScenicId().stream().map(id -> { - return appScenicService.getDetails(id).getData(); - }).toList(); - } else { - list = scenicService.list(new ScenicReqQuery()).getData(); + list = account.getScenicId().stream().map(id -> scenicRepository.getScenicBasic(id)).toList(); + } else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) { + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + list = scenicRepository.list(query); } return ApiResponse.success(list); } diff --git a/src/main/java/com/ycwl/basic/controller/pc/ScenicV2Controller.java b/src/main/java/com/ycwl/basic/controller/pc/ScenicV2Controller.java new file mode 100644 index 0000000..52b7f8b --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/pc/ScenicV2Controller.java @@ -0,0 +1,336 @@ +package com.ycwl.basic.controller.pc; + +import com.ycwl.basic.integration.scenic.dto.config.BatchConfigRequest; +import com.ycwl.basic.integration.scenic.dto.config.BatchUpdateResponse; +import com.ycwl.basic.integration.scenic.dto.config.CreateConfigRequest; +import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO; +import com.ycwl.basic.integration.scenic.dto.config.UpdateConfigRequest; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2ListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest; +import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService; +import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService; +import com.ycwl.basic.utils.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * @Author:longbinbin + * @Date:2024/12/26 + * 景区管理 V2 版本控制器 - 基于 zt-scenic 集成服务 + */ +@Slf4j +@RestController +@RequestMapping("/api/scenic/v2") +@RequiredArgsConstructor +public class ScenicV2Controller { + + private final ScenicIntegrationService scenicIntegrationService; + private final ScenicConfigIntegrationService scenicConfigIntegrationService; + + // ========== 景区基础 CRUD 操作 ========== + + /** + * 景区V2核心信息分页列表 + */ + @GetMapping("/") + public ApiResponse listScenics(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Integer status, + @RequestParam(required = false) String name) { + log.info("分页查询景区核心信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name); + + // 参数验证:限制pageSize最大值为100 + if (pageSize > 100) { + pageSize = 100; + } + + try { + ScenicV2ListResponse response = scenicIntegrationService.listScenics(page, pageSize, status, name); + return ApiResponse.success(response); + } catch (Exception e) { + log.error("分页查询景区核心信息列表失败", e); + return ApiResponse.fail("分页查询景区列表失败: " + e.getMessage()); + } + } + + /** + * 景区V2带配置信息分页列表 + */ + @GetMapping("/with-config") + public ApiResponse listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Integer status, + @RequestParam(required = false) String name) { + log.info("分页查询景区带配置信息列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name); + + // 参数验证:限制pageSize最大值为100 + if (pageSize > 100) { + pageSize = 100; + } + + try { + ScenicV2WithConfigListResponse response = scenicIntegrationService.listScenicsWithConfig(page, pageSize, status, name); + return ApiResponse.success(response); + } catch (Exception e) { + log.error("分页查询景区带配置信息列表失败", e); + return ApiResponse.fail("分页查询景区带配置信息列表失败: " + e.getMessage()); + } + } + + /** + * 查询单个景区详情 + */ + @GetMapping("/{scenicId}") + public ApiResponse getScenic(@PathVariable Long scenicId) { + log.info("查询景区详情, scenicId: {}", scenicId); + try { + ScenicV2DTO scenic = scenicIntegrationService.getScenic(scenicId); + return ApiResponse.success(scenic); + } catch (Exception e) { + log.error("查询景区详情失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("查询景区详情失败: " + e.getMessage()); + } + } + + /** + * 查询景区列表(支持筛选和分页)- 高级筛选 + */ + @PostMapping("/filter") + public ApiResponse filterScenics(@RequestBody @Valid ScenicFilterRequest request) { + log.info("高级筛选景区列表, 筛选条件数: {}, 页码: {}, 页大小: {}", + request.getFilters().size(), request.getPage(), request.getPageSize()); + try { + ScenicFilterPageResponse response = scenicIntegrationService.filterScenics(request); + return ApiResponse.success(response); + } catch (Exception e) { + log.error("高级筛选景区列表失败", e); + return ApiResponse.fail("高级筛选景区列表失败: " + e.getMessage()); + } + } + + /** + * 新增景区 + */ + @PostMapping("/create") + public ApiResponse createScenic(@RequestBody @Valid CreateScenicRequest request) { + log.info("新增景区, name: {}, mpId: {}", request.getName(), request.getMpId()); + try { + ScenicV2DTO scenic = scenicIntegrationService.createScenic(request); + return ApiResponse.success(scenic); + } catch (Exception e) { + log.error("新增景区失败, name: {}", request.getName(), e); + return ApiResponse.fail("新增景区失败: " + e.getMessage()); + } + } + + /** + * 修改景区 + */ + @PutMapping("/{scenicId}") + public ApiResponse updateScenic(@PathVariable Long scenicId, + @RequestBody @Valid UpdateScenicRequest request) { + log.info("修改景区, scenicId: {}", scenicId); + try { + ScenicV2DTO scenic = scenicIntegrationService.updateScenic(scenicId, request); + return ApiResponse.success(scenic); + } catch (Exception e) { + log.error("修改景区失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("修改景区失败: " + e.getMessage()); + } + } + + /** + * 删除景区 + */ + @DeleteMapping("/{scenicId}") + public ApiResponse deleteScenic(@PathVariable Long scenicId) { + log.info("删除景区, scenicId: {}", scenicId); + try { + scenicIntegrationService.deleteScenic(scenicId); + return ApiResponse.success(null); + } catch (Exception e) { + log.error("删除景区失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("删除景区失败: " + e.getMessage()); + } + } + + /** + * 景区列表查询(默认1000条) + * 只支持根据状态筛选 + */ + @GetMapping("/list") + public ApiResponse listScenicsByStatus(@RequestParam(required = false) Integer status) { + log.info("查询景区列表, status: {}", status); + try { + // 默认查询1000条数据,第1页 + ScenicV2ListResponse scenics = scenicIntegrationService.listScenics(1, 1000, status, null); + return ApiResponse.success(scenics); + } catch (Exception e) { + log.error("查询景区列表失败, status: {}", status, e); + return ApiResponse.fail("查询景区列表失败: " + e.getMessage()); + } + } + + + // ========== 景区配置管理 ========== + + /** + * 获取景区及其配置信息 + */ + @GetMapping("/{scenicId}/with-config") + public ApiResponse getScenicWithConfig(@PathVariable Long scenicId) { + log.info("获取景区配置信息, scenicId: {}", scenicId); + try { + ScenicV2WithConfigDTO scenic = scenicIntegrationService.getScenicWithConfig(scenicId); + return ApiResponse.success(scenic); + } catch (Exception e) { + log.error("获取景区配置信息失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("获取景区配置信息失败: " + e.getMessage()); + } + } + + /** + * 获取景区扁平化配置 + */ + @GetMapping("/{scenicId}/flat-config") + public ApiResponse> getScenicFlatConfig(@PathVariable Long scenicId) { + log.info("获取景区扁平化配置, scenicId: {}", scenicId); + try { + Map config = scenicIntegrationService.getScenicFlatConfig(scenicId); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("获取景区扁平化配置失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("获取景区扁平化配置失败: " + e.getMessage()); + } + } + + /** + * 获取景区配置列表 + */ + @GetMapping("/{scenicId}/config") + public ApiResponse> listConfigs(@PathVariable Long scenicId) { + log.info("获取景区配置列表, scenicId: {}", scenicId); + try { + List configs = scenicConfigIntegrationService.listConfigs(scenicId); + return ApiResponse.success(configs); + } catch (Exception e) { + log.error("获取景区配置列表失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("获取景区配置列表失败: " + e.getMessage()); + } + } + + /** + * 根据配置键获取配置 + */ + @GetMapping("/{scenicId}/config/{configKey}") + public ApiResponse getConfigByKey(@PathVariable Long scenicId, + @PathVariable String configKey) { + log.info("根据键获取景区配置, scenicId: {}, configKey: {}", scenicId, configKey); + try { + ScenicConfigV2DTO config = scenicConfigIntegrationService.getConfigByKey(scenicId, configKey); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("根据键获取景区配置失败, scenicId: {}, configKey: {}", scenicId, configKey, e); + return ApiResponse.fail("获取配置失败: " + e.getMessage()); + } + } + + /** + * 创建景区配置 + */ + @PostMapping("/{scenicId}/config") + public ApiResponse createConfig(@PathVariable Long scenicId, + @RequestBody @Valid CreateConfigRequest request) { + log.info("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey()); + try { + ScenicConfigV2DTO config = scenicConfigIntegrationService.createConfig(scenicId, request); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("创建景区配置失败, scenicId: {}, configKey: {}", scenicId, request.getConfigKey(), e); + return ApiResponse.fail("创建配置失败: " + e.getMessage()); + } + } + + /** + * 更新景区配置 + */ + @PutMapping("/{scenicId}/config/{configId}") + public ApiResponse updateConfig(@PathVariable Long scenicId, + @PathVariable String configId, + @RequestBody @Valid UpdateConfigRequest request) { + log.info("更新景区配置, scenicId: {}, configId: {}", scenicId, configId); + try { + ScenicConfigV2DTO config = scenicConfigIntegrationService.updateConfig(scenicId, configId, request); + return ApiResponse.success(config); + } catch (Exception e) { + log.error("更新景区配置失败, scenicId: {}, configId: {}", scenicId, configId, e); + return ApiResponse.fail("更新配置失败: " + e.getMessage()); + } + } + + /** + * 删除景区配置 + */ + @DeleteMapping("/{scenicId}/config/{configId}") + public ApiResponse deleteConfig(@PathVariable Long scenicId, @PathVariable String configId) { + log.info("删除景区配置, scenicId: {}, configId: {}", scenicId, configId); + try { + scenicConfigIntegrationService.deleteConfig(scenicId, configId); + return ApiResponse.success(null); + } catch (Exception e) { + log.error("删除景区配置失败, scenicId: {}, configId: {}", scenicId, configId, e); + return ApiResponse.fail("删除配置失败: " + e.getMessage()); + } + } + + /** + * 批量更新景区配置 + */ + @PutMapping("/{scenicId}/config/batch") + public ApiResponse batchUpdateConfigs(@PathVariable Long scenicId, + @RequestBody @Valid BatchConfigRequest request) { + log.info("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size()); + try { + BatchUpdateResponse response = scenicConfigIntegrationService.batchUpdateConfigs(scenicId, request); + return ApiResponse.success(response); + } catch (Exception e) { + log.error("批量更新景区配置失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("批量更新配置失败: " + e.getMessage()); + } + } + + /** + * 扁平化批量更新景区配置 + */ + @PutMapping("/{scenicId}/flat-config") + public ApiResponse batchFlatUpdateConfigs(@PathVariable Long scenicId, + @RequestBody Map configs) { + log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size()); + try { + BatchUpdateResponse response = scenicConfigIntegrationService.batchFlatUpdateConfigs(scenicId, configs); + return ApiResponse.success(response); + } catch (Exception e) { + log.error("扁平化批量更新景区配置失败, scenicId: {}", scenicId, e); + return ApiResponse.fail("扁平化批量更新配置失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java b/src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java new file mode 100644 index 0000000..e16608f --- /dev/null +++ b/src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java @@ -0,0 +1,54 @@ +package com.ycwl.basic.dto; + +import com.ycwl.basic.pricing.dto.ProductItem; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 移动端下单请求DTO + */ +@Data +public class MobileOrderRequest { + + /** + * 商品列表 + */ + private List products; + + /** + * 人脸ID(必填,用于权限验证) + */ + private Long faceId; + + /** + * 预期原价(用于价格验证) + */ + private BigDecimal expectedOriginalAmount; + + /** + * 预期最终价格(用于价格验证) + */ + private BigDecimal expectedFinalAmount; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon = true; + + /** + * 用户输入的券码 + */ + private String voucherCode; + + /** + * 是否自动使用券码优惠 + */ + private Boolean autoUseVoucher = true; + + /** + * 订单备注 + */ + private String remarks; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java b/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java index 568e056..1070bb7 100644 --- a/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java +++ b/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java @@ -1,6 +1,7 @@ package com.ycwl.basic.exception; import com.ycwl.basic.enums.BizCodeEnum; +import com.ycwl.basic.order.exception.DuplicatePurchaseException; import com.ycwl.basic.utils.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException; @@ -106,4 +107,18 @@ public class CustomExceptionHandle { public ApiResponse handle(SizeLimitExceededException sizeLimitExceededException) { return ApiResponse.buildResponse(415, "文件过大,请重新上传"); } + + /** + * 重复购买异常处理 + */ + @ExceptionHandler(value = DuplicatePurchaseException.class) + public ApiResponse handle(HttpServletResponse response, DuplicatePurchaseException exception) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + LOGGER.warn("检测到重复购买: productType={}, productId={}, existingOrderId={}, existingOrderNo={}", + exception.getProductType(), exception.getProductId(), + exception.getExistingOrderId(), exception.getExistingOrderNo()); + + // 返回友好的错误信息给前端 + return ApiResponse.buildResponse(4001, exception.getFriendlyMessage()); + } } diff --git a/src/main/java/com/ycwl/basic/integration/common/config/FeignConfig.java b/src/main/java/com/ycwl/basic/integration/common/config/FeignConfig.java new file mode 100644 index 0000000..de1a873 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/config/FeignConfig.java @@ -0,0 +1,32 @@ +package com.ycwl.basic.integration.common.config; + +import com.ycwl.basic.integration.common.exception.FeignErrorDecoder; +import feign.RequestInterceptor; +import feign.codec.ErrorDecoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class FeignConfig { + + private final FeignErrorDecoder feignErrorDecoder; + + @Bean + public ErrorDecoder errorDecoder() { + return feignErrorDecoder; + } + + @Bean + public RequestInterceptor requestInterceptor() { + return template -> { + template.header("Accept", "application/json"); + template.header("Content-Type", "application/json"); + // 可以在这里添加统一的鉴权头 + // template.header("Authorization", "Bearer " + getToken()); + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java b/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java new file mode 100644 index 0000000..8baa61a --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/config/IntegrationProperties.java @@ -0,0 +1,43 @@ +package com.ycwl.basic.integration.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +@Data +@Component +@RefreshScope +@ConfigurationProperties(prefix = "integration") +public class IntegrationProperties { + + /** + * 景区服务配置 + */ + private ScenicConfig scenic = new ScenicConfig(); + + @Data + public static class ScenicConfig { + /** + * 是否启用景区服务集成 + */ + private boolean enabled = true; + + /** + * 服务名称 + */ + private String serviceName = "zt-scenic"; + + /** + * 超时配置(毫秒) + */ + private int connectTimeout = 5000; + private int readTimeout = 10000; + + /** + * 重试配置 + */ + private boolean retryEnabled = false; + private int maxRetries = 3; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/exception/FeignErrorDecoder.java b/src/main/java/com/ycwl/basic/integration/common/exception/FeignErrorDecoder.java new file mode 100644 index 0000000..8d00405 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/exception/FeignErrorDecoder.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.integration.common.exception; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.integration.common.response.CommonResponse; +import feign.Response; +import feign.codec.ErrorDecoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +public class FeignErrorDecoder implements ErrorDecoder { + + private final ErrorDecoder defaultDecoder = new Default(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public Exception decode(String methodKey, Response response) { + log.warn("Feign调用失败, methodKey: {}, status: {}, reason: {}", + methodKey, response.status(), response.reason()); + + try { + if (response.body() != null) { + String body = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8); + log.warn("响应内容: {}", body); + + try { + CommonResponse errorResponse = objectMapper.readValue(body, CommonResponse.class); + if (errorResponse.getCode() != null && !errorResponse.getCode().equals(200)) { + return new IntegrationException( + errorResponse.getCode(), + errorResponse.getMessage() != null ? errorResponse.getMessage() : "服务调用失败", + extractServiceName(methodKey) + ); + } + } catch (Exception e) { + log.warn("解析错误响应失败", e); + } + } + } catch (IOException e) { + log.error("读取响应体失败", e); + } + + return new IntegrationException( + response.status(), + String.format("服务调用失败: %s", response.reason()), + extractServiceName(methodKey) + ); + } + + private String extractServiceName(String methodKey) { + if (methodKey != null && methodKey.contains("#")) { + return methodKey.substring(0, methodKey.indexOf("#")); + } + return "unknown"; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/exception/IntegrationException.java b/src/main/java/com/ycwl/basic/integration/common/exception/IntegrationException.java new file mode 100644 index 0000000..d6d4657 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/exception/IntegrationException.java @@ -0,0 +1,33 @@ +package com.ycwl.basic.integration.common.exception; + +import lombok.Getter; + +@Getter +public class IntegrationException extends RuntimeException { + private final Integer code; + private final String serviceName; + + public IntegrationException(Integer code, String message) { + super(message); + this.code = code; + this.serviceName = null; + } + + public IntegrationException(Integer code, String message, String serviceName) { + super(message); + this.code = code; + this.serviceName = serviceName; + } + + public IntegrationException(Integer code, String message, Throwable cause) { + super(message, cause); + this.code = code; + this.serviceName = null; + } + + public IntegrationException(Integer code, String message, String serviceName, Throwable cause) { + super(message, cause); + this.code = code; + this.serviceName = serviceName; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/response/CommonResponse.java b/src/main/java/com/ycwl/basic/integration/common/response/CommonResponse.java new file mode 100644 index 0000000..2d7d7f1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/response/CommonResponse.java @@ -0,0 +1,40 @@ +package com.ycwl.basic.integration.common.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CommonResponse { + private Integer code; + private String message; + private T data; + + public static CommonResponse success() { + return new CommonResponse<>(200, "OK", null); + } + + public static CommonResponse success(T data) { + return new CommonResponse<>(200, "OK", data); + } + + public static CommonResponse success(String message, T data) { + return new CommonResponse<>(200, message, data); + } + + public static CommonResponse error(Integer code, String message) { + return new CommonResponse<>(code, message, null); + } + + public static CommonResponse error(String message) { + return new CommonResponse<>(5000, message, null); + } + + public boolean isSuccess() { + return code != null && code == 200; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/response/PageResponse.java b/src/main/java/com/ycwl/basic/integration/common/response/PageResponse.java new file mode 100644 index 0000000..fa4a664 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/response/PageResponse.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.integration.common.response; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PageResponse { + private List list; + private Integer total; + private Integer page; + private Integer pageSize; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/common/util/ConfigValueUtil.java b/src/main/java/com/ycwl/basic/integration/common/util/ConfigValueUtil.java new file mode 100644 index 0000000..a38797b --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/common/util/ConfigValueUtil.java @@ -0,0 +1,397 @@ +package com.ycwl.basic.integration.common.util; + +import com.ycwl.basic.utils.JacksonUtil; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 配置值转换工具类 + * + * 提供统一的配置Map值类型转换方法,支持多种数据类型的安全转换 + */ +public class ConfigValueUtil { + + /** + * 从配置Map中获取Integer值 + * + * @param config 配置Map + * @param key 配置键 + * @return Integer值,如果转换失败返回null + */ + public static Integer getIntValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + if (value instanceof Integer) return (Integer) value; + if (value instanceof Number) return ((Number) value).intValue(); + if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** + * 从配置Map中获取Long值 + * + * @param config 配置Map + * @param key 配置键 + * @return Long值,如果转换失败返回null + */ + public static Long getLongValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + if (value instanceof Long) return (Long) value; + if (value instanceof Number) return ((Number) value).longValue(); + if (value instanceof String) { + try { + return Long.parseLong((String) value); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** + * 从配置Map中获取Float值 + * + * @param config 配置Map + * @param key 配置键 + * @return Float值,如果转换失败返回null + */ + public static Float getFloatValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + if (value instanceof Float) return (Float) value; + if (value instanceof Double) return ((Double) value).floatValue(); + if (value instanceof Number) return ((Number) value).floatValue(); + if (value instanceof String) { + try { + return Float.parseFloat((String) value); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** + * 从配置Map中获取Double值 + * + * @param config 配置Map + * @param key 配置键 + * @return Double值,如果转换失败返回null + */ + public static Double getDoubleValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + if (value instanceof Double) return (Double) value; + if (value instanceof Number) return ((Number) value).doubleValue(); + if (value instanceof String) { + try { + return Double.parseDouble((String) value); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** + * 从配置Map中获取BigDecimal值 + * + * @param config 配置Map + * @param key 配置键 + * @return BigDecimal值,如果转换失败返回null + */ + public static BigDecimal getBigDecimalValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + if (value instanceof BigDecimal) return (BigDecimal) value; + if (value instanceof String) { + try { + return new BigDecimal((String) value); + } catch (NumberFormatException e) { + return null; + } + } + if (value instanceof Number) { + return new BigDecimal(value.toString()); + } + return null; + } + + /** + * 从配置Map中获取String值 + * 如果值是复杂对象(Map/List),会自动转换为JSON字符串 + * + * @param config 配置Map + * @param key 配置键 + * @return String值,如果value为null返回null + */ + public static String getStringValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + + // 如果是基础类型,直接转字符串 + if (value instanceof String || value instanceof Number || value instanceof Boolean) { + return value.toString(); + } + + // 如果是复杂对象(Map, List等),转换为JSON字符串 + try { + return JacksonUtil.toJSONString(value); + } catch (Exception e) { + // JSON转换失败,降级为toString + return value.toString(); + } + } + + /** + * 从配置Map中获取Boolean值 + * + * @param config 配置Map + * @param key 配置键 + * @return Boolean值,如果转换失败返回null + */ + public static Boolean getBooleanValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + if (value instanceof Boolean) return (Boolean) value; + if (value instanceof String) { + String str = (String) value; + if ("true".equalsIgnoreCase(str) || "1".equals(str)) return true; + if ("false".equalsIgnoreCase(str) || "0".equals(str)) return false; + } + if (value instanceof Number) { + return ((Number) value).intValue() != 0; + } + return null; + } + + /** + * 从配置Map中获取枚举值 + * + * @param config 配置Map + * @param key 配置键 + * @param enumClass 枚举类型 + * @param 枚举类型泛型 + * @return 枚举值,如果转换失败返回null + */ + public static > T getEnumValue(Map config, String key, Class enumClass) { + Object value = config.get(key); + if (value == null) return null; + try { + if (value instanceof String) { + return Enum.valueOf(enumClass, (String) value); + } + return Enum.valueOf(enumClass, value.toString()); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * 从配置Map中获取Integer值,如果为null则返回默认值 + * + * @param config 配置Map + * @param key 配置键 + * @param defaultValue 默认值 + * @return Integer值或默认值 + */ + public static Integer getIntValue(Map config, String key, Integer defaultValue) { + Integer value = getIntValue(config, key); + return value != null ? value : defaultValue; + } + + /** + * 从配置Map中获取String值,如果为null则返回默认值 + * + * @param config 配置Map + * @param key 配置键 + * @param defaultValue 默认值 + * @return String值或默认值 + */ + public static String getStringValue(Map config, String key, String defaultValue) { + String value = getStringValue(config, key); + return value != null ? value : defaultValue; + } + + /** + * 从配置Map中获取Boolean值,如果为null则返回默认值 + * + * @param config 配置Map + * @param key 配置键 + * @param defaultValue 默认值 + * @return Boolean值或默认值 + */ + public static Boolean getBooleanValue(Map config, String key, Boolean defaultValue) { + Boolean value = getBooleanValue(config, key); + return value != null ? value : defaultValue; + } + + // ========== 对象和JSON转换方法 ========== + + /** + * 从配置Map中获取原始对象值 + * + * @param config 配置Map + * @param key 配置键 + * @return 原始Object值 + */ + public static Object getObjectValue(Map config, String key) { + return config.get(key); + } + + /** + * 从配置Map中获取并转换为指定类型的对象 + * 支持JSON字符串自动反序列化 + * + * @param config 配置Map + * @param key 配置键 + * @param clazz 目标类型 + * @param 目标类型泛型 + * @return 转换后的对象,如果转换失败返回null + */ + @SuppressWarnings("unchecked") + public static T getObjectValue(Map config, String key, Class clazz) { + Object value = config.get(key); + if (value == null) return null; + + // 如果类型匹配,直接返回 + if (clazz.isInstance(value)) { + return (T) value; + } + + // 如果是String类型的JSON,尝试反序列化 + if (value instanceof String && !clazz.equals(String.class)) { + try { + return JacksonUtil.parseObject((String) value, clazz); + } catch (Exception e) { + return null; + } + } + + // 如果目标是String,使用增强的字符串转换 + if (clazz.equals(String.class)) { + return (T) getStringValue(config, key); + } + + // 其他情况尝试JSON转换 + try { + String json = JacksonUtil.toJSONString(value); + return JacksonUtil.parseObject(json, clazz); + } catch (Exception e) { + return null; + } + } + + /** + * 从配置Map中获取Map类型的值 + * + * @param config 配置Map + * @param key 配置键 + * @return Map值,如果转换失败返回null + */ + @SuppressWarnings("unchecked") + public static Map getMapValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + + if (value instanceof Map) { + return (Map) value; + } + + if (value instanceof String) { + try { + return JacksonUtil.parseObject((String) value, Map.class); + } catch (Exception e) { + return null; + } + } + + return null; + } + + /** + * 从配置Map中获取List类型的值 + * + * @param config 配置Map + * @param key 配置键 + * @return List值,如果转换失败返回null + */ + @SuppressWarnings("unchecked") + public static List getListValue(Map config, String key) { + Object value = config.get(key); + if (value == null) return null; + + if (value instanceof List) { + return (List) value; + } + + if (value instanceof String) { + try { + return JacksonUtil.parseObject((String) value, List.class); + } catch (Exception e) { + return null; + } + } + + return null; + } + + /** + * 从配置Map中获取指定元素类型的List值 + * + * @param config 配置Map + * @param key 配置键 + * @param elementClass List元素类型 + * @param List元素类型泛型 + * @return 指定类型的List,如果转换失败返回null + */ + public static List getListValue(Map config, String key, Class elementClass) { + Object value = config.get(key); + if (value == null) return null; + + if (value instanceof String) { + try { + return JacksonUtil.parseArray((String) value, elementClass); + } catch (Exception e) { + return null; + } + } + + try { + String json = JacksonUtil.toJSONString(value); + return JacksonUtil.parseArray(json, elementClass); + } catch (Exception e) { + return null; + } + } + + /** + * 检查配置键是否存在 + * + * @param config 配置Map + * @param key 配置键 + * @return true如果键存在,false如果不存在 + */ + public static boolean hasKey(Map config, String key) { + return config != null && config.containsKey(key); + } + + /** + * 检查配置键是否存在且值不为null + * + * @param config 配置Map + * @param key 配置键 + * @return true如果键存在且值不为null + */ + public static boolean hasNonNullValue(Map config, String key) { + return config != null && config.containsKey(key) && config.get(key) != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/client/DefaultConfigClient.java b/src/main/java/com/ycwl/basic/integration/scenic/client/DefaultConfigClient.java new file mode 100644 index 0000000..7c8e99e --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/client/DefaultConfigClient.java @@ -0,0 +1,28 @@ +package com.ycwl.basic.integration.scenic.client; + +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.scenic.dto.config.DefaultConfigDTO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@FeignClient(name = "zt-scenic", contextId = "scenic-default-config", path = "/api/scenic/default-config") +public interface DefaultConfigClient { + + @GetMapping("/") + CommonResponse> listDefaultConfigs(); + + @GetMapping("/{configKey}") + CommonResponse getDefaultConfig(@PathVariable("configKey") String configKey); + + @PostMapping("/") + CommonResponse createDefaultConfig(@RequestBody DefaultConfigDTO request); + + @PutMapping("/{configKey}") + CommonResponse updateDefaultConfig(@PathVariable("configKey") String configKey, + @RequestBody DefaultConfigDTO request); + + @DeleteMapping("/{configKey}") + CommonResponse deleteDefaultConfig(@PathVariable("configKey") String configKey); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/client/ScenicConfigV2Client.java b/src/main/java/com/ycwl/basic/integration/scenic/client/ScenicConfigV2Client.java new file mode 100644 index 0000000..31c1709 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/client/ScenicConfigV2Client.java @@ -0,0 +1,44 @@ +package com.ycwl.basic.integration.scenic.client; + +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.scenic.dto.config.*; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@FeignClient(name = "zt-scenic", contextId = "scenic-config-v2", path = "/api/scenic/config/v2") +public interface ScenicConfigV2Client { + + @GetMapping("/{scenicId}") + CommonResponse> listConfigs(@PathVariable("scenicId") Long scenicId); + + @GetMapping("/{scenicId}/key/{configKey}") + CommonResponse getConfigByKey(@PathVariable("scenicId") Long scenicId, + @PathVariable("configKey") String configKey); + + @GetMapping("/{scenicId}/keys") + CommonResponse> getFlatConfigs(@PathVariable("scenicId") Long scenicId); + + @PostMapping("/{scenicId}") + CommonResponse createConfig(@PathVariable("scenicId") Long scenicId, + @RequestBody CreateConfigRequest request); + + @PutMapping("/{scenicId}/{id}") + CommonResponse updateConfig(@PathVariable("scenicId") Long scenicId, + @PathVariable("id") String id, + @RequestBody UpdateConfigRequest request); + + @DeleteMapping("/{scenicId}/{id}") + CommonResponse deleteConfig(@PathVariable("scenicId") Long scenicId, + @PathVariable("id") String id); + + @PostMapping("/{scenicId}/batch") + CommonResponse batchUpdateConfigs(@PathVariable("scenicId") Long scenicId, + @RequestBody BatchConfigRequest request); + + @PostMapping("/{scenicId}/batchFlatUpdate") + CommonResponse batchFlatUpdateConfigs(@PathVariable("scenicId") Long scenicId, + @RequestBody Map configs); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/client/ScenicV2Client.java b/src/main/java/com/ycwl/basic/integration/scenic/client/ScenicV2Client.java new file mode 100644 index 0000000..0405fcf --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/client/ScenicV2Client.java @@ -0,0 +1,51 @@ +package com.ycwl.basic.integration.scenic.client; + +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2ListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@FeignClient(name = "zt-scenic", contextId = "scenic-v2", path = "/api/scenic/v2") +public interface ScenicV2Client { + + @GetMapping("/{scenicId}") + CommonResponse getScenic(@PathVariable("scenicId") Long scenicId); + + @GetMapping("/{scenicId}/with-config") + CommonResponse getScenicWithConfig(@PathVariable("scenicId") Long scenicId); + + + @PostMapping("/") + CommonResponse createScenic(@RequestBody CreateScenicRequest request); + + @PutMapping("/{scenicId}") + CommonResponse updateScenic(@PathVariable("scenicId") Long scenicId, + @RequestBody UpdateScenicRequest request); + + @DeleteMapping("/{scenicId}") + CommonResponse deleteScenic(@PathVariable("scenicId") Long scenicId); + + @PostMapping("/filter") + CommonResponse filterScenics(@RequestBody ScenicFilterRequest request); + + @GetMapping("/") + CommonResponse listScenics(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Integer status, + @RequestParam(required = false) String name); + + @GetMapping("/with-config") + CommonResponse listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(required = false) Integer status, + @RequestParam(required = false) String name); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/config/ScenicIntegrationConfig.java b/src/main/java/com/ycwl/basic/integration/scenic/config/ScenicIntegrationConfig.java new file mode 100644 index 0000000..5f7adec --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/config/ScenicIntegrationConfig.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.integration.scenic.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@ConfigurationProperties(prefix = "integration.scenic") +public class ScenicIntegrationConfig { + + public ScenicIntegrationConfig() { + log.info("ZT-Scenic集成配置初始化完成"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/config/BatchConfigRequest.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/BatchConfigRequest.java new file mode 100644 index 0000000..47b3800 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/BatchConfigRequest.java @@ -0,0 +1,26 @@ +package com.ycwl.basic.integration.scenic.dto.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class BatchConfigRequest { + @JsonProperty("configs") + @NotEmpty(message = "配置列表不能为空") + @Valid + private List configs; + + @Data + public static class BatchConfigItem { + @JsonProperty("configKey") + @NotEmpty(message = "配置键不能为空") + private String configKey; + + @JsonProperty("configValue") + private String configValue; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/config/BatchUpdateResponse.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/BatchUpdateResponse.java new file mode 100644 index 0000000..d3f35b6 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/BatchUpdateResponse.java @@ -0,0 +1,16 @@ +package com.ycwl.basic.integration.scenic.dto.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class BatchUpdateResponse { + @JsonProperty("updatedCount") + private Integer updatedCount; + + @JsonProperty("createdCount") + private Integer createdCount; + + @JsonProperty("message") + private String message; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/config/CreateConfigRequest.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/CreateConfigRequest.java new file mode 100644 index 0000000..b5e9dd9 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/CreateConfigRequest.java @@ -0,0 +1,23 @@ +package com.ycwl.basic.integration.scenic.dto.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +@Data +public class CreateConfigRequest { + @JsonProperty("configKey") + @NotBlank(message = "配置键不能为空") + private String configKey; + + @JsonProperty("configValue") + private String configValue; + + @JsonProperty("configType") + @NotBlank(message = "配置类型不能为空") + private String configType; + + @JsonProperty("description") + private String description; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/config/DefaultConfigDTO.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/DefaultConfigDTO.java new file mode 100644 index 0000000..987327b --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/DefaultConfigDTO.java @@ -0,0 +1,19 @@ +package com.ycwl.basic.integration.scenic.dto.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class DefaultConfigDTO { + @JsonProperty("configKey") + private String configKey; + + @JsonProperty("configValue") + private String configValue; + + @JsonProperty("configType") + private String configType; + + @JsonProperty("description") + private String description; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/config/ScenicConfigV2DTO.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/ScenicConfigV2DTO.java new file mode 100644 index 0000000..6e9f5b7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/ScenicConfigV2DTO.java @@ -0,0 +1,36 @@ +package com.ycwl.basic.integration.scenic.dto.config; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ScenicConfigV2DTO { + @JsonProperty("id") + private String id; + + @JsonProperty("scenicID") + @JsonAlias({"scenicId", "scenicID"}) + private String scenicId; + + @JsonProperty("configKey") + private String configKey; + + @JsonProperty("configValue") + private String configValue; + + @JsonProperty("configType") + private String configType; + + @JsonProperty("description") + private String description; + + @JsonProperty("isActive") + private Integer isActive; + + @JsonProperty("createTime") + private Long createTime; + + @JsonProperty("updateTime") + private Long updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/config/UpdateConfigRequest.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/UpdateConfigRequest.java new file mode 100644 index 0000000..518ae50 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/config/UpdateConfigRequest.java @@ -0,0 +1,16 @@ +package com.ycwl.basic.integration.scenic.dto.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UpdateConfigRequest { + @JsonProperty("configKey") + private String configKey; + + @JsonProperty("configValue") + private String configValue; + + @JsonProperty("description") + private String description; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/FilterCondition.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/FilterCondition.java new file mode 100644 index 0000000..7a35425 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/FilterCondition.java @@ -0,0 +1,20 @@ +package com.ycwl.basic.integration.scenic.dto.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +@Data +public class FilterCondition { + @JsonProperty("configKey") + @NotBlank(message = "配置键不能为空") + private String configKey; + + @JsonProperty("configValue") + @NotBlank(message = "配置值不能为空") + private String configValue; + + @JsonProperty("operator") + private String operator = "eq"; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/ScenicFilterPageResponse.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/ScenicFilterPageResponse.java new file mode 100644 index 0000000..154cb2e --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/ScenicFilterPageResponse.java @@ -0,0 +1,30 @@ +package com.ycwl.basic.integration.scenic.dto.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class ScenicFilterPageResponse { + @JsonProperty("list") + private List list; + + @JsonProperty("total") + private Long total; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("pageSize") + private Integer pageSize; + + @Data + public static class ScenicFilterItem { + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/ScenicFilterRequest.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/ScenicFilterRequest.java new file mode 100644 index 0000000..08bb6ff --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/filter/ScenicFilterRequest.java @@ -0,0 +1,22 @@ +package com.ycwl.basic.integration.scenic.dto.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class ScenicFilterRequest { + @JsonProperty("filters") + @NotEmpty(message = "筛选条件不能为空") + @Valid + private List filters; + + @JsonProperty("page") + private Integer page = 1; + + @JsonProperty("pageSize") + private Integer pageSize = 20; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/CreateScenicRequest.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/CreateScenicRequest.java new file mode 100644 index 0000000..78a6c80 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/CreateScenicRequest.java @@ -0,0 +1,21 @@ +package com.ycwl.basic.integration.scenic.dto.scenic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Data +public class CreateScenicRequest { + @JsonProperty("name") + @NotBlank(message = "景区名称不能为空") + private String name; + + @JsonProperty("mpId") + @NotNull(message = "小程序ID不能为空") + private Integer mpId; + + @JsonProperty("status") + private Integer status = 1; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2DTO.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2DTO.java new file mode 100644 index 0000000..4814f9f --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2DTO.java @@ -0,0 +1,25 @@ +package com.ycwl.basic.integration.scenic.dto.scenic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ScenicV2DTO { + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("mpId") + private Integer mpId; + + @JsonProperty("status") + private Integer status; + + @JsonProperty("createTime") + private Long createTime; + + @JsonProperty("updateTime") + private Long updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2ListResponse.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2ListResponse.java new file mode 100644 index 0000000..c660579 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2ListResponse.java @@ -0,0 +1,21 @@ +package com.ycwl.basic.integration.scenic.dto.scenic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class ScenicV2ListResponse { + @JsonProperty("list") + private List list; + + @JsonProperty("total") + private Integer total; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("pageSize") + private Integer pageSize; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2WithConfigDTO.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2WithConfigDTO.java new file mode 100644 index 0000000..18e1dbb --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2WithConfigDTO.java @@ -0,0 +1,14 @@ +package com.ycwl.basic.integration.scenic.dto.scenic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ScenicV2WithConfigDTO extends ScenicV2DTO { + @JsonProperty("config") + private Map config; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2WithConfigListResponse.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2WithConfigListResponse.java new file mode 100644 index 0000000..4540036 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/ScenicV2WithConfigListResponse.java @@ -0,0 +1,21 @@ +package com.ycwl.basic.integration.scenic.dto.scenic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class ScenicV2WithConfigListResponse { + @JsonProperty("list") + private List list; + + @JsonProperty("total") + private Integer total; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("pageSize") + private Integer pageSize; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/UpdateScenicRequest.java b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/UpdateScenicRequest.java new file mode 100644 index 0000000..b76b5d4 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/dto/scenic/UpdateScenicRequest.java @@ -0,0 +1,16 @@ +package com.ycwl.basic.integration.scenic.dto.scenic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UpdateScenicRequest { + @JsonProperty("name") + private String name; + + @JsonProperty("mpId") + private Integer mpId; + + @JsonProperty("status") + private Integer status; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/example/ScenicIntegrationExample.java b/src/main/java/com/ycwl/basic/integration/scenic/example/ScenicIntegrationExample.java new file mode 100644 index 0000000..f1f50e3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/example/ScenicIntegrationExample.java @@ -0,0 +1,78 @@ +package com.ycwl.basic.integration.scenic.example; + +import com.ycwl.basic.integration.scenic.dto.config.CreateConfigRequest; +import com.ycwl.basic.integration.scenic.dto.filter.FilterCondition; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; +import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService; +import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +/** + * ZT-Scenic集成服务使用示例 + * 仅供参考,实际使用时根据业务需要调用相应的服务方法 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ScenicIntegrationExample { + + private final ScenicIntegrationService scenicIntegrationService; + private final ScenicConfigIntegrationService scenicConfigIntegrationService; + + /** + * 示例:创建景区并设置配置 + */ + public void createScenicWithConfig() { + try { + // 1. 创建景区 + CreateScenicRequest createRequest = new CreateScenicRequest(); + createRequest.setName("测试景区"); + createRequest.setMpId(1001); + + var scenic = scenicIntegrationService.createScenic(createRequest); + log.info("创建景区成功: {}", scenic.getName()); + + // 2. 为景区添加配置 + CreateConfigRequest configRequest = new CreateConfigRequest(); + configRequest.setConfigKey("tour_time"); + configRequest.setConfigValue("120"); + configRequest.setConfigType("int"); + configRequest.setDescription("游览时长"); + + var config = scenicConfigIntegrationService.createConfig( + Long.valueOf(scenic.getId()), configRequest); + log.info("创建配置成功: {} = {}", config.getConfigKey(), config.getConfigValue()); + + } catch (Exception e) { + log.error("创建景区和配置失败", e); + } + } + + /** + * 示例:筛选景区 + */ + public void filterScenics() { + try { + FilterCondition condition = new FilterCondition(); + condition.setConfigKey("tour_time"); + condition.setConfigValue("120"); + condition.setOperator("gte"); + + ScenicFilterRequest filterRequest = new ScenicFilterRequest(); + filterRequest.setFilters(Collections.singletonList(condition)); + filterRequest.setPage(1); + filterRequest.setPageSize(10); + + var result = scenicIntegrationService.filterScenics(filterRequest); + log.info("筛选到 {} 个景区", result.getTotal()); + + } catch (Exception e) { + log.error("筛选景区失败", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/service/DefaultConfigIntegrationService.java b/src/main/java/com/ycwl/basic/integration/scenic/service/DefaultConfigIntegrationService.java new file mode 100644 index 0000000..bb48ebe --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/service/DefaultConfigIntegrationService.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.integration.scenic.service; + +import com.ycwl.basic.integration.common.exception.IntegrationException; +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.scenic.client.DefaultConfigClient; +import com.ycwl.basic.integration.scenic.dto.config.DefaultConfigDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultConfigIntegrationService { + + private final DefaultConfigClient defaultConfigClient; + + public List listDefaultConfigs() { + log.info("获取默认配置列表"); + CommonResponse> response = defaultConfigClient.listDefaultConfigs(); + return handleResponse(response, "获取默认配置列表失败"); + } + + public DefaultConfigDTO getDefaultConfig(String configKey) { + log.info("获取指定默认配置, configKey: {}", configKey); + CommonResponse response = defaultConfigClient.getDefaultConfig(configKey); + return handleResponse(response, "获取指定默认配置失败"); + } + + public DefaultConfigDTO createDefaultConfig(DefaultConfigDTO request) { + log.info("创建默认配置, configKey: {}", request.getConfigKey()); + CommonResponse response = defaultConfigClient.createDefaultConfig(request); + return handleResponse(response, "创建默认配置失败"); + } + + public DefaultConfigDTO updateDefaultConfig(String configKey, DefaultConfigDTO request) { + log.info("更新默认配置, configKey: {}", configKey); + CommonResponse response = defaultConfigClient.updateDefaultConfig(configKey, request); + return handleResponse(response, "更新默认配置失败"); + } + + public void deleteDefaultConfig(String configKey) { + log.info("删除默认配置, configKey: {}", configKey); + CommonResponse response = defaultConfigClient.deleteDefaultConfig(configKey); + handleResponse(response, "删除默认配置失败"); + } + + private T handleResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.isSuccess()) { + String msg = response != null && response.getMessage() != null + ? response.getMessage() + : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, "zt-scenic"); + } + return response.getData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicConfigIntegrationService.java b/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicConfigIntegrationService.java new file mode 100644 index 0000000..39151fd --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicConfigIntegrationService.java @@ -0,0 +1,80 @@ +package com.ycwl.basic.integration.scenic.service; + +import com.ycwl.basic.integration.common.exception.IntegrationException; +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.scenic.client.ScenicConfigV2Client; +import com.ycwl.basic.integration.scenic.dto.config.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ScenicConfigIntegrationService { + + private final ScenicConfigV2Client scenicConfigV2Client; + + public List listConfigs(Long scenicId) { + log.info("获取景区配置列表, scenicId: {}", scenicId); + CommonResponse> response = scenicConfigV2Client.listConfigs(scenicId); + return handleResponse(response, "获取景区配置列表失败"); + } + + public ScenicConfigV2DTO getConfigByKey(Long scenicId, String configKey) { + log.info("根据键获取景区配置, scenicId: {}, configKey: {}", scenicId, configKey); + CommonResponse response = scenicConfigV2Client.getConfigByKey(scenicId, configKey); + return handleResponse(response, "根据键获取景区配置失败"); + } + + public Map getFlatConfigs(Long scenicId) { + log.info("获取景区扁平化配置, scenicId: {}", scenicId); + CommonResponse> response = scenicConfigV2Client.getFlatConfigs(scenicId); + return handleResponse(response, "获取景区扁平化配置失败"); + } + + public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) { + log.info("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey()); + CommonResponse response = scenicConfigV2Client.createConfig(scenicId, request); + return handleResponse(response, "创建景区配置失败"); + } + + public ScenicConfigV2DTO updateConfig(Long scenicId, String id, UpdateConfigRequest request) { + log.info("更新景区配置, scenicId: {}, id: {}", scenicId, id); + CommonResponse response = scenicConfigV2Client.updateConfig(scenicId, id, request); + return handleResponse(response, "更新景区配置失败"); + } + + public void deleteConfig(Long scenicId, String id) { + log.info("删除景区配置, scenicId: {}, id: {}", scenicId, id); + CommonResponse response = scenicConfigV2Client.deleteConfig(scenicId, id); + handleResponse(response, "删除景区配置失败"); + } + + public BatchUpdateResponse batchUpdateConfigs(Long scenicId, BatchConfigRequest request) { + log.info("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size()); + CommonResponse response = scenicConfigV2Client.batchUpdateConfigs(scenicId, request); + return handleResponse(response, "批量更新景区配置失败"); + } + + public BatchUpdateResponse batchFlatUpdateConfigs(Long scenicId, Map configs) { + log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size()); + CommonResponse response = scenicConfigV2Client.batchFlatUpdateConfigs(scenicId, configs); + return handleResponse(response, "扁平化批量更新景区配置失败"); + } + + + private T handleResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.isSuccess()) { + String msg = response != null && response.getMessage() != null + ? response.getMessage() + : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, "zt-scenic"); + } + return response.getData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicIntegrationService.java b/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicIntegrationService.java new file mode 100644 index 0000000..e101398 --- /dev/null +++ b/src/main/java/com/ycwl/basic/integration/scenic/service/ScenicIntegrationService.java @@ -0,0 +1,93 @@ +package com.ycwl.basic.integration.scenic.service; + +import com.ycwl.basic.integration.common.exception.IntegrationException; +import com.ycwl.basic.integration.common.response.CommonResponse; +import com.ycwl.basic.integration.scenic.client.ScenicConfigV2Client; +import com.ycwl.basic.integration.scenic.client.ScenicV2Client; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterPageResponse; +import com.ycwl.basic.integration.scenic.dto.filter.ScenicFilterRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.CreateScenicRequest; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2ListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.UpdateScenicRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ScenicIntegrationService { + + private final ScenicV2Client scenicV2Client; + private final ScenicConfigV2Client scenicConfigV2Client; + + public ScenicV2DTO getScenic(Long scenicId) { + log.info("获取景区信息, scenicId: {}", scenicId); + CommonResponse response = scenicV2Client.getScenic(scenicId); + return handleResponse(response, "获取景区信息失败"); + } + + public ScenicV2WithConfigDTO getScenicWithConfig(Long scenicId) { + log.info("获取景区配置信息, scenicId: {}", scenicId); + CommonResponse response = scenicV2Client.getScenicWithConfig(scenicId); + return handleResponse(response, "获取景区配置信息失败"); + } + + public Map getScenicFlatConfig(Long scenicId) { + log.info("获取景区扁平化配置, scenicId: {}", scenicId); + CommonResponse> response = scenicConfigV2Client.getFlatConfigs(scenicId); + return handleResponse(response, "获取景区扁平化配置失败"); + } + + public ScenicV2DTO createScenic(CreateScenicRequest request) { + log.info("创建景区, name: {}", request.getName()); + CommonResponse response = scenicV2Client.createScenic(request); + return handleResponse(response, "创建景区失败"); + } + + public ScenicV2DTO updateScenic(Long scenicId, UpdateScenicRequest request) { + log.info("更新景区信息, scenicId: {}", scenicId); + CommonResponse response = scenicV2Client.updateScenic(scenicId, request); + return handleResponse(response, "更新景区信息失败"); + } + + public void deleteScenic(Long scenicId) { + log.info("删除景区, scenicId: {}", scenicId); + CommonResponse response = scenicV2Client.deleteScenic(scenicId); + handleResponse(response, "删除景区失败"); + } + + public ScenicFilterPageResponse filterScenics(ScenicFilterRequest request) { + log.info("筛选景区, filters: {}", request.getFilters().size()); + CommonResponse response = scenicV2Client.filterScenics(request); + return handleResponse(response, "筛选景区失败"); + } + + public ScenicV2ListResponse listScenics(Integer page, Integer pageSize, Integer status, String name) { + log.info("分页查询景区列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name); + CommonResponse response = scenicV2Client.listScenics(page, pageSize, status, name); + return handleResponse(response, "分页查询景区列表失败"); + } + + public ScenicV2WithConfigListResponse listScenicsWithConfig(Integer page, Integer pageSize, Integer status, String name) { + log.info("分页查询景区带配置列表, page: {}, pageSize: {}, status: {}, name: {}", page, pageSize, status, name); + CommonResponse response = scenicV2Client.listScenicsWithConfig(page, pageSize, status, name); + return handleResponse(response, "分页查询景区带配置列表失败"); + } + + private T handleResponse(CommonResponse response, String errorMessage) { + if (response == null || !response.isSuccess()) { + String msg = response != null && response.getMessage() != null + ? response.getMessage() + : errorMessage; + Integer code = response != null ? response.getCode() : 5000; + throw new IntegrationException(code, msg, "zt-scenic"); + } + return response.getData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/mapper/ScenicMapper.java b/src/main/java/com/ycwl/basic/mapper/ScenicMapper.java deleted file mode 100644 index 6d4f8a8..0000000 --- a/src/main/java/com/ycwl/basic/mapper/ScenicMapper.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.ycwl.basic.mapper; - -import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; -import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO; -import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; -import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; -import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq; -import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; -import com.ycwl.basic.utils.ApiResponse; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -/** - * @Author:longbinbin - * @Date:2024/12/2 10:07 - * 景区管理表 - */ -@Mapper -public interface ScenicMapper { - List list(ScenicReqQuery scenicReqQuery); - - ScenicEntity get(Long id); - - ScenicRespVO getById(Long id); - - int add(ScenicAddOrUpdateReq scenic); - - int deleteById(Long id); - - int update(ScenicAddOrUpdateReq scenic); - - int updateStatus(Long id); - - ScenicConfigEntity getConfig(Long scenicId); - /** - * 添加景区配置 - * - * @param scenicConfig - * @return - */ - int addConfig(ScenicConfigEntity scenicConfig); - - /** - * 修改景区配置 - * - * @param scenicConfigEntity - * @return - */ - int updateConfigById(ScenicConfigEntity scenicConfigEntity); - - /** - * 根据景区id删除配置 - * - * @param scenicId - */ - void deleteConfigByScenicId(Long scenicId); - - List appList(ScenicReqQuery scenicReqQuery); - - ScenicRespVO getAppById(Long id); - - /** - * 通过经纬度计算景区距离 - * - * @param scenicIndexVO - * @return - */ - List scenicListByLnLa(ScenicIndexVO scenicIndexVO); -} diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java index 3a502f4..a7c3c0f 100644 --- a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java +++ b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java @@ -67,17 +67,17 @@ public class ScenicConfigEntity { /** * 是否开启全部免费 */ - private Integer allFree; + private Boolean allFree; /** * 是否禁用源视频 * 0-否 1-是 */ - private Integer disableSourceVideo; + private Boolean disableSourceVideo; /** * 是否禁用源图片 * 0-否 1-是 */ - private Integer disableSourceImage; + private Boolean disableSourceImage; private Integer templateNewVideoType; /** * 是否开启防录屏 @@ -130,5 +130,5 @@ public class ScenicConfigEntity { * 是否启用券码功能 * 0-禁用 1-启用 */ - private Integer voucherEnable; + private Boolean voucherEnable; } diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicEntity.java b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicEntity.java index f0c69fc..be34ffb 100644 --- a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicEntity.java +++ b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicEntity.java @@ -1,11 +1,8 @@ package com.ycwl.basic.model.pc.scenic.entity; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; -import java.util.Date; /** * @Author:longbinbin @@ -13,15 +10,17 @@ import java.util.Date; * 景区管理表 */ @Data -@TableName("scenic") public class ScenicEntity { - @TableId private Long id; /** * 景区名称 */ private String name; private Integer mpId; + private String phone; + private String logoUrl; + // 封面图 + private String coverUrl; /** * 景区介绍 */ @@ -58,11 +57,10 @@ public class ScenicEntity { * 状态 1启用0关闭 */ private String status; - private Date createTime; - private Date updateTime; /** * 景区源素材价格,元 */ + private String kfCodeUrl; private BigDecimal price; private BigDecimal sourceVideoPrice; private BigDecimal sourceImagePrice; diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicReqQuery.java b/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicReqQuery.java index 048decc..4a55a33 100644 --- a/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicReqQuery.java +++ b/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicReqQuery.java @@ -23,51 +23,9 @@ public class ScenicReqQuery extends BaseQueryParameterReq { */ // 景区名称 private String name; - /** - * 景区介绍 - */ - // 景区介绍 - private String introduction; - /** - * 经度 - */ - // 经度 - private BigDecimal longitude; - /*** - * 纬度 - */ - // 纬度 - private BigDecimal latitude; - /** - * 半径(km) - */ - // 半径(km) - private BigDecimal radius; - /** - * 省份 - */ - // 省份 - private String province; - /** - * 城市 - */ - // 城市 - private String city; - /** - * 区 - */ - // 区 - private String area; - /** - * 详细地址 - */ - // 详细地址 - private String address; /** * 状态 1启用0关闭 */ // 状态 1启用0关闭 private String status; - private Date startTime; - private Date endTime; } diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicConfigResp.java b/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicConfigResp.java index c0743f6..80d3bd2 100644 --- a/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicConfigResp.java +++ b/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicConfigResp.java @@ -34,9 +34,9 @@ public class ScenicConfigResp { * 视频保存时间 */ private Integer videoStoreDay; - private Integer allFree; - private Integer disableSourceVideo; - private Integer disableSourceImage; + private Boolean allFree; + private Boolean disableSourceVideo; + private Boolean disableSourceImage; private Integer antiScreenRecordType; private Integer videoSourceStoreDay; private Integer imageSourceStoreDay; @@ -45,9 +45,5 @@ public class ScenicConfigResp { private String imageSourcePackHint = ""; private String videoSourcePackHint = ""; - /** - * 是否启用券码功能 - * 0-禁用 1-启用 - */ - private Integer voucherEnable; + private Boolean voucherEnable; } diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java b/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java index 63e23c0..99ce301 100644 --- a/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java +++ b/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java @@ -74,19 +74,6 @@ public class ScenicRespVO { */ // 详细地址 private String address; - /** - * 状态 1启用0关闭 - */ - // 状态 1启用0关闭 - private Integer status; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - private Date createTime; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - private Date updateTime; - // 景区源素材价格,元 - private BigDecimal price; - private BigDecimal sourceVideoPrice; - private BigDecimal sourceImagePrice; // 镜头数 private Integer lensNum; private String kfCodeUrl; diff --git a/src/main/java/com/ycwl/basic/order/controller/OrderV2Controller.java b/src/main/java/com/ycwl/basic/order/controller/OrderV2Controller.java new file mode 100644 index 0000000..c5c698c --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/controller/OrderV2Controller.java @@ -0,0 +1,89 @@ +package com.ycwl.basic.order.controller; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.order.dto.*; +import com.ycwl.basic.order.service.IOrderService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +/** + * 订单管理控制器V2 - 管理端 + */ +@Slf4j +@RestController +@RequestMapping("/api/order/v2") +@RequiredArgsConstructor +public class OrderV2Controller { + + private final IOrderService orderService; + + /** + * 分页查询订单列表 + */ + @PostMapping("/page") + public ApiResponse> pageOrders(@RequestBody OrderV2PageRequest request) { + log.info("分页查询订单列表: {}", request); + try { + PageInfo pageInfo = orderService.pageOrders(request); + return ApiResponse.success(pageInfo); + } catch (Exception e) { + log.error("分页查询订单列表失败", e); + return ApiResponse.fail("查询失败:" + e.getMessage()); + } + } + + /** + * 查询订单详情 + */ + @GetMapping("/detail/{orderId}") + public ApiResponse getOrderDetail(@PathVariable("orderId") Long orderId) { + log.info("查询订单详情: orderId={}", orderId); + try { + OrderV2DetailResponse detail = orderService.getOrderDetail(orderId); + if (detail == null) { + return ApiResponse.fail("订单不存在"); + } + return ApiResponse.success(detail); + } catch (Exception e) { + log.error("查询订单详情失败: orderId={}", orderId, e); + return ApiResponse.fail("查询失败:" + e.getMessage()); + } + } + + /** + * 更新订单备注 + */ + @PutMapping("/remark/{orderId}") + public ApiResponse updateOrderRemarks(@PathVariable("orderId") Long orderId, + @RequestBody OrderRemarkRequest request) { + log.info("更新订单备注: orderId={}, remarks={}", orderId, request.getRemarks()); + try { + boolean success = orderService.updateOrderRemarks(orderId, request.getRemarks()); + if (success) { + return ApiResponse.success("备注更新成功"); + } else { + return ApiResponse.fail("备注更新失败"); + } + } catch (Exception e) { + log.error("更新订单备注失败: orderId={}", orderId, e); + return ApiResponse.fail("更新失败:" + e.getMessage()); + } + } + + /** + * 申请退款 + */ + @PostMapping("/refund") + public ApiResponse createRefund(@RequestBody RefundRequest request) { + log.info("申请退款: {}", request); + try { + Long refundId = orderService.createRefundRecord(request); + return ApiResponse.success("退款申请已提交,退款ID:" + refundId); + } catch (Exception e) { + log.error("申请退款失败: {}", request, e); + return ApiResponse.fail("退款申请失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/OrderRemarkRequest.java b/src/main/java/com/ycwl/basic/order/dto/OrderRemarkRequest.java new file mode 100644 index 0000000..6b8563f --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/OrderRemarkRequest.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +/** + * 订单备注修改请求DTO + */ +@Data +public class OrderRemarkRequest { + + /** + * 订单备注 + */ + private String remarks; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/OrderV2DetailResponse.java b/src/main/java/com/ycwl/basic/order/dto/OrderV2DetailResponse.java new file mode 100644 index 0000000..543e663 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/OrderV2DetailResponse.java @@ -0,0 +1,150 @@ +package com.ycwl.basic.order.dto; + +import com.ycwl.basic.order.entity.OrderDiscountV2; +import com.ycwl.basic.order.entity.OrderItemV2; +import com.ycwl.basic.order.entity.OrderRefundV2; +import com.ycwl.basic.order.enums.OrderStatus; +import com.ycwl.basic.order.enums.PaymentStatus; +import com.ycwl.basic.order.enums.RefundStatus; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 订单详情响应DTO + */ +@Data +public class OrderV2DetailResponse { + + /** + * 订单ID + */ + private Long id; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 小程序openId + */ + private String openId; + + /** + * 人脸ID + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 订单状态 + */ + private OrderStatus orderStatus; + + /** + * 订单状态描述 + */ + private String orderStatusDesc; + + /** + * 支付状态 + */ + private PaymentStatus paymentStatus; + + /** + * 支付状态描述 + */ + private String paymentStatusDesc; + + /** + * 退款状态 + */ + private RefundStatus refundStatus; + + /** + * 退款状态描述 + */ + private String refundStatusDesc; + + /** + * 总退款金额 + */ + private BigDecimal totalRefundAmount; + + /** + * 订单备注 + */ + private String remarks; + + /** + * 支付时间 + */ + private Date payTime; + + /** + * 完成时间 + */ + private Date completeTime; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 创建人 + */ + private Long createBy; + + /** + * 更新人 + */ + private Long updateBy; + + /** + * 订单商品明细列表 + */ + private List orderItems; + + /** + * 订单优惠记录列表 + */ + private List orderDiscounts; + + /** + * 订单退款记录列表 + */ + private List orderRefunds; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/OrderV2ListResponse.java b/src/main/java/com/ycwl/basic/order/dto/OrderV2ListResponse.java new file mode 100644 index 0000000..22d317f --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/OrderV2ListResponse.java @@ -0,0 +1,131 @@ +package com.ycwl.basic.order.dto; + +import com.ycwl.basic.order.enums.OrderStatus; +import com.ycwl.basic.order.enums.PaymentStatus; +import com.ycwl.basic.order.enums.RefundStatus; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 订单列表响应DTO + */ +@Data +public class OrderV2ListResponse { + + /** + * 订单ID + */ + private Long id; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 会员ID + */ + private Long memberId; + + /** + * 小程序openId + */ + private String openId; + + /** + * 人脸ID + */ + private Long faceId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 原始金额 + */ + private BigDecimal originalAmount; + + /** + * 优惠金额 + */ + private BigDecimal discountAmount; + + /** + * 最终金额 + */ + private BigDecimal finalAmount; + + /** + * 订单状态 + */ + private OrderStatus orderStatus; + + /** + * 订单状态描述 + */ + private String orderStatusDesc; + + /** + * 支付状态 + */ + private PaymentStatus paymentStatus; + + /** + * 支付状态描述 + */ + private String paymentStatusDesc; + + /** + * 退款状态 + */ + private RefundStatus refundStatus; + + /** + * 退款状态描述 + */ + private String refundStatusDesc; + + /** + * 总退款金额 + */ + private BigDecimal totalRefundAmount; + + /** + * 订单备注 + */ + private String remarks; + + /** + * 支付时间 + */ + private Date payTime; + + /** + * 完成时间 + */ + private Date completeTime; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 商品数量(订单中商品种类数) + */ + private Integer itemCount; + + /** + * 商品总数量(所有商品数量之和) + */ + private Integer totalQuantity; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/OrderV2PageRequest.java b/src/main/java/com/ycwl/basic/order/dto/OrderV2PageRequest.java new file mode 100644 index 0000000..f94b705 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/OrderV2PageRequest.java @@ -0,0 +1,72 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +import java.util.Date; + +/** + * 订单分页查询请求DTO + */ +@Data +public class OrderV2PageRequest { + + /** + * 页码(从1开始) + */ + private Integer pageNum = 1; + + /** + * 每页大小 + */ + private Integer pageSize = 10; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 用户ID(移动端查询时自动设置) + */ + private Long memberId; + + /** + * 景区ID + */ + private Long scenicId; + + /** + * 订单状态 + */ + private String orderStatus; + + /** + * 支付状态 + */ + private String paymentStatus; + + /** + * 退款状态 + */ + private String refundStatus; + + /** + * 开始创建时间 + */ + private Date createTimeStart; + + /** + * 结束创建时间 + */ + private Date createTimeEnd; + + /** + * 开始支付时间 + */ + private Date payTimeStart; + + /** + * 结束支付时间 + */ + private Date payTimeEnd; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java b/src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java new file mode 100644 index 0000000..9ba7164 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/PaymentCallbackResponse.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +/** + * 支付回调响应DTO + */ +@Data +public class PaymentCallbackResponse { + + /** + * 处理是否成功 + */ + private boolean success; + + /** + * 响应消息 + */ + private String message; + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 支付状态变化类型 + */ + private String statusChangeType; + + /** + * 创建成功响应 + */ + public static PaymentCallbackResponse success(Long orderId, String orderNo, String statusChangeType) { + PaymentCallbackResponse response = new PaymentCallbackResponse(); + response.success = true; + response.message = "回调处理成功"; + response.orderId = orderId; + response.orderNo = orderNo; + response.statusChangeType = statusChangeType; + return response; + } + + /** + * 创建失败响应 + */ + public static PaymentCallbackResponse failure(String message) { + PaymentCallbackResponse response = new PaymentCallbackResponse(); + response.success = false; + response.message = message; + return response; + } + + /** + * 创建失败响应(包含订单信息) + */ + public static PaymentCallbackResponse failure(String message, Long orderId, String orderNo) { + PaymentCallbackResponse response = new PaymentCallbackResponse(); + response.success = false; + response.message = message; + response.orderId = orderId; + response.orderNo = orderNo; + return response; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java new file mode 100644 index 0000000..8f32f4e --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsRequest.java @@ -0,0 +1,17 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +/** + * 获取支付参数请求DTO + * 所有参数都是可选的,系统会自动生成商品名称和描述 + */ +@Data +public class PaymentParamsRequest { + + // 预留字段,目前所有信息都由系统自动生成 + // 可以在未来版本中扩展支持自定义参数 + + public PaymentParamsRequest() { + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java new file mode 100644 index 0000000..9760d68 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/PaymentParamsResponse.java @@ -0,0 +1,76 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 支付参数响应DTO + */ +@Data +public class PaymentParamsResponse { + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 支付金额 + */ + private BigDecimal payAmount; + + /** + * 是否需要支付(false表示免费订单) + */ + private Boolean needPay; + + /** + * 支付参数(微信小程序调起支付所需的参数) + * 包含:appId, timeStamp, nonceStr, package, signType, paySign等 + */ + private Map paymentParams; + + /** + * 支付描述信息 + */ + private String description; + + /** + * 商品名称 + */ + private String goodsName; + + /** + * 创建成功的支付参数响应 + */ + public static PaymentParamsResponse success(Long orderId, String orderNo, BigDecimal payAmount, + Boolean needPay, Map paymentParams) { + PaymentParamsResponse response = new PaymentParamsResponse(); + response.orderId = orderId; + response.orderNo = orderNo; + response.payAmount = payAmount; + response.needPay = needPay; + response.paymentParams = paymentParams; + return response; + } + + /** + * 创建免费订单的响应 + */ + public static PaymentParamsResponse free(Long orderId, String orderNo) { + PaymentParamsResponse response = new PaymentParamsResponse(); + response.orderId = orderId; + response.orderNo = orderNo; + response.payAmount = BigDecimal.ZERO; + response.needPay = false; + response.paymentParams = null; + return response; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/dto/RefundRequest.java b/src/main/java/com/ycwl/basic/order/dto/RefundRequest.java new file mode 100644 index 0000000..a3cb265 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/dto/RefundRequest.java @@ -0,0 +1,52 @@ +package com.ycwl.basic.order.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 退款申请请求DTO + */ +@Data +public class RefundRequest { + + /** + * 订单ID + */ + private Long orderId; + + /** + * 退款类型 + */ + private String refundType; + + /** + * 退款金额 + */ + private BigDecimal refundAmount; + + /** + * 退款手续费 + */ + private BigDecimal refundFee = BigDecimal.ZERO; + + /** + * 退款原因 + */ + private String refundReason; + + /** + * 退款详细说明 + */ + private String refundDescription; + + /** + * 操作备注 + */ + private String operatorRemarks; + + /** + * 退款渠道 + */ + private String refundChannel = "ORIGINAL"; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/entity/OrderDiscountV2.java b/src/main/java/com/ycwl/basic/order/entity/OrderDiscountV2.java new file mode 100644 index 0000000..04ccdb7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/entity/OrderDiscountV2.java @@ -0,0 +1,91 @@ +package com.ycwl.basic.order.entity; + +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 com.ycwl.basic.order.enums.DiscountType; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 订单优惠记录表实体V2 + */ +@Data +@TableName("order_discount_v2") +public class OrderDiscountV2 { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 订单ID + */ + @TableField("order_id") + private Long orderId; + + /** + * 优惠类型 + */ + @TableField("discount_type") + private DiscountType discountType; + + /** + * 优惠名称 + */ + @TableField("discount_name") + private String discountName; + + /** + * 优惠金额 + */ + @TableField("discount_amount") + private BigDecimal discountAmount; + + /** + * 优惠比例 + */ + @TableField("discount_rate") + private BigDecimal discountRate; + + /** + * 显示顺序 + */ + @TableField("sort_order") + private Integer sortOrder; + + /** + * 优惠券ID + */ + @TableField("coupon_id") + private Long couponId; + + /** + * 优惠券码 + */ + @TableField("coupon_code") + private String couponCode; + + /** + * 券码 + */ + @TableField("voucher_code") + private String voucherCode; + + /** + * 券码批次ID + */ + @TableField("voucher_batch_id") + private Long voucherBatchId; + + /** + * 创建时间 + */ + @TableField("create_time") + private Date createTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/entity/OrderItemV2.java b/src/main/java/com/ycwl/basic/order/entity/OrderItemV2.java new file mode 100644 index 0000000..2ff3353 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/entity/OrderItemV2.java @@ -0,0 +1,84 @@ +package com.ycwl.basic.order.entity; + +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 lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 订单商品明细表实体V2 + */ +@Data +@TableName("order_item_v2") +public class OrderItemV2 { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 订单ID + */ + @TableField("order_id") + private Long orderId; + + /** + * 商品类型 + */ + @TableField("product_type") + private String productType; + + /** + * 商品ID + */ + @TableField("product_id") + private String productId; + + /** + * 商品名称 + */ + @TableField("product_name") + private String productName; + + /** + * 商品数量 + */ + @TableField("quantity") + private Integer quantity; + + /** + * 单价 + */ + @TableField("unit_price") + private BigDecimal unitPrice; + + /** + * 原始小计 + */ + @TableField("original_amount") + private BigDecimal originalAmount; + + /** + * 最终小计 + */ + @TableField("final_amount") + private BigDecimal finalAmount; + + /** + * 创建时间 + */ + @TableField("create_time") + private Date createTime; + + /** + * 更新时间 + */ + @TableField("update_time") + private Date updateTime; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/entity/OrderRefundV2.java b/src/main/java/com/ycwl/basic/order/entity/OrderRefundV2.java new file mode 100644 index 0000000..917677d --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/entity/OrderRefundV2.java @@ -0,0 +1,146 @@ +package com.ycwl.basic.order.entity; + +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 com.ycwl.basic.order.enums.RefundStatus; +import com.ycwl.basic.order.enums.RefundType; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 订单退款记录表实体V2 + */ +@Data +@TableName("order_refund_v2") +public class OrderRefundV2 { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 订单ID + */ + @TableField("order_id") + private Long orderId; + + /** + * 退款单号 + */ + @TableField("refund_no") + private String refundNo; + + /** + * 退款类型 + */ + @TableField("refund_type") + private RefundType refundType; + + /** + * 退款金额 + */ + @TableField("refund_amount") + private BigDecimal refundAmount; + + /** + * 退款手续费 + */ + @TableField("refund_fee") + private BigDecimal refundFee; + + /** + * 退款状态 + */ + @TableField("refund_status") + private RefundStatus refundStatus; + + /** + * 退款原因 + */ + @TableField("refund_reason") + private String refundReason; + + /** + * 退款详细说明 + */ + @TableField("refund_description") + private String refundDescription; + + /** + * 申请人ID + */ + @TableField("apply_by") + private Long applyBy; + + /** + * 审批人ID + */ + @TableField("approve_by") + private Long approveBy; + + /** + * 操作备注 + */ + @TableField("operator_remarks") + private String operatorRemarks; + + /** + * 支付平台退款单号 + */ + @TableField("payment_refund_id") + private String paymentRefundId; + + /** + * 退款渠道 + */ + @TableField("refund_channel") + private String refundChannel; + + /** + * 申请时间 + */ + @TableField("apply_time") + private Date applyTime; + + /** + * 审批时间 + */ + @TableField("approve_time") + private Date approveTime; + + /** + * 完成时间 + */ + @TableField("complete_time") + private Date completeTime; + + /** + * 创建时间 + */ + @TableField("create_time") + private Date createTime; + + /** + * 更新时间 + */ + @TableField("update_time") + private Date updateTime; + + /** + * 创建人 + */ + @TableField("create_by") + private Long createBy; + + /** + * 更新人 + */ + @TableField("update_by") + private Long updateBy; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/entity/OrderV2.java b/src/main/java/com/ycwl/basic/order/entity/OrderV2.java new file mode 100644 index 0000000..cc5e0dc --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/entity/OrderV2.java @@ -0,0 +1,153 @@ +package com.ycwl.basic.order.entity; + +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 com.ycwl.basic.order.enums.OrderStatus; +import com.ycwl.basic.order.enums.PaymentStatus; +import com.ycwl.basic.order.enums.RefundStatus; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 订单主表实体V2 + */ +@Data +@TableName("order_v2") +public class OrderV2 { + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 订单号 + */ + @TableField("order_no") + private String orderNo; + + /** + * 会员ID + */ + @TableField("member_id") + private Long memberId; + + /** + * 小程序openId + */ + @TableField("open_id") + private String openId; + + /** + * 人脸ID + */ + @TableField("face_id") + private Long faceId; + + /** + * 景区ID + */ + @TableField("scenic_id") + private Long scenicId; + + /** + * 原始金额 + */ + @TableField("original_amount") + private BigDecimal originalAmount; + + /** + * 优惠金额 + */ + @TableField("discount_amount") + private BigDecimal discountAmount; + + /** + * 最终金额 + */ + @TableField("final_amount") + private BigDecimal finalAmount; + + /** + * 订单状态 + */ + @TableField("order_status") + private OrderStatus orderStatus; + + /** + * 支付状态 + */ + @TableField("payment_status") + private PaymentStatus paymentStatus; + + /** + * 退款状态 + */ + @TableField("refund_status") + private RefundStatus refundStatus; + + /** + * 总退款金额 + */ + @TableField("total_refund_amount") + private BigDecimal totalRefundAmount; + + /** + * 订单备注 + */ + @TableField("remarks") + private String remarks; + + /** + * 支付时间 + */ + @TableField("pay_time") + private Date payTime; + + /** + * 完成时间 + */ + @TableField("complete_time") + private Date completeTime; + + /** + * 创建时间 + */ + @TableField("create_time") + private Date createTime; + + /** + * 更新时间 + */ + @TableField("update_time") + private Date updateTime; + + /** + * 创建人 + */ + @TableField("create_by") + private Long createBy; + + /** + * 更新人 + */ + @TableField("update_by") + private Long updateBy; + + /** + * 是否删除 + */ + @TableField("deleted") + private Integer deleted; + + /** + * 删除时间 + */ + @TableField("deleted_at") + private Date deletedAt; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/enums/DiscountType.java b/src/main/java/com/ycwl/basic/order/enums/DiscountType.java new file mode 100644 index 0000000..6ccf1f0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/enums/DiscountType.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.order.enums; + +/** + * 优惠类型枚举 + */ +public enum DiscountType { + + /** + * 优惠券 + */ + COUPON("COUPON", "优惠券"), + + /** + * 券码 + */ + VOUCHER("VOUCHER", "券码"), + + /** + * 限时立减 + */ + LIMITED_TIME("LIMITED_TIME", "限时立减"), + + /** + * 套餐优惠 + */ + BUNDLE("BUNDLE", "套餐优惠"), + + /** + * 一口价优惠 + */ + FIXED_PRICE("FIXED_PRICE", "一口价优惠"); + + private final String code; + private final String description; + + DiscountType(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static DiscountType fromCode(String code) { + for (DiscountType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown DiscountType code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/enums/OrderStatus.java b/src/main/java/com/ycwl/basic/order/enums/OrderStatus.java new file mode 100644 index 0000000..97f1777 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/enums/OrderStatus.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.order.enums; + +/** + * 订单状态枚举 + */ +public enum OrderStatus { + + /** + * 待支付 + */ + PENDING_PAYMENT("PENDING_PAYMENT", "待支付"), + + /** + * 已支付 + */ + PAID("PAID", "已支付"), + + /** + * 处理中 + */ + PROCESSING("PROCESSING", "处理中"), + + /** + * 已完成 + */ + COMPLETED("COMPLETED", "已完成"), + + /** + * 已取消 + */ + CANCELLED("CANCELLED", "已取消"), + + /** + * 退款中 + */ + REFUNDING("REFUNDING", "退款中"), + + /** + * 已退款 + */ + REFUNDED("REFUNDED", "已退款"); + + private final String code; + private final String description; + + OrderStatus(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static OrderStatus fromCode(String code) { + for (OrderStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown OrderStatus code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/enums/PaymentStatus.java b/src/main/java/com/ycwl/basic/order/enums/PaymentStatus.java new file mode 100644 index 0000000..02e9602 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/enums/PaymentStatus.java @@ -0,0 +1,50 @@ +package com.ycwl.basic.order.enums; + +/** + * 支付状态枚举 + */ +public enum PaymentStatus { + + /** + * 未支付 + */ + UNPAID("UNPAID", "未支付"), + + /** + * 已支付 + */ + PAID("PAID", "已支付"), + + /** + * 已退款 + */ + REFUNDED("REFUNDED", "已退款"); + + private final String code; + private final String description; + + PaymentStatus(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static PaymentStatus fromCode(String code) { + for (PaymentStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown PaymentStatus code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/enums/RefundStatus.java b/src/main/java/com/ycwl/basic/order/enums/RefundStatus.java new file mode 100644 index 0000000..d52e83d --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/enums/RefundStatus.java @@ -0,0 +1,55 @@ +package com.ycwl.basic.order.enums; + +/** + * 退款状态枚举 + */ +public enum RefundStatus { + + /** + * 无退款 + */ + NO_REFUND("NO_REFUND", "无退款"), + + /** + * 部分退款 + */ + PARTIAL_REFUND("PARTIAL_REFUND", "部分退款"), + + /** + * 全额退款 + */ + FULL_REFUND("FULL_REFUND", "全额退款"), + + /** + * 退款处理中 + */ + REFUND_PROCESSING("REFUND_PROCESSING", "退款处理中"); + + private final String code; + private final String description; + + RefundStatus(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static RefundStatus fromCode(String code) { + for (RefundStatus status : values()) { + if (status.code.equals(code)) { + return status; + } + } + throw new IllegalArgumentException("Unknown RefundStatus code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/enums/RefundType.java b/src/main/java/com/ycwl/basic/order/enums/RefundType.java new file mode 100644 index 0000000..7111883 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/enums/RefundType.java @@ -0,0 +1,50 @@ +package com.ycwl.basic.order.enums; + +/** + * 退款类型枚举 + */ +public enum RefundType { + + /** + * 全额退款 + */ + FULL_REFUND("FULL_REFUND", "全额退款"), + + /** + * 部分退款 + */ + PARTIAL_REFUND("PARTIAL_REFUND", "部分退款"), + + /** + * 商品退款 + */ + ITEM_REFUND("ITEM_REFUND", "商品退款"); + + private final String code; + private final String description; + + RefundType(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + /** + * 根据code获取枚举 + */ + public static RefundType fromCode(String code) { + for (RefundType type : values()) { + if (type.code.equals(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown RefundType code: " + code); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/event/OrderEventListener.java b/src/main/java/com/ycwl/basic/order/event/OrderEventListener.java new file mode 100644 index 0000000..5fb92ec --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/event/OrderEventListener.java @@ -0,0 +1,28 @@ +package com.ycwl.basic.order.event; + +/** + * 订单事件监听器接口 + */ +public interface OrderEventListener { + + /** + * 支付状态变更事件 + * + * @param event 支付状态变更事件 + */ + void onPaymentStatusChanged(PaymentStatusChangeEvent event); + + /** + * 退款状态变更事件 + * + * @param event 退款状态变更事件 + */ + void onRefundStatusChanged(RefundStatusChangeEvent event); + + /** + * 订单状态变更事件 + * + * @param event 订单状态变更事件 + */ + void onOrderStatusChanged(OrderStatusChangeEvent event); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/event/OrderEventManager.java b/src/main/java/com/ycwl/basic/order/event/OrderEventManager.java new file mode 100644 index 0000000..c6161da --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/event/OrderEventManager.java @@ -0,0 +1,125 @@ +package com.ycwl.basic.order.event; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * 订单事件管理器 + * 管理事件监听器的注册和事件分发 + */ +@Slf4j +@Component +public class OrderEventManager { + + private final List listeners = new ArrayList<>(); + private final Executor eventExecutor = Executors.newFixedThreadPool(5, + r -> new Thread(r, "order-event-thread")); + + @PostConstruct + public void init() { + log.info("订单事件管理器初始化完成"); + } + + /** + * 注册事件监听器 + * + * @param listener 监听器实例 + */ + public void registerListener(OrderEventListener listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + log.info("注册订单事件监听器: {}", listener.getClass().getSimpleName()); + } + } + + /** + * 移除事件监听器 + * + * @param listener 监听器实例 + */ + public void removeListener(OrderEventListener listener) { + if (listeners.remove(listener)) { + log.info("移除订单事件监听器: {}", listener.getClass().getSimpleName()); + } + } + + /** + * 发布支付状态变更事件 + * + * @param event 支付状态变更事件 + */ + public void publishPaymentStatusChangeEvent(PaymentStatusChangeEvent event) { + log.info("发布支付状态变更事件: orderId={}, {} -> {}", + event.getOrderId(), event.getOldPaymentStatus(), event.getNewPaymentStatus()); + + CompletableFuture.runAsync(() -> { + for (OrderEventListener listener : listeners) { + try { + listener.onPaymentStatusChanged(event); + } catch (Exception e) { + log.error("处理支付状态变更事件失败: listener={}, orderId={}", + listener.getClass().getSimpleName(), event.getOrderId(), e); + } + } + }, eventExecutor); + } + + /** + * 发布退款状态变更事件 + * + * @param event 退款状态变更事件 + */ + public void publishRefundStatusChangeEvent(RefundStatusChangeEvent event) { + log.info("发布退款状态变更事件: orderId={}, refundId={}, {} -> {}", + event.getOrderId(), event.getRefundId(), + event.getOldRefundStatus(), event.getNewRefundStatus()); + + CompletableFuture.runAsync(() -> { + for (OrderEventListener listener : listeners) { + try { + listener.onRefundStatusChanged(event); + } catch (Exception e) { + log.error("处理退款状态变更事件失败: listener={}, orderId={}, refundId={}", + listener.getClass().getSimpleName(), event.getOrderId(), event.getRefundId(), e); + } + } + }, eventExecutor); + } + + /** + * 发布订单状态变更事件 + * + * @param event 订单状态变更事件 + */ + public void publishOrderStatusChangeEvent(OrderStatusChangeEvent event) { + log.info("发布订单状态变更事件: orderId={}, {} -> {}", + event.getOrderId(), event.getOldOrderStatus(), event.getNewOrderStatus()); + + CompletableFuture.runAsync(() -> { + for (OrderEventListener listener : listeners) { + try { + listener.onOrderStatusChanged(event); + } catch (Exception e) { + log.error("处理订单状态变更事件失败: listener={}, orderId={}", + listener.getClass().getSimpleName(), event.getOrderId(), e); + } + } + }, eventExecutor); + } + + /** + * 获取已注册的监听器数量 + * + * @return 监听器数量 + */ + public int getListenerCount() { + return listeners.size(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/event/OrderStatusChangeEvent.java b/src/main/java/com/ycwl/basic/order/event/OrderStatusChangeEvent.java new file mode 100644 index 0000000..93ea868 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/event/OrderStatusChangeEvent.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.order.event; + +import com.ycwl.basic.order.enums.OrderStatus; +import lombok.Data; + +import java.util.Date; + +/** + * 订单状态变更事件 + */ +@Data +public class OrderStatusChangeEvent { + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 旧的订单状态 + */ + private OrderStatus oldOrderStatus; + + /** + * 新的订单状态 + */ + private OrderStatus newOrderStatus; + + /** + * 变更时间 + */ + private Date changeTime; + + /** + * 变更原因 + */ + private String changeReason; + + /** + * 操作人ID + */ + private Long operatorId; + + public OrderStatusChangeEvent(Long orderId, String orderNo, + OrderStatus oldStatus, OrderStatus newStatus, + String changeReason, Long operatorId) { + this.orderId = orderId; + this.orderNo = orderNo; + this.oldOrderStatus = oldStatus; + this.newOrderStatus = newStatus; + this.changeReason = changeReason; + this.operatorId = operatorId; + this.changeTime = new Date(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/event/PaymentStatusChangeEvent.java b/src/main/java/com/ycwl/basic/order/event/PaymentStatusChangeEvent.java new file mode 100644 index 0000000..8a5d6d3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/event/PaymentStatusChangeEvent.java @@ -0,0 +1,60 @@ +package com.ycwl.basic.order.event; + +import com.ycwl.basic.order.enums.PaymentStatus; +import lombok.Data; + +import java.util.Date; + +/** + * 支付状态变更事件 + */ +@Data +public class PaymentStatusChangeEvent { + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 旧的支付状态 + */ + private PaymentStatus oldPaymentStatus; + + /** + * 新的支付状态 + */ + private PaymentStatus newPaymentStatus; + + /** + * 变更时间 + */ + private Date changeTime; + + /** + * 变更原因 + */ + private String changeReason; + + /** + * 操作人ID + */ + private Long operatorId; + + public PaymentStatusChangeEvent(Long orderId, String orderNo, + PaymentStatus oldStatus, PaymentStatus newStatus, + String changeReason, Long operatorId) { + this.orderId = orderId; + this.orderNo = orderNo; + this.oldPaymentStatus = oldStatus; + this.newPaymentStatus = newStatus; + this.changeReason = changeReason; + this.operatorId = operatorId; + this.changeTime = new Date(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/event/RefundStatusChangeEvent.java b/src/main/java/com/ycwl/basic/order/event/RefundStatusChangeEvent.java new file mode 100644 index 0000000..ce6949a --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/event/RefundStatusChangeEvent.java @@ -0,0 +1,79 @@ +package com.ycwl.basic.order.event; + +import com.ycwl.basic.order.enums.RefundStatus; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 退款状态变更事件 + */ +@Data +public class RefundStatusChangeEvent { + + /** + * 订单ID + */ + private Long orderId; + + /** + * 订单号 + */ + private String orderNo; + + /** + * 退款记录ID + */ + private Long refundId; + + /** + * 退款单号 + */ + private String refundNo; + + /** + * 旧的退款状态 + */ + private RefundStatus oldRefundStatus; + + /** + * 新的退款状态 + */ + private RefundStatus newRefundStatus; + + /** + * 退款金额 + */ + private BigDecimal refundAmount; + + /** + * 变更时间 + */ + private Date changeTime; + + /** + * 变更原因 + */ + private String changeReason; + + /** + * 操作人ID + */ + private Long operatorId; + + public RefundStatusChangeEvent(Long orderId, String orderNo, Long refundId, String refundNo, + RefundStatus oldStatus, RefundStatus newStatus, + BigDecimal refundAmount, String changeReason, Long operatorId) { + this.orderId = orderId; + this.orderNo = orderNo; + this.refundId = refundId; + this.refundNo = refundNo; + this.oldRefundStatus = oldStatus; + this.newRefundStatus = newStatus; + this.refundAmount = refundAmount; + this.changeReason = changeReason; + this.operatorId = operatorId; + this.changeTime = new Date(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/event/impl/PaymentStatusChangeListener.java b/src/main/java/com/ycwl/basic/order/event/impl/PaymentStatusChangeListener.java new file mode 100644 index 0000000..baf6a90 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/event/impl/PaymentStatusChangeListener.java @@ -0,0 +1,120 @@ +package com.ycwl.basic.order.event.impl; + +import com.ycwl.basic.order.event.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; + +/** + * 支付状态变更监听器示例 + * 应用启动后自动注册到事件管理器 + */ +@Slf4j +@Component +public class PaymentStatusChangeListener implements OrderEventListener { + + @Resource + private OrderEventManager orderEventManager; + + @PostConstruct + public void init() { + // 应用启动后自动注册到事件管理器 + orderEventManager.registerListener(this); + log.info("支付状态变更监听器注册完成"); + } + + @Override + public void onPaymentStatusChanged(PaymentStatusChangeEvent event) { + log.info("处理支付状态变更事件: orderId={}, orderNo={}, {} -> {}, reason={}", + event.getOrderId(), event.getOrderNo(), + event.getOldPaymentStatus(), event.getNewPaymentStatus(), + event.getChangeReason()); + + // 根据支付状态执行相应的业务逻辑 + switch (event.getNewPaymentStatus()) { + case PAID: + handlePaymentSuccess(event); + break; + case REFUNDED: + handlePaymentRefunded(event); + break; + case UNPAID: + handlePaymentPending(event); + break; + default: + log.warn("未处理的支付状态: {}", event.getNewPaymentStatus()); + } + } + + @Override + public void onRefundStatusChanged(RefundStatusChangeEvent event) { + log.info("处理退款状态变更事件: orderId={}, refundId={}, {} -> {}", + event.getOrderId(), event.getRefundId(), + event.getOldRefundStatus(), event.getNewRefundStatus()); + + // 可以在这里添加退款相关的业务逻辑 + // 比如发送通知、更新库存等 + } + + @Override + public void onOrderStatusChanged(OrderStatusChangeEvent event) { + log.info("处理订单状态变更事件: orderId={}, {} -> {}", + event.getOrderId(), event.getOldOrderStatus(), event.getNewOrderStatus()); + + // 可以在这里添加订单状态相关的业务逻辑 + // 比如发送用户通知、触发后续流程等 + } + + /** + * 处理支付成功事件 + */ + private void handlePaymentSuccess(PaymentStatusChangeEvent event) { + log.info("处理支付成功: orderId={}", event.getOrderId()); + + // 这里可以添加支付成功后的业务逻辑: + // 1. 发送支付成功通知 + // 2. 更新商品库存 + // 3. 生成相关凭证 + // 4. 发送短信/邮件通知 + // 5. 启动履约流程 + + // 示例:记录支付成功日志 + log.info("订单支付成功处理完成: orderId={}, changeTime={}", + event.getOrderId(), event.getChangeTime()); + } + + /** + * 处理支付退款事件 + */ + private void handlePaymentRefunded(PaymentStatusChangeEvent event) { + log.info("处理支付退款: orderId={}", event.getOrderId()); + + // 这里可以添加支付退款后的业务逻辑: + // 1. 恢复商品库存 + // 2. 取消相关服务 + // 3. 发送退款通知 + // 4. 更新会员积分 + + // 示例:记录退款处理日志 + log.info("订单退款处理完成: orderId={}, changeTime={}", + event.getOrderId(), event.getChangeTime()); + } + + /** + * 处理支付待处理事件 + */ + private void handlePaymentPending(PaymentStatusChangeEvent event) { + log.info("处理支付待处理: orderId={}", event.getOrderId()); + + // 这里可以添加支付待处理的业务逻辑: + // 1. 设置支付超时提醒 + // 2. 预留库存 + // 3. 发送支付提醒 + + // 示例:记录待支付处理日志 + log.info("订单待支付处理完成: orderId={}, changeTime={}", + event.getOrderId(), event.getChangeTime()); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/exception/DuplicatePurchaseException.java b/src/main/java/com/ycwl/basic/order/exception/DuplicatePurchaseException.java new file mode 100644 index 0000000..b0e2569 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/exception/DuplicatePurchaseException.java @@ -0,0 +1,58 @@ +package com.ycwl.basic.order.exception; + +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.pricing.enums.ProductType; + +/** + * 重复购买异常 + * 当用户尝试购买已经购买过的内容时抛出此异常 + */ +public class DuplicatePurchaseException extends BaseException { + + private final Long existingOrderId; + private final String existingOrderNo; + private final ProductType productType; + private final String productId; + + public DuplicatePurchaseException(String message, Long existingOrderId, String existingOrderNo, + ProductType productType, String productId) { + super(message); + this.existingOrderId = existingOrderId; + this.existingOrderNo = existingOrderNo; + this.productType = productType; + this.productId = productId; + } + + public DuplicatePurchaseException(String message, Long existingOrderId, String existingOrderNo, + ProductType productType) { + this(message, existingOrderId, existingOrderNo, productType, null); + } + + public Long getExistingOrderId() { + return existingOrderId; + } + + public String getExistingOrderNo() { + return existingOrderNo; + } + + public ProductType getProductType() { + return productType; + } + + public String getProductId() { + return productId; + } + + /** + * 获取友好的错误消息 + */ + public String getFriendlyMessage() { + String productDesc = productType != null ? productType.getDescription() : "商品"; + if (productId != null) { + return String.format("您已购买过该%s(商品ID:%s),订单号:%s", productDesc, productId, existingOrderNo); + } else { + return String.format("您已购买过%s,订单号:%s", productDesc, existingOrderNo); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/mapper/OrderDiscountMapper.java b/src/main/java/com/ycwl/basic/order/mapper/OrderDiscountMapper.java new file mode 100644 index 0000000..86b1a82 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/mapper/OrderDiscountMapper.java @@ -0,0 +1,12 @@ +package com.ycwl.basic.order.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.order.entity.OrderDiscountV2; +import org.apache.ibatis.annotations.Mapper; + +/** + * 订单优惠记录表Mapper接口 + */ +@Mapper +public interface OrderDiscountMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/mapper/OrderItemMapper.java b/src/main/java/com/ycwl/basic/order/mapper/OrderItemMapper.java new file mode 100644 index 0000000..89b9df0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/mapper/OrderItemMapper.java @@ -0,0 +1,12 @@ +package com.ycwl.basic.order.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.order.entity.OrderItemV2; +import org.apache.ibatis.annotations.Mapper; + +/** + * 订单商品明细表Mapper接口 + */ +@Mapper +public interface OrderItemMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/mapper/OrderRefundMapper.java b/src/main/java/com/ycwl/basic/order/mapper/OrderRefundMapper.java new file mode 100644 index 0000000..cb226e7 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/mapper/OrderRefundMapper.java @@ -0,0 +1,12 @@ +package com.ycwl.basic.order.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.order.entity.OrderRefundV2; +import org.apache.ibatis.annotations.Mapper; + +/** + * 订单退款记录表Mapper接口 + */ +@Mapper +public interface OrderRefundMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/mapper/OrderV2Mapper.java b/src/main/java/com/ycwl/basic/order/mapper/OrderV2Mapper.java new file mode 100644 index 0000000..a6031a2 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/mapper/OrderV2Mapper.java @@ -0,0 +1,12 @@ +package com.ycwl.basic.order.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ycwl.basic.order.entity.OrderV2; +import org.apache.ibatis.annotations.Mapper; + +/** + * 订单表Mapper接口 + */ +@Mapper +public interface OrderV2Mapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/service/IOrderService.java b/src/main/java/com/ycwl/basic/order/service/IOrderService.java new file mode 100644 index 0000000..f41d982 --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/service/IOrderService.java @@ -0,0 +1,145 @@ +package com.ycwl.basic.order.service; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.dto.MobileOrderRequest; +import com.ycwl.basic.order.dto.*; +import com.ycwl.basic.order.entity.OrderV2; +import com.ycwl.basic.order.enums.RefundStatus; +import com.ycwl.basic.pricing.dto.PriceCalculationResult; +import com.ycwl.basic.order.dto.PaymentParamsRequest; +import com.ycwl.basic.order.dto.PaymentParamsResponse; +import com.ycwl.basic.order.dto.PaymentCallbackResponse; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 订单服务接口 + */ +public interface IOrderService { + + /** + * 创建订单 + * + * @param request 移动端下单请求 + * @param userId 用户ID + * @param scenicId 景区ID + * @param priceResult 价格计算结果 + * @return 订单ID + */ + Long createOrder(MobileOrderRequest request, Long userId, Long scenicId, PriceCalculationResult priceResult); + + /** + * 根据订单号查询订单 + * + * @param orderNo 订单号 + * @return 订单信息 + */ + OrderV2 getByOrderNo(String orderNo); + + /** + * 根据订单ID查询订单 + * + * @param orderId 订单ID + * @return 订单信息 + */ + OrderV2 getById(Long orderId); + + /** + * 更新订单状态 + * + * @param orderId 订单ID + * @param orderStatus 订单状态 + * @param paymentStatus 支付状态 + * @return 更新结果 + */ + boolean updateOrderStatus(Long orderId, String orderStatus, String paymentStatus); + + /** + * 生成订单号 + * + * @return 订单号 + */ + String generateOrderNo(); + + // ====== 新增方法 ====== + + /** + * 分页查询订单列表 + * + * @param request 分页查询请求 + * @return 分页结果 + */ + PageInfo pageOrders(OrderV2PageRequest request); + + /** + * 查询订单详情(包含商品明细、优惠记录、退款记录) + * + * @param orderId 订单ID + * @return 订单详情 + */ + OrderV2DetailResponse getOrderDetail(Long orderId); + + /** + * 更新订单备注 + * + * @param orderId 订单ID + * @param remarks 备注内容 + * @return 更新结果 + */ + boolean updateOrderRemarks(Long orderId, String remarks); + + /** + * 更新支付状态 + * + * @param orderId 订单ID + * @param paymentStatus 支付状态 + * @return 更新结果 + */ + boolean updatePaymentStatus(Long orderId, String paymentStatus); + + /** + * 更新退款状态 + * + * @param orderId 订单ID + * @param refundStatus 退款状态 + * @return 更新结果 + */ + boolean updateRefundStatus(Long orderId, RefundStatus refundStatus); + + /** + * 创建退款记录 + * + * @param request 退款申请请求 + * @return 退款记录ID + */ + Long createRefundRecord(RefundRequest request); + + /** + * 根据用户ID分页查询订单列表(移动端使用) + * + * @param request 分页查询请求(已设置用户ID) + * @return 分页结果 + */ + PageInfo pageOrdersByUser(OrderV2PageRequest request); + + // ====== 支付相关方法 ====== + + /** + * 获取订单支付参数 + * + * @param orderId 订单ID + * @param userId 用户ID(用于权限验证和获取openId) + * @param request 支付参数请求 + * @return 支付参数响应 + */ + PaymentParamsResponse getPaymentParams(Long orderId, Long userId, PaymentParamsRequest request); + + /** + * 处理支付回调 + * + * @param scenicId 景区ID + * @param request HTTP请求对象 + * @return 回调处理结果 + */ + PaymentCallbackResponse handlePaymentCallback(Long scenicId, HttpServletRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java new file mode 100644 index 0000000..fa14b0a --- /dev/null +++ b/src/main/java/com/ycwl/basic/order/service/impl/OrderServiceImpl.java @@ -0,0 +1,1017 @@ +package com.ycwl.basic.order.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.dto.MobileOrderRequest; +import com.ycwl.basic.exception.BaseException; +import com.ycwl.basic.mapper.MemberMapper; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.member.resp.MemberRespVO; +import com.ycwl.basic.order.dto.*; +import com.ycwl.basic.order.entity.OrderDiscountV2; +import com.ycwl.basic.order.entity.OrderItemV2; +import com.ycwl.basic.order.entity.OrderRefundV2; +import com.ycwl.basic.order.entity.OrderV2; +import com.ycwl.basic.order.enums.*; +import com.ycwl.basic.order.exception.DuplicatePurchaseException; +import com.ycwl.basic.order.mapper.OrderDiscountMapper; +import com.ycwl.basic.order.mapper.OrderItemMapper; +import com.ycwl.basic.order.mapper.OrderV2Mapper; +import com.ycwl.basic.order.mapper.OrderRefundMapper; +import com.ycwl.basic.order.service.IOrderService; +import com.ycwl.basic.order.event.*; +import com.ycwl.basic.pricing.dto.DiscountDetail; +import com.ycwl.basic.pricing.dto.PriceCalculationResult; +import com.ycwl.basic.pricing.dto.ProductItem; +import com.ycwl.basic.pricing.dto.VoucherInfo; +import com.ycwl.basic.pricing.dto.CouponUseRequest; +import com.ycwl.basic.pricing.entity.PriceProductConfig; +import com.ycwl.basic.pricing.entity.PriceTierConfig; +import com.ycwl.basic.pricing.exception.PriceCalculationException; +import com.ycwl.basic.pricing.service.ICouponService; +import com.ycwl.basic.pricing.service.IProductConfigService; +import com.ycwl.basic.pricing.service.IVoucherService; +import com.ycwl.basic.pay.adapter.IPayAdapter; +import com.ycwl.basic.pay.entity.CreateOrderRequest; +import com.ycwl.basic.pay.entity.CreateOrderResponse; +import com.ycwl.basic.pay.entity.PayResponse; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.service.pc.ScenicService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.servlet.http.HttpServletRequest; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 订单服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class OrderServiceImpl implements IOrderService { + + private final OrderV2Mapper orderV2Mapper; + private final OrderItemMapper orderItemMapper; + private final OrderDiscountMapper orderDiscountMapper; + private final OrderRefundMapper orderRefundMapper; + private final OrderEventManager orderEventManager; + private final ScenicService scenicService; + private final MemberMapper memberMapper; + private final ICouponService couponService; + private final IVoucherService voucherService; + private final IProductConfigService productConfigService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createOrder(MobileOrderRequest request, Long userId, Long scenicId, PriceCalculationResult priceResult) { + Date now = new Date(); + MemberRespVO member = memberMapper.getById(userId); + + // 1. 检查重复购买 + log.info("开始检查重复购买: userId={}, faceId={}, scenicId={}, productCount={}", + userId, request.getFaceId(), scenicId, request.getProducts().size()); + checkDuplicatePurchase(userId, request.getFaceId(), scenicId, request.getProducts()); + log.info("重复购买检查通过: userId={}, faceId={}, scenicId={}", + userId, request.getFaceId(), scenicId); + + // 2. 生成订单号 + String orderNo = generateOrderNo(); + + // 3. 创建订单基础信息 + OrderV2 order = new OrderV2(); + order.setOrderNo(orderNo); + order.setMemberId(userId); + order.setOpenId(member.getOpenId()); // 临时使用faceId,实际应传入openId + order.setFaceId(request.getFaceId()); + order.setScenicId(scenicId); + + order.setOriginalAmount(priceResult.getOriginalAmount()); + order.setDiscountAmount(priceResult.getDiscountAmount()); + order.setFinalAmount(priceResult.getFinalAmount()); + + order.setOrderStatus(OrderStatus.PENDING_PAYMENT); + order.setPaymentStatus(PaymentStatus.UNPAID); + order.setRefundStatus(RefundStatus.NO_REFUND); + order.setTotalRefundAmount(BigDecimal.ZERO); + + order.setRemarks(request.getRemarks()); + order.setCreateTime(now); + order.setUpdateTime(now); + order.setCreateBy(userId); + order.setUpdateBy(userId); + order.setDeleted(0); + + // 保存订单 + orderV2Mapper.insert(order); + Long orderId = order.getId(); + + log.info("订单基础信息创建成功: orderId={}, orderNo={}", orderId, orderNo); + + // 4. 保存订单商品明细 + for (ProductItem product : request.getProducts()) { + // 重新计算商品价格信息并获取商品名称 + ProductPriceAndNameInfo priceInfo = calculateProductItemPriceAndName(product); + + OrderItemV2 orderItem = new OrderItemV2(); + orderItem.setOrderId(orderId); + orderItem.setProductType(product.getProductType().name()); + orderItem.setProductId(product.getProductId()); + orderItem.setProductName(priceInfo.getProductName()); // 使用配置中的商品名称 + orderItem.setQuantity(product.getQuantity() != null ? product.getQuantity() : 1); + orderItem.setUnitPrice(product.getUnitPrice()); + orderItem.setOriginalAmount(product.getOriginalPrice()); + orderItem.setFinalAmount(product.getSubtotal()); + orderItem.setCreateTime(now); + orderItem.setUpdateTime(now); + + orderItemMapper.insert(orderItem); + } + + log.info("订单商品明细保存成功: orderId={}, itemCount={}", orderId, request.getProducts().size()); + + // 5. 记录使用的优惠券信息并标记为已使用 + if (priceResult.getUsedCoupon() != null) { + OrderDiscountV2 couponDiscount = new OrderDiscountV2(); + couponDiscount.setOrderId(orderId); + couponDiscount.setDiscountType(DiscountType.COUPON); + couponDiscount.setDiscountName(priceResult.getUsedCoupon().getCouponName()); + couponDiscount.setDiscountAmount(priceResult.getUsedCoupon().getActualDiscountAmount()); + couponDiscount.setSortOrder(3); // 优惠券显示顺序 + couponDiscount.setCouponId(priceResult.getUsedCoupon().getCouponId()); + couponDiscount.setCreateTime(now); + + orderDiscountMapper.insert(couponDiscount); + log.info("优惠券记录保存成功: orderId={}, couponId={}, discountAmount={}", + orderId, priceResult.getUsedCoupon().getCouponId(), priceResult.getUsedCoupon().getActualDiscountAmount()); + + // 标记优惠券为已使用 + try { + CouponUseRequest couponUseRequest = new CouponUseRequest(); + couponUseRequest.setCouponId(priceResult.getUsedCoupon().getCouponId()); + couponUseRequest.setUserId(userId); + couponUseRequest.setOrderId(orderNo); + couponUseRequest.setOriginalAmount(priceResult.getOriginalAmount()); + couponUseRequest.setDiscountAmount(priceResult.getUsedCoupon().getActualDiscountAmount()); + couponUseRequest.setScenicId(scenicId.toString()); + + couponService.useCoupon(couponUseRequest); + log.info("优惠券状态更新成功: couponId={}, orderId={}", + priceResult.getUsedCoupon().getCouponId(), orderNo); + } catch (Exception e) { + log.error("优惠券状态更新失败: couponId={}, orderId={}, error={}", + priceResult.getUsedCoupon().getCouponId(), orderNo, e.getMessage(), e); + // 这里可以选择抛出异常或者记录错误继续执行,根据业务需求决定 + throw new BaseException("优惠券使用失败: " + e.getMessage()); + } + } + + // 6. 记录使用的券码信息并标记为已使用 + if (priceResult.getUsedVoucher() != null) { + VoucherInfo voucherInfo = priceResult.getUsedVoucher(); + OrderDiscountV2 voucherDiscount = new OrderDiscountV2(); + voucherDiscount.setOrderId(orderId); + voucherDiscount.setDiscountType(DiscountType.VOUCHER); + voucherDiscount.setDiscountName(voucherInfo.getBatchName()); + voucherDiscount.setDiscountAmount(voucherInfo.getActualDiscountAmount()); + voucherDiscount.setSortOrder(2); // 券码显示顺序低于限时立减 + voucherDiscount.setVoucherCode(voucherInfo.getVoucherCode()); + voucherDiscount.setCreateTime(now); + + orderDiscountMapper.insert(voucherDiscount); + log.info("券码记录保存成功: orderId={}, voucherCode={}, discountAmount={}", + orderId, voucherInfo.getVoucherCode(), voucherInfo.getActualDiscountAmount()); + + // 标记券码为已使用 + try { + String remark = "订单使用,订单号:" + orderNo; + voucherService.markVoucherAsUsed(voucherInfo.getVoucherCode(), remark); + log.info("券码状态更新成功: voucherCode={}, orderId={}", + voucherInfo.getVoucherCode(), orderNo); + } catch (Exception e) { + log.error("券码状态更新失败: voucherCode={}, orderId={}, error={}", + voucherInfo.getVoucherCode(), orderNo, e.getMessage(), e); + // 这里可以选择抛出异常或者记录错误继续执行,根据业务需求决定 + throw new BaseException("券码使用失败: " + e.getMessage()); + } + } + + // 7. 记录其他优惠信息(如限时立减等) + if (priceResult.getDiscountDetails() != null) { + for (DiscountDetail discount : priceResult.getDiscountDetails()) { + // 跳过已经记录的优惠券和券码 + if ("VOUCHER".equals(discount.getDiscountType()) || "COUPON".equals(discount.getDiscountType())) { + continue; + } + + OrderDiscountV2 otherDiscount = new OrderDiscountV2(); + otherDiscount.setOrderId(orderId); + otherDiscount.setDiscountType(DiscountType.fromCode(discount.getDiscountType())); + otherDiscount.setDiscountName(discount.getDiscountName()); + otherDiscount.setDiscountAmount(discount.getDiscountAmount()); + otherDiscount.setSortOrder(discount.getSortOrder()); + otherDiscount.setCreateTime(now); + + orderDiscountMapper.insert(otherDiscount); + log.info("其他优惠记录保存成功: orderId={}, type={}, discountAmount={}", + orderId, discount.getDiscountType(), discount.getDiscountAmount()); + } + } + + log.info("订单创建完成: orderId={}, orderNo={}, finalAmount={}", + orderId, orderNo, priceResult.getFinalAmount()); + + return orderId; + } + + @Override + public OrderV2 getByOrderNo(String orderNo) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("order_no", orderNo); + queryWrapper.eq("deleted", 0); + return orderV2Mapper.selectOne(queryWrapper); + } + + @Override + public OrderV2 getById(Long orderId) { + return orderV2Mapper.selectById(orderId); + } + + @Override + public boolean updateOrderStatus(Long orderId, String orderStatus, String paymentStatus) { + OrderV2 order = new OrderV2(); + order.setId(orderId); + order.setOrderStatus(OrderStatus.fromCode(orderStatus)); + order.setPaymentStatus(PaymentStatus.fromCode(paymentStatus)); + order.setUpdateTime(new Date()); + + if ("PAID".equals(paymentStatus)) { + order.setPayTime(new Date()); + } + + if ("COMPLETED".equals(orderStatus)) { + order.setCompleteTime(new Date()); + } + + return orderV2Mapper.updateById(order) > 0; + } + + @Override + public String generateOrderNo() { + // 生成订单号:ORDER + 时间戳 + 3位随机数 + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + String timestamp = sdf.format(new Date()); + int random = (int) (Math.random() * 900) + 100; + return "ORDER" + timestamp + random; + } + + // ====== 新增方法实现 ====== + + @Override + public PageInfo pageOrders(OrderV2PageRequest request) { + PageHelper.startPage(request.getPageNum(), request.getPageSize()); + + QueryWrapper queryWrapper = buildOrderQueryWrapper(request); + List orders = orderV2Mapper.selectList(queryWrapper); + + // 转换为响应DTO + List responseList = orders.stream() + .map(this::convertToListResponse) + .collect(Collectors.toList()); + + return new PageInfo<>(responseList); + } + + @Override + public OrderV2DetailResponse getOrderDetail(Long orderId) { + // 查询订单基础信息 + OrderV2 order = orderV2Mapper.selectById(orderId); + if (order == null || order.getDeleted() == 1) { + return null; + } + + // 查询订单商品明细 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", orderId); + List orderItems = orderItemMapper.selectList(itemQuery); + + // 查询订单优惠记录 + QueryWrapper discountQuery = new QueryWrapper<>(); + discountQuery.eq("order_id", orderId).orderByAsc("sort_order"); + List orderDiscounts = orderDiscountMapper.selectList(discountQuery); + + // 查询订单退款记录 + QueryWrapper refundQuery = new QueryWrapper<>(); + refundQuery.eq("order_id", orderId).orderByDesc("create_time"); + List orderRefunds = orderRefundMapper.selectList(refundQuery); + + // 转换为响应DTO + return convertToDetailResponse(order, orderItems, orderDiscounts, orderRefunds); + } + + @Override + @Transactional + public boolean updateOrderRemarks(Long orderId, String remarks) { + OrderV2 order = new OrderV2(); + order.setId(orderId); + order.setRemarks(remarks); + order.setUpdateTime(new Date()); + + return orderV2Mapper.updateById(order) > 0; + } + + @Override + @Transactional + public boolean updatePaymentStatus(Long orderId, String paymentStatus) { + // 先查询当前订单状态 + OrderV2 currentOrder = orderV2Mapper.selectById(orderId); + if (currentOrder == null) { + log.warn("订单不存在: orderId={}", orderId); + return false; + } + + PaymentStatus oldPaymentStatus = currentOrder.getPaymentStatus(); + PaymentStatus newPaymentStatus = PaymentStatus.fromCode(paymentStatus); + + // 更新订单支付状态 + OrderV2 order = new OrderV2(); + order.setId(orderId); + order.setPaymentStatus(newPaymentStatus); + order.setUpdateTime(new Date()); + + if ("PAID".equals(paymentStatus)) { + order.setPayTime(new Date()); + // 支付完成后,订单状态也需要更新 + order.setOrderStatus(OrderStatus.PAID); + } + + boolean success = orderV2Mapper.updateById(order) > 0; + + // 发布支付状态变更事件 + if (success && !oldPaymentStatus.equals(newPaymentStatus)) { + PaymentStatusChangeEvent event = new PaymentStatusChangeEvent( + orderId, currentOrder.getOrderNo(), + oldPaymentStatus, newPaymentStatus, + "支付状态更新", null + ); + orderEventManager.publishPaymentStatusChangeEvent(event); + } + + return success; + } + + @Override + @Transactional + public boolean updateRefundStatus(Long orderId, RefundStatus refundStatus) { + // 先查询当前订单状态 + OrderV2 currentOrder = orderV2Mapper.selectById(orderId); + if (currentOrder == null) { + log.warn("订单不存在: orderId={}", orderId); + return false; + } + + RefundStatus oldRefundStatus = currentOrder.getRefundStatus(); + + // 更新订单退款状态 + OrderV2 order = new OrderV2(); + order.setId(orderId); + order.setRefundStatus(refundStatus); + order.setUpdateTime(new Date()); + + // 根据退款状态更新订单状态 + if (RefundStatus.FULL_REFUND.equals(refundStatus)) { + order.setOrderStatus(OrderStatus.REFUNDED); + } else if (RefundStatus.REFUND_PROCESSING.equals(refundStatus)) { + order.setOrderStatus(OrderStatus.REFUNDING); + } + + boolean success = orderV2Mapper.updateById(order) > 0; + + // 发布退款状态变更事件 + if (success && !oldRefundStatus.equals(refundStatus)) { + RefundStatusChangeEvent event = new RefundStatusChangeEvent( + orderId, currentOrder.getOrderNo(), null, null, + oldRefundStatus, refundStatus, null, + "退款状态更新", null + ); + orderEventManager.publishRefundStatusChangeEvent(event); + } + + return success; + } + + @Override + @Transactional + public Long createRefundRecord(RefundRequest request) { + Date now = new Date(); + + // 生成退款单号 + String refundNo = generateRefundNo(); + + // 创建退款记录 + OrderRefundV2 refund = new OrderRefundV2(); + refund.setOrderId(request.getOrderId()); + refund.setRefundNo(refundNo); + refund.setRefundType(RefundType.fromCode(request.getRefundType())); + refund.setRefundAmount(request.getRefundAmount()); + refund.setRefundFee(request.getRefundFee()); + refund.setRefundStatus(com.ycwl.basic.order.enums.RefundStatus.REFUND_PROCESSING); + refund.setRefundReason(request.getRefundReason()); + refund.setRefundDescription(request.getRefundDescription()); + refund.setOperatorRemarks(request.getOperatorRemarks()); + refund.setRefundChannel(request.getRefundChannel()); + refund.setApplyTime(now); + refund.setCreateTime(now); + refund.setUpdateTime(now); + + orderRefundMapper.insert(refund); + + // 更新订单退款状态 + updateRefundStatus(request.getOrderId(), com.ycwl.basic.order.enums.RefundStatus.REFUND_PROCESSING); + + log.info("退款记录创建成功: refundId={}, refundNo={}, orderId={}, refundAmount={}", + refund.getId(), refundNo, request.getOrderId(), request.getRefundAmount()); + + return refund.getId(); + } + + @Override + public PageInfo pageOrdersByUser(OrderV2PageRequest request) { + // 移动端查询,确保只查询当前用户的订单 + return pageOrders(request); + } + + // ====== 私有辅助方法 ====== + + /** + * 构建订单查询条件 + */ + private QueryWrapper buildOrderQueryWrapper(OrderV2PageRequest request) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("deleted", 0); + + if (request.getOrderNo() != null && !request.getOrderNo().trim().isEmpty()) { + queryWrapper.like("order_no", request.getOrderNo().trim()); + } + + if (request.getMemberId() != null) { + queryWrapper.eq("member_id", request.getMemberId()); + } + + if (request.getScenicId() != null) { + queryWrapper.eq("scenic_id", request.getScenicId()); + } + + if (request.getOrderStatus() != null && !request.getOrderStatus().trim().isEmpty()) { + queryWrapper.eq("order_status", request.getOrderStatus().trim()); + } + + if (request.getPaymentStatus() != null && !request.getPaymentStatus().trim().isEmpty()) { + queryWrapper.eq("payment_status", request.getPaymentStatus().trim()); + } + + if (request.getRefundStatus() != null && !request.getRefundStatus().trim().isEmpty()) { + queryWrapper.eq("refund_status", request.getRefundStatus().trim()); + } + + if (request.getCreateTimeStart() != null) { + queryWrapper.ge("create_time", request.getCreateTimeStart()); + } + + if (request.getCreateTimeEnd() != null) { + queryWrapper.le("create_time", request.getCreateTimeEnd()); + } + + if (request.getPayTimeStart() != null) { + queryWrapper.ge("pay_time", request.getPayTimeStart()); + } + + if (request.getPayTimeEnd() != null) { + queryWrapper.le("pay_time", request.getPayTimeEnd()); + } + + queryWrapper.orderByDesc("create_time"); + + return queryWrapper; + } + + /** + * 转换为列表响应DTO + */ + private OrderV2ListResponse convertToListResponse(OrderV2 order) { + OrderV2ListResponse response = new OrderV2ListResponse(); + response.setId(order.getId()); + response.setOrderNo(order.getOrderNo()); + response.setMemberId(order.getMemberId()); + response.setOpenId(order.getOpenId()); + response.setFaceId(order.getFaceId()); + response.setScenicId(order.getScenicId()); + response.setOriginalAmount(order.getOriginalAmount()); + response.setDiscountAmount(order.getDiscountAmount()); + response.setFinalAmount(order.getFinalAmount()); + response.setOrderStatus(order.getOrderStatus()); + response.setOrderStatusDesc(order.getOrderStatus().getDescription()); + response.setPaymentStatus(order.getPaymentStatus()); + response.setPaymentStatusDesc(order.getPaymentStatus().getDescription()); + response.setRefundStatus(order.getRefundStatus()); + response.setRefundStatusDesc(order.getRefundStatus().getDescription()); + response.setTotalRefundAmount(order.getTotalRefundAmount()); + response.setRemarks(order.getRemarks()); + response.setPayTime(order.getPayTime()); + response.setCompleteTime(order.getCompleteTime()); + response.setCreateTime(order.getCreateTime()); + response.setUpdateTime(order.getUpdateTime()); + + // 查询商品数量信息 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", order.getId()); + List items = orderItemMapper.selectList(itemQuery); + response.setItemCount(items.size()); + response.setTotalQuantity(items.stream().mapToInt(OrderItemV2::getQuantity).sum()); + + return response; + } + + /** + * 转换为详情响应DTO + */ + private OrderV2DetailResponse convertToDetailResponse(OrderV2 order, + List orderItems, + List orderDiscounts, + List orderRefunds) { + OrderV2DetailResponse response = new OrderV2DetailResponse(); + response.setId(order.getId()); + response.setOrderNo(order.getOrderNo()); + response.setMemberId(order.getMemberId()); + response.setOpenId(order.getOpenId()); + response.setFaceId(order.getFaceId()); + response.setScenicId(order.getScenicId()); + response.setOriginalAmount(order.getOriginalAmount()); + response.setDiscountAmount(order.getDiscountAmount()); + response.setFinalAmount(order.getFinalAmount()); + response.setOrderStatus(order.getOrderStatus()); + response.setOrderStatusDesc(order.getOrderStatus().getDescription()); + response.setPaymentStatus(order.getPaymentStatus()); + response.setPaymentStatusDesc(order.getPaymentStatus().getDescription()); + response.setRefundStatus(order.getRefundStatus()); + response.setRefundStatusDesc(order.getRefundStatus().getDescription()); + response.setTotalRefundAmount(order.getTotalRefundAmount()); + response.setRemarks(order.getRemarks()); + response.setPayTime(order.getPayTime()); + response.setCompleteTime(order.getCompleteTime()); + response.setCreateTime(order.getCreateTime()); + response.setUpdateTime(order.getUpdateTime()); + response.setCreateBy(order.getCreateBy()); + response.setUpdateBy(order.getUpdateBy()); + + response.setOrderItems(orderItems); + response.setOrderDiscounts(orderDiscounts); + response.setOrderRefunds(orderRefunds); + + return response; + } + + /** + * 生成退款单号 + */ + private String generateRefundNo() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + String timestamp = sdf.format(new Date()); + int random = (int) (Math.random() * 900) + 100; + return "REFUND" + timestamp + random; + } + + // ====== 支付相关方法实现 ====== + + @Override + public PaymentParamsResponse getPaymentParams(Long orderId, Long userId, PaymentParamsRequest request) { + log.info("获取支付参数: orderId={}, userId={}", orderId, userId); + + // 1. 查询订单 + OrderV2 order = orderV2Mapper.selectById(orderId); + if (order == null) { + throw new RuntimeException("订单不存在"); + } + + // 2. 验证订单权限 + if (!userId.equals(order.getMemberId())) { + throw new RuntimeException("无权获取该订单的支付参数"); + } + + // 3. 验证订单状态 + if (order.getPaymentStatus() != PaymentStatus.UNPAID) { + throw new RuntimeException("订单状态不允许支付"); + } + + // 4. 获取用户openId(从订单中获取) + String openId = order.getOpenId(); + if (openId == null || openId.trim().isEmpty()) { + throw new RuntimeException("用户openId不存在,无法发起支付"); + } + + // 5. 检查是否为免费订单 + if (order.getFinalAmount().compareTo(BigDecimal.ZERO) == 0) { + log.info("免费订单,无需支付: orderId={}", orderId); + return PaymentParamsResponse.free(orderId, order.getOrderNo()); + } + + // 6. 获取景区支付适配器 + IPayAdapter payAdapter = scenicService.getScenicPayAdapter(order.getScenicId()); + + try { + // 7. 生成商品名称和描述 + String goodsName = generateGoodsName(orderId); + String description = goodsName; // 直接使用商品名称作为描述 + + // 8. 创建支付订单请求 + CreateOrderRequest payRequest = new CreateOrderRequest() + .setOrderNo(order.getOrderNo()) + .setPriceInCents(order.getFinalAmount().multiply(BigDecimal.valueOf(100)).intValue()) + .setDescription(description) + .setGoodsName(goodsName) + .setUserIdentify(openId) + .setNotifyUrl("https://zhentuai.com/api/mobile/order/v2/payment/callback/" + order.getScenicId()); + + // 9. 调用支付适配器创建订单 + CreateOrderResponse payResponse = payAdapter.createOrder(payRequest); + + // 10. 获取支付参数 + java.util.Map paymentParams = payAdapter.getPaymentParams(payResponse); + + // 11. 构建响应 + PaymentParamsResponse response = PaymentParamsResponse.success( + orderId, + order.getOrderNo(), + order.getFinalAmount(), + !payResponse.isSkipPay(), + paymentParams + ); + response.setDescription(description); + response.setGoodsName(goodsName); + + log.info("支付参数生成成功: orderId={}, needPay={}, amount={}", + orderId, !payResponse.isSkipPay(), order.getFinalAmount()); + + return response; + + } catch (Exception e) { + log.error("获取支付参数失败: orderId={}, error={}", orderId, e.getMessage(), e); + throw new RuntimeException("获取支付参数失败: " + e.getMessage()); + } + } + + @Override + public PaymentCallbackResponse handlePaymentCallback(Long scenicId, HttpServletRequest request) { + log.info("处理支付回调: scenicId={}", scenicId); + + try { + // 1. 获取支付适配器 + IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId); + + // 2. 解析回调数据 + PayResponse callbackResponse = payAdapter.handleCallback(request); + log.info("解析支付回调数据: {}", callbackResponse); + + if (callbackResponse == null || callbackResponse.getOrderNo() == null) { + return PaymentCallbackResponse.failure("回调数据解析失败"); + } + + // 3. 查询订单 + String orderNo = callbackResponse.getOrderNo(); + OrderV2 order = getByOrderNo(orderNo); + if (order == null) { + return PaymentCallbackResponse.failure("订单不存在", null, orderNo); + } + + // 4. 处理不同的支付状态变更 + String statusChangeType = null; + if (callbackResponse.isPay()) { + // 支付成功 + statusChangeType = "PAID"; + updatePaymentStatus(order.getId(), PaymentStatus.PAID.name()); + + // 触发支付成功事件 + orderEventManager.publishPaymentStatusChangeEvent( + new PaymentStatusChangeEvent(order.getId(), order.getOrderNo(), + PaymentStatus.UNPAID, PaymentStatus.PAID, "支付回调成功", null) + ); + + } else if (callbackResponse.isCancel()) { + // 支付取消 - 这种情况下支付状态保持未支付,不需要特别处理 + statusChangeType = "CANCELLED"; + log.info("支付被取消,支付状态保持未支付: orderId={}", order.getId()); + + } else if (callbackResponse.isRefund()) { + // 退款 + statusChangeType = "REFUNDED"; + updatePaymentStatus(order.getId(), PaymentStatus.REFUNDED.name()); + updateRefundStatus(order.getId(), RefundStatus.FULL_REFUND); + + // 触发退款事件 + orderEventManager.publishRefundStatusChangeEvent( + new RefundStatusChangeEvent(order.getId(), order.getOrderNo(), null, null, + RefundStatus.NO_REFUND, RefundStatus.FULL_REFUND, + order.getFinalAmount(), "支付回调退款", null) + ); + } + + log.info("支付回调处理成功: orderId={}, orderNo={}, statusChangeType={}", + order.getId(), orderNo, statusChangeType); + + return PaymentCallbackResponse.success(order.getId(), orderNo, statusChangeType); + + } catch (Exception e) { + log.error("处理支付回调失败: scenicId={}, error={}", scenicId, e.getMessage(), e); + return PaymentCallbackResponse.failure("回调处理失败: " + e.getMessage()); + } + } + + /** + * 生成商品名称 + * 根据订单的商品类型生成用于支付页面展示的商品名称 + * 如果是一种商品类型,显示具体类型名称;如果是多种,显示"多项景区商品" + */ + private String generateGoodsName(Long orderId) { + // 查询订单商品明细 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", orderId); + java.util.List orderItems = orderItemMapper.selectList(itemQuery); + + if (orderItems.isEmpty()) { + return "景区商品"; + } + + // 获取所有不同的商品类型 + java.util.Set productTypes = new java.util.HashSet<>(); + for (OrderItemV2 item : orderItems) { + productTypes.add(item.getProductType()); + } + + // 如果只有一种商品类型,返回具体类型名称 + if (productTypes.size() == 1) { + String productType = productTypes.iterator().next(); + return getProductTypeName(productType); + } else { + // 如果有多种商品类型,返回通用名称 + return "多项景区商品"; + } + } + + /** + * 获取商品类型中文名称 + */ + private String getProductTypeName(String productType) { + return switch (productType) { + case "VLOG_VIDEO" -> "Vlog视频"; + case "RECORDING_SET" -> "录像集"; + case "PHOTO_SET" -> "照相集"; + case "PHOTO_PRINT" -> "照片打印"; + case "MACHINE_PRINT" -> "一体机打印"; + default -> "景区商品"; + }; + } + + /** + * 商品价格和名称信息内部DTO + */ + private static class ProductPriceAndNameInfo { + private BigDecimal unitPrice; + private BigDecimal originalPrice; + private String productName; + + public ProductPriceAndNameInfo(BigDecimal unitPrice, BigDecimal originalPrice, String productName) { + this.unitPrice = unitPrice; + this.originalPrice = originalPrice; + this.productName = productName; + } + + public BigDecimal getUnitPrice() { return unitPrice; } + public BigDecimal getOriginalPrice() { return originalPrice; } + public String getProductName() { return productName; } + } + + /** + * 重新计算商品价格信息并获取商品名称 + * 基于定价配置重新计算商品的单价、原价和商品名称 + */ + private ProductPriceAndNameInfo calculateProductItemPriceAndName(ProductItem product) { + String productTypeCode = product.getProductType().getCode(); + String productId = product.getProductId() != null ? product.getProductId() : "default"; + + BigDecimal unitPrice; + BigDecimal originalPrice; + String productName; + + try { + // 1. 优先使用基于product_id的阶梯定价 + PriceTierConfig tierConfig = productConfigService.getTierConfig( + productTypeCode, productId, product.getQuantity()); + + if (tierConfig != null) { + unitPrice = tierConfig.getPrice(); + originalPrice = tierConfig.getOriginalPrice(); + + // 阶梯定价没有商品名称,需要从基础配置获取 + PriceProductConfig baseConfig = productConfigService.getProductConfig(productTypeCode, productId); + productName = (baseConfig != null && baseConfig.getProductName() != null) + ? baseConfig.getProductName() + : getProductTypeName(product.getProductType().name()); + + log.debug("使用阶梯定价: productType={}, productId={}, quantity={}, price={}, originalPrice={}, productName={}", + productTypeCode, productId, product.getQuantity(), unitPrice, originalPrice, productName); + } else { + // 2. 使用基于product_id的基础配置 + PriceProductConfig baseConfig; + try { + baseConfig = productConfigService.getProductConfig(productTypeCode, productId); + } catch (Exception e) { + log.warn("未找到具体商品配置: productType={}, productId={}, 使用default配置", + productTypeCode, productId); + baseConfig = productConfigService.getProductConfig(productTypeCode, "default"); + } + + if (baseConfig == null) { + throw new PriceCalculationException( + String.format("未找到商品价格配置: productType=%s, productId=%s", productTypeCode, productId)); + } + + unitPrice = baseConfig.getBasePrice(); + originalPrice = baseConfig.getOriginalPrice(); + productName = (baseConfig.getProductName() != null) + ? baseConfig.getProductName() + : getProductTypeName(product.getProductType().name()); + + // 3. 处理按数量计价的商品类型 + if (product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.PHOTO_PRINT || + product.getProductType() == com.ycwl.basic.pricing.enums.ProductType.MACHINE_PRINT) { + if (product.getQuantity() != null && product.getQuantity() > 0) { + unitPrice = unitPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + if (originalPrice != null) { + originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity())); + } + } + } + } + + // 4. 设置计算后的价格到ProductItem + product.setUnitPrice(unitPrice); + product.setOriginalPrice(originalPrice != null ? originalPrice : unitPrice); + + // 5. 计算小计(单价 × 购买数量) + int purchaseCount = product.getPurchaseCount() != null ? product.getPurchaseCount() : 1; + BigDecimal subtotal = unitPrice.multiply(BigDecimal.valueOf(purchaseCount)); + product.setSubtotal(subtotal); + + log.debug("商品价格和名称计算完成: productType={}, productId={}, unitPrice={}, originalPrice={}, subtotal={}, productName={}", + productTypeCode, productId, unitPrice, originalPrice, subtotal, productName); + + return new ProductPriceAndNameInfo(unitPrice, originalPrice != null ? originalPrice : unitPrice, productName); + + } catch (Exception e) { + log.error("计算商品价格失败: productType={}, productId={}, error={}", + productTypeCode, productId, e.getMessage(), e); + + // 兜底处理:使用默认价格和类型名称 + BigDecimal defaultPrice = BigDecimal.ZERO; + String fallbackName = getProductTypeName(product.getProductType().name()); + + product.setUnitPrice(defaultPrice); + product.setOriginalPrice(defaultPrice); + product.setSubtotal(defaultPrice); + + return new ProductPriceAndNameInfo(defaultPrice, defaultPrice, fallbackName); + } + } + + /** + * 检查重复购买 + * 防止用户重复购买相同内容 + * + * @param userId 用户ID + * @param faceId 人脸ID + * @param scenicId 景区ID + * @param products 商品列表 + * @throws DuplicatePurchaseException 如果检测到重复购买 + */ + private void checkDuplicatePurchase(Long userId, Long faceId, Long scenicId, List products) { + for (ProductItem product : products) { + switch (product.getProductType()) { + case VLOG_VIDEO: + checkVideoAlreadyPurchased(userId, faceId, scenicId, product.getProductId()); + break; + case RECORDING_SET: + case PHOTO_SET: + checkSetAlreadyPurchased(userId, faceId, scenicId, product.getProductType()); + break; + case PHOTO_PRINT: + case MACHINE_PRINT: + // 打印类商品允许重复购买,跳过检查 + log.debug("跳过打印类商品重复购买检查: productType={}, productId={}", + product.getProductType(), product.getProductId()); + break; + default: + log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType()); + break; + } + } + } + + /** + * 检查视频是否已经购买 + * + * @param userId 用户ID + * @param faceId 人脸ID + * @param scenicId 景区ID + * @param videoId 视频ID + * @throws DuplicatePurchaseException 如果已购买 + */ + private void checkVideoAlreadyPurchased(Long userId, Long faceId, Long scenicId, String videoId) { + // 构建查询条件:查找已支付的有效订单中包含该视频的订单 + QueryWrapper orderQuery = new QueryWrapper<>(); + orderQuery.eq("member_id", userId) + .eq("face_id", faceId) + .eq("scenic_id", scenicId) + .eq("payment_status", PaymentStatus.PAID.getCode()) + .in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode()) + .eq("deleted", 0); + + List existingOrders = orderV2Mapper.selectList(orderQuery); + + for (OrderV2 order : existingOrders) { + // 检查订单明细中是否包含该视频 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", order.getId()) + .eq("product_type", com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO.name()) + .eq("product_id", videoId); + + long count = orderItemMapper.selectCount(itemQuery); + if (count > 0) { + log.warn("检测到重复购买视频: userId={}, faceId={}, scenicId={}, videoId={}, existingOrderId={}", + userId, faceId, scenicId, videoId, order.getId()); + throw new DuplicatePurchaseException( + "您已购买过此视频", + order.getId(), + order.getOrderNo(), + com.ycwl.basic.pricing.enums.ProductType.VLOG_VIDEO, + videoId + ); + } + } + + log.debug("视频重复购买检查通过: userId={}, faceId={}, scenicId={}, videoId={}", + userId, faceId, scenicId, videoId); + } + + /** + * 检查套餐(录像集/照相集)是否已经购买 + * + * @param userId 用户ID + * @param faceId 人脸ID + * @param scenicId 景区ID + * @param productType 商品类型 + * @throws DuplicatePurchaseException 如果已购买 + */ + private void checkSetAlreadyPurchased(Long userId, Long faceId, Long scenicId, + com.ycwl.basic.pricing.enums.ProductType productType) { + // 构建查询条件:查找已支付的有效订单中包含该类型套餐的订单 + QueryWrapper orderQuery = new QueryWrapper<>(); + orderQuery.eq("member_id", userId) + .eq("face_id", faceId) + .eq("scenic_id", scenicId) + .eq("payment_status", PaymentStatus.PAID.getCode()) + .in("order_status", OrderStatus.PAID.getCode(), OrderStatus.PROCESSING.getCode(), OrderStatus.COMPLETED.getCode()) + .eq("deleted", 0); + + List existingOrders = orderV2Mapper.selectList(orderQuery); + + for (OrderV2 order : existingOrders) { + // 检查订单明细中是否包含该类型的套餐 + QueryWrapper itemQuery = new QueryWrapper<>(); + itemQuery.eq("order_id", order.getId()) + .eq("product_type", productType.name()); + + long count = orderItemMapper.selectCount(itemQuery); + if (count > 0) { + log.warn("检测到重复购买套餐: userId={}, faceId={}, scenicId={}, productType={}, existingOrderId={}", + userId, faceId, scenicId, productType, order.getId()); + throw new DuplicatePurchaseException( + "您已购买过此类型的套餐", + order.getId(), + order.getOrderNo(), + productType + ); + } + } + + log.debug("套餐重复购买检查通过: userId={}, faceId={}, scenicId={}, productType={}", + userId, faceId, scenicId, productType); + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md index c1c0ebf..03c701a 100644 --- a/src/main/java/com/ycwl/basic/pricing/CLAUDE.md +++ b/src/main/java/com/ycwl/basic/pricing/CLAUDE.md @@ -438,7 +438,7 @@ public interface IDiscountDetectionService { @Component public class FlashSaleDiscountProvider implements IDiscountProvider { @Override - public String getProviderType() { return "FLASH_SALE"; } + public String getProviderType() { return "LIMITED_TIME"; } @Override public int getPriority() { return 90; } // 介于券码和优惠券之间 diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponClaimRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponClaimRequest.java new file mode 100644 index 0000000..fc27050 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponClaimRequest.java @@ -0,0 +1,51 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +/** + * 优惠券领取请求DTO + */ +@Data +public class CouponClaimRequest { + + /** + * 用户ID + */ + private Long userId; + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 景区ID(可选,记录在哪个景区领取) + */ + private String scenicId; + + /** + * 领取来源(可选,如:活动页面、签到奖励等) + */ + private String claimSource; + + public CouponClaimRequest() { + } + + public CouponClaimRequest(Long userId, Long couponId) { + this.userId = userId; + this.couponId = couponId; + } + + public CouponClaimRequest(Long userId, Long couponId, String scenicId) { + this.userId = userId; + this.couponId = couponId; + this.scenicId = scenicId; + } + + public CouponClaimRequest(Long userId, Long couponId, String scenicId, String claimSource) { + this.userId = userId; + this.couponId = couponId; + this.scenicId = scenicId; + this.claimSource = claimSource; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/CouponClaimResult.java b/src/main/java/com/ycwl/basic/pricing/dto/CouponClaimResult.java new file mode 100644 index 0000000..d9173ed --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/CouponClaimResult.java @@ -0,0 +1,100 @@ +package com.ycwl.basic.pricing.dto; + +import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord; +import lombok.Data; + +import java.util.Date; + +/** + * 优惠券领取结果DTO + */ +@Data +public class CouponClaimResult { + + /** + * 是否成功 + */ + private boolean success; + + /** + * 错误消息(失败时) + */ + private String errorMessage; + + /** + * 错误码(失败时) + */ + private String errorCode; + + /** + * 领取记录ID(成功时) + */ + private Long claimRecordId; + + /** + * 优惠券ID + */ + private Long couponId; + + /** + * 优惠券名称 + */ + private String couponName; + + /** + * 领取时间 + */ + private Date claimTime; + + /** + * 用户ID + */ + private Long userId; + + /** + * 景区ID + */ + private String scenicId; + + /** + * 创建成功结果 + */ + public static CouponClaimResult success(PriceCouponClaimRecord record, String couponName) { + CouponClaimResult result = new CouponClaimResult(); + result.success = true; + result.claimRecordId = record.getId(); + result.couponId = record.getCouponId(); + result.couponName = couponName; + result.claimTime = record.getClaimTime(); + result.userId = record.getUserId(); + result.scenicId = record.getScenicId(); + return result; + } + + /** + * 创建失败结果 + */ + public static CouponClaimResult failure(String errorCode, String errorMessage) { + CouponClaimResult result = new CouponClaimResult(); + result.success = false; + result.errorCode = errorCode; + result.errorMessage = errorMessage; + return result; + } + + /** + * 创建失败结果(仅错误消息) + */ + public static CouponClaimResult failure(String errorMessage) { + return failure("CLAIM_FAILED", errorMessage); + } + + // 常用错误码常量 + public static final String ERROR_COUPON_NOT_FOUND = "COUPON_NOT_FOUND"; + public static final String ERROR_COUPON_EXPIRED = "COUPON_EXPIRED"; + public static final String ERROR_COUPON_INACTIVE = "COUPON_INACTIVE"; + public static final String ERROR_COUPON_OUT_OF_STOCK = "COUPON_OUT_OF_STOCK"; + public static final String ERROR_ALREADY_CLAIMED = "ALREADY_CLAIMED"; + public static final String ERROR_INVALID_PARAMS = "INVALID_PARAMS"; + public static final String ERROR_SYSTEM_ERROR = "SYSTEM_ERROR"; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java index e7bacc8..1ffff6c 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/DiscountDetail.java @@ -44,7 +44,7 @@ public class DiscountDetail { detail.setDiscountName("限时立减"); detail.setDiscountAmount(discountAmount); detail.setDescription("限时优惠,立即享受"); - detail.setSortOrder(2); // 限时立减排在券码后面 + detail.setSortOrder(1); // 限时立减排在最前面 return detail; } @@ -70,7 +70,7 @@ public class DiscountDetail { detail.setDiscountName("券码优惠"); detail.setDiscountAmount(discountAmount); detail.setDescription(String.format("券码 %s - %s", voucherCode, discountTypeName)); - detail.setSortOrder(1); // 券码优先级最高,排在最前面 + detail.setSortOrder(2); // 券码显示顺序低于限时立减 return detail; } diff --git a/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java b/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java new file mode 100644 index 0000000..d7e49dd --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/MobilePriceCalculationRequest.java @@ -0,0 +1,62 @@ +package com.ycwl.basic.pricing.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 移动端价格计算请求DTO + */ +@Data +public class MobilePriceCalculationRequest { + + /** + * 商品列表 + */ + private List products; + + /** + * 人脸ID(必填,用于权限验证) + */ + private Long faceId; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon = true; + + /** + * 用户输入的券码 + */ + private String voucherCode; + + /** + * 是否自动使用券码优惠 + */ + private Boolean autoUseVoucher = true; + + /** + * 是否仅预览优惠(不实际使用) + */ + private Boolean previewOnly = false; + + /** + * 转换为标准价格计算请求 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @return 标准价格计算请求 + */ + public PriceCalculationRequest toStandardRequest(Long userId, Long scenicId) { + PriceCalculationRequest request = new PriceCalculationRequest(); + request.setProducts(this.products); + request.setUserId(userId); + request.setScenicId(scenicId); + request.setFaceId(this.faceId); + request.setAutoUseCoupon(this.autoUseCoupon); + request.setVoucherCode(this.voucherCode); + request.setAutoUseVoucher(this.autoUseVoucher); + request.setPreviewOnly(this.previewOnly); + return request; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java index 3acd166..21a84ff 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/ICouponService.java @@ -3,6 +3,8 @@ package com.ycwl.basic.pricing.service; import com.ycwl.basic.pricing.dto.CouponInfo; import com.ycwl.basic.pricing.dto.CouponUseRequest; import com.ycwl.basic.pricing.dto.CouponUseResult; +import com.ycwl.basic.pricing.dto.CouponClaimRequest; +import com.ycwl.basic.pricing.dto.CouponClaimResult; import com.ycwl.basic.pricing.dto.ProductItem; import com.ycwl.basic.pricing.entity.PriceCouponConfig; @@ -59,4 +61,12 @@ public interface ICouponService { * @return 可用优惠券列表 */ List getUserAvailableCoupons(Long userId); + + /** + * 领取优惠券(内部调用方法) + * + * @param request 领取请求 + * @return 领取结果 + */ + CouponClaimResult claimCoupon(CouponClaimRequest request); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java b/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java index 4d2a562..28c6d07 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IDiscountProvider.java @@ -14,7 +14,7 @@ public interface IDiscountProvider { /** * 获取提供者类型 - * @return 提供者类型标识,如 "COUPON", "VOUCHER", "FLASH_SALE" 等 + * @return 提供者类型标识,如 "COUPON", "VOUCHER", "LIMITED_TIME" 等 */ String getProviderType(); diff --git a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java index a37de38..1e14571 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java +++ b/src/main/java/com/ycwl/basic/pricing/service/IPricingManagementService.java @@ -2,6 +2,8 @@ package com.ycwl.basic.pricing.service; import com.ycwl.basic.pricing.entity.*; +import java.math.BigDecimal; + /** * 价格管理服务接口(用于配置管理,手动处理时间字段) */ @@ -90,4 +92,21 @@ public interface IPricingManagementService { * 删除一口价配置 */ boolean deleteBundleConfig(Long id); + + // ==================== 快速设置价格 ==================== + + /** + * 快速设置商品基础价格(内部方法,如果存在则更新,不存在则新增) + * @param productType 商品类型 + * @param productId 商品ID + * @param scenicId 景区ID + * @param productName 商品名称 + * @param basePrice 基础价格 + * @param originalPrice 原价(可选,可为null) + * @param unit 价格单位(可选,可为null,默认为"元") + * @return 配置的ID + */ + Long quickSetupProductPrice(String productType, String productId, String scenicId, + String productName, BigDecimal basePrice, + BigDecimal originalPrice, String unit); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java index 82bbc13..7235076 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/CouponServiceImpl.java @@ -196,4 +196,90 @@ public class CouponServiceImpl implements ICouponService { info.setActualDiscountAmount(actualDiscountAmount); return info; } + + @Override + @Transactional + public CouponClaimResult claimCoupon(CouponClaimRequest request) { + log.info("开始领取优惠券: userId={}, couponId={}, scenicId={}", + request.getUserId(), request.getCouponId(), request.getScenicId()); + + try { + // 1. 参数验证 + if (request.getUserId() == null || request.getCouponId() == null) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_INVALID_PARAMS, "用户ID和优惠券ID不能为空"); + } + + // 2. 查询优惠券配置 + PriceCouponConfig coupon = couponConfigMapper.selectById(request.getCouponId()); + if (coupon == null || coupon.getDeleted() == 1) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_NOT_FOUND, "优惠券不存在"); + } + + // 3. 检查优惠券是否启用 + if (!Boolean.TRUE.equals(coupon.getIsActive())) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_INACTIVE, "优惠券已停用"); + } + + // 4. 检查优惠券有效期 + LocalDateTime now = LocalDateTime.now(); + if (coupon.getValidFrom() != null && now.isBefore(coupon.getValidFrom())) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_EXPIRED, "优惠券尚未生效"); + } + if (coupon.getValidUntil() != null && now.isAfter(coupon.getValidUntil())) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_EXPIRED, "优惠券已过期"); + } + + // 5. 检查库存(如果有总量限制) + if (coupon.getTotalQuantity() != null && coupon.getUsedQuantity() != null) { + if (coupon.getUsedQuantity() >= coupon.getTotalQuantity()) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, "优惠券已领完"); + } + } + + // 6. 检查用户是否已经领取过该优惠券 + PriceCouponClaimRecord existingRecord = couponClaimRecordMapper.selectUserCouponRecord( + request.getUserId(), request.getCouponId()); + if (existingRecord != null) { + return CouponClaimResult.failure(CouponClaimResult.ERROR_ALREADY_CLAIMED, "您已经领取过该优惠券"); + } + + // 7. 创建领取记录 + Date claimTime = new Date(); + PriceCouponClaimRecord claimRecord = new PriceCouponClaimRecord(); + claimRecord.setCouponId(request.getCouponId()); + claimRecord.setUserId(request.getUserId()); + claimRecord.setClaimTime(claimTime); + claimRecord.setStatus(CouponStatus.CLAIMED); + claimRecord.setScenicId(request.getScenicId()); + claimRecord.setCreateTime(claimTime); + claimRecord.setUpdateTime(claimTime); + claimRecord.setDeleted(0); + + // 8. 插入领取记录 + int insertResult = couponClaimRecordMapper.insert(claimRecord); + if (insertResult <= 0) { + log.error("插入优惠券领取记录失败: userId={}, couponId={}", + request.getUserId(), request.getCouponId()); + return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "领取失败,请稍后重试"); + } + + // 9. 更新优惠券已使用数量(如果有总量限制) + if (coupon.getTotalQuantity() != null) { + int updatedUsedQuantity = (coupon.getUsedQuantity() == null ? 0 : coupon.getUsedQuantity()) + 1; + coupon.setUsedQuantity(updatedUsedQuantity); + couponConfigMapper.updateById(coupon); + } + + log.info("优惠券领取成功: userId={}, couponId={}, claimRecordId={}", + request.getUserId(), request.getCouponId(), claimRecord.getId()); + + // 10. 返回成功结果 + return CouponClaimResult.success(claimRecord, coupon.getCouponName()); + + } catch (Exception e) { + log.error("领取优惠券失败: userId={}, couponId={}", + request.getUserId(), request.getCouponId(), e); + return CouponClaimResult.failure(CouponClaimResult.ERROR_SYSTEM_ERROR, "系统错误,领取失败:" + e.getMessage()); + } + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java index ec402f4..867e143 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/PricingManagementServiceImpl.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.util.Date; /** @@ -165,4 +166,49 @@ public class PricingManagementServiceImpl implements IPricingManagementService { log.info("删除一口价配置: id={}", id); return bundleConfigMapper.deleteById(id) > 0; } + + // ==================== 快速设置价格 ==================== + + @Override + @Transactional + public Long quickSetupProductPrice(String productType, String productId, String scenicId, + String productName, BigDecimal basePrice, + BigDecimal originalPrice, String unit) { + log.info("快速设置商品价格: productType={}, productId={}, scenicId={}, basePrice={}", + productType, productId, scenicId, basePrice); + + // 查询是否已存在配置 + PriceProductConfig existingConfig = productConfigMapper.selectByProductTypeAndId(productType, productId); + + if (existingConfig != null) { + // 存在则更新 + existingConfig.setScenicId(scenicId); + existingConfig.setProductName(productName); + existingConfig.setBasePrice(basePrice); + existingConfig.setOriginalPrice(originalPrice); + existingConfig.setUnit(unit != null ? unit : "元"); + existingConfig.setUpdateTime(new Date()); + + productConfigMapper.updateProductConfig(existingConfig); + log.info("更新商品价格配置: id={}", existingConfig.getId()); + return existingConfig.getId(); + } else { + // 不存在则新增 + PriceProductConfig newConfig = new PriceProductConfig(); + newConfig.setProductType(productType); + newConfig.setProductId(productId); + newConfig.setScenicId(scenicId); + newConfig.setProductName(productName); + newConfig.setBasePrice(basePrice); + newConfig.setOriginalPrice(originalPrice); + newConfig.setUnit(unit != null ? unit : "元"); + newConfig.setIsActive(true); + newConfig.setCreateTime(new Date()); + newConfig.setUpdateTime(new Date()); + + productConfigMapper.insertProductConfig(newConfig); + log.info("创建新商品价格配置: id={}", newConfig.getId()); + return newConfig.getId(); + } + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java index 19d178c..d74a4dc 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -238,6 +238,5 @@ public class VoucherPrintServiceImpl implements VoucherPrintService { content += "赠品兑换码"; content += "有效期:"+sdf2.format(new Date())+""; FeiETicketPrinter.doPrint("550519002", content, 1); - // 小票配对码、赠品兑换码、抵扣兑换码 } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/repository/ScenicRepository.java b/src/main/java/com/ycwl/basic/repository/ScenicRepository.java index 4016eb2..b3db8bd 100644 --- a/src/main/java/com/ycwl/basic/repository/ScenicRepository.java +++ b/src/main/java/com/ycwl/basic/repository/ScenicRepository.java @@ -1,30 +1,45 @@ package com.ycwl.basic.repository; -import com.ycwl.basic.utils.JacksonUtil; +import com.ycwl.basic.facebody.enums.FaceBodyAdapterType; +import com.ycwl.basic.integration.common.util.ConfigValueUtil; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2ListResponse; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2WithConfigDTO; +import com.ycwl.basic.integration.scenic.service.ScenicIntegrationService; +import com.ycwl.basic.integration.scenic.service.ScenicConfigIntegrationService; +import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO; import com.ycwl.basic.mapper.MpConfigMapper; import com.ycwl.basic.mapper.MpNotifyConfigMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.model.pc.mp.MpNotifyConfigEntity; import com.ycwl.basic.model.pc.mp.ScenicMpNotifyVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; +import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; +import com.ycwl.basic.pay.enums.PayAdapterType; +import com.ycwl.basic.storage.enums.StorageType; +import com.ycwl.basic.utils.JacksonUtil; +import com.ycwl.basic.util.ScenicConfigManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.List; +import java.util.Map; @Component public class ScenicRepository { - @Autowired - private ScenicMapper scenicMapper; @Autowired private MpConfigMapper mpConfigMapper; @Autowired private RedisTemplate redisTemplate; + @Autowired + private ScenicIntegrationService scenicIntegrationService; + @Autowired + private ScenicConfigIntegrationService scenicConfigIntegrationService; public static final String SCENIC_CACHE_KEY = "scenic:%s"; + public static final String SCENIC_BASIC_CACHE_KEY = "scenic:basic:%s"; public static final String SCENIC_FULL_CACHE_KEY = "scenic:f%s"; public static final String SCENIC_CONFIG_CACHE_KEY = "scenic:%s:config"; public static final String SCENIC_MP_CACHE_KEY = "scenic:%s:mp"; @@ -32,26 +47,78 @@ public class ScenicRepository { @Autowired private MpNotifyConfigMapper mpNotifyConfigMapper; + public ScenicV2DTO getScenicBasic(Long id) { + String key = String.format(SCENIC_BASIC_CACHE_KEY, id); + try { + ScenicV2DTO scenicDTO = scenicIntegrationService.getScenic(id); + + // 请求成功,写入缓存 + if (scenicDTO != null) { + redisTemplate.opsForValue().set( + key, + JacksonUtil.toJSONString(scenicDTO) + ); + } + return scenicDTO; + } catch (Exception e) { + // 请求失败,尝试从缓存获取历史成功数据 + String cacheKey = key; + if (redisTemplate.hasKey(cacheKey)) { + return JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), ScenicV2DTO.class); + } + // 缓存也没有,返回null + return null; + } + } + public ScenicEntity getScenic(Long id) { - if (redisTemplate.hasKey(String.format(SCENIC_CACHE_KEY, id))) { - return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(SCENIC_CACHE_KEY, id)), ScenicEntity.class); + String key = String.format(SCENIC_CACHE_KEY, id); + try { + ScenicV2WithConfigDTO scenicDTO = scenicIntegrationService.getScenicWithConfig(id); + ScenicEntity scenicEntity = convertToScenicEntity(scenicDTO); + + // 请求成功,写入缓存 + if (scenicEntity != null) { + redisTemplate.opsForValue().set( + key, + JacksonUtil.toJSONString(scenicEntity) + ); + } + return scenicEntity; + } catch (Exception e) { + // 请求失败,尝试从缓存获取历史成功数据 + String cacheKey = key; + if (redisTemplate.hasKey(cacheKey)) { + return JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), ScenicEntity.class); + } + // 缓存也没有,返回null + return null; } - ScenicEntity scenic = scenicMapper.get(id); - if (scenic != null) { - redisTemplate.opsForValue().set(String.format(SCENIC_CACHE_KEY, id), JacksonUtil.toJSONString(scenic)); - } - return scenic; } public ScenicConfigEntity getScenicConfig(Long scenicId) { - if (redisTemplate.hasKey(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId))) { - return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId)), ScenicConfigEntity.class); + String key = String.format(SCENIC_CONFIG_CACHE_KEY, scenicId); + try { + ScenicV2WithConfigDTO scenicWithConfigDTO = scenicIntegrationService.getScenicWithConfig(scenicId); + ScenicConfigEntity configEntity = convertToScenicConfigEntity(scenicWithConfigDTO, scenicId); + + // 请求成功,写入缓存 + if (configEntity != null) { + redisTemplate.opsForValue().set( + key, + JacksonUtil.toJSONString(configEntity) + ); + } + return configEntity; + } catch (Exception e) { + // 请求失败,尝试从缓存获取历史成功数据 + String cacheKey = key; + if (redisTemplate.hasKey(cacheKey)) { + return JacksonUtil.parseObject(redisTemplate.opsForValue().get(cacheKey), ScenicConfigEntity.class); + } + // 缓存也没有,返回null + return null; } - ScenicConfigEntity scenicConfig = scenicMapper.getConfig(scenicId); - if (scenicConfig != null) { - redisTemplate.opsForValue().set(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId), JacksonUtil.toJSONString(scenicConfig)); - } - return scenicConfig; } public MpConfigEntity getScenicMpConfig(Long scenicId) { @@ -121,12 +188,191 @@ public class ScenicRepository { return null; } + public List list(ScenicReqQuery scenicReqQuery) { + try { + // 将 ScenicReqQuery 参数转换为 zt-scenic 服务需要的参数 + Integer page = scenicReqQuery.getPageNum(); + Integer pageSize = scenicReqQuery.getPageSize(); + Integer status = null; + if (scenicReqQuery.getStatus() != null) { + status = Integer.valueOf(scenicReqQuery.getStatus()); + } + String name = scenicReqQuery.getName(); + + // 调用 zt-scenic 服务的 list 方法 + ScenicV2ListResponse response = scenicIntegrationService.listScenics(page, pageSize, status, name); + + // 将 ScenicV2DTO 列表转换为 ScenicEntity 列表 + if (response != null && response.getList() != null) { + return response.getList(); + } + return new java.util.ArrayList<>(); + } catch (Exception e) { + // 如果调用失败,返回空列表 + return new java.util.ArrayList<>(); + } + } + public void clearCache(Long scenicId) { redisTemplate.delete(String.format(SCENIC_CACHE_KEY, scenicId)); + redisTemplate.delete(String.format(SCENIC_BASIC_CACHE_KEY, scenicId)); redisTemplate.delete(String.format(SCENIC_FULL_CACHE_KEY, scenicId)); redisTemplate.delete(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId)); redisTemplate.delete(String.format(SCENIC_MP_CACHE_KEY, scenicId)); redisTemplate.delete(String.format(SCENIC_MP_NOTIFY_CACHE_KEY, scenicId)); } + + private ScenicEntity convertToScenicEntity(ScenicV2DTO dto) { + if (dto == null) { + return null; + } + ScenicEntity entity = new ScenicEntity(); + entity.setId(Long.parseLong(dto.getId())); + entity.setName(dto.getName()); + entity.setMpId(dto.getMpId()); + entity.setStatus(dto.getStatus().toString()); + return entity; + } + + private ScenicEntity convertToScenicEntity(ScenicV2WithConfigDTO dto) { + if (dto == null) { + return null; + } + ScenicEntity entity = new ScenicEntity(); + entity.setId(Long.parseLong(dto.getId())); + entity.setName(dto.getName()); + entity.setMpId(dto.getMpId()); + entity.setStatus(dto.getStatus().toString()); + if (dto.getConfig() != null) { + entity.setAddress(ConfigValueUtil.getStringValue(dto.getConfig(), "address")); + entity.setArea(ConfigValueUtil.getStringValue(dto.getConfig(), "area")); + entity.setCity(ConfigValueUtil.getStringValue(dto.getConfig(), "city")); + entity.setProvince(ConfigValueUtil.getStringValue(dto.getConfig(), "province")); + entity.setLatitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "latitude")); + entity.setLongitude(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "longitude")); + entity.setRadius(ConfigValueUtil.getBigDecimalValue(dto.getConfig(), "radius")); + entity.setPhone(ConfigValueUtil.getStringValue(dto.getConfig(), "phone")); + entity.setLogoUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "logoUrl")); + entity.setCoverUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "coverUrl")); + entity.setKfCodeUrl(ConfigValueUtil.getStringValue(dto.getConfig(), "kfCodeUrl")); + } + return entity; + } + + private ScenicConfigEntity convertToScenicConfigEntity(ScenicV2WithConfigDTO dto, Long scenicId) { + if (dto == null || dto.getConfig() == null) { + return null; + } + + ScenicConfigEntity entity = new ScenicConfigEntity(); + entity.setScenicId(scenicId); + + java.util.Map config = dto.getConfig(); + + entity.setBookRoutine(ConfigValueUtil.getIntValue(config, "bookRoutine")); + entity.setForceFinishTime(ConfigValueUtil.getIntValue(config, "forceFinishTime")); + entity.setTourTime(ConfigValueUtil.getIntValue(config, "tourTime")); + entity.setSampleStoreDay(ConfigValueUtil.getIntValue(config, "sampleStoreDay")); + entity.setFaceStoreDay(ConfigValueUtil.getIntValue(config, "faceStoreDay")); + entity.setVideoStoreDay(ConfigValueUtil.getIntValue(config, "videoStoreDay")); + entity.setAllFree(ConfigValueUtil.getBooleanValue(config, "allFree")); + entity.setDisableSourceVideo(ConfigValueUtil.getBooleanValue(config, "disableSourceVideo")); + entity.setDisableSourceImage(ConfigValueUtil.getBooleanValue(config, "disableSourceImage")); + entity.setTemplateNewVideoType(ConfigValueUtil.getIntValue(config, "templateNewVideoType")); + entity.setAntiScreenRecordType(ConfigValueUtil.getIntValue(config, "antiScreenRecordType")); + entity.setVideoSourceStoreDay(ConfigValueUtil.getIntValue(config, "videoSourceStoreDay")); + entity.setImageSourceStoreDay(ConfigValueUtil.getIntValue(config, "imageSourceStoreDay")); + entity.setUserSourceExpireDay(ConfigValueUtil.getIntValue(config, "userSourceExpireDay")); + entity.setFaceDetectHelperThreshold(ConfigValueUtil.getIntValue(config, "faceDetectHelperThreshold")); + entity.setPhotoFreeNum(ConfigValueUtil.getIntValue(config, "photoFreeNum")); + entity.setVideoFreeNum(ConfigValueUtil.getIntValue(config, "videoFreeNum")); + entity.setVoucherEnable(ConfigValueUtil.getBooleanValue(config, "voucherEnable")); + + entity.setFaceScoreThreshold(ConfigValueUtil.getFloatValue(config, "faceScoreThreshold")); + entity.setBrokerDirectRate(ConfigValueUtil.getBigDecimalValue(config, "brokerDirectRate")); + + entity.setWatermarkType(ConfigValueUtil.getStringValue(config, "watermarkType")); + entity.setWatermarkScenicText(ConfigValueUtil.getStringValue(config, "watermarkScenicText")); + entity.setWatermarkDtFormat(ConfigValueUtil.getStringValue(config, "watermarkDtFormat")); + entity.setImageSourcePackHint(ConfigValueUtil.getStringValue(config, "imageSourcePackHint")); + entity.setVideoSourcePackHint(ConfigValueUtil.getStringValue(config, "videoSourcePackHint")); + entity.setExtraNotificationTime(ConfigValueUtil.getStringValue(config, "extraNotificationTime")); + + entity.setStoreType(ConfigValueUtil.getEnumValue(config, "storeType", StorageType.class)); + entity.setStoreConfigJson(ConfigValueUtil.getStringValue(config, "storeConfigJson")); + entity.setTmpStoreType(ConfigValueUtil.getEnumValue(config, "tmpStoreType", StorageType.class)); + entity.setTmpStoreConfigJson(ConfigValueUtil.getStringValue(config, "tmpStoreConfigJson")); + entity.setLocalStoreType(ConfigValueUtil.getEnumValue(config, "localStoreType", StorageType.class)); + entity.setLocalStoreConfigJson(ConfigValueUtil.getStringValue(config, "localStoreConfigJson")); + + entity.setFaceType(ConfigValueUtil.getEnumValue(config, "faceType", FaceBodyAdapterType.class)); + entity.setFaceConfigJson(ConfigValueUtil.getStringValue(config, "faceConfigJson")); + + entity.setPayType(ConfigValueUtil.getEnumValue(config, "payType", PayAdapterType.class)); + entity.setPayConfigJson(ConfigValueUtil.getStringValue(config, "payConfigJson")); + + return entity; + } + + /** + * 获取景区配置管理器 + * + * @param scenicId 景区ID + * @return ScenicConfigManager实例,如果获取失败返回null + */ + public ScenicConfigManager getScenicConfigManager(Long scenicId) { + try { + List configList = scenicConfigIntegrationService.listConfigs(scenicId); + if (configList != null) { + return new ScenicConfigManager(configList); + } + return null; + } catch (Exception e) { + return null; + } + } + + /** + * 获取景区配置管理器,带缓存支持 + * + * @param scenicId 景区ID + * @return ScenicConfigManager实例,如果获取失败返回null + */ + public ScenicConfigManager getScenicConfigManagerWithCache(Long scenicId) { + String key = String.format(SCENIC_CONFIG_CACHE_KEY + ":manager", scenicId); + + try { + List configList = + scenicConfigIntegrationService.listConfigs(scenicId); + if (configList != null) { + ScenicConfigManager manager = new ScenicConfigManager(configList); + + // 请求成功,写入缓存(将配置列表序列化存储) + redisTemplate.opsForValue().set( + key, + JacksonUtil.toJSONString(configList) + ); + + return manager; + } + return null; + } catch (Exception e) { + // 请求失败,尝试从缓存获取历史成功数据 + if (redisTemplate.hasKey(key)) { + try { + String cachedConfigJson = redisTemplate.opsForValue().get(key); + @SuppressWarnings("unchecked") + List cachedConfigList = + JacksonUtil.parseArray(cachedConfigJson, ScenicConfigV2DTO.class); + return new ScenicConfigManager(cachedConfigList); + } catch (Exception cacheException) { + // 缓存解析失败,返回null + return null; + } + } + // 缓存也没有,返回null + return null; + } + } } diff --git a/src/main/java/com/ycwl/basic/service/PriceCacheService.java b/src/main/java/com/ycwl/basic/service/PriceCacheService.java new file mode 100644 index 0000000..6962b8e --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/PriceCacheService.java @@ -0,0 +1,238 @@ +package com.ycwl.basic.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ycwl.basic.pricing.dto.PriceCalculationResult; +import com.ycwl.basic.pricing.dto.ProductItem; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.List; + +/** + * 价格缓存服务 + * 使用 Redis 实现价格查询结果的缓存,缓存时间为5分钟 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PriceCacheService { + + private final StringRedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + // 缓存Key前缀 + private static final String CACHE_PREFIX = "pricing:cache:"; + + // 缓存过期时间:5分钟 + private static final Duration CACHE_DURATION = Duration.ofMinutes(5); + + /** + * 生成缓存键 + * 格式:pricing:cache:{userId}:{scenicId}:{productsHash} + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @return 缓存键 + */ + public String generateCacheKey(Long userId, Long scenicId, List products) { + String productsHash = calculateProductsHash(products); + return CACHE_PREFIX + userId + ":" + scenicId + ":" + productsHash; + } + + /** + * 计算商品列表的哈希值 + * 基于商品类型、ID、购买数量等必传字段生成唯一哈希,忽略可选的quantity字段 + * + * @param products 商品列表 + * @return 哈希值 + */ + private String calculateProductsHash(List products) { + try { + // 构建缓存用的商品信息字符串,只包含必传字段 + StringBuilder cacheInfo = new StringBuilder(); + for (ProductItem product : products) { + cacheInfo.append(product.getProductType()).append(":") + .append(product.getProductId()).append(":") + .append(product.getPurchaseCount() != null ? product.getPurchaseCount() : 1).append(":") + .append(product.getScenicId()).append(";"); + } + + // 计算SHA-256哈希 + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(cacheInfo.toString().getBytes()); + + // 转换为16进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + // 返回前16位作为哈希值(足够避免冲突) + return hexString.substring(0, 16); + + } catch (NoSuchAlgorithmException e) { + log.error("计算商品列表哈希值失败", e); + // 降级策略:使用关键字段的hashCode + StringBuilder fallback = new StringBuilder(); + for (ProductItem product : products) { + fallback.append(product.getProductType()).append(":") + .append(product.getProductId()).append(":") + .append(product.getPurchaseCount() != null ? product.getPurchaseCount() : 1).append(";"); + } + return String.valueOf(fallback.toString().hashCode()); + } + } + + /** + * 缓存价格计算结果 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @param result 价格计算结果 + * @return 缓存键 + */ + public String cachePriceResult(Long userId, Long scenicId, List products, PriceCalculationResult result) { + String cacheKey = generateCacheKey(userId, scenicId, products); + + try { + String resultJson = objectMapper.writeValueAsString(result); + redisTemplate.opsForValue().set(cacheKey, resultJson, CACHE_DURATION); + + log.info("价格计算结果已缓存: cacheKey={}, userId={}, scenicId={}", + cacheKey, userId, scenicId); + + return cacheKey; + + } catch (JsonProcessingException e) { + log.error("缓存价格计算结果失败: userId={}, scenicId={}", userId, scenicId, e); + return null; + } + } + + /** + * 获取缓存的价格计算结果 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @return 缓存的价格计算结果,如果不存在则返回null + */ + public PriceCalculationResult getCachedPriceResult(Long userId, Long scenicId, List products) { + String cacheKey = generateCacheKey(userId, scenicId, products); + return getCachedPriceResult(cacheKey); + } + + /** + * 根据缓存键获取价格计算结果 + * + * @param cacheKey 缓存键 + * @return 缓存的价格计算结果,如果不存在则返回null + */ + public PriceCalculationResult getCachedPriceResult(String cacheKey) { + try { + String resultJson = redisTemplate.opsForValue().get(cacheKey); + if (resultJson == null) { + log.debug("价格缓存不存在: cacheKey={}", cacheKey); + return null; + } + + PriceCalculationResult result = objectMapper.readValue(resultJson, PriceCalculationResult.class); + log.info("获取到缓存的价格计算结果: cacheKey={}", cacheKey); + + return result; + + } catch (JsonProcessingException e) { + log.error("解析缓存的价格计算结果失败: cacheKey={}", cacheKey, e); + return null; + } + } + + /** + * 检查价格缓存是否存在 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @return true如果缓存存在且未过期 + */ + public boolean isCacheExists(Long userId, Long scenicId, List products) { + String cacheKey = generateCacheKey(userId, scenicId, products); + return Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey)); + } + + /** + * 验证并消费价格缓存(一次性使用) + * 查询缓存结果后立即删除,确保缓存只能被使用一次 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @return 缓存的价格计算结果,如果不存在则返回null + */ + public PriceCalculationResult validateAndConsumePriceCache(Long userId, Long scenicId, List products) { + String cacheKey = generateCacheKey(userId, scenicId, products); + + try { + // 使用 Redis 的 GETDEL 命令原子性地获取并删除缓存 + String resultJson = redisTemplate.opsForValue().getAndDelete(cacheKey); + + if (resultJson == null) { + log.warn("价格缓存不存在或已过期: userId={}, scenicId={}, cacheKey={}", + userId, scenicId, cacheKey); + return null; + } + + PriceCalculationResult result = objectMapper.readValue(resultJson, PriceCalculationResult.class); + log.info("价格缓存验证成功并已消费: userId={}, scenicId={}, cacheKey={}", + userId, scenicId, cacheKey); + + return result; + + } catch (JsonProcessingException e) { + log.error("解析价格缓存失败: userId={}, scenicId={}, cacheKey={}", + userId, scenicId, cacheKey, e); + // 缓存解析失败时也要删除无效缓存 + redisTemplate.delete(cacheKey); + return null; + } + } + + /** + * 删除价格缓存 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @return true如果删除成功 + */ + public boolean deletePriceCache(Long userId, Long scenicId, List products) { + String cacheKey = generateCacheKey(userId, scenicId, products); + return Boolean.TRUE.equals(redisTemplate.delete(cacheKey)); + } + + /** + * 获取缓存剩余过期时间 + * + * @param userId 用户ID + * @param scenicId 景区ID + * @param products 商品列表 + * @return 剩余时间(秒),-1表示永不过期,-2表示不存在 + */ + public long getCacheExpireTime(Long userId, Long scenicId, List products) { + String cacheKey = generateCacheKey(userId, scenicId, products); + Long expire = redisTemplate.getExpire(cacheKey); + return expire != null ? expire : -2; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/service/custom/CustomUploadTaskService.java b/src/main/java/com/ycwl/basic/service/custom/CustomUploadTaskService.java index 14251de..44ef69a 100644 --- a/src/main/java/com/ycwl/basic/service/custom/CustomUploadTaskService.java +++ b/src/main/java/com/ycwl/basic/service/custom/CustomUploadTaskService.java @@ -13,7 +13,6 @@ import com.ycwl.basic.facebody.entity.AddFaceResp; import com.ycwl.basic.mapper.DeviceMapper; import com.ycwl.basic.mapper.CustomUploadTaskMapper; import com.ycwl.basic.mapper.FaceSampleMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.custom.entity.CustomUploadTaskEntity; diff --git a/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java b/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java index b2168f1..1be26b3 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java +++ b/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java @@ -9,6 +9,7 @@ import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO; import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterReq; import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterRespVO; import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; +import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.utils.ApiResponse; @@ -20,7 +21,7 @@ import java.util.List; * @Date:2024/12/6 10:23 */ public interface AppScenicService { - ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery); + ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery); ApiResponse deviceCountByScenicId(Long scenicId); diff --git a/src/main/java/com/ycwl/basic/service/mobile/impl/AppScenicServiceImpl.java b/src/main/java/com/ycwl/basic/service/mobile/impl/AppScenicServiceImpl.java index 3a7825b..c9bd91f 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/impl/AppScenicServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/mobile/impl/AppScenicServiceImpl.java @@ -1,10 +1,11 @@ package com.ycwl.basic.service.mobile.impl; import cn.hutool.core.bean.BeanUtil; -import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; -import com.ycwl.basic.constant.BaseContextHandler; -import com.ycwl.basic.mapper.*; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; +import com.ycwl.basic.mapper.DeviceMapper; +import com.ycwl.basic.mapper.ExtraDeviceMapper; +import com.ycwl.basic.mapper.ScenicAccountMapper; import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; @@ -16,9 +17,11 @@ import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterRespVO; import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity; +import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.repository.DeviceRepository; +import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.pc.ScenicAccountService; import com.ycwl.basic.utils.ApiResponse; @@ -30,6 +33,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -43,8 +47,6 @@ import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; @Service public class AppScenicServiceImpl implements AppScenicService { - @Autowired - private ScenicMapper scenicMapper; @Autowired private DeviceMapper deviceMapper; @Autowired @@ -59,12 +61,19 @@ public class AppScenicServiceImpl implements AppScenicService { private ExtraDeviceMapper extraDeviceMapper; @Autowired private RedisTemplate redisTemplate; + @Autowired + private ScenicRepository scenicRepository; @Override - public ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery) { - PageHelper.startPage(scenicReqQuery.getPageNum(), scenicReqQuery.getPageSize()); - List list = scenicMapper.appList(scenicReqQuery); - PageInfo pageInfo = new PageInfo<>(list); + public ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery) { + + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); + List list = scenicList.stream().map(scenic -> { + return scenicRepository.getScenic(Long.valueOf(scenic.getId())); + }).toList(); + PageInfo pageInfo = new PageInfo<>(list); return ApiResponse.success(pageInfo); } @@ -77,7 +86,27 @@ public class AppScenicServiceImpl implements AppScenicService { @Override public ApiResponse getDetails(Long id) { - ScenicRespVO scenicRespVO = scenicMapper.getAppById(id); + ScenicEntity scenic = scenicRepository.getScenic(id); + ScenicRespVO scenicRespVO = new ScenicRespVO(); + + // 将ScenicEntity的值通过set/get方式写入到ScenicRespVO + if (scenic != null) { + scenicRespVO.setId(scenic.getId()); + scenicRespVO.setName(scenic.getName()); + scenicRespVO.setPhone(scenic.getPhone()); + scenicRespVO.setLogoUrl(scenic.getLogoUrl()); + scenicRespVO.setCoverUrl(scenic.getCoverUrl()); + scenicRespVO.setIntroduction(scenic.getIntroduction()); + scenicRespVO.setLongitude(scenic.getLongitude()); + scenicRespVO.setLatitude(scenic.getLatitude()); + scenicRespVO.setRadius(scenic.getRadius()); + scenicRespVO.setProvince(scenic.getProvince()); + scenicRespVO.setCity(scenic.getCity()); + scenicRespVO.setArea(scenic.getArea()); + scenicRespVO.setAddress(scenic.getAddress()); + scenicRespVO.setKfCodeUrl(scenic.getKfCodeUrl()); + } + ScenicDeviceCountVO scenicDeviceCountVO = deviceMapper.deviceCountByScenicId(id); scenicRespVO.setLensNum(scenicDeviceCountVO.getTotalDeviceCount()); return ApiResponse.success(scenicRespVO); @@ -171,8 +200,59 @@ public class AppScenicServiceImpl implements AppScenicService { @Override public List scenicListByLnLa(ScenicIndexVO scenicIndexVO) { - List scenicAppVOS = scenicMapper.scenicListByLnLa(scenicIndexVO); - return scenicAppVOS.stream().filter(scenic -> scenic.getDistance().compareTo(scenic.getRadius().multiply(BigDecimal.valueOf(1_000L))) < 0).toList(); + // 从 scenicRepository 获取所有景区(1000个) + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageNum(1); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); + + List list = new ArrayList<>(); + + // 为每个景区获取详细信息(包含经纬度) + for (ScenicV2DTO scenicDTO : scenicList) { + try { + // 获取景区详细信息(包含经纬度) + ScenicEntity scenicEntity = scenicRepository.getScenic(Long.parseLong(scenicDTO.getId())); + if (scenicEntity != null && scenicEntity.getLatitude() != null && scenicEntity.getLongitude() != null) { + // 计算距离 + BigDecimal distance = calculateDistance( + scenicIndexVO.getLatitude(), + scenicIndexVO.getLongitude(), + scenicEntity.getLatitude(), + scenicEntity.getLongitude() + ); + + // 根据距离和范围筛选景区 + if (scenicEntity.getRadius() != null && + distance.compareTo(scenicEntity.getRadius().multiply(BigDecimal.valueOf(1_000L))) < 0) { + + // 转换为 ScenicAppVO + ScenicAppVO scenicAppVO = new ScenicAppVO(); + scenicAppVO.setId(scenicEntity.getId()); + scenicAppVO.setName(scenicEntity.getName()); + scenicAppVO.setPhone(scenicEntity.getPhone()); + scenicAppVO.setIntroduction(scenicEntity.getIntroduction()); + scenicAppVO.setCoverUrl(scenicEntity.getCoverUrl()); + scenicAppVO.setLongitude(scenicEntity.getLongitude()); + scenicAppVO.setLatitude(scenicEntity.getLatitude()); + scenicAppVO.setRadius(scenicEntity.getRadius()); + scenicAppVO.setProvince(scenicEntity.getProvince()); + scenicAppVO.setCity(scenicEntity.getCity()); + scenicAppVO.setArea(scenicEntity.getArea()); + scenicAppVO.setAddress(scenicEntity.getAddress()); + scenicAppVO.setDistance(distance); + scenicAppVO.setDeviceNum(deviceRepository.getAllDeviceByScenicId(scenicEntity.getId()).size()); + + list.add(scenicAppVO); + } + } + } catch (Exception e) { + // 单个景区获取失败,继续处理下一个 + continue; + } + } + + return list; } @Override @@ -214,4 +294,29 @@ public class AppScenicServiceImpl implements AppScenicService { deviceRespVOList.addAll(0, extraDeviceList); return ApiResponse.success(deviceRespVOList); } + + /** + * 计算两点之间的距离(米) + * 使用 Haversine 公式 + */ + private BigDecimal calculateDistance(BigDecimal lat1, BigDecimal lon1, BigDecimal lat2, BigDecimal lon2) { + if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) { + return BigDecimal.ZERO; + } + + final double R = 6371000; // 地球半径(米) + + double lat1Rad = Math.toRadians(lat1.doubleValue()); + double lat2Rad = Math.toRadians(lat2.doubleValue()); + double deltaLat = Math.toRadians(lat2.subtract(lat1).doubleValue()); + double deltaLon = Math.toRadians(lon2.subtract(lon1).doubleValue()); + + double a = Math.sin(deltaLat/2) * Math.sin(deltaLat/2) + + Math.cos(lat1Rad) * Math.cos(lat2Rad) * + Math.sin(deltaLon/2) * Math.sin(deltaLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + double distance = R * c; + return BigDecimal.valueOf(distance); + } } diff --git a/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java b/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java index 0ae63a9..0a2c89d 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java @@ -139,9 +139,9 @@ public class GoodsServiceImpl implements GoodsService { List goods = faceEntry.getValue(); return goods.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).keySet().stream().filter(type -> { if (Integer.valueOf(1).equals(type)) { - return !Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo()); + return !Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo()); } else if (Integer.valueOf(2).equals(type)) { - return !Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage()); + return !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage()); } return true; }).map(type -> { diff --git a/src/main/java/com/ycwl/basic/service/pc/ScenicService.java b/src/main/java/com/ycwl/basic/service/pc/ScenicService.java index 15ea13d..c70fc51 100644 --- a/src/main/java/com/ycwl/basic/service/pc/ScenicService.java +++ b/src/main/java/com/ycwl/basic/service/pc/ScenicService.java @@ -1,11 +1,8 @@ package com.ycwl.basic.service.pc; -import com.github.pagehelper.PageInfo; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; -import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; -import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.pay.adapter.IPayAdapter; import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.utils.ApiResponse; @@ -17,23 +14,7 @@ import java.util.List; * @Date:2024/12/3 15:22 */ public interface ScenicService { - ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery); - ApiResponse> list(ScenicReqQuery scenicReqQuery); - ApiResponse getById(Long id); - ApiResponse add(ScenicAddOrUpdateReq scenicAddOrUpdateReq); - ApiResponse deleteById(Long id); - ApiResponse update(ScenicAddOrUpdateReq scenicAddOrUpdateReq); - ApiResponse updateStatus(Long id); - ApiResponse addConfig(ScenicConfigEntity scenicConfig); - /** - * 修改景区配置 - * @param scenicConfig - * @return - */ - ApiResponse updateConfigById(ScenicConfigEntity scenicConfig); - - ScenicConfigEntity getConfig(Long id); - void saveConfig(Long configId, ScenicConfigEntity config); + ApiResponse> list(ScenicReqQuery scenicReqQuery); IStorageAdapter getScenicStorageAdapter(Long scenicId); diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java index f262405..b8f5664 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java @@ -324,7 +324,7 @@ public class FaceServiceImpl implements FaceService { } } else { // 重新切视频逻辑 - if (scenicConfig != null && !Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { + if (scenicConfig != null && !Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) { long videoCount = memberSourceEntityList.stream().filter(item -> item.getType().equals(1)).count(); long photoCount = memberSourceEntityList.stream().filter(item -> item.getType().equals(2)).count(); if (photoCount > videoCount) { @@ -443,7 +443,7 @@ public class FaceServiceImpl implements FaceService { sourceVideoContent.setLockType(-1); sourceImageContent.setLockType(-1); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(faceRespVO.getScenicId()); - if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) { + if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) { IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 2, faceId); sourceImageContent.setSourceType(isBuyRespVO.getGoodsType()); sourceImageContent.setContentId(isBuyRespVO.getGoodsId()); @@ -462,7 +462,7 @@ public class FaceServiceImpl implements FaceService { sourceImageContent.setFreeCount((int) freeCount); contentList.add(sourceImageContent); } - if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { + if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) { IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 1, faceId); sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType()); sourceVideoContent.setContentId(isBuyRespVO.getGoodsId()); diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java index 1832c1c..6c12663 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/OrderServiceImpl.java @@ -57,6 +57,7 @@ import com.ycwl.basic.utils.SnowFlakeUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.stereotype.Service; @@ -78,7 +79,7 @@ import java.util.stream.Collectors; * @Date:2024/12/3 13:54 */ @Slf4j -@Service +@Service("OrderServiceOldImpl") @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) public class OrderServiceImpl implements OrderService { @@ -86,6 +87,7 @@ public class OrderServiceImpl implements OrderService { private OrderMapper orderMapper; @Autowired + @Qualifier(value = "OrderServiceOldImpl") private OrderServiceImpl self; @Autowired private SourceMapper sourceMapper; diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java index 5f5a6ca..232299e 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/ScenicServiceImpl.java @@ -1,38 +1,29 @@ package com.ycwl.basic.service.pc.impl; -import com.ycwl.basic.utils.JacksonUtil; -import com.github.pagehelper.PageHelper; -import com.github.pagehelper.PageInfo; import com.ycwl.basic.facebody.FaceBodyFactory; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; -import com.ycwl.basic.mapper.ScenicAccountMapper; -import com.ycwl.basic.mapper.ScenicMapper; -import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; -import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.pay.PayFactory; import com.ycwl.basic.pay.adapter.IPayAdapter; import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.pc.ScenicService; -import com.ycwl.basic.service.task.TaskFaceService; import com.ycwl.basic.storage.StorageFactory; import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.exceptions.StorageUnsupportedException; +import com.ycwl.basic.util.ScenicConfigManager; +import com.ycwl.basic.util.TtlCacheMap; import com.ycwl.basic.utils.ApiResponse; -import com.ycwl.basic.utils.SnowFlakeUtil; +import com.ycwl.basic.utils.JacksonUtil; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME; +import java.util.concurrent.TimeUnit; /** * @Author:longbinbin @@ -41,199 +32,39 @@ import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME; @Slf4j @Service public class ScenicServiceImpl implements ScenicService { - @Autowired - private ScenicMapper scenicMapper; - @Autowired - private ScenicAccountMapper scenicAccountMapper; - @Autowired - private TaskFaceService taskFaceService; @Autowired private ScenicRepository scenicRepository; + + // TTL缓存配置,默认10分钟过期 + private static final long DEFAULT_CACHE_TTL_MINUTES = 10; + + // 使用TTL缓存替代静态Map + private static final TtlCacheMap scenicStorageAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicTmpStorageAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicLocalStorageAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicFaceBodyAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); + private static final TtlCacheMap scenicPayAdapterCache = + new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES)); @Override - public ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery) { - PageHelper.startPage(scenicReqQuery.getPageNum(), scenicReqQuery.getPageSize()); - List list = scenicMapper.list(scenicReqQuery); - PageInfo pageInfo = new PageInfo<>(list); - return ApiResponse.success(pageInfo); + @Deprecated + public ApiResponse> list(ScenicReqQuery scenicReqQuery) { + return ApiResponse.success(scenicRepository.list(scenicReqQuery)); } - @Override - public ApiResponse> list(ScenicReqQuery scenicReqQuery) { - return ApiResponse.success(scenicMapper.list(scenicReqQuery)); - } - - @Override - public ApiResponse getById(Long id) { - return ApiResponse.success(scenicMapper.getById(id)); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public ApiResponse add(ScenicAddOrUpdateReq scenicAddReq) { - Long scenicId = SnowFlakeUtil.getLongId(); - scenicAddReq.setId(scenicId); - int add = scenicMapper.add(scenicAddReq); - ScenicAccountEntity scenicAccount = scenicAccountMapper.getByAccount(scenicAddReq.getAccount()); - if (scenicAccount == null) { - scenicAccount = new ScenicAccountEntity(); - scenicAccount.setId(SnowFlakeUtil.getLongId()); - scenicAccount.setName(scenicAddReq.getName() + "管理员"); - scenicAccount.setAccount(scenicAddReq.getAccount()); - scenicAccount.setPassword(scenicAddReq.getPassword()); - scenicAccount.setIsSuper(1); - scenicAccountMapper.add(scenicAccount); - } - scenicAccountMapper.addAccountScenicRelation(scenicAccount.getId(), scenicId, 1); - if (add > 0) { - return ApiResponse.success(true); - } else { - return ApiResponse.fail("景区添加失败"); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public ApiResponse deleteById(Long id) { - int i = scenicMapper.deleteById(id); - if (i > 0) { - scenicAccountMapper.deleteRelationByScenicId(id); - IFaceBodyAdapter adapter = getScenicFaceBodyAdapter(id); - Thread.ofVirtual().start(() -> { - adapter.deleteFaceDb(id.toString()); - adapter.deleteFaceDb(USER_FACE_DB_NAME + id); - }); - scenicMapper.deleteConfigByScenicId(id); - scenicRepository.clearCache(id); - scenicFaceBodyAdapterMap.remove(id); - scenicStorageAdapterMap.remove(id); - scenicPayAdapterMap.remove(id); - return ApiResponse.success(true); - }else { - return ApiResponse.fail("景区删除失败"); - } - } - - @Override - public ApiResponse update(ScenicAddOrUpdateReq scenicUpdateReq) { - if (scenicUpdateReq.getId() == null) { - return ApiResponse.fail("参数错误"); - } - if (StringUtils.isNotBlank(scenicUpdateReq.getAccount()) && StringUtils.isNotBlank(scenicUpdateReq.getPassword())) { - ScenicAccountEntity scenicAccount = scenicAccountMapper.getByAccount(scenicUpdateReq.getAccount()); - if (scenicAccount != null) { - if (!scenicAccount.getScenicId().equals(scenicUpdateReq.getId())) { - return ApiResponse.fail("账号已存在"); - } - } - ScenicAccountEntity account = scenicAccountMapper.getSuperAccountOfScenic(scenicUpdateReq.getId()); - if (account != null) { - account.setAccount(scenicUpdateReq.getAccount()); - account.setPassword(scenicUpdateReq.getPassword()); - scenicAccountMapper.update(account); - } else { - account = new ScenicAccountEntity(); - account.setId(SnowFlakeUtil.getLongId()); - account.setName(scenicUpdateReq.getName() + "管理员"); - account.setAccount(scenicUpdateReq.getAccount()); - account.setPassword(scenicUpdateReq.getPassword()); - account.setIsSuper(1); - scenicAccountMapper.add(account); - } - scenicAccountMapper.addAccountScenicRelation(account.getId(), scenicUpdateReq.getId(), 1); - } - int i = scenicMapper.update(scenicUpdateReq); - if (i > 0) { - scenicRepository.clearCache(scenicUpdateReq.getId()); - scenicFaceBodyAdapterMap.remove(scenicUpdateReq.getId()); - scenicStorageAdapterMap.remove(scenicUpdateReq.getId()); - scenicTmpStorageAdapterMap.remove(scenicUpdateReq.getId()); - scenicLocalStorageAdapterMap.remove(scenicUpdateReq.getId()); - scenicPayAdapterMap.remove(scenicUpdateReq.getId()); - return ApiResponse.success(true); - }else { - return ApiResponse.fail("景区修改失败"); - } - - } - - @Override - public ApiResponse updateStatus(Long id) { - int i = scenicMapper.updateStatus(id); - if (i > 0) { - scenicRepository.clearCache(id); - scenicFaceBodyAdapterMap.remove(id); - scenicStorageAdapterMap.remove(id); - scenicTmpStorageAdapterMap.remove(id); - scenicLocalStorageAdapterMap.remove(id); - scenicPayAdapterMap.remove(id); - return ApiResponse.success(true); - }else { - return ApiResponse.fail("景区状态修改失败"); - } - } - - @Override - public ApiResponse addConfig(ScenicConfigEntity scenicConfig) { - if (scenicConfig.getId() == null) { - scenicConfig.setId(SnowFlakeUtil.getLongId()); - } - int i = scenicMapper.addConfig(scenicConfig); - if (i > 0) { - scenicRepository.clearCache(scenicConfig.getScenicId()); - return ApiResponse.success(true); - }else { - return ApiResponse.fail("景区配置添加失败"); - } - } - - @Override - public ApiResponse updateConfigById(ScenicConfigEntity scenicConfig) { - int i = scenicMapper.updateConfigById(scenicConfig); - if (i > 0) { - scenicRepository.clearCache(scenicConfig.getScenicId()); - scenicFaceBodyAdapterMap.remove(scenicConfig.getScenicId()); - scenicStorageAdapterMap.remove(scenicConfig.getScenicId()); - scenicTmpStorageAdapterMap.remove(scenicConfig.getScenicId()); - scenicLocalStorageAdapterMap.remove(scenicConfig.getScenicId()); - scenicPayAdapterMap.remove(scenicConfig.getScenicId()); - return ApiResponse.success(true); - }else { - return ApiResponse.fail("景区配置修改失败"); - } - } - - @Override - public ScenicConfigEntity getConfig(Long id) { - return scenicRepository.getScenicConfig(id); - } - - @Override - public void saveConfig(Long configId, ScenicConfigEntity config) { - config.setId(configId); - if (config.getScenicId() == null) { - throw new RuntimeException("景区ID不能为空"); - } - scenicMapper.updateConfigById(config); - scenicRepository.clearCache(config.getScenicId()); - scenicFaceBodyAdapterMap.remove(config.getScenicId()); - scenicStorageAdapterMap.remove(config.getScenicId()); - scenicTmpStorageAdapterMap.remove(config.getScenicId()); - scenicLocalStorageAdapterMap.remove(config.getScenicId()); - scenicPayAdapterMap.remove(config.getScenicId()); - } - - - private static final Map scenicStorageAdapterMap = new ConcurrentHashMap<>(); @Override public IStorageAdapter getScenicStorageAdapter(Long scenicId) { - return scenicStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicStorageAdapterCache.computeIfAbsent(scenicId, (key) -> { IStorageAdapter adapter; - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); - if (scenicConfig != null && scenicConfig.getStoreType() != null) { + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); + if (Strings.isNotBlank(scenicConfig.getString("store_type"))) { try { - adapter = StorageFactory.get(scenicConfig.getStoreType()); - adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getStoreConfigJson(), Map.class)); + adapter = StorageFactory.get(scenicConfig.getString("store_type")); + adapter.loadConfig(scenicConfig.getObject("store_config_json", Map.class)); } catch (StorageUnsupportedException ignored) { return StorageFactory.use("video"); } @@ -243,16 +74,15 @@ public class ScenicServiceImpl implements ScenicService { return adapter; }); } - private static final Map scenicTmpStorageAdapterMap = new ConcurrentHashMap<>(); @Override public IStorageAdapter getScenicTmpStorageAdapter(Long scenicId) { - return scenicTmpStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicTmpStorageAdapterCache.computeIfAbsent(scenicId, (key) -> { IStorageAdapter adapter; - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); - if (scenicConfig != null && scenicConfig.getTmpStoreType() != null) { + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); + if (Strings.isNotBlank(scenicConfig.getString("tmp_store_type"))) { try { - adapter = StorageFactory.get(scenicConfig.getTmpStoreType()); - adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getTmpStoreConfigJson(), Map.class)); + adapter = StorageFactory.get(scenicConfig.getString("tmp_store_type")); + adapter.loadConfig(scenicConfig.getObject("tmp_store_config_json", Map.class)); } catch (StorageUnsupportedException ignored) { return getScenicStorageAdapter(scenicId); } @@ -262,16 +92,15 @@ public class ScenicServiceImpl implements ScenicService { return adapter; }); } - private static final Map scenicLocalStorageAdapterMap = new ConcurrentHashMap<>(); @Override public IStorageAdapter getScenicLocalStorageAdapter(Long scenicId) { - return scenicLocalStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicLocalStorageAdapterCache.computeIfAbsent(scenicId, (key) -> { IStorageAdapter adapter; - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); - if (scenicConfig != null && scenicConfig.getLocalStoreType() != null) { + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); + if (Strings.isNotBlank(scenicConfig.getString("local_store_type"))) { try { - adapter = StorageFactory.get(scenicConfig.getLocalStoreType()); - adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getLocalStoreConfigJson(), Map.class)); + adapter = StorageFactory.get(scenicConfig.getString("local_store_type")); + adapter.loadConfig(scenicConfig.getObject("local_store_config_json", Map.class)); } catch (StorageUnsupportedException ignored) { return getScenicStorageAdapter(scenicId); } @@ -282,15 +111,14 @@ public class ScenicServiceImpl implements ScenicService { }); } - private static final Map scenicFaceBodyAdapterMap = new ConcurrentHashMap<>(); @Override public IFaceBodyAdapter getScenicFaceBodyAdapter(Long scenicId) { - return scenicFaceBodyAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicFaceBodyAdapterCache.computeIfAbsent(scenicId, (key) -> { IFaceBodyAdapter adapter; - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); - if (scenicConfig != null && scenicConfig.getFaceType() != null) { - adapter = FaceBodyFactory.getAdapter(scenicConfig.getFaceType()); - adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getFaceConfigJson(), Map.class)); + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); + if (Strings.isNotBlank(scenicConfig.getString("face_type"))) { + adapter = FaceBodyFactory.getAdapter(scenicConfig.getString("face_type")); + adapter.loadConfig(scenicConfig.getObject("face_config_json", Map.class)); } else { adapter = FaceBodyFactory.use(); } @@ -298,19 +126,108 @@ public class ScenicServiceImpl implements ScenicService { }); } - private static final Map scenicPayAdapterMap = new ConcurrentHashMap<>(); @Override public IPayAdapter getScenicPayAdapter(Long scenicId) { - return scenicPayAdapterMap.computeIfAbsent(scenicId, (key) -> { + return scenicPayAdapterCache.computeIfAbsent(scenicId, (key) -> { IPayAdapter adapter; - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); - if (scenicConfig != null && scenicConfig.getPayType() != null) { - adapter = PayFactory.getAdapter(scenicConfig.getPayType()); - adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getPayConfigJson(), Map.class)); + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId); + if (Strings.isNotBlank(scenicConfig.getString("pay_type"))) { + adapter = PayFactory.getAdapter(scenicConfig.getString("pay_type")); + adapter.loadConfig(scenicConfig.getObject("pay_config_json", Map.class)); } else { adapter = PayFactory.use(); } return adapter; }); } + + // ==================== 缓存管理方法 ==================== + + /** + * 清除指定景区的所有适配器缓存 + * + * @param scenicId 景区ID + */ + public void clearScenicAdapterCache(Long scenicId) { + log.info("清除景区 {} 的所有适配器缓存", scenicId); + scenicStorageAdapterCache.remove(scenicId); + scenicTmpStorageAdapterCache.remove(scenicId); + scenicLocalStorageAdapterCache.remove(scenicId); + scenicFaceBodyAdapterCache.remove(scenicId); + scenicPayAdapterCache.remove(scenicId); + } + + /** + * 清除所有适配器缓存 + */ + public void clearAllAdapterCache() { + log.info("清除所有适配器缓存"); + scenicStorageAdapterCache.clear(); + scenicTmpStorageAdapterCache.clear(); + scenicLocalStorageAdapterCache.clear(); + scenicFaceBodyAdapterCache.clear(); + scenicPayAdapterCache.clear(); + } + + /** + * 手动触发过期缓存清理 + * + * @return 清理的过期缓存项总数 + */ + public int cleanupExpiredCache() { + log.info("手动触发过期缓存清理"); + int totalCleaned = 0; + totalCleaned += scenicStorageAdapterCache.cleanupExpired(); + totalCleaned += scenicTmpStorageAdapterCache.cleanupExpired(); + totalCleaned += scenicLocalStorageAdapterCache.cleanupExpired(); + totalCleaned += scenicFaceBodyAdapterCache.cleanupExpired(); + totalCleaned += scenicPayAdapterCache.cleanupExpired(); + log.info("清理了 {} 个过期缓存项", totalCleaned); + return totalCleaned; + } + + /** + * 获取缓存统计信息 + * + * @return 缓存统计信息 + */ + public String getCacheStats() { + StringBuilder stats = new StringBuilder(); + stats.append("=== ScenicServiceImpl 缓存统计信息 ===\n"); + stats.append("Storage Adapter Cache: ").append(scenicStorageAdapterCache.getStats()).append("\n"); + stats.append("Tmp Storage Adapter Cache: ").append(scenicTmpStorageAdapterCache.getStats()).append("\n"); + stats.append("Local Storage Adapter Cache: ").append(scenicLocalStorageAdapterCache.getStats()).append("\n"); + stats.append("FaceBody Adapter Cache: ").append(scenicFaceBodyAdapterCache.getStats()).append("\n"); + stats.append("Pay Adapter Cache: ").append(scenicPayAdapterCache.getStats()).append("\n"); + return stats.toString(); + } + + /** + * 重置所有缓存统计信息 + */ + public void resetCacheStats() { + log.info("重置所有缓存统计信息"); + scenicStorageAdapterCache.resetStats(); + scenicTmpStorageAdapterCache.resetStats(); + scenicLocalStorageAdapterCache.resetStats(); + scenicFaceBodyAdapterCache.resetStats(); + scenicPayAdapterCache.resetStats(); + } + + /** + * 获取指定景区缓存的剩余TTL时间 + * + * @param scenicId 景区ID + * @return 各类型适配器缓存的剩余TTL时间(毫秒) + */ + public String getScenicCacheTtl(Long scenicId) { + StringBuilder ttlInfo = new StringBuilder(); + ttlInfo.append("景区 ").append(scenicId).append(" 缓存TTL信息:\n"); + ttlInfo.append("Storage: ").append(scenicStorageAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("TmpStorage: ").append(scenicTmpStorageAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("LocalStorage: ").append(scenicLocalStorageAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("FaceBody: ").append(scenicFaceBodyAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + ttlInfo.append("Pay: ").append(scenicPayAdapterCache.getRemainTtl(scenicId)).append("ms\n"); + return ttlInfo.toString(); + } } diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/TemplateServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/TemplateServiceImpl.java index 602afbf..ce47950 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/TemplateServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/TemplateServiceImpl.java @@ -7,6 +7,8 @@ import com.ycwl.basic.model.pc.template.entity.TemplateConfigEntity; import com.ycwl.basic.model.pc.template.entity.TemplateEntity; import com.ycwl.basic.model.pc.template.req.TemplateReqQuery; import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; +import com.ycwl.basic.pricing.enums.ProductType; +import com.ycwl.basic.pricing.service.IPricingManagementService; import com.ycwl.basic.service.pc.TemplateService; import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.utils.ApiResponse; @@ -30,6 +32,8 @@ public class TemplateServiceImpl implements TemplateService { private TemplateMapper templateMapper; @Autowired private TemplateRepository templateRepository; + @Autowired + private IPricingManagementService pricingManagementService; @Override public ApiResponse> pageQuery(TemplateReqQuery templateReqQuery) { @@ -75,6 +79,7 @@ public class TemplateServiceImpl implements TemplateService { templateMapper.add(item); }); } + pricingManagementService.quickSetupProductPrice(ProductType.VLOG_VIDEO.getCode(), template.getId().toString(), template.getScenicId().toString(), template.getName(), template.getPrice(), template.getSlashPrice(), "个"); if (i > 0) { return ApiResponse.success(true); }else { @@ -112,6 +117,7 @@ public class TemplateServiceImpl implements TemplateService { }); } templateRepository.clearTemplateCache(template.getId()); + pricingManagementService.quickSetupProductPrice(ProductType.VLOG_VIDEO.getCode(), template.getId().toString(), template.getScenicId().toString(), template.getName(), template.getPrice(), template.getSlashPrice(), "个"); if (i > 0) { return ApiResponse.success(true); }else { diff --git a/src/main/java/com/ycwl/basic/task/DownloadNotificationTasker.java b/src/main/java/com/ycwl/basic/task/DownloadNotificationTasker.java index 8a9104e..8936eeb 100644 --- a/src/main/java/com/ycwl/basic/task/DownloadNotificationTasker.java +++ b/src/main/java/com/ycwl/basic/task/DownloadNotificationTasker.java @@ -1,9 +1,9 @@ package com.ycwl.basic.task; import cn.hutool.core.date.DateUtil; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.mapper.CouponMapper; import com.ycwl.basic.mapper.MemberMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.model.pc.coupon.req.CouponQueryReq; import com.ycwl.basic.model.pc.coupon.resp.CouponRespVO; @@ -12,8 +12,6 @@ import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; -import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; import com.ycwl.basic.notify.NotifyFactory; import com.ycwl.basic.notify.adapters.INotifyAdapter; import com.ycwl.basic.notify.entity.NotifyContent; @@ -47,10 +45,6 @@ public class DownloadNotificationTasker { @Autowired private MemberMapper memberMapper; @Autowired - private TemplateRepository templateRepository; - @Autowired - private ScenicMapper scenicMapper; - @Autowired private CouponMapper couponMapper; @Scheduled(cron = "0 0 21 * * *") @@ -168,7 +162,9 @@ public class DownloadNotificationTasker { @Scheduled(cron = "0 0 * * * *") public void sendExtraDownloadNotification() { log.info("开始执行定时任务"); - List scenicList = scenicMapper.list(new ScenicReqQuery()); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); if (scenicList.isEmpty()) { return; } @@ -177,7 +173,7 @@ public class DownloadNotificationTasker { int currentHour = calendar.get(Calendar.HOUR_OF_DAY); calendar.clear(); scenicList.parallelStream().forEach(scenic -> { - Long scenicId = scenic.getId(); + Long scenicId = Long.parseLong(scenic.getId()); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig == null) { return; diff --git a/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java b/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java index bbe3d0b..2b00449 100644 --- a/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java +++ b/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java @@ -7,7 +7,6 @@ import com.ycwl.basic.facebody.adapter.AliFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.mapper.FaceMapper; import com.ycwl.basic.mapper.FaceSampleMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.TemplateMapper; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO; @@ -40,8 +39,6 @@ import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME; @EnableScheduling @Slf4j public class DynamicTaskGenerator { - @Autowired - private ScenicMapper scenicMapper; @Autowired private TemplateMapper templateMapper; @Autowired diff --git a/src/main/java/com/ycwl/basic/task/FaceCleaner.java b/src/main/java/com/ycwl/basic/task/FaceCleaner.java index 386b802..b63b586 100644 --- a/src/main/java/com/ycwl/basic/task/FaceCleaner.java +++ b/src/main/java/com/ycwl/basic/task/FaceCleaner.java @@ -4,9 +4,9 @@ import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import com.ycwl.basic.constant.StorageConstant; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.mapper.FaceMapper; import com.ycwl.basic.mapper.FaceSampleMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.model.pc.face.entity.FaceEntity; @@ -16,7 +16,6 @@ import com.ycwl.basic.model.pc.faceSample.req.FaceSampleReqQuery; import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.pc.source.req.SourceReqQuery; import com.ycwl.basic.model.pc.source.resp.SourceRespVO; import com.ycwl.basic.model.pc.video.req.VideoReqQuery; @@ -46,8 +45,6 @@ import static com.ycwl.basic.constant.StorageConstant.VIID_FACE; @Slf4j @Profile("prod") public class FaceCleaner { - @Autowired - private ScenicMapper scenicMapper; @Autowired private FaceSampleMapper faceSampleMapper; @Autowired @@ -64,25 +61,27 @@ public class FaceCleaner { @Scheduled(cron = "0 0 1 * * ?") public void deleteExpireSample(){ - ScenicReqQuery scenicQuery = new ScenicReqQuery(); - List scenicList = scenicMapper.list(scenicQuery); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); scenicList.parallelStream().forEach(scenic -> { - log.info("当前景区{},开始删除人脸样本", scenic.getId()); - IFaceBodyAdapter adapter = scenicService.getScenicFaceBodyAdapter(scenic.getId()); - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenic.getId()); + Long scenicId = Long.parseLong(scenic.getId()); + log.info("当前景区{},开始删除人脸样本", scenicId); + IFaceBodyAdapter adapter = scenicService.getScenicFaceBodyAdapter(scenicId); + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); Integer sampleStoreDay = scenicConfig.getSampleStoreDay(); if (sampleStoreDay == null) { log.info("当前景区{},人脸样本保存天数未设置,默认7天", scenic.getId()); sampleStoreDay = 7; } Date sampleEndDate = DateUtil.offsetDay(DateUtil.beginOfDay(new Date()), -sampleStoreDay); - List faceSampleList = faceSampleMapper.listEntityBeforeDate(scenic.getId(), sampleEndDate); + List faceSampleList = faceSampleMapper.listEntityBeforeDate(scenicId, sampleEndDate); if (faceSampleList.isEmpty()) { log.info("当前景区{},人脸样本为空", scenic.getId()); return; } faceSampleList.forEach(faceSample -> { - boolean success = adapter.deleteFace(String.valueOf(scenic.getId()), faceSample.getId().toString()); + boolean success = adapter.deleteFace(scenic.getId(), faceSample.getId().toString()); if (success) { log.info("当前景区{},人脸样本ID{},删除成功", scenic.getId(), faceSample.getId()); faceSampleMapper.deleteById(faceSample.getId()); @@ -95,21 +94,23 @@ public class FaceCleaner { @Scheduled(cron = "0 45 2 * * ?") public void deleteExpireFace() { - ScenicReqQuery scenicQuery = new ScenicReqQuery(); - List scenicList = scenicMapper.list(scenicQuery); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); scenicList.parallelStream().forEach(scenic -> { + Long scenicId = Long.parseLong(scenic.getId()); log.info("当前景区{},开始删除用户人脸", scenic.getId()); - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenic.getId()); - IFaceBodyAdapter adapter = scenicService.getScenicFaceBodyAdapter(scenic.getId()); + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); + IFaceBodyAdapter adapter = scenicService.getScenicFaceBodyAdapter(scenicId); Integer faceStoreDay = scenicConfig.getFaceStoreDay(); if (faceStoreDay == null) { log.info("当前景区{},人脸样本保存天数未设置,默认3天", scenic.getName()); faceStoreDay = 3; } FaceReqQuery req = new FaceReqQuery(); - req.setScenicId(scenic.getId()); + req.setScenicId(scenicId); Date faceEndDate = DateUtil.offsetDay(DateUtil.beginOfDay(new Date()), -faceStoreDay); - List list = faceMapper.listUnpaidEntityBeforeDate(scenic.getId(), faceEndDate); + List list = faceMapper.listUnpaidEntityBeforeDate(scenicId, faceEndDate); list.forEach(face -> { boolean result = adapter.deleteFace(USER_FACE_DB_NAME+face.getScenicId(), face.getId().toString()); if (result) { @@ -130,10 +131,12 @@ public class FaceCleaner { @Scheduled(cron = "0 0 1 * * ?") public void deleteNotBuySource(){ - ScenicReqQuery scenicQuery = new ScenicReqQuery(); - List scenicList = scenicMapper.list(scenicQuery); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); scenicList.parallelStream().forEach(scenic -> { - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenic.getId()); + Long scenicId = Long.valueOf(scenic.getId()); + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig == null) { log.info("当前景区{},无配置信息", scenic.getName()); return; @@ -144,17 +147,19 @@ public class FaceCleaner { } int expireDay = scenicConfig.getUserSourceExpireDay(); Date endDate = DateUtil.offsetDay(DateUtil.beginOfDay(new Date()), -expireDay); - int deleteCount = sourceMapper.deleteNotBuyRelations(scenic.getId(), endDate); + int deleteCount = sourceMapper.deleteNotBuyRelations(scenicId, endDate); log.info("当前景区{},删除关联素材{}个", scenic.getName(), deleteCount); }); } @Scheduled(cron = "0 15 1 * * ?") public void deleteNotBuyVideos(){ - ScenicReqQuery scenicQuery = new ScenicReqQuery(); - List scenicList = scenicMapper.list(scenicQuery); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); scenicList.parallelStream().forEach(scenic -> { - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenic.getId()); + Long scenicId = Long.valueOf(scenic.getId()); + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig == null) { log.info("当前景区{},无配置信息", scenic.getName()); return; @@ -165,7 +170,7 @@ public class FaceCleaner { } int expireDay = scenicConfig.getVideoStoreDay(); Date endDate = DateUtil.offsetDay(DateUtil.beginOfDay(new Date()), -expireDay); - int deleteCount = videoMapper.deleteNotBuyRelations(scenic.getId(), endDate); + int deleteCount = videoMapper.deleteNotBuyRelations(scenicId, endDate); int deleteVideoCount = videoMapper.deleteUselessVideo(); log.info("当前景区{},删除VLOG关系{}个,删除VLOG记录{}个", scenic.getName(), deleteCount, deleteVideoCount); }); @@ -173,10 +178,12 @@ public class FaceCleaner { @Scheduled(cron = "0 30 1 * * ?") public void deleteExpiredSource(){ - ScenicReqQuery scenicQuery = new ScenicReqQuery(); - List scenicList = scenicMapper.list(scenicQuery); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); scenicList.parallelStream().forEach(scenic -> { - ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenic.getId()); + Long scenicId = Long.valueOf(scenic.getId()); + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig == null) { log.info("当前景区{},无配置信息", scenic.getName()); return; @@ -193,10 +200,10 @@ public class FaceCleaner { } else { log.info("当前景区{},原始素材保存天数未设置,默认7天", scenic.getName()); } - if (Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { + if (Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) { return; } - if (Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) { + if (Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) { return; } log.info("当前景区{},开始删除原始素材", scenic.getName()); @@ -240,12 +247,16 @@ public class FaceCleaner { log.info("开始清理源视频素材文件"); List list = sourceMapper.list(new SourceReqQuery()); ArrayList adapterIdentity = new ArrayList<>(); - scenicMapper.list(new ScenicReqQuery()).forEach(scenic -> { - if (disableDeleteScenicIds.contains(scenic.getId().toString())) { + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); + scenicList.forEach(scenic -> { + Long scenicId = Long.valueOf(scenic.getId()); + if (disableDeleteScenicIds.contains(scenic.getId())) { log.info("景区【{}】禁止删除文件,跳过!", scenic.getName()); return; } - IStorageAdapter adapter = scenicService.getScenicStorageAdapter(scenic.getId()); + IStorageAdapter adapter = scenicService.getScenicStorageAdapter(scenicId); String identity = adapter.identity(); if (!adapterIdentity.contains(identity)) { log.info("因为Identity相同,跳过"); @@ -291,12 +302,16 @@ public class FaceCleaner { log.info("开始清理视频文件"); List list = videoMapper.list(new VideoReqQuery()); ArrayList adapterIdentity = new ArrayList<>(); - scenicMapper.list(new ScenicReqQuery()).forEach(scenic -> { - if (disableDeleteScenicIds.contains(scenic.getId().toString())) { + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); + scenicList.forEach(scenic -> { + Long scenicId = Long.valueOf(scenic.getId()); + if (disableDeleteScenicIds.contains(scenic.getId())) { log.info("景区【{}】禁止删除文件,跳过!", scenic.getName()); return; } - IStorageAdapter adapter = scenicService.getScenicStorageAdapter(scenic.getId()); + IStorageAdapter adapter = scenicService.getScenicStorageAdapter(scenicId); String identity = adapter.identity(); if (!adapterIdentity.contains(identity)) { adapterIdentity.add(identity); diff --git a/src/main/java/com/ycwl/basic/task/ScenicStatsTask.java b/src/main/java/com/ycwl/basic/task/ScenicStatsTask.java index 755b516..4b3764c 100644 --- a/src/main/java/com/ycwl/basic/task/ScenicStatsTask.java +++ b/src/main/java/com/ycwl/basic/task/ScenicStatsTask.java @@ -1,14 +1,14 @@ package com.ycwl.basic.task; import cn.hutool.core.date.DateUtil; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.mapper.ScenicDeviceStatsMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.StatisticsMapper; import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq; import com.ycwl.basic.model.mobile.statistic.resp.AppStatisticsFunnelVO; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.pc.scenicDeviceStats.entity.ScenicDeviceStatsEntity; +import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppStatisticsService; import com.ycwl.basic.utils.ApiResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -31,7 +31,8 @@ public class ScenicStatsTask { @Autowired private AppStatisticsService statisticsService; @Autowired - private ScenicMapper scenicMapper; + private ScenicRepository scenicRepository; + @Scheduled(cron = "0 1 0 * * *") public void countDeviceStats() { Date yesterdayStart = DateUtil.beginOfDay(DateUtil.yesterday()); @@ -58,16 +59,19 @@ public class ScenicStatsTask { public void countScenicStats() { Date yesterdayStart = DateUtil.beginOfDay(DateUtil.yesterday()); Date yesterdayEnd = DateUtil.endOfDay(yesterdayStart); - List list = scenicMapper.list(new ScenicReqQuery()); - list.forEach((scenic) -> { - CommonQueryReq query = new CommonQueryReq(); - query.setScenicId(scenic.getId()); - query.setStartTime(yesterdayStart); - query.setEndTime(yesterdayEnd); - query.setRealtime(true); - ApiResponse resp = statisticsService.userConversionFunnel(query); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); + scenicList.forEach((scenic) -> { + CommonQueryReq commonQueryReq = new CommonQueryReq(); + Long scenicId = Long.valueOf(scenic.getId()); + commonQueryReq.setScenicId(scenicId); + commonQueryReq.setStartTime(yesterdayStart); + commonQueryReq.setEndTime(yesterdayEnd); + commonQueryReq.setRealtime(true); + ApiResponse resp = statisticsService.userConversionFunnel(commonQueryReq); AppStatisticsFunnelVO data = resp.getData(); - statisticsMapper.insertStat(scenic.getId(), yesterdayStart, data); + statisticsMapper.insertStat(scenicId, yesterdayStart, data); }); } } diff --git a/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java b/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java index 2ed6b09..8cf630d 100644 --- a/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java +++ b/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java @@ -2,21 +2,16 @@ package com.ycwl.basic.task; import cn.hutool.core.date.DateUtil; import com.ycwl.basic.biz.TemplateBiz; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.mapper.FaceMapper; -import com.ycwl.basic.mapper.FaceSampleMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.TemplateMapper; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; import com.ycwl.basic.model.pc.face.req.FaceReqQuery; import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; -import com.ycwl.basic.model.task.resp.SearchFaceRespVo; import com.ycwl.basic.repository.ScenicRepository; -import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.service.pc.FaceService; -import com.ycwl.basic.service.task.TaskFaceService; import com.ycwl.basic.service.task.impl.TaskTaskServiceImpl; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -37,23 +32,21 @@ public class VideoTaskGenerator { @Autowired private FaceMapper faceMapper; @Autowired - private TaskFaceService taskFaceService; - @Autowired private TemplateBiz templateBiz; @Autowired private TaskTaskServiceImpl taskTaskService; @Autowired private TemplateMapper templateMapper; @Autowired - private ScenicMapper scenicMapper; - @Autowired private ScenicRepository scenicRepository; @Autowired private FaceService faceService; @Scheduled(cron = "0 0 * * * *") public void generateVideoTask() { - List scenicList = scenicMapper.list(new ScenicReqQuery()); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); if (scenicList.isEmpty()) { return; } @@ -63,7 +56,7 @@ public class VideoTaskGenerator { int currentHour = calendar.get(Calendar.HOUR_OF_DAY); calendar.clear(); scenicList.parallelStream().forEach(scenic -> { - Long scenicId = scenic.getId(); + Long scenicId = Long.valueOf(scenic.getId()); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); if (scenicConfig == null) { log.info("当前景区{},无配置信息", scenic.getName()); @@ -79,11 +72,11 @@ public class VideoTaskGenerator { if (contentList.isEmpty()) { return; } - FaceReqQuery query = new FaceReqQuery(); - query.setScenicId(scenicId); - query.setStartTime(DateUtil.beginOfDay(new Date())); - query.setEndTime(DateUtil.endOfDay(new Date())); - List list = faceMapper.list(query); + FaceReqQuery faceReqQuery = new FaceReqQuery(); + faceReqQuery.setScenicId(scenicId); + faceReqQuery.setStartTime(DateUtil.beginOfDay(new Date())); + faceReqQuery.setEndTime(DateUtil.endOfDay(new Date())); + List list = faceMapper.list(faceReqQuery); list.forEach(face -> { faceService.matchFaceId(face.getId(), false); if (Integer.valueOf(3).equals(scenicConfig.getBookRoutine())) { diff --git a/src/main/java/com/ycwl/basic/util/ScenicConfigManager.java b/src/main/java/com/ycwl/basic/util/ScenicConfigManager.java new file mode 100644 index 0000000..3da9397 --- /dev/null +++ b/src/main/java/com/ycwl/basic/util/ScenicConfigManager.java @@ -0,0 +1,376 @@ +package com.ycwl.basic.util; + +import com.ycwl.basic.integration.common.util.ConfigValueUtil; +import com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 景区配置管理器 + * + * 提供类型安全的配置值获取功能,支持多种数据类型的自动转换, + * 当类型不兼容时返回null而不是抛出异常。 + */ +public class ScenicConfigManager { + + private final Map configMap; + + /** + * 从配置列表构造管理器 + * + * @param configList 配置项列表 + */ + public ScenicConfigManager(List configList) { + this.configMap = new HashMap<>(); + if (configList != null) { + for (ScenicConfigV2DTO config : configList) { + if (config.getConfigKey() != null && config.getConfigValue() != null) { + this.configMap.put(config.getConfigKey(), config.getConfigValue()); + } + } + } + } + + /** + * 从配置Map构造管理器 + * + * @param configMap 配置Map + */ + public ScenicConfigManager(Map configMap) { + this.configMap = configMap != null ? new HashMap<>(configMap) : new HashMap<>(); + } + + /** + * 获取字符串值 + * + * @param key 配置键 + * @return 字符串值,如果键不存在或转换失败返回null + */ + public String getString(String key) { + return ConfigValueUtil.getStringValue(configMap, key); + } + + /** + * 获取字符串值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return 字符串值或默认值 + */ + public String getString(String key, String defaultValue) { + return ConfigValueUtil.getStringValue(configMap, key, defaultValue); + } + + /** + * 获取整数值 + * + * @param key 配置键 + * @return Integer值,如果键不存在或转换失败返回null + */ + public Integer getInteger(String key) { + return ConfigValueUtil.getIntValue(configMap, key); + } + + /** + * 获取整数值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return Integer值或默认值 + */ + public Integer getInteger(String key, Integer defaultValue) { + return ConfigValueUtil.getIntValue(configMap, key, defaultValue); + } + + /** + * 获取长整数值 + * + * @param key 配置键 + * @return Long值,如果键不存在或转换失败返回null + */ + public Long getLong(String key) { + return ConfigValueUtil.getLongValue(configMap, key); + } + + /** + * 获取长整数值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return Long值或默认值 + */ + public Long getLong(String key, Long defaultValue) { + Long value = ConfigValueUtil.getLongValue(configMap, key); + return value != null ? value : defaultValue; + } + + /** + * 获取浮点数值 + * + * @param key 配置键 + * @return Float值,如果键不存在或转换失败返回null + */ + public Float getFloat(String key) { + return ConfigValueUtil.getFloatValue(configMap, key); + } + + /** + * 获取浮点数值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return Float值或默认值 + */ + public Float getFloat(String key, Float defaultValue) { + Float value = ConfigValueUtil.getFloatValue(configMap, key); + return value != null ? value : defaultValue; + } + + /** + * 获取双精度浮点数值 + * + * @param key 配置键 + * @return Double值,如果键不存在或转换失败返回null + */ + public Double getDouble(String key) { + return ConfigValueUtil.getDoubleValue(configMap, key); + } + + /** + * 获取双精度浮点数值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return Double值或默认值 + */ + public Double getDouble(String key, Double defaultValue) { + Double value = ConfigValueUtil.getDoubleValue(configMap, key); + return value != null ? value : defaultValue; + } + + /** + * 获取高精度小数值 + * + * @param key 配置键 + * @return BigDecimal值,如果键不存在或转换失败返回null + */ + public BigDecimal getBigDecimal(String key) { + return ConfigValueUtil.getBigDecimalValue(configMap, key); + } + + /** + * 获取高精度小数值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return BigDecimal值或默认值 + */ + public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) { + BigDecimal value = ConfigValueUtil.getBigDecimalValue(configMap, key); + return value != null ? value : defaultValue; + } + + /** + * 获取布尔值 + * + * @param key 配置键 + * @return Boolean值,如果键不存在或转换失败返回null + */ + public Boolean getBoolean(String key) { + return ConfigValueUtil.getBooleanValue(configMap, key); + } + + /** + * 获取布尔值,如果为null则返回默认值 + * + * @param key 配置键 + * @param defaultValue 默认值 + * @return Boolean值或默认值 + */ + public Boolean getBoolean(String key, Boolean defaultValue) { + return ConfigValueUtil.getBooleanValue(configMap, key, defaultValue); + } + + /** + * 获取枚举值 + * + * @param key 配置键 + * @param enumClass 枚举类型 + * @param 枚举类型泛型 + * @return 枚举值,如果键不存在或转换失败返回null + */ + public > T getEnum(String key, Class enumClass) { + return ConfigValueUtil.getEnumValue(configMap, key, enumClass); + } + + /** + * 获取枚举值,如果为null则返回默认值 + * + * @param key 配置键 + * @param enumClass 枚举类型 + * @param defaultValue 默认值 + * @param 枚举类型泛型 + * @return 枚举值或默认值 + */ + public > T getEnum(String key, Class enumClass, T defaultValue) { + T value = ConfigValueUtil.getEnumValue(configMap, key, enumClass); + return value != null ? value : defaultValue; + } + + /** + * 获取原始对象值 + * + * @param key 配置键 + * @return 原始Object值 + */ + public Object getObject(String key) { + return ConfigValueUtil.getObjectValue(configMap, key); + } + + /** + * 获取并转换为指定类型的对象 + * + * @param key 配置键 + * @param clazz 目标类型 + * @param 目标类型泛型 + * @return 转换后的对象,如果转换失败返回null + */ + public T getObject(String key, Class clazz) { + return ConfigValueUtil.getObjectValue(configMap, key, clazz); + } + + /** + * 获取Map类型的值 + * + * @param key 配置键 + * @return Map值,如果转换失败返回null + */ + public Map getMap(String key) { + return ConfigValueUtil.getMapValue(configMap, key); + } + + /** + * 获取List类型的值 + * + * @param key 配置键 + * @return List值,如果转换失败返回null + */ + public List getList(String key) { + return ConfigValueUtil.getListValue(configMap, key); + } + + /** + * 获取指定元素类型的List值 + * + * @param key 配置键 + * @param elementClass List元素类型 + * @param List元素类型泛型 + * @return 指定类型的List,如果转换失败返回null + */ + public List getList(String key, Class elementClass) { + return ConfigValueUtil.getListValue(configMap, key, elementClass); + } + + /** + * 检查配置键是否存在 + * + * @param key 配置键 + * @return true如果键存在,false如果不存在 + */ + public boolean hasKey(String key) { + return ConfigValueUtil.hasKey(configMap, key); + } + + /** + * 检查配置键是否存在且值不为null + * + * @param key 配置键 + * @return true如果键存在且值不为null + */ + public boolean hasNonNullValue(String key) { + return ConfigValueUtil.hasNonNullValue(configMap, key); + } + + /** + * 获取所有配置键 + * + * @return 配置键集合 + */ + public Set getAllKeys() { + return new HashSet<>(configMap.keySet()); + } + + /** + * 获取配置项数量 + * + * @return 配置项数量 + */ + public int size() { + return configMap.size(); + } + + /** + * 检查配置是否为空 + * + * @return true如果没有配置项 + */ + public boolean isEmpty() { + return configMap.isEmpty(); + } + + /** + * 获取所有配置的拷贝 + * + * @return 配置Map的拷贝 + */ + public Map getAllConfigs() { + return new HashMap<>(configMap); + } + + /** + * 根据键前缀过滤配置 + * + * @param prefix 键前缀 + * @return 匹配前缀的配置Map + */ + public Map getConfigsByPrefix(String prefix) { + if (prefix == null) { + return new HashMap<>(); + } + + return configMap.entrySet().stream() + .filter(entry -> entry.getKey() != null && entry.getKey().startsWith(prefix)) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + } + + /** + * 创建新的ScenicConfigManager,包含当前配置的子集 + * + * @param keys 要包含的配置键 + * @return 包含指定键配置的新管理器 + */ + public ScenicConfigManager subset(Set keys) { + Map subsetMap = new HashMap<>(); + if (keys != null) { + for (String key : keys) { + if (configMap.containsKey(key)) { + subsetMap.put(key, configMap.get(key)); + } + } + } + return new ScenicConfigManager(subsetMap); + } + + @Override + public String toString() { + return "ScenicConfigManager{" + + "configCount=" + configMap.size() + + ", keys=" + configMap.keySet() + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/util/TtlCacheMap.java b/src/main/java/com/ycwl/basic/util/TtlCacheMap.java new file mode 100644 index 0000000..fc5f91f --- /dev/null +++ b/src/main/java/com/ycwl/basic/util/TtlCacheMap.java @@ -0,0 +1,340 @@ +package com.ycwl.basic.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; + +/** + * 带TTL(生存时间)的缓存Map工具类 + * + * @param 键类型 + * @param 值类型 + */ +@Slf4j +public class TtlCacheMap { + + /** + * 缓存项包装类 + */ + private static class CacheItem { + private final V value; + private final long expireTime; + + public CacheItem(V value, long ttlMillis) { + this.value = value; + this.expireTime = System.currentTimeMillis() + ttlMillis; + } + + public V getValue() { + return value; + } + + public boolean isExpired() { + return System.currentTimeMillis() > expireTime; + } + + public long getRemainTtl() { + return Math.max(0, expireTime - System.currentTimeMillis()); + } + } + + private final ConcurrentHashMap> cache; + private final long defaultTtlMillis; + private final ReentrantReadWriteLock lock; + private final ScheduledExecutorService cleanupExecutor; + + // 统计信息 + private volatile long hitCount = 0; + private volatile long missCount = 0; + private volatile long expiredCount = 0; + + /** + * 构造函数 + * + * @param defaultTtlMillis 默认TTL时间(毫秒) + */ + public TtlCacheMap(long defaultTtlMillis) { + this.cache = new ConcurrentHashMap<>(); + this.defaultTtlMillis = defaultTtlMillis; + this.lock = new ReentrantReadWriteLock(); + this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "TtlCacheMap-Cleanup"); + t.setDaemon(true); + return t; + }); + + // 启动定期清理任务,每分钟清理一次过期条目 + this.cleanupExecutor.scheduleWithFixedDelay(this::cleanupExpired, 60, 60, TimeUnit.SECONDS); + } + + /** + * 构造函数,使用默认TTL为10分钟 + */ + public TtlCacheMap() { + this(TimeUnit.MINUTES.toMillis(10)); + } + + /** + * 获取缓存值,如果不存在或过期则通过supplier创建 + * + * @param key 缓存键 + * @param valueSupplier 值提供器 + * @return 缓存值 + */ + public V computeIfAbsent(K key, Function valueSupplier) { + return computeIfAbsent(key, valueSupplier, defaultTtlMillis); + } + + /** + * 获取缓存值,如果不存在或过期则通过supplier创建 + * + * @param key 缓存键 + * @param valueSupplier 值提供器 + * @param ttlMillis TTL时间(毫秒) + * @return 缓存值 + */ + public V computeIfAbsent(K key, Function valueSupplier, long ttlMillis) { + lock.readLock().lock(); + try { + CacheItem item = cache.get(key); + if (item != null && !item.isExpired()) { + hitCount++; + return item.getValue(); + } + } finally { + lock.readLock().unlock(); + } + + // 缓存不存在或已过期,需要重新创建 + lock.writeLock().lock(); + try { + // 双重检查,防止重复创建 + CacheItem item = cache.get(key); + if (item != null && !item.isExpired()) { + hitCount++; + return item.getValue(); + } + + if (item != null && item.isExpired()) { + expiredCount++; + cache.remove(key); + } + + // 创建新值 + missCount++; + V value = valueSupplier.apply(key); + if (value != null) { + cache.put(key, new CacheItem<>(value, ttlMillis)); + } + return value; + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 直接放入缓存 + * + * @param key 缓存键 + * @param value 缓存值 + */ + public void put(K key, V value) { + put(key, value, defaultTtlMillis); + } + + /** + * 直接放入缓存 + * + * @param key 缓存键 + * @param value 缓存值 + * @param ttlMillis TTL时间(毫秒) + */ + public void put(K key, V value, long ttlMillis) { + lock.writeLock().lock(); + try { + cache.put(key, new CacheItem<>(value, ttlMillis)); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 获取缓存值 + * + * @param key 缓存键 + * @return 缓存值,如果不存在或过期返回null + */ + public V get(K key) { + lock.readLock().lock(); + try { + CacheItem item = cache.get(key); + if (item != null) { + if (!item.isExpired()) { + hitCount++; + return item.getValue(); + } else { + // 异步清理过期项 + cleanupExecutor.execute(() -> { + lock.writeLock().lock(); + try { + CacheItem expiredItem = cache.get(key); + if (expiredItem != null && expiredItem.isExpired()) { + cache.remove(key); + expiredCount++; + } + } finally { + lock.writeLock().unlock(); + } + }); + } + } + missCount++; + return null; + } finally { + lock.readLock().unlock(); + } + } + + /** + * 移除缓存项 + * + * @param key 缓存键 + * @return 被移除的值,如果不存在返回null + */ + public V remove(K key) { + lock.writeLock().lock(); + try { + CacheItem item = cache.remove(key); + return item != null ? item.getValue() : null; + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 清空所有缓存 + */ + public void clear() { + lock.writeLock().lock(); + try { + cache.clear(); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 检查缓存键是否存在且未过期 + * + * @param key 缓存键 + * @return true如果存在且未过期 + */ + public boolean containsKey(K key) { + return get(key) != null; + } + + /** + * 获取缓存大小(包含过期项) + * + * @return 缓存大小 + */ + public int size() { + return cache.size(); + } + + /** + * 检查缓存是否为空 + * + * @return true如果为空 + */ + public boolean isEmpty() { + return cache.isEmpty(); + } + + /** + * 手动触发过期清理 + * + * @return 清理的过期项数量 + */ + public int cleanupExpired() { + lock.writeLock().lock(); + try { + int cleanedCount = 0; + var iterator = cache.entrySet().iterator(); + while (iterator.hasNext()) { + var entry = iterator.next(); + if (entry.getValue().isExpired()) { + iterator.remove(); + cleanedCount++; + expiredCount++; + } + } + if (cleanedCount > 0) { + log.debug("清理了 {} 个过期缓存项", cleanedCount); + } + return cleanedCount; + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 获取缓存统计信息 + * + * @return 统计信息字符串 + */ + public String getStats() { + long total = hitCount + missCount; + double hitRate = total > 0 ? (double) hitCount / total * 100 : 0; + + return String.format( + "TtlCacheMap Stats: size=%d, hits=%d, misses=%d, expired=%d, hitRate=%.2f%%", + cache.size(), hitCount, missCount, expiredCount, hitRate + ); + } + + /** + * 重置统计信息 + */ + public void resetStats() { + hitCount = 0; + missCount = 0; + expiredCount = 0; + } + + /** + * 获取剩余TTL时间 + * + * @param key 缓存键 + * @return 剩余TTL毫秒数,如果不存在或已过期返回0 + */ + public long getRemainTtl(K key) { + lock.readLock().lock(); + try { + CacheItem item = cache.get(key); + return item != null ? item.getRemainTtl() : 0; + } finally { + lock.readLock().unlock(); + } + } + + /** + * 关闭清理线程池,释放资源 + */ + public void shutdown() { + cleanupExecutor.shutdown(); + try { + if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + cleanupExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + cleanupExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0d93806..1ba1137 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,4 +3,21 @@ server: spring: application: - name: zt \ No newline at end of file + name: zt + +# Feign配置(简化版,基于Nacos服务发现) +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 10000 + loggerLevel: full + decode404: true + okhttp: + enabled: true + +# 开发环境日志配置 +logging: + level: + com.ycwl.basic.integration.scenic.client: DEBUG \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index dc970dc..510d250 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -3,4 +3,9 @@ server: spring: application: - name: zt \ No newline at end of file + name: zt + +# 生产环境日志级别 +logging: + level: + com.ycwl.basic.integration.scenic.client: WARN \ No newline at end of file diff --git a/src/main/resources/mapper/ScenicMapper.xml b/src/main/resources/mapper/ScenicMapper.xml deleted file mode 100644 index 8c94752..0000000 --- a/src/main/resources/mapper/ScenicMapper.xml +++ /dev/null @@ -1,267 +0,0 @@ - - - - - insert into scenic(id, `name`, introduction, phone, cover_url, longitude, latitude, radius, province, city, area, address, price, kf_code_url, kf_phone, logo_url, source_video_price, source_image_price) - values (#{id}, #{name}, #{introduction}, #{phone}, #{coverUrl},#{longitude}, #{latitude}, #{radius}, #{province}, #{city}, #{area}, #{address}, #{price}, #{kfCodeUrl}, #{kfPhone}, #{logoUrl}, #{sourceVideoPrice}, #{sourceImagePrice}) - - - insert into scenic_config(id, scenic_id, create_time) - values (#{id}, #{scenicId}, now()) - - - update - scenic - - - `name`=#{name}, - - - `phone`=#{phone}, - - - introduction=#{introduction}, - - - cover_url=#{coverUrl}, - - - longitude=#{longitude}, - - - latitude=#{latitude}, - - - radius=#{radius}, - - - status=#{status}, - - - province=#{province}, - - - city=#{city}, - - - area=#{area}, - - - address=#{address}, - - - kf_code_url=#{kfCodeUrl}, - - - kf_phone=#{kfPhone}, - - - logo_url=#{logoUrl}, - - - price=#{price}, - - - source_video_price=#{sourceVideoPrice}, - - - source_image_price=#{sourceImagePrice}, - - - where id = #{id} - - - update - scenic - set status = (CASE - status - WHEN 1 THEN - 0 - WHEN 0 THEN - 1 - END) - where id = #{id} - - - update scenic_config - - - start_time=#{startTime}, - - - end_time=#{endTime}, - - - is_default=#{isDefault}, - - all_free=#{allFree}, - book_routine=#{bookRoutine}, - tour_time=#{tourTime}, - sample_store_day=#{sampleStoreDay}, - face_store_day=#{faceStoreDay}, - video_store_day=#{videoStoreDay}, - template_new_video_type=#{templateNewVideoType}, - anti_screen_record_type=#{antiScreenRecordType}, - disable_source_video=#{disableSourceVideo}, - disable_source_image=#{disableSourceImage}, - video_source_store_day=#{videoSourceStoreDay}, - image_source_store_day=#{imageSourceStoreDay}, - user_source_expire_day=#{userSourceExpireDay}, - face_score_threshold=#{faceScoreThreshold}, - force_finish_time=#{forceFinishTime}, - face_detect_helper_threshold=#{faceDetectHelperThreshold}, - store_type=#{storeType}, - store_config_json=#{storeConfigJson}, - tmp_store_type=#{tmpStoreType}, - tmp_store_config_json=#{tmpStoreConfigJson}, - local_store_type=#{localStoreType}, - local_store_config_json=#{localStoreConfigJson}, - broker_direct_rate=#{brokerDirectRate}, - watermark_type=#{watermarkType}, - watermark_scenic_text=#{watermarkScenicText}, - watermark_dt_format=#{watermarkDtFormat}, - face_type=#{faceType}, - face_config_json=#{faceConfigJson}, - pay_type=#{payType}, - pay_config_json=#{payConfigJson}, - image_source_pack_hint=#{imageSourcePackHint}, - video_source_pack_hint=#{videoSourcePackHint}, - extra_notification_time=#{extraNotificationTime}, - photo_free_num= #{photoFreeNum}, - video_free_num= #{videoFreeNum}, - voucher_enable= #{voucherEnable} - - where id = #{id} - - - delete from scenic where id = #{id} - - - delete from scenic_config where scenic_id = #{scenicId} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test/java/com/ycwl/basic/task/FaceCleanerTest.java b/src/test/java/com/ycwl/basic/task/FaceCleanerTest.java index d2a6406..d144ef0 100644 --- a/src/test/java/com/ycwl/basic/task/FaceCleanerTest.java +++ b/src/test/java/com/ycwl/basic/task/FaceCleanerTest.java @@ -5,9 +5,9 @@ import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.profile.DefaultProfile; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.mapper.FaceMapper; import com.ycwl.basic.mapper.FaceSampleMapper; -import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; @@ -41,8 +41,6 @@ public class FaceCleanerTest { @Autowired private FaceSampleMapper faceSampleMapper; @Autowired - private ScenicMapper scenicMapper; - @Autowired private FaceCleaner faceCleaner; @Autowired private ScenicService scenicService; @@ -51,7 +49,7 @@ public class FaceCleanerTest { @Test public void testA() { - ScenicEntity scenic = scenicMapper.get(3980001650692722688L); + ScenicEntity scenic = scenicRepository.getScenic(3980001650692722688L); log.info("当前景区{},开始删除人脸样本", scenic.getId()); IFaceBodyAdapter adapter = scenicService.getScenicFaceBodyAdapter(scenic.getId()); int sampleStoreDay = 1; @@ -79,8 +77,9 @@ public class FaceCleanerTest { @Test public void test() { - ScenicReqQuery scenicQuery = new ScenicReqQuery(); - List scenicList = scenicMapper.list(scenicQuery); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); scenicList.forEach(scenic -> { log.info("当前景区{},开始删除人脸样本", scenic.getName()); }); diff --git a/src/test/java/com/ycwl/basic/task/ScenicStatsTaskTest.java b/src/test/java/com/ycwl/basic/task/ScenicStatsTaskTest.java index 3593d7b..a0c8e1a 100644 --- a/src/test/java/com/ycwl/basic/task/ScenicStatsTaskTest.java +++ b/src/test/java/com/ycwl/basic/task/ScenicStatsTaskTest.java @@ -1,12 +1,13 @@ package com.ycwl.basic.task; import cn.hutool.core.date.DateUtil; -import com.ycwl.basic.mapper.ScenicMapper; +import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.mapper.StatisticsMapper; import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq; import com.ycwl.basic.model.mobile.statistic.resp.AppStatisticsFunnelVO; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.mobile.AppStatisticsService; import com.ycwl.basic.utils.ApiResponse; import lombok.extern.slf4j.Slf4j; @@ -27,11 +28,11 @@ public class ScenicStatsTaskTest { @Autowired private ScenicStatsTask task; @Autowired - private ScenicMapper scenicMapper; - @Autowired private AppStatisticsService statisticsService; @Autowired private StatisticsMapper statisticsMapper; + @Autowired + private ScenicRepository scenicRepository; @Test public void testA() { @@ -42,17 +43,20 @@ public class ScenicStatsTaskTest { public void testB() { Calendar calendar = Calendar.getInstance(); calendar.set(2025, Calendar.MAY, 1); - List list = scenicMapper.list(new ScenicReqQuery()); + ScenicReqQuery query = new ScenicReqQuery(); + query.setPageSize(1000); + List scenicList = scenicRepository.list(query); while (calendar.getTime().getTime() < System.currentTimeMillis()) { System.out.println(calendar.getTime()); - list.forEach((scenic) -> { - CommonQueryReq query = new CommonQueryReq(); - query.setScenicId(scenic.getId()); - query.setStartTime(DateUtil.beginOfDay(calendar.getTime())); - query.setEndTime(DateUtil.endOfDay(calendar.getTime())); - ApiResponse resp = statisticsService.userConversionFunnel(query); + scenicList.forEach((scenic) -> { + Long scenicId = Long.valueOf(scenic.getId()); + CommonQueryReq commonQueryReq = new CommonQueryReq(); + commonQueryReq.setScenicId(scenicId); + commonQueryReq.setStartTime(DateUtil.beginOfDay(calendar.getTime())); + commonQueryReq.setEndTime(DateUtil.endOfDay(calendar.getTime())); + ApiResponse resp = statisticsService.userConversionFunnel(commonQueryReq); AppStatisticsFunnelVO data = resp.getData(); - statisticsMapper.insertStat(scenic.getId(), DateUtil.beginOfDay(calendar.getTime()), data); + statisticsMapper.insertStat(scenicId, DateUtil.beginOfDay(calendar.getTime()), data); }); calendar.add(Calendar.DAY_OF_MONTH, 1); }