Merge branch 'refs/heads/order_v2'

# Conflicts:
#	src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java
This commit is contained in:
2025-08-30 10:53:11 +08:00
112 changed files with 7427 additions and 948 deletions

View File

@@ -88,7 +88,7 @@ public class OrderBiz {
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
priceObj.setScenicAllPrice(scenic.getPrice()); priceObj.setScenicAllPrice(scenic.getPrice());
if (scenicConfig != null) { if (scenicConfig != null) {
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) { if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
// 景区全免 // 景区全免
priceObj.setFree(true); priceObj.setFree(true);
priceObj.setPrice(BigDecimal.ZERO); priceObj.setPrice(BigDecimal.ZERO);

View File

@@ -50,10 +50,10 @@ public class PriceBiz {
}).forEach(goodsList::add); }).forEach(goodsList::add);
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
if (scenicConfig != null) { if (scenicConfig != null) {
if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceVideo())) {
goodsList.add(new GoodsListRespVO(1L, "录像集")); goodsList.add(new GoodsListRespVO(1L, "录像集"));
} }
if (!Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) { if (!Boolean.TRUE.equals(scenicConfig.getDisableSourceImage())) {
goodsList.add(new GoodsListRespVO(2L, "照片集")); goodsList.add(new GoodsListRespVO(2L, "照片集"));
} }
} }
@@ -92,7 +92,7 @@ public class PriceBiz {
} }
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
if (scenicConfig != null) { if (scenicConfig != null) {
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) { if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
// 景区全免 // 景区全免
respVO.setFree(true); respVO.setFree(true);
respVO.setSlashPrice(BigDecimal.ZERO); respVO.setSlashPrice(BigDecimal.ZERO);

View File

@@ -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);
}
}
}

View File

@@ -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<PriceCalculationResult> 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<FaceRespVO> 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<String> 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<FaceRespVO> 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<PageInfo<OrderV2ListResponse>> 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<OrderV2ListResponse> 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<OrderV2DetailResponse> 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<PaymentParamsResponse> 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";
}
}
}

View File

@@ -3,12 +3,12 @@ package com.ycwl.basic.controller.mobile;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.constant.BaseContextHandler; 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.ScenicAppVO;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO; import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; 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.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.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp; import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; 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.mobile.AppScenicService;
import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; 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.ArrayList;
import java.util.List; import java.util.List;
@@ -49,7 +52,7 @@ public class AppScenicController {
// 分页查询景区列表 // 分页查询景区列表
@PostMapping("/page") @PostMapping("/page")
public ApiResponse<PageInfo<ScenicAppVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){ public ApiResponse<PageInfo<ScenicEntity>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){
String userId = BaseContextHandler.getUserId(); String userId = BaseContextHandler.getUserId();
if (ENABLED_USER_IDs.contains(userId)) { if (ENABLED_USER_IDs.contains(userId)) {
return appScenicService.pageQuery(scenicReqQuery); return appScenicService.pageQuery(scenicReqQuery);

View File

@@ -2,6 +2,7 @@ package com.ycwl.basic.controller.mobile.manage;
import com.ycwl.basic.annotation.IgnoreToken; import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.constant.BaseContextHandler; 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.ScenicLoginOldRespVO;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq; import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO; 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.entity.ScenicAccountEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; 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.AppScenicService;
import com.ycwl.basic.service.pc.ScenicAccountService; import com.ycwl.basic.service.pc.ScenicAccountService;
import com.ycwl.basic.service.pc.ScenicService; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
/** /**
@@ -45,6 +47,8 @@ public class AppScenicAccountController {
private AppScenicService scenicService; private AppScenicService scenicService;
@Autowired @Autowired
private ScenicService adminScenicService; private ScenicService adminScenicService;
@Autowired
private ScenicRepository scenicRepository;
// 登录 // 登录
@PostMapping("/login") @PostMapping("/login")
@@ -73,8 +77,8 @@ public class AppScenicAccountController {
} }
@GetMapping("/myScenicList") @GetMapping("/myScenicList")
public ApiResponse<List<ScenicRespVO>> myScenicList() { public ApiResponse<List<ScenicV2DTO>> myScenicList() {
List<ScenicRespVO> list; List<ScenicV2DTO> list = Collections.emptyList();
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) { if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
String userId = BaseContextHandler.getUserId(); String userId = BaseContextHandler.getUserId();
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId)); ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
@@ -82,10 +86,12 @@ public class AppScenicAccountController {
return ApiResponse.fail("景区账号未绑定景区"); return ApiResponse.fail("景区账号未绑定景区");
} }
list = account.getScenicId().stream() list = account.getScenicId().stream()
.map(id -> scenicService.getDetails(id).getData()) .map(id -> scenicRepository.getScenicBasic(id))
.toList(); .toList();
} else { } else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
list = adminScenicService.list(new ScenicReqQuery()).getData(); ScenicReqQuery query = new ScenicReqQuery();
query.setPageSize(1000);
list = scenicRepository.list(query);
} }
return ApiResponse.success(list); return ApiResponse.success(list);
} }

View File

@@ -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<List<DefaultConfigDTO>> listDefaultConfigs() {
log.info("获取默认配置列表");
try {
List<DefaultConfigDTO> configs = defaultConfigIntegrationService.listDefaultConfigs();
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("获取默认配置列表失败", e);
return ApiResponse.fail("获取默认配置列表失败: " + e.getMessage());
}
}
/**
* 根据配置键获取默认配置
*/
@GetMapping("/{configKey}")
public ApiResponse<DefaultConfigDTO> 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<DefaultConfigDTO> 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<DefaultConfigDTO> 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<Void> 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());
}
}
}

View File

@@ -1,13 +1,12 @@
package com.ycwl.basic.controller.pc; package com.ycwl.basic.controller.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; 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.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.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.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.AppScenicService;
import com.ycwl.basic.service.mobile.AppStatisticsService; import com.ycwl.basic.service.mobile.AppStatisticsService;
import com.ycwl.basic.service.pc.ScenicAccountService; 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.storage.enums.StorageAcl;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil; 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.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired; 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.io.File;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT; import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
/** /**
@@ -50,68 +53,6 @@ public class ScenicController {
@Autowired @Autowired
private ScenicAccountService accountService; private ScenicAccountService accountService;
// 分页查询景区
@PostMapping("/page")
public ApiResponse<PageInfo<ScenicRespVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery) {
return scenicService.pageQuery(scenicReqQuery);
}
// 查询景区列表
@PostMapping("/list")
public ApiResponse<List<ScenicRespVO>> list(@RequestBody ScenicReqQuery scenicReqQuery) {
return scenicService.list(scenicReqQuery);
}
// 查询景区详情
@GetMapping("/getDetail/{id}")
public ApiResponse<ScenicRespVO> getDetail(@PathVariable Long id) {
return scenicService.getById(id);
}
// 新增景区
@PostMapping("/add")
public ApiResponse<Boolean> add(@RequestBody ScenicAddOrUpdateReq scenicAddReq) {
return scenicService.add(scenicAddReq);
}
// 删除景区
@GetMapping("/delete/{id}")
public ApiResponse<Boolean> delete(@PathVariable Long id) {
return scenicService.deleteById(id);
}
// 修改景区
@PostMapping("/update")
public ApiResponse<Boolean> update(@RequestBody ScenicAddOrUpdateReq scenicAddReq) {
return scenicService.update(scenicAddReq);
}
// 修改景区状态
@GetMapping("/updateStatus/{id}")
public ApiResponse<Boolean> updateStatus(@PathVariable Long id) {
return scenicService.updateStatus(id);
}
// 新增景区配置
@PostMapping("/addConfig")
public ApiResponse<Boolean> addConfig(@RequestBody ScenicConfigEntity scenicConfig) {
return scenicService.addConfig(scenicConfig);
}
// 修改景区配置
@PostMapping("/updateConfig")
public ApiResponse<Boolean> updateConfig(@RequestBody ScenicConfigEntity scenicConfig) {
return scenicService.updateConfigById(scenicConfig);
}
// 查询景区配置
@GetMapping("/config/{id}")
public ApiResponse<ScenicConfigEntity> 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下载小程序二维码 // 根据景区ID下载小程序二维码
@GetMapping("/{id}/QRCode") @GetMapping("/{id}/QRCode")
public ApiResponse<String> downloadQrCode(@PathVariable Long id) { public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
@@ -167,19 +108,19 @@ public class ScenicController {
} }
@GetMapping("/myScenicList") @GetMapping("/myScenicList")
public ApiResponse<List<ScenicRespVO>> myScenicList() { public ApiResponse<List<ScenicV2DTO>> myScenicList() {
List<ScenicRespVO> list = Collections.emptyList(); List<ScenicV2DTO> list = Collections.emptyList();
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) { if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
String userId = BaseContextHandler.getUserId(); String userId = BaseContextHandler.getUserId();
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId)); ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
if (account == null || account.getScenicId().isEmpty()) { if (account == null || account.getScenicId().isEmpty()) {
return ApiResponse.fail("景区账号未绑定景区"); return ApiResponse.fail("景区账号未绑定景区");
} }
list = account.getScenicId().stream().map(id -> { list = account.getScenicId().stream().map(id -> scenicRepository.getScenicBasic(id)).toList();
return appScenicService.getDetails(id).getData(); } else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
}).toList(); ScenicReqQuery query = new ScenicReqQuery();
} else { query.setPageSize(1000);
list = scenicService.list(new ScenicReqQuery()).getData(); list = scenicRepository.list(query);
} }
return ApiResponse.success(list); return ApiResponse.success(list);
} }

View File

@@ -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<ScenicV2ListResponse> 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<ScenicV2WithConfigListResponse> 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<ScenicV2DTO> 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<ScenicFilterPageResponse> 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<ScenicV2DTO> 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<ScenicV2DTO> 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<Void> 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<ScenicV2ListResponse> 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<ScenicV2WithConfigDTO> 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<Map<String, Object>> getScenicFlatConfig(@PathVariable Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
try {
Map<String, Object> 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<List<ScenicConfigV2DTO>> listConfigs(@PathVariable Long scenicId) {
log.info("获取景区配置列表, scenicId: {}", scenicId);
try {
List<ScenicConfigV2DTO> 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<ScenicConfigV2DTO> 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<ScenicConfigV2DTO> 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<ScenicConfigV2DTO> 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<Void> 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<BatchUpdateResponse> 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<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable Long scenicId,
@RequestBody Map<String, Object> 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());
}
}
}

View File

@@ -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<ProductItem> 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;
}

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.exception; package com.ycwl.basic.exception;
import com.ycwl.basic.enums.BizCodeEnum; import com.ycwl.basic.enums.BizCodeEnum;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException; import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException;
@@ -106,4 +107,18 @@ public class CustomExceptionHandle {
public ApiResponse<String> handle(SizeLimitExceededException sizeLimitExceededException) { public ApiResponse<String> handle(SizeLimitExceededException sizeLimitExceededException) {
return ApiResponse.buildResponse(415, "文件过大,请重新上传"); return ApiResponse.buildResponse(415, "文件过大,请重新上传");
} }
/**
* 重复购买异常处理
*/
@ExceptionHandler(value = DuplicatePurchaseException.class)
public ApiResponse<String> 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());
}
} }

View File

@@ -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());
};
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -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<T> {
private Integer code;
private String message;
private T data;
public static <T> CommonResponse<T> success() {
return new CommonResponse<>(200, "OK", null);
}
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<>(200, "OK", data);
}
public static <T> CommonResponse<T> success(String message, T data) {
return new CommonResponse<>(200, message, data);
}
public static <T> CommonResponse<T> error(Integer code, String message) {
return new CommonResponse<>(code, message, null);
}
public static <T> CommonResponse<T> error(String message) {
return new CommonResponse<>(5000, message, null);
}
public boolean isSuccess() {
return code != null && code == 200;
}
}

View File

@@ -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<T> {
private List<T> list;
private Integer total;
private Integer page;
private Integer pageSize;
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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 <T> 枚举类型泛型
* @return 枚举值,如果转换失败返回null
*/
public static <T extends Enum<T>> T getEnumValue(Map<String, Object> config, String key, Class<T> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> config, String key) {
return config.get(key);
}
/**
* 从配置Map中获取并转换为指定类型的对象
* 支持JSON字符串自动反序列化
*
* @param config 配置Map
* @param key 配置键
* @param clazz 目标类型
* @param <T> 目标类型泛型
* @return 转换后的对象,如果转换失败返回null
*/
@SuppressWarnings("unchecked")
public static <T> T getObjectValue(Map<String, Object> config, String key, Class<T> 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<String, Object> getMapValue(Map<String, Object> config, String key) {
Object value = config.get(key);
if (value == null) return null;
if (value instanceof Map) {
return (Map<String, Object>) 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<Object> getListValue(Map<String, Object> config, String key) {
Object value = config.get(key);
if (value == null) return null;
if (value instanceof List) {
return (List<Object>) 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 <T> List元素类型泛型
* @return 指定类型的List,如果转换失败返回null
*/
public static <T> List<T> getListValue(Map<String, Object> config, String key, Class<T> 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<String, Object> config, String key) {
return config != null && config.containsKey(key);
}
/**
* 检查配置键是否存在且值不为null
*
* @param config 配置Map
* @param key 配置键
* @return true如果键存在且值不为null
*/
public static boolean hasNonNullValue(Map<String, Object> config, String key) {
return config != null && config.containsKey(key) && config.get(key) != null;
}
}

View File

@@ -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<List<DefaultConfigDTO>> listDefaultConfigs();
@GetMapping("/{configKey}")
CommonResponse<DefaultConfigDTO> getDefaultConfig(@PathVariable("configKey") String configKey);
@PostMapping("/")
CommonResponse<DefaultConfigDTO> createDefaultConfig(@RequestBody DefaultConfigDTO request);
@PutMapping("/{configKey}")
CommonResponse<DefaultConfigDTO> updateDefaultConfig(@PathVariable("configKey") String configKey,
@RequestBody DefaultConfigDTO request);
@DeleteMapping("/{configKey}")
CommonResponse<Void> deleteDefaultConfig(@PathVariable("configKey") String configKey);
}

View File

@@ -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<List<ScenicConfigV2DTO>> listConfigs(@PathVariable("scenicId") Long scenicId);
@GetMapping("/{scenicId}/key/{configKey}")
CommonResponse<ScenicConfigV2DTO> getConfigByKey(@PathVariable("scenicId") Long scenicId,
@PathVariable("configKey") String configKey);
@GetMapping("/{scenicId}/keys")
CommonResponse<Map<String, Object>> getFlatConfigs(@PathVariable("scenicId") Long scenicId);
@PostMapping("/{scenicId}")
CommonResponse<ScenicConfigV2DTO> createConfig(@PathVariable("scenicId") Long scenicId,
@RequestBody CreateConfigRequest request);
@PutMapping("/{scenicId}/{id}")
CommonResponse<ScenicConfigV2DTO> updateConfig(@PathVariable("scenicId") Long scenicId,
@PathVariable("id") String id,
@RequestBody UpdateConfigRequest request);
@DeleteMapping("/{scenicId}/{id}")
CommonResponse<Void> deleteConfig(@PathVariable("scenicId") Long scenicId,
@PathVariable("id") String id);
@PostMapping("/{scenicId}/batch")
CommonResponse<BatchUpdateResponse> batchUpdateConfigs(@PathVariable("scenicId") Long scenicId,
@RequestBody BatchConfigRequest request);
@PostMapping("/{scenicId}/batchFlatUpdate")
CommonResponse<BatchUpdateResponse> batchFlatUpdateConfigs(@PathVariable("scenicId") Long scenicId,
@RequestBody Map<String, Object> configs);
}

View File

@@ -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<ScenicV2DTO> getScenic(@PathVariable("scenicId") Long scenicId);
@GetMapping("/{scenicId}/with-config")
CommonResponse<ScenicV2WithConfigDTO> getScenicWithConfig(@PathVariable("scenicId") Long scenicId);
@PostMapping("/")
CommonResponse<ScenicV2DTO> createScenic(@RequestBody CreateScenicRequest request);
@PutMapping("/{scenicId}")
CommonResponse<ScenicV2DTO> updateScenic(@PathVariable("scenicId") Long scenicId,
@RequestBody UpdateScenicRequest request);
@DeleteMapping("/{scenicId}")
CommonResponse<Void> deleteScenic(@PathVariable("scenicId") Long scenicId);
@PostMapping("/filter")
CommonResponse<ScenicFilterPageResponse> filterScenics(@RequestBody ScenicFilterRequest request);
@GetMapping("/")
CommonResponse<ScenicV2ListResponse> 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<ScenicV2WithConfigListResponse> listScenicsWithConfig(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String name);
}

View File

@@ -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集成配置初始化完成");
}
}

View File

@@ -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<BatchConfigItem> configs;
@Data
public static class BatchConfigItem {
@JsonProperty("configKey")
@NotEmpty(message = "配置键不能为空")
private String configKey;
@JsonProperty("configValue")
private String configValue;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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<ScenicFilterItem> 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;
}
}

View File

@@ -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<FilterCondition> filters;
@JsonProperty("page")
private Integer page = 1;
@JsonProperty("pageSize")
private Integer pageSize = 20;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<ScenicV2DTO> list;
@JsonProperty("total")
private Integer total;
@JsonProperty("page")
private Integer page;
@JsonProperty("pageSize")
private Integer pageSize;
}

View File

@@ -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<String, Object> config;
}

View File

@@ -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<ScenicV2WithConfigDTO> list;
@JsonProperty("total")
private Integer total;
@JsonProperty("page")
private Integer page;
@JsonProperty("pageSize")
private Integer pageSize;
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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<DefaultConfigDTO> listDefaultConfigs() {
log.info("获取默认配置列表");
CommonResponse<List<DefaultConfigDTO>> response = defaultConfigClient.listDefaultConfigs();
return handleResponse(response, "获取默认配置列表失败");
}
public DefaultConfigDTO getDefaultConfig(String configKey) {
log.info("获取指定默认配置, configKey: {}", configKey);
CommonResponse<DefaultConfigDTO> response = defaultConfigClient.getDefaultConfig(configKey);
return handleResponse(response, "获取指定默认配置失败");
}
public DefaultConfigDTO createDefaultConfig(DefaultConfigDTO request) {
log.info("创建默认配置, configKey: {}", request.getConfigKey());
CommonResponse<DefaultConfigDTO> response = defaultConfigClient.createDefaultConfig(request);
return handleResponse(response, "创建默认配置失败");
}
public DefaultConfigDTO updateDefaultConfig(String configKey, DefaultConfigDTO request) {
log.info("更新默认配置, configKey: {}", configKey);
CommonResponse<DefaultConfigDTO> response = defaultConfigClient.updateDefaultConfig(configKey, request);
return handleResponse(response, "更新默认配置失败");
}
public void deleteDefaultConfig(String configKey) {
log.info("删除默认配置, configKey: {}", configKey);
CommonResponse<Void> response = defaultConfigClient.deleteDefaultConfig(configKey);
handleResponse(response, "删除默认配置失败");
}
private <T> T handleResponse(CommonResponse<T> 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();
}
}

View File

@@ -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<ScenicConfigV2DTO> listConfigs(Long scenicId) {
log.info("获取景区配置列表, scenicId: {}", scenicId);
CommonResponse<List<ScenicConfigV2DTO>> response = scenicConfigV2Client.listConfigs(scenicId);
return handleResponse(response, "获取景区配置列表失败");
}
public ScenicConfigV2DTO getConfigByKey(Long scenicId, String configKey) {
log.info("根据键获取景区配置, scenicId: {}, configKey: {}", scenicId, configKey);
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.getConfigByKey(scenicId, configKey);
return handleResponse(response, "根据键获取景区配置失败");
}
public Map<String, Object> getFlatConfigs(Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
return handleResponse(response, "获取景区扁平化配置失败");
}
public ScenicConfigV2DTO createConfig(Long scenicId, CreateConfigRequest request) {
log.info("创建景区配置, scenicId: {}, configKey: {}", scenicId, request.getConfigKey());
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.createConfig(scenicId, request);
return handleResponse(response, "创建景区配置失败");
}
public ScenicConfigV2DTO updateConfig(Long scenicId, String id, UpdateConfigRequest request) {
log.info("更新景区配置, scenicId: {}, id: {}", scenicId, id);
CommonResponse<ScenicConfigV2DTO> response = scenicConfigV2Client.updateConfig(scenicId, id, request);
return handleResponse(response, "更新景区配置失败");
}
public void deleteConfig(Long scenicId, String id) {
log.info("删除景区配置, scenicId: {}, id: {}", scenicId, id);
CommonResponse<Void> response = scenicConfigV2Client.deleteConfig(scenicId, id);
handleResponse(response, "删除景区配置失败");
}
public BatchUpdateResponse batchUpdateConfigs(Long scenicId, BatchConfigRequest request) {
log.info("批量更新景区配置, scenicId: {}, configs count: {}", scenicId, request.getConfigs().size());
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchUpdateConfigs(scenicId, request);
return handleResponse(response, "批量更新景区配置失败");
}
public BatchUpdateResponse batchFlatUpdateConfigs(Long scenicId, Map<String, Object> configs) {
log.info("扁平化批量更新景区配置, scenicId: {}, configs count: {}", scenicId, configs.size());
CommonResponse<BatchUpdateResponse> response = scenicConfigV2Client.batchFlatUpdateConfigs(scenicId, configs);
return handleResponse(response, "扁平化批量更新景区配置失败");
}
private <T> T handleResponse(CommonResponse<T> 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();
}
}

View File

@@ -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<ScenicV2DTO> response = scenicV2Client.getScenic(scenicId);
return handleResponse(response, "获取景区信息失败");
}
public ScenicV2WithConfigDTO getScenicWithConfig(Long scenicId) {
log.info("获取景区配置信息, scenicId: {}", scenicId);
CommonResponse<ScenicV2WithConfigDTO> response = scenicV2Client.getScenicWithConfig(scenicId);
return handleResponse(response, "获取景区配置信息失败");
}
public Map<String, Object> getScenicFlatConfig(Long scenicId) {
log.info("获取景区扁平化配置, scenicId: {}", scenicId);
CommonResponse<Map<String, Object>> response = scenicConfigV2Client.getFlatConfigs(scenicId);
return handleResponse(response, "获取景区扁平化配置失败");
}
public ScenicV2DTO createScenic(CreateScenicRequest request) {
log.info("创建景区, name: {}", request.getName());
CommonResponse<ScenicV2DTO> response = scenicV2Client.createScenic(request);
return handleResponse(response, "创建景区失败");
}
public ScenicV2DTO updateScenic(Long scenicId, UpdateScenicRequest request) {
log.info("更新景区信息, scenicId: {}", scenicId);
CommonResponse<ScenicV2DTO> response = scenicV2Client.updateScenic(scenicId, request);
return handleResponse(response, "更新景区信息失败");
}
public void deleteScenic(Long scenicId) {
log.info("删除景区, scenicId: {}", scenicId);
CommonResponse<Void> response = scenicV2Client.deleteScenic(scenicId);
handleResponse(response, "删除景区失败");
}
public ScenicFilterPageResponse filterScenics(ScenicFilterRequest request) {
log.info("筛选景区, filters: {}", request.getFilters().size());
CommonResponse<ScenicFilterPageResponse> 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<ScenicV2ListResponse> 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<ScenicV2WithConfigListResponse> response = scenicV2Client.listScenicsWithConfig(page, pageSize, status, name);
return handleResponse(response, "分页查询景区带配置列表失败");
}
private <T> T handleResponse(CommonResponse<T> 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();
}
}

View File

@@ -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<ScenicRespVO> 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<ScenicAppVO> appList(ScenicReqQuery scenicReqQuery);
ScenicRespVO getAppById(Long id);
/**
* 通过经纬度计算景区距离
*
* @param scenicIndexVO
* @return
*/
List<ScenicAppVO> scenicListByLnLa(ScenicIndexVO scenicIndexVO);
}

View File

@@ -67,17 +67,17 @@ public class ScenicConfigEntity {
/** /**
* 是否开启全部免费 * 是否开启全部免费
*/ */
private Integer allFree; private Boolean allFree;
/** /**
* 是否禁用源视频 * 是否禁用源视频
* 0-否 1-是 * 0-否 1-是
*/ */
private Integer disableSourceVideo; private Boolean disableSourceVideo;
/** /**
* 是否禁用源图片 * 是否禁用源图片
* 0-否 1-是 * 0-否 1-是
*/ */
private Integer disableSourceImage; private Boolean disableSourceImage;
private Integer templateNewVideoType; private Integer templateNewVideoType;
/** /**
* 是否开启防录屏 * 是否开启防录屏
@@ -130,5 +130,5 @@ public class ScenicConfigEntity {
* 是否启用券码功能 * 是否启用券码功能
* 0-禁用 1-启用 * 0-禁用 1-启用
*/ */
private Integer voucherEnable; private Boolean voucherEnable;
} }

View File

@@ -1,11 +1,8 @@
package com.ycwl.basic.model.pc.scenic.entity; package com.ycwl.basic.model.pc.scenic.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date;
/** /**
* @Author:longbinbin * @Author:longbinbin
@@ -13,15 +10,17 @@ import java.util.Date;
* 景区管理表 * 景区管理表
*/ */
@Data @Data
@TableName("scenic")
public class ScenicEntity { public class ScenicEntity {
@TableId
private Long id; private Long id;
/** /**
* 景区名称 * 景区名称
*/ */
private String name; private String name;
private Integer mpId; private Integer mpId;
private String phone;
private String logoUrl;
// 封面图
private String coverUrl;
/** /**
* 景区介绍 * 景区介绍
*/ */
@@ -58,11 +57,10 @@ public class ScenicEntity {
* 状态 1启用0关闭 * 状态 1启用0关闭
*/ */
private String status; private String status;
private Date createTime;
private Date updateTime;
/** /**
* 景区源素材价格,元 * 景区源素材价格,元
*/ */
private String kfCodeUrl;
private BigDecimal price; private BigDecimal price;
private BigDecimal sourceVideoPrice; private BigDecimal sourceVideoPrice;
private BigDecimal sourceImagePrice; private BigDecimal sourceImagePrice;

View File

@@ -23,51 +23,9 @@ public class ScenicReqQuery extends BaseQueryParameterReq {
*/ */
// 景区名称 // 景区名称
private String name; 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关闭
*/ */
// 状态 1启用0关闭 // 状态 1启用0关闭
private String status; private String status;
private Date startTime;
private Date endTime;
} }

View File

@@ -34,9 +34,9 @@ public class ScenicConfigResp {
* 视频保存时间 * 视频保存时间
*/ */
private Integer videoStoreDay; private Integer videoStoreDay;
private Integer allFree; private Boolean allFree;
private Integer disableSourceVideo; private Boolean disableSourceVideo;
private Integer disableSourceImage; private Boolean disableSourceImage;
private Integer antiScreenRecordType; private Integer antiScreenRecordType;
private Integer videoSourceStoreDay; private Integer videoSourceStoreDay;
private Integer imageSourceStoreDay; private Integer imageSourceStoreDay;
@@ -45,9 +45,5 @@ public class ScenicConfigResp {
private String imageSourcePackHint = ""; private String imageSourcePackHint = "";
private String videoSourcePackHint = ""; private String videoSourcePackHint = "";
/** private Boolean voucherEnable;
* 是否启用券码功能
* 0-禁用 1-启用
*/
private Integer voucherEnable;
} }

View File

@@ -74,19 +74,6 @@ public class ScenicRespVO {
*/ */
// 详细地址 // 详细地址
private String address; 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 Integer lensNum;
private String kfCodeUrl; private String kfCodeUrl;

View File

@@ -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<PageInfo<OrderV2ListResponse>> pageOrders(@RequestBody OrderV2PageRequest request) {
log.info("分页查询订单列表: {}", request);
try {
PageInfo<OrderV2ListResponse> pageInfo = orderService.pageOrders(request);
return ApiResponse.success(pageInfo);
} catch (Exception e) {
log.error("分页查询订单列表失败", e);
return ApiResponse.fail("查询失败:" + e.getMessage());
}
}
/**
* 查询订单详情
*/
@GetMapping("/detail/{orderId}")
public ApiResponse<OrderV2DetailResponse> 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<String> 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<String> 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());
}
}
}

View File

@@ -0,0 +1,15 @@
package com.ycwl.basic.order.dto;
import lombok.Data;
/**
* 订单备注修改请求DTO
*/
@Data
public class OrderRemarkRequest {
/**
* 订单备注
*/
private String remarks;
}

View File

@@ -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<OrderItemV2> orderItems;
/**
* 订单优惠记录列表
*/
private List<OrderDiscountV2> orderDiscounts;
/**
* 订单退款记录列表
*/
private List<OrderRefundV2> orderRefunds;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.order.dto;
import lombok.Data;
/**
* 获取支付参数请求DTO
* 所有参数都是可选的,系统会自动生成商品名称和描述
*/
@Data
public class PaymentParamsRequest {
// 预留字段,目前所有信息都由系统自动生成
// 可以在未来版本中扩展支持自定义参数
public PaymentParamsRequest() {
}
}

View File

@@ -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<String, Object> paymentParams;
/**
* 支付描述信息
*/
private String description;
/**
* 商品名称
*/
private String goodsName;
/**
* 创建成功的支付参数响应
*/
public static PaymentParamsResponse success(Long orderId, String orderNo, BigDecimal payAmount,
Boolean needPay, Map<String, Object> 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;
}
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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<OrderEventListener> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<OrderDiscountV2> {
}

View File

@@ -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<OrderItemV2> {
}

View File

@@ -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<OrderRefundV2> {
}

View File

@@ -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<OrderV2> {
}

View File

@@ -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<OrderV2ListResponse> 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<OrderV2ListResponse> 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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -438,7 +438,7 @@ public interface IDiscountDetectionService {
@Component @Component
public class FlashSaleDiscountProvider implements IDiscountProvider { public class FlashSaleDiscountProvider implements IDiscountProvider {
@Override @Override
public String getProviderType() { return "FLASH_SALE"; } public String getProviderType() { return "LIMITED_TIME"; }
@Override @Override
public int getPriority() { return 90; } // 介于券码和优惠券之间 public int getPriority() { return 90; } // 介于券码和优惠券之间

View File

@@ -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;
}
}

View File

@@ -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";
}

View File

@@ -44,7 +44,7 @@ public class DiscountDetail {
detail.setDiscountName("限时立减"); detail.setDiscountName("限时立减");
detail.setDiscountAmount(discountAmount); detail.setDiscountAmount(discountAmount);
detail.setDescription("限时优惠,立即享受"); detail.setDescription("限时优惠,立即享受");
detail.setSortOrder(2); // 限时立减排在券码后 detail.setSortOrder(1); // 限时立减排在最前
return detail; return detail;
} }
@@ -70,7 +70,7 @@ public class DiscountDetail {
detail.setDiscountName("券码优惠"); detail.setDiscountName("券码优惠");
detail.setDiscountAmount(discountAmount); detail.setDiscountAmount(discountAmount);
detail.setDescription(String.format("券码 %s - %s", voucherCode, discountTypeName)); detail.setDescription(String.format("券码 %s - %s", voucherCode, discountTypeName));
detail.setSortOrder(1); // 券码优先级最高,排在最前面 detail.setSortOrder(2); // 券码显示顺序低于限时立减
return detail; return detail;
} }

View File

@@ -0,0 +1,62 @@
package com.ycwl.basic.pricing.dto;
import lombok.Data;
import java.util.List;
/**
* 移动端价格计算请求DTO
*/
@Data
public class MobilePriceCalculationRequest {
/**
* 商品列表
*/
private List<ProductItem> 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;
}
}

View File

@@ -3,6 +3,8 @@ package com.ycwl.basic.pricing.service;
import com.ycwl.basic.pricing.dto.CouponInfo; import com.ycwl.basic.pricing.dto.CouponInfo;
import com.ycwl.basic.pricing.dto.CouponUseRequest; import com.ycwl.basic.pricing.dto.CouponUseRequest;
import com.ycwl.basic.pricing.dto.CouponUseResult; 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.dto.ProductItem;
import com.ycwl.basic.pricing.entity.PriceCouponConfig; import com.ycwl.basic.pricing.entity.PriceCouponConfig;
@@ -59,4 +61,12 @@ public interface ICouponService {
* @return 可用优惠券列表 * @return 可用优惠券列表
*/ */
List<CouponInfo> getUserAvailableCoupons(Long userId); List<CouponInfo> getUserAvailableCoupons(Long userId);
/**
* 领取优惠券(内部调用方法)
*
* @param request 领取请求
* @return 领取结果
*/
CouponClaimResult claimCoupon(CouponClaimRequest request);
} }

View File

@@ -14,7 +14,7 @@ public interface IDiscountProvider {
/** /**
* 获取提供者类型 * 获取提供者类型
* @return 提供者类型标识,如 "COUPON", "VOUCHER", "FLASH_SALE" 等 * @return 提供者类型标识,如 "COUPON", "VOUCHER", "LIMITED_TIME" 等
*/ */
String getProviderType(); String getProviderType();

View File

@@ -2,6 +2,8 @@ package com.ycwl.basic.pricing.service;
import com.ycwl.basic.pricing.entity.*; import com.ycwl.basic.pricing.entity.*;
import java.math.BigDecimal;
/** /**
* 价格管理服务接口(用于配置管理,手动处理时间字段) * 价格管理服务接口(用于配置管理,手动处理时间字段)
*/ */
@@ -90,4 +92,21 @@ public interface IPricingManagementService {
* 删除一口价配置 * 删除一口价配置
*/ */
boolean deleteBundleConfig(Long id); 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);
} }

View File

@@ -196,4 +196,90 @@ public class CouponServiceImpl implements ICouponService {
info.setActualDiscountAmount(actualDiscountAmount); info.setActualDiscountAmount(actualDiscountAmount);
return info; 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());
}
}
} }

View File

@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
/** /**
@@ -165,4 +166,49 @@ public class PricingManagementServiceImpl implements IPricingManagementService {
log.info("删除一口价配置: id={}", id); log.info("删除一口价配置: id={}", id);
return bundleConfigMapper.deleteById(id) > 0; 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();
}
}
} }

View File

@@ -238,6 +238,5 @@ public class VoucherPrintServiceImpl implements VoucherPrintService {
content += "<C>赠品兑换码</C>"; content += "<C>赠品兑换码</C>";
content += "<C>有效期:"+sdf2.format(new Date())+"</C>"; content += "<C>有效期:"+sdf2.format(new Date())+"</C>";
FeiETicketPrinter.doPrint("550519002", content, 1); FeiETicketPrinter.doPrint("550519002", content, 1);
// 小票配对码、赠品兑换码、抵扣兑换码
} }
} }

View File

@@ -1,30 +1,45 @@
package com.ycwl.basic.repository; 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.MpConfigMapper;
import com.ycwl.basic.mapper.MpNotifyConfigMapper; 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.MpConfigEntity;
import com.ycwl.basic.model.pc.mp.MpNotifyConfigEntity; import com.ycwl.basic.model.pc.mp.MpNotifyConfigEntity;
import com.ycwl.basic.model.pc.mp.ScenicMpNotifyVO; 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.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; 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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
import java.util.Map;
@Component @Component
public class ScenicRepository { public class ScenicRepository {
@Autowired
private ScenicMapper scenicMapper;
@Autowired @Autowired
private MpConfigMapper mpConfigMapper; private MpConfigMapper mpConfigMapper;
@Autowired @Autowired
private RedisTemplate<String, String> redisTemplate; private RedisTemplate<String, String> redisTemplate;
@Autowired
private ScenicIntegrationService scenicIntegrationService;
@Autowired
private ScenicConfigIntegrationService scenicConfigIntegrationService;
public static final String SCENIC_CACHE_KEY = "scenic:%s"; 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_FULL_CACHE_KEY = "scenic:f%s";
public static final String SCENIC_CONFIG_CACHE_KEY = "scenic:%s:config"; public static final String SCENIC_CONFIG_CACHE_KEY = "scenic:%s:config";
public static final String SCENIC_MP_CACHE_KEY = "scenic:%s:mp"; public static final String SCENIC_MP_CACHE_KEY = "scenic:%s:mp";
@@ -32,26 +47,78 @@ public class ScenicRepository {
@Autowired @Autowired
private MpNotifyConfigMapper mpNotifyConfigMapper; 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) { public ScenicEntity getScenic(Long id) {
if (redisTemplate.hasKey(String.format(SCENIC_CACHE_KEY, id))) { String key = String.format(SCENIC_CACHE_KEY, id);
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(SCENIC_CACHE_KEY, id)), ScenicEntity.class); 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) { public ScenicConfigEntity getScenicConfig(Long scenicId) {
if (redisTemplate.hasKey(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId))) { String key = String.format(SCENIC_CONFIG_CACHE_KEY, scenicId);
return JacksonUtil.parseObject(redisTemplate.opsForValue().get(String.format(SCENIC_CONFIG_CACHE_KEY, scenicId)), ScenicConfigEntity.class); 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) { public MpConfigEntity getScenicMpConfig(Long scenicId) {
@@ -121,12 +188,191 @@ public class ScenicRepository {
return null; return null;
} }
public List<ScenicV2DTO> 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) { public void clearCache(Long scenicId) {
redisTemplate.delete(String.format(SCENIC_CACHE_KEY, 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_FULL_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_CONFIG_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_CACHE_KEY, scenicId));
redisTemplate.delete(String.format(SCENIC_MP_NOTIFY_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<String, Object> 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<ScenicConfigV2DTO> 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<com.ycwl.basic.integration.scenic.dto.config.ScenicConfigV2DTO> 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<ScenicConfigV2DTO> cachedConfigList =
JacksonUtil.parseArray(cachedConfigJson, ScenicConfigV2DTO.class);
return new ScenicConfigManager(cachedConfigList);
} catch (Exception cacheException) {
// 缓存解析失败,返回null
return null;
}
}
// 缓存也没有,返回null
return null;
}
}
} }

View File

@@ -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<ProductItem> products) {
String productsHash = calculateProductsHash(products);
return CACHE_PREFIX + userId + ":" + scenicId + ":" + productsHash;
}
/**
* 计算商品列表的哈希值
* 基于商品类型、ID、购买数量等必传字段生成唯一哈希,忽略可选的quantity字段
*
* @param products 商品列表
* @return 哈希值
*/
private String calculateProductsHash(List<ProductItem> 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<ProductItem> 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<ProductItem> 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<ProductItem> 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<ProductItem> 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<ProductItem> 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<ProductItem> products) {
String cacheKey = generateCacheKey(userId, scenicId, products);
Long expire = redisTemplate.getExpire(cacheKey);
return expire != null ? expire : -2;
}
}

View File

@@ -13,7 +13,6 @@ import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.mapper.DeviceMapper; import com.ycwl.basic.mapper.DeviceMapper;
import com.ycwl.basic.mapper.CustomUploadTaskMapper; import com.ycwl.basic.mapper.CustomUploadTaskMapper;
import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.ScenicMapper;
import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.custom.entity.CustomUploadTaskEntity; import com.ycwl.basic.model.custom.entity.CustomUploadTaskEntity;

View File

@@ -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.ScenicRegisterReq;
import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterRespVO; import com.ycwl.basic.model.mobile.scenic.account.ScenicRegisterRespVO;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; 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.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
@@ -20,7 +21,7 @@ import java.util.List;
* @Date:2024/12/6 10:23 * @Date:2024/12/6 10:23
*/ */
public interface AppScenicService { public interface AppScenicService {
ApiResponse<PageInfo<ScenicAppVO>> pageQuery(ScenicReqQuery scenicReqQuery); ApiResponse<PageInfo<ScenicEntity>> pageQuery(ScenicReqQuery scenicReqQuery);
ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(Long scenicId); ApiResponse<ScenicDeviceCountVO> deviceCountByScenicId(Long scenicId);

View File

@@ -1,10 +1,11 @@
package com.ycwl.basic.service.mobile.impl; package com.ycwl.basic.service.mobile.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.*; 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.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; 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.entity.DeviceEntity;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO; 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.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.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService; import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.pc.ScenicAccountService; import com.ycwl.basic.service.pc.ScenicAccountService;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
@@ -30,6 +33,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -43,8 +47,6 @@ import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
@Service @Service
public class AppScenicServiceImpl implements AppScenicService { public class AppScenicServiceImpl implements AppScenicService {
@Autowired
private ScenicMapper scenicMapper;
@Autowired @Autowired
private DeviceMapper deviceMapper; private DeviceMapper deviceMapper;
@Autowired @Autowired
@@ -59,12 +61,19 @@ public class AppScenicServiceImpl implements AppScenicService {
private ExtraDeviceMapper extraDeviceMapper; private ExtraDeviceMapper extraDeviceMapper;
@Autowired @Autowired
private RedisTemplate<String, String> redisTemplate; private RedisTemplate<String, String> redisTemplate;
@Autowired
private ScenicRepository scenicRepository;
@Override @Override
public ApiResponse<PageInfo<ScenicAppVO>> pageQuery(ScenicReqQuery scenicReqQuery) { public ApiResponse<PageInfo<ScenicEntity>> pageQuery(ScenicReqQuery scenicReqQuery) {
PageHelper.startPage(scenicReqQuery.getPageNum(), scenicReqQuery.getPageSize());
List<ScenicAppVO> list = scenicMapper.appList(scenicReqQuery); ScenicReqQuery query = new ScenicReqQuery();
PageInfo<ScenicAppVO> pageInfo = new PageInfo<>(list); query.setPageSize(1000);
List<ScenicV2DTO> scenicList = scenicRepository.list(query);
List<ScenicEntity> list = scenicList.stream().map(scenic -> {
return scenicRepository.getScenic(Long.valueOf(scenic.getId()));
}).toList();
PageInfo<ScenicEntity> pageInfo = new PageInfo<>(list);
return ApiResponse.success(pageInfo); return ApiResponse.success(pageInfo);
} }
@@ -77,7 +86,27 @@ public class AppScenicServiceImpl implements AppScenicService {
@Override @Override
public ApiResponse<ScenicRespVO> getDetails(Long id) { public ApiResponse<ScenicRespVO> 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); ScenicDeviceCountVO scenicDeviceCountVO = deviceMapper.deviceCountByScenicId(id);
scenicRespVO.setLensNum(scenicDeviceCountVO.getTotalDeviceCount()); scenicRespVO.setLensNum(scenicDeviceCountVO.getTotalDeviceCount());
return ApiResponse.success(scenicRespVO); return ApiResponse.success(scenicRespVO);
@@ -171,8 +200,59 @@ public class AppScenicServiceImpl implements AppScenicService {
@Override @Override
public List<ScenicAppVO> scenicListByLnLa(ScenicIndexVO scenicIndexVO) { public List<ScenicAppVO> scenicListByLnLa(ScenicIndexVO scenicIndexVO) {
List<ScenicAppVO> scenicAppVOS = scenicMapper.scenicListByLnLa(scenicIndexVO); // 从 scenicRepository 获取所有景区(1000个)
return scenicAppVOS.stream().filter(scenic -> scenic.getDistance().compareTo(scenic.getRadius().multiply(BigDecimal.valueOf(1_000L))) < 0).toList(); ScenicReqQuery query = new ScenicReqQuery();
query.setPageNum(1);
query.setPageSize(1000);
List<ScenicV2DTO> scenicList = scenicRepository.list(query);
List<ScenicAppVO> 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 @Override
@@ -214,4 +294,29 @@ public class AppScenicServiceImpl implements AppScenicService {
deviceRespVOList.addAll(0, extraDeviceList); deviceRespVOList.addAll(0, extraDeviceList);
return ApiResponse.success(deviceRespVOList); 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);
}
} }

View File

@@ -139,9 +139,9 @@ public class GoodsServiceImpl implements GoodsService {
List<SourceRespVO> goods = faceEntry.getValue(); List<SourceRespVO> goods = faceEntry.getValue();
return goods.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).keySet().stream().filter(type -> { return goods.stream().collect(Collectors.groupingBy(SourceRespVO::getType)).keySet().stream().filter(type -> {
if (Integer.valueOf(1).equals(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)) { } else if (Integer.valueOf(2).equals(type)) {
return !Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage()); return !Boolean.TRUE.equals(scenicConfig.getDisableSourceImage());
} }
return true; return true;
}).map(type -> { }).map(type -> {

View File

@@ -1,11 +1,8 @@
package com.ycwl.basic.service.pc; package com.ycwl.basic.service.pc;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
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.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.pay.adapter.IPayAdapter; import com.ycwl.basic.pay.adapter.IPayAdapter;
import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
@@ -17,23 +14,7 @@ import java.util.List;
* @Date:2024/12/3 15:22 * @Date:2024/12/3 15:22
*/ */
public interface ScenicService { public interface ScenicService {
ApiResponse<PageInfo<ScenicRespVO>> pageQuery(ScenicReqQuery scenicReqQuery); ApiResponse<List<ScenicV2DTO>> list(ScenicReqQuery scenicReqQuery);
ApiResponse<List<ScenicRespVO>> list(ScenicReqQuery scenicReqQuery);
ApiResponse<ScenicRespVO> getById(Long id);
ApiResponse<Boolean> add(ScenicAddOrUpdateReq scenicAddOrUpdateReq);
ApiResponse<Boolean> deleteById(Long id);
ApiResponse<Boolean> update(ScenicAddOrUpdateReq scenicAddOrUpdateReq);
ApiResponse<Boolean> updateStatus(Long id);
ApiResponse<Boolean> addConfig(ScenicConfigEntity scenicConfig);
/**
* 修改景区配置
* @param scenicConfig
* @return
*/
ApiResponse<Boolean> updateConfigById(ScenicConfigEntity scenicConfig);
ScenicConfigEntity getConfig(Long id);
void saveConfig(Long configId, ScenicConfigEntity config);
IStorageAdapter getScenicStorageAdapter(Long scenicId); IStorageAdapter getScenicStorageAdapter(Long scenicId);

View File

@@ -324,7 +324,7 @@ public class FaceServiceImpl implements FaceService {
} }
} else { } 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 videoCount = memberSourceEntityList.stream().filter(item -> item.getType().equals(1)).count();
long photoCount = memberSourceEntityList.stream().filter(item -> item.getType().equals(2)).count(); long photoCount = memberSourceEntityList.stream().filter(item -> item.getType().equals(2)).count();
if (photoCount > videoCount) { if (photoCount > videoCount) {
@@ -443,7 +443,7 @@ public class FaceServiceImpl implements FaceService {
sourceVideoContent.setLockType(-1); sourceVideoContent.setLockType(-1);
sourceImageContent.setLockType(-1); sourceImageContent.setLockType(-1);
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(faceRespVO.getScenicId()); 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); IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 2, faceId);
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType()); sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
sourceImageContent.setContentId(isBuyRespVO.getGoodsId()); sourceImageContent.setContentId(isBuyRespVO.getGoodsId());
@@ -462,7 +462,7 @@ public class FaceServiceImpl implements FaceService {
sourceImageContent.setFreeCount((int) freeCount); sourceImageContent.setFreeCount((int) freeCount);
contentList.add(sourceImageContent); 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); IsBuyRespVO isBuyRespVO = orderBiz.isBuy(userId, faceRespVO.getScenicId(), 1, faceId);
sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType()); sourceVideoContent.setSourceType(isBuyRespVO.getGoodsType());
sourceVideoContent.setContentId(isBuyRespVO.getGoodsId()); sourceVideoContent.setContentId(isBuyRespVO.getGoodsId());

View File

@@ -57,6 +57,7 @@ import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; 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.Scope;
import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -78,7 +79,7 @@ import java.util.stream.Collectors;
* @Date:2024/12/3 13:54 * @Date:2024/12/3 13:54
*/ */
@Slf4j @Slf4j
@Service @Service("OrderServiceOldImpl")
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class OrderServiceImpl implements OrderService { public class OrderServiceImpl implements OrderService {
@@ -86,6 +87,7 @@ public class OrderServiceImpl implements OrderService {
private OrderMapper orderMapper; private OrderMapper orderMapper;
@Autowired @Autowired
@Qualifier(value = "OrderServiceOldImpl")
private OrderServiceImpl self; private OrderServiceImpl self;
@Autowired @Autowired
private SourceMapper sourceMapper; private SourceMapper sourceMapper;

View File

@@ -1,38 +1,29 @@
package com.ycwl.basic.service.pc.impl; 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.FaceBodyFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter; import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.mapper.ScenicAccountMapper; import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.mapper.ScenicMapper;
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.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.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.pay.PayFactory; import com.ycwl.basic.pay.PayFactory;
import com.ycwl.basic.pay.adapter.IPayAdapter; import com.ycwl.basic.pay.adapter.IPayAdapter;
import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.ScenicService; 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.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter; import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.exceptions.StorageUnsupportedException; 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.ApiResponse;
import com.ycwl.basic.utils.SnowFlakeUtil; import com.ycwl.basic.utils.JacksonUtil;
import lombok.extern.slf4j.Slf4j; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME;
/** /**
* @Author:longbinbin * @Author:longbinbin
@@ -41,199 +32,39 @@ import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME;
@Slf4j @Slf4j
@Service @Service
public class ScenicServiceImpl implements ScenicService { public class ScenicServiceImpl implements ScenicService {
@Autowired
private ScenicMapper scenicMapper;
@Autowired
private ScenicAccountMapper scenicAccountMapper;
@Autowired
private TaskFaceService taskFaceService;
@Autowired @Autowired
private ScenicRepository scenicRepository; private ScenicRepository scenicRepository;
@Override // TTL缓存配置,默认10分钟过期
public ApiResponse<PageInfo<ScenicRespVO>> pageQuery(ScenicReqQuery scenicReqQuery) { private static final long DEFAULT_CACHE_TTL_MINUTES = 10;
PageHelper.startPage(scenicReqQuery.getPageNum(), scenicReqQuery.getPageSize());
List<ScenicRespVO> list = scenicMapper.list(scenicReqQuery); // 使用TTL缓存替代静态Map
PageInfo<ScenicRespVO> pageInfo = new PageInfo<>(list); private static final TtlCacheMap<Long, IStorageAdapter> scenicStorageAdapterCache =
return ApiResponse.success(pageInfo); new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES));
} private static final TtlCacheMap<Long, IStorageAdapter> scenicTmpStorageAdapterCache =
new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES));
private static final TtlCacheMap<Long, IStorageAdapter> scenicLocalStorageAdapterCache =
new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES));
private static final TtlCacheMap<Long, IFaceBodyAdapter> scenicFaceBodyAdapterCache =
new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES));
private static final TtlCacheMap<Long, IPayAdapter> scenicPayAdapterCache =
new TtlCacheMap<>(TimeUnit.MINUTES.toMillis(DEFAULT_CACHE_TTL_MINUTES));
@Override @Override
public ApiResponse<List<ScenicRespVO>> list(ScenicReqQuery scenicReqQuery) { @Deprecated
return ApiResponse.success(scenicMapper.list(scenicReqQuery)); public ApiResponse<List<ScenicV2DTO>> list(ScenicReqQuery scenicReqQuery) {
return ApiResponse.success(scenicRepository.list(scenicReqQuery));
} }
@Override
public ApiResponse<ScenicRespVO> getById(Long id) {
return ApiResponse.success(scenicMapper.getById(id));
}
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResponse<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Long, IStorageAdapter> scenicStorageAdapterMap = new ConcurrentHashMap<>();
@Override @Override
public IStorageAdapter getScenicStorageAdapter(Long scenicId) { public IStorageAdapter getScenicStorageAdapter(Long scenicId) {
return scenicStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { return scenicStorageAdapterCache.computeIfAbsent(scenicId, (key) -> {
IStorageAdapter adapter; IStorageAdapter adapter;
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null && scenicConfig.getStoreType() != null) { if (Strings.isNotBlank(scenicConfig.getString("store_type"))) {
try { try {
adapter = StorageFactory.get(scenicConfig.getStoreType()); adapter = StorageFactory.get(scenicConfig.getString("store_type"));
adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getStoreConfigJson(), Map.class)); adapter.loadConfig(scenicConfig.getObject("store_config_json", Map.class));
} catch (StorageUnsupportedException ignored) { } catch (StorageUnsupportedException ignored) {
return StorageFactory.use("video"); return StorageFactory.use("video");
} }
@@ -243,16 +74,15 @@ public class ScenicServiceImpl implements ScenicService {
return adapter; return adapter;
}); });
} }
private static final Map<Long, IStorageAdapter> scenicTmpStorageAdapterMap = new ConcurrentHashMap<>();
@Override @Override
public IStorageAdapter getScenicTmpStorageAdapter(Long scenicId) { public IStorageAdapter getScenicTmpStorageAdapter(Long scenicId) {
return scenicTmpStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { return scenicTmpStorageAdapterCache.computeIfAbsent(scenicId, (key) -> {
IStorageAdapter adapter; IStorageAdapter adapter;
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null && scenicConfig.getTmpStoreType() != null) { if (Strings.isNotBlank(scenicConfig.getString("tmp_store_type"))) {
try { try {
adapter = StorageFactory.get(scenicConfig.getTmpStoreType()); adapter = StorageFactory.get(scenicConfig.getString("tmp_store_type"));
adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getTmpStoreConfigJson(), Map.class)); adapter.loadConfig(scenicConfig.getObject("tmp_store_config_json", Map.class));
} catch (StorageUnsupportedException ignored) { } catch (StorageUnsupportedException ignored) {
return getScenicStorageAdapter(scenicId); return getScenicStorageAdapter(scenicId);
} }
@@ -262,16 +92,15 @@ public class ScenicServiceImpl implements ScenicService {
return adapter; return adapter;
}); });
} }
private static final Map<Long, IStorageAdapter> scenicLocalStorageAdapterMap = new ConcurrentHashMap<>();
@Override @Override
public IStorageAdapter getScenicLocalStorageAdapter(Long scenicId) { public IStorageAdapter getScenicLocalStorageAdapter(Long scenicId) {
return scenicLocalStorageAdapterMap.computeIfAbsent(scenicId, (key) -> { return scenicLocalStorageAdapterCache.computeIfAbsent(scenicId, (key) -> {
IStorageAdapter adapter; IStorageAdapter adapter;
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null && scenicConfig.getLocalStoreType() != null) { if (Strings.isNotBlank(scenicConfig.getString("local_store_type"))) {
try { try {
adapter = StorageFactory.get(scenicConfig.getLocalStoreType()); adapter = StorageFactory.get(scenicConfig.getString("local_store_type"));
adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getLocalStoreConfigJson(), Map.class)); adapter.loadConfig(scenicConfig.getObject("local_store_config_json", Map.class));
} catch (StorageUnsupportedException ignored) { } catch (StorageUnsupportedException ignored) {
return getScenicStorageAdapter(scenicId); return getScenicStorageAdapter(scenicId);
} }
@@ -282,15 +111,14 @@ public class ScenicServiceImpl implements ScenicService {
}); });
} }
private static final Map<Long, IFaceBodyAdapter> scenicFaceBodyAdapterMap = new ConcurrentHashMap<>();
@Override @Override
public IFaceBodyAdapter getScenicFaceBodyAdapter(Long scenicId) { public IFaceBodyAdapter getScenicFaceBodyAdapter(Long scenicId) {
return scenicFaceBodyAdapterMap.computeIfAbsent(scenicId, (key) -> { return scenicFaceBodyAdapterCache.computeIfAbsent(scenicId, (key) -> {
IFaceBodyAdapter adapter; IFaceBodyAdapter adapter;
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null && scenicConfig.getFaceType() != null) { if (Strings.isNotBlank(scenicConfig.getString("face_type"))) {
adapter = FaceBodyFactory.getAdapter(scenicConfig.getFaceType()); adapter = FaceBodyFactory.getAdapter(scenicConfig.getString("face_type"));
adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getFaceConfigJson(), Map.class)); adapter.loadConfig(scenicConfig.getObject("face_config_json", Map.class));
} else { } else {
adapter = FaceBodyFactory.use(); adapter = FaceBodyFactory.use();
} }
@@ -298,19 +126,108 @@ public class ScenicServiceImpl implements ScenicService {
}); });
} }
private static final Map<Long, IPayAdapter> scenicPayAdapterMap = new ConcurrentHashMap<>();
@Override @Override
public IPayAdapter getScenicPayAdapter(Long scenicId) { public IPayAdapter getScenicPayAdapter(Long scenicId) {
return scenicPayAdapterMap.computeIfAbsent(scenicId, (key) -> { return scenicPayAdapterCache.computeIfAbsent(scenicId, (key) -> {
IPayAdapter adapter; IPayAdapter adapter;
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
if (scenicConfig != null && scenicConfig.getPayType() != null) { if (Strings.isNotBlank(scenicConfig.getString("pay_type"))) {
adapter = PayFactory.getAdapter(scenicConfig.getPayType()); adapter = PayFactory.getAdapter(scenicConfig.getString("pay_type"));
adapter.loadConfig(JacksonUtil.parseObject(scenicConfig.getPayConfigJson(), Map.class)); adapter.loadConfig(scenicConfig.getObject("pay_config_json", Map.class));
} else { } else {
adapter = PayFactory.use(); adapter = PayFactory.use();
} }
return adapter; 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();
}
} }

View File

@@ -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.entity.TemplateEntity;
import com.ycwl.basic.model.pc.template.req.TemplateReqQuery; import com.ycwl.basic.model.pc.template.req.TemplateReqQuery;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; 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.service.pc.TemplateService;
import com.ycwl.basic.repository.TemplateRepository; import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.utils.ApiResponse; import com.ycwl.basic.utils.ApiResponse;
@@ -30,6 +32,8 @@ public class TemplateServiceImpl implements TemplateService {
private TemplateMapper templateMapper; private TemplateMapper templateMapper;
@Autowired @Autowired
private TemplateRepository templateRepository; private TemplateRepository templateRepository;
@Autowired
private IPricingManagementService pricingManagementService;
@Override @Override
public ApiResponse<PageInfo<TemplateRespVO>> pageQuery(TemplateReqQuery templateReqQuery) { public ApiResponse<PageInfo<TemplateRespVO>> pageQuery(TemplateReqQuery templateReqQuery) {
@@ -75,6 +79,7 @@ public class TemplateServiceImpl implements TemplateService {
templateMapper.add(item); templateMapper.add(item);
}); });
} }
pricingManagementService.quickSetupProductPrice(ProductType.VLOG_VIDEO.getCode(), template.getId().toString(), template.getScenicId().toString(), template.getName(), template.getPrice(), template.getSlashPrice(), "");
if (i > 0) { if (i > 0) {
return ApiResponse.success(true); return ApiResponse.success(true);
}else { }else {
@@ -112,6 +117,7 @@ public class TemplateServiceImpl implements TemplateService {
}); });
} }
templateRepository.clearTemplateCache(template.getId()); 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) { if (i > 0) {
return ApiResponse.success(true); return ApiResponse.success(true);
}else { }else {

Some files were not shown because too many files have changed in this diff Show More