Compare commits

...

44 Commits

Author SHA1 Message Date
cdd434317f feat(pricing): 集成定价服务并优化价格查询逻辑- 在 OrderBiz 中添加 IPriceCalculationService 依赖,用于计算价格
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 重构 queryPrice 方法,使用定价服务计算价格而不是直接从数据库读取
- 移除 PriceObj 中未使用的 scenicAllPrice 字段
- 删除 ScenicEntity 和 ScenicAddOrUpdateReq 中的冗余价格字段
-优化 ProductConfigServiceImpl 中的 getTierConfig 方法,增加参数校验
2025-09-01 09:21:26 +08:00
f91b98c68e feat(order): 增加重复购买检查功能
- 为VLOG_VIDEO、RECORDING_SET 和 PHOTO_SET 类型的产品添加重复购买检查
- 使用 sourceMapper 和 videoMapper 查询用户已购买的产品数量- 根据查询结果设置产品数量,避免重复购买
-优化了价格计算流程,先检查缓存再进行重复购买检查
2025-08-31 14:36:24 +08:00
a9d64402f2 refactor(scenic): 将时间戳类型改为 Date 类型
- 在 ScenicConfigV2DTO 和 ScenicV2DTO 类中,将 createTime 和 updateTime 字段的类型从 Long 改为 Date
- 这个改动可以更方便地进行时间操作和格式化,提高代码的可读性和易用性
2025-08-30 18:09:42 +08:00
ada7158a48 refactor(basic): 重构景区配置管理逻辑
- 移除 AppOrderV2Controller 中的价格缓存逻辑
- 修正 VoucherServiceImpl 中的购买数量计算方式- 重构 ScenicRepository 中的景区配置获取逻辑
-增加 ScenicConfigManager 的扁平化配置和驼峰转换功能
2025-08-30 16:21:40 +08:00
047feec045 refactor(pricing): 重构适用商品类型处理逻辑
- 移除 ProductTypeListTypeHandler,直接在实体类中处理 JSON转换
- 为 PriceVoucherBatchConfig 添加 ObjectMapper 静态实例和日志记录
- 实现 JSON 字符串与 ProductType 列表之间的转换方法- 更新数据库映射,将 applicableProducts 映射为 JSON 字符串
- 优化 VoucherServiceImpl 中的产品适用性检查逻辑
2025-08-30 15:55:26 +08:00
966568156c feat(voucher): 增加券码适用商品类型功能
- 在 VoucherBatchCreateReq、VoucherBatchResp 和 VoucherInfo 中添加适用商品类型列表字段
- 在 PriceVoucherBatchConfig 中添加适用商品类型列表字段,并使用 ProductTypeListTypeHandler 进行 JSON 序列化和反序列化
- 实现 ProductTypeListTypeHandler 以处理商品类型列表的 JSON 序列化和反序列化
- 更新 VoucherBatchServiceImpl 和 VoucherServiceImpl 以支持适用商品类型的筛选和计算
2025-08-30 15:31:35 +08:00
57b087a4fb refactor(order): 重构订单创建和支付参数获取逻辑
- 新增 createOrderCompact 方法实现旧版订单创建逻辑
- 新增 getPaymentParams 方法获取支付参数
- 更新 AppOrderV2Controller调用新的订单创建和支付参数获取方法
- 在 OrderMapper 中添加 getOrderItems 方法获取订单详情- 更新 VideoRepository 接口,增加根据人脸和模板 ID 获取视频列表的方法
- 在 OrderServiceImpl 中实现新的订单创建和支付参数获取逻辑
- 更新 OrderService 接口,添加新的方法声明
- 在 OrderMapper.xml 中添加新的 SQL 查询语句
2025-08-30 14:25:28 +08:00
607c5bc057 refactor(print): 优化门票打印逻辑
- 移除了 AppClaimController 中的重复代码
- 在 VoucherPrintServiceImpl 中增加了打印配置的判断和警告日志
- 保留了 FeiETicketPrinter 的调用逻辑,增加了配置判断
2025-08-30 13:18:26 +08:00
fc8818a595 feat(voucher): 电子凭证打印增加预约功能
- 在 AppClaimController 中添加了对 morphId 的非空判断,只有在 morphId 存在时才进行打印操作
- 在 VoucherPrintServiceImpl 中增加了景点配置的检查,包括预约功能是否启用和指定的经纪人 ID
2025-08-30 12:59:03 +08:00
b1deabc7c1 feat(pricing): 新增打印小票和查询券码批次配置功能
- 新增 AppClaimController 控制器处理移动设备端的领券请求
- 实现 ClaimReq 和 ClaimResp 模型类用于领券请求和响应
- 在 VoucherPrintService 接口中新增打印小票方法
- 在VoucherPrintServiceImpl 中实现打印小票和查询券码批次配置的逻辑
- 更新 PriceVoucherBatchConfigMapper 接口和 XML 文件,添加查询券码批次配置的方法
2025-08-30 12:52:08 +08:00
1ac375e491 refactor(pricing): 移除商品阶梯定价中的 default 配置逻辑
- 删除了尝试使用 default 配置的代码块
- 保留了缓存注释(已注释)
- 优化了日志输出,当找不到配置时直接记录警告日志
2025-08-30 11:31:26 +08:00
60af636639 Merge branch 'refs/heads/order_v2'
# Conflicts:
#	src/main/java/com/ycwl/basic/controller/mobile/manage/AppScenicAccountController.java
2025-08-30 10:53:11 +08:00
792deb5c4d feat(order): 添加重复购买检测功能
- 新增 DuplicatePurchaseException 类用于处理重复购买异常
- 在 OrderServiceImpl 中实现重复购买检查逻辑
- 更新 CustomExceptionHandle 以处理新的重复购买异常
-优化订单创建流程,在生成订单号前增加重复购买检查
2025-08-30 10:52:26 +08:00
93a424058a feat(order): 优化订单创建逻辑,增加商品价格和名称计算
- 新增 calculateProductItemPriceAndName 方法,用于重新计算商品价格信息并获取商品名称
- 更新订单创建流程,使用计算后的商品价格和名称信息
- 引入 IProductConfigService 接口,用于获取商品配置信息
- 优化异常处理,确保在价格计算失败时有兜底方案
2025-08-29 17:39:52 +08:00
98ae9f2930 refactor(order): 重构订单相关代码并优化商品哈希计算逻辑
- 修改 DiscountType 枚举,将 FLASH_SALE 改为 LIMITED_TIME
- 优化 OrderServiceImpl 中的商品信息设置逻辑,增加空值判断
- 更新 IDiscountProvider 接口和 FlashSaleDiscountProvider 类中的提供者类型标识- 优化 ScenicServiceImpl 中的字符串判空逻辑,使用 Strings.isNotBlank 方法
- 重构 PriceCacheService 中的商品列表哈希值计算逻辑,仅基于必传字段生成哈希
2025-08-29 16:54:46 +08:00
e2b760caab feat(order): 完善订单创建和支付流程
- 添加优惠券和券码的使用记录及状态更新
- 优化支付成功、取消和退款的处理逻辑
- 增加异常处理,确保事务一致性
2025-08-29 16:20:07 +08:00
5a66856e72 feat(order): 添加支付相关接口和功能
- 新增获取支付参数接口和处理支付回调接口
- 实现支付参数获取和支付回调处理的逻辑
- 添加支付相关数据传输对象(DTO)
- 修改订单服务接口和实现类,增加支付相关方法
2025-08-29 15:32:47 +08:00
bc2b2fb10f refactor(basic): 修改移动端下单接口路径
- 将 "/add-order" 路径修改为 "/add"
- 优化接口路径,使其更简洁
2025-08-29 14:51:24 +08:00
4dac46bb46 refactor(order): 调整优惠排序逻辑
- 将券码优惠的排序顺序从 1 调整为 2,使其显示顺序低于限时立减- 将限时立减优惠的排序顺序从 2 调整为 1,使其显示在最前面
2025-08-29 14:50:49 +08:00
3fbfb7df54 feat(coupon): 添加优惠券领取功能
- 新增 CouponClaimRequest 和 CouponClaimResult 类用于处理优惠券领取请求和结果
- 在 ICouponService 接口中添加 claimCoupon 方法
- 在 CouponServiceImpl 中实现 claimCoupon 方法,包括参数验证、优惠券查询、库存检查、记录创建等步骤
- 优化日志记录和异常处理
2025-08-29 13:49:30 +08:00
346c484cbc refactor(order): 将 OrderMapper 重命名为 OrderV2Mapper- 将 OrderMapper 接口重命名为 OrderV2Mapper
- 更新了相关服务类中的 Mapper引用
- 修改了 OrderServiceImpl 中的字段名从 orderMapper改为 orderV2Mapper
- 更新了与订单相关的所有方法中对 Mapper 的调用
2025-08-29 12:41:12 +08:00
e95e0a04ff feat(order): 新增订单管理功能 V2
- 新增订单创建、查询、备注更新、申请退款等接口
- 添加订单相关实体类和枚举类
- 实现订单事件监听器,处理支付、退款、订单状态变化
- 优化移动端订单创建逻辑,集成订单服务
2025-08-28 18:42:47 +08:00
af79a5ffa6 feat(basic): 新增移动端下单请求DTO和价格缓存服务- 创建 MobileOrderRequest 类用于移动端下单请求
- 实现 PriceCacheService 类提供价格缓存相关功能
- 使用 Redis 缓存价格计算结果,提高查询效率
2025-08-28 18:14:34 +08:00
5c2629237e feat(mobile): 新增移动端订单V2接口
- 添加 AppOrderV2Controller 控制器,实现移动端价格计算和下单功能
- 新增 MobilePriceCalculationRequest DTO 类,用于移动端价格计算请求- 集成 Redis 缓存机制,提升价格查询性能- 实现人脸权限验证和价格缓存验证逻辑
- 优化日志记录和异常处理
2025-08-28 18:13:59 +08:00
798ff3b9b5 feat(service): 使用带TTL的缓存Map替换静态Map
- 新增TtlCacheMap类,用于实现带生存时间的缓存
- 在ScenicServiceImpl中使用TtlCacheMap替换原有的ConcurrentHashMap
- 为不同类型的适配器创建了对应的缓存Map
- 优化了缓存获取逻辑,增加了TTL支持
- 添加了缓存清理和统计功能
2025-08-28 16:02:30 +08:00
46fb255e66 refactor(pc): 重构景区配置管理
- 引入 ScenicConfigManager 类替代 ScenicConfigEntity
- 优化景区存储、临时存储、本地存储、人脸身体识别和支付适配器的获取逻辑
- 使用 getString 和 getObject 方法替代直接解析 JSON 对象
2025-08-28 15:57:36 +08:00
f451b835b9 feat(pricing): 添加快速设置商品价格功能并集成到模板服务
- 在 PricingManagementServiceImpl 中实现 quickSetupProductPrice 方法,用于快速设置商品价格
- 在 IPricingManagementService 接口中添加 quickSetupProductPrice 方法的声明
- 在 TemplateServiceImpl 中调用 quickSetupProductPrice 方法,为模板设置价格
2025-08-28 12:09:47 +08:00
c7d5399931 refactor(scenic): 将 scenic 模块列表接口中的 total 字段类型从 Long 改为 Integer
- 修改了 ScenicV2ListResponse 和 ScenicV2WithConfigListResponse 类中的 total 字段类型
- 此更改统一了 total 字段的类型,提高了代码的一致性和可维护性
2025-08-28 09:57:39 +08:00
5bb2bc1ac3 refactor(PageResponse): 将 total 字段类型从 Long 改为 Integer
- 修改了 PageResponse 类中 total 字段的类型,以更好地与前端交互
- 这个改动解决了后端与前端之间关于 total 类型不一致的问题
2025-08-28 09:55:40 +08:00
95d8b742ee feat(scenic): 添加景区配置管理器并集成缓存支持
- 新增 ScenicConfigManager 类,用于管理和获取景区配置
- 在 ScenicRepository 中添加获取景区配置管理器的方法
- 实现了带缓存支持的景区配置获取,提高性能
2025-08-28 09:52:43 +08:00
ff320ba3e8 feat(AppScenicServiceImpl): 添加景区设备数量字段
- 在 AppScenicServiceImpl 类中,为 scenicAppVO 对象添加 deviceNum 字段
- 通过 deviceRepository.getAllDeviceByScenicId 方法获取景区设备数量并设置到 scenicAppVO 中
2025-08-27 17:10:42 +08:00
98bbaccb3a refactor(biz): 优化代码中的条件判断逻辑
- 将 Integer 类型的比较改为 Boolean 类型的比较,提高代码可读性和性能
- 修改涉及 scenicConfig 的条件判断,使用 Boolean.TRUE进行比较
- 优化部分代码结构,保持逻辑一致性
2025-08-27 16:40:32 +08:00
f2ac6aaea0 refactor(scenic): 重构景区相关接口和缓存机制
- 移除 ScenicMapper 接口,将相关方法移至 ScenicRepository
- 修改景区列表查询逻辑,使用 ScenicRepository 的 list 方法
- 优化景区详情获取方式,使用 ScenicRepository 的 getScenicBasic 方法
- 重构缓存机制,增加对景区基本信息的缓存
- 优化 AppScenicService 和 ScenicService接口,使用 ScenicV2DTO 替代 ScenicRespV
2025-08-27 16:37:57 +08:00
21f76ff9c5 refactor(scenic): 重构景区相关接口和数据结构
-移除了 ScenicMapper 中的冗余方法
- 更新了 ScenicEntity 和 ScenicRespVO 的字段结构
- 重构了 ScenicRepository 中的缓存逻辑
- 优化了 AppScenicServiceImpl 中的景区详情获取方法
2025-08-27 10:25:51 +08:00
7d40b8043d feat(basic): 添加默认配置管理功能
- 实现了默认配置的列表获取、单个配置获取、创建、更新和删除功能- 使用日志记录操作信息- 异常处理确保错误信息返回给客户端
2025-08-27 10:12:08 +08:00
b67fb87989 refactor(basic): 移除景区控制器中的冗余代码
- 删除了 ScenicController 类中多个未使用的 API 方法
- 保留了下载小程序二维码的功能
-简化了代码结构,提高了代码可维护性
2025-08-27 10:07:30 +08:00
42e7b7da95 feat(AppScenicAccountController):修复并优化景区列表获取功能- 初始化 list 为 Collections.emptyList(),避免空指针异常
- 增加对 ADMIN 角色的处理,使其能够获取景区列表
- 优化代码结构,提高可读性和维护性
2025-08-27 10:07:14 +08:00
6bc94a65a6 feat(scenic): 优化景区信息获取与缓存机制
-移除景区信息查询相关冗余代码
- 增加缓存逻辑,提高景区信息获取效率
- 更新 ScenicRepository 中的 getScenic 和 getScenicConfig 方法
- 重构 ScenicServiceImpl 中的 list 方法
- 删除 ScenicService 接口中未使用的多个方法
2025-08-27 10:07:01 +08:00
7c2db2ad22 refactor(scenic): 重构景区管理接口并新增 V2 版本
- 新增 ScenicV2Controller 控制器,实现景区 V2 版本的 CRUD操作和配置管理
- 移除 ScenicConfigWithDefaultClient 和 ScenicMetaClient 接口- 更新 ScenicV2Client接口,添加分页查询方法
- 删除 ConfigWithDefaultResponse、BatchSetFieldEnabledRequest、EnabledFieldsResponse、FieldConfigDTO 和 SetFieldEnabledRequest 类
- 新增 ScenicV2ListResponse 和 ScenicV2WithConfigListResponse 类- 更新 ScenicConfigIntegrationService 和 ScenicIntegrationService,移除与配置相关的方法
- 删除 ScenicMetaIntegrationService 类
2025-08-27 00:11:00 +08:00
f6bd7e48a3 refactor(basic): 将 ScenicConfigEntity 中的 allFree 字段类型从 Integer 改为 Boolean
- 修改了 OrderBiz、PriceBiz 中的相关代码,使用 Boolean.TRUE 进行比较
- 更新了 ScenicConfigEntity 和 ScenicConfigResp 中 allFree 字段的类型
- 在 ScenicRepository 中使用 ConfigValueUtil.getBooleanValue 方法获取 allFree 的值
2025-08-26 14:29:45 +08:00
f0aeb27566 refactor(scenic): 重构景区配置相关代码
- 为 FeignClient 添加 contextId 属性,提高服务调用的可读性
- 更新 ScenicIntegrationService 中的接口调用方式
- 修改 ScenicConfigEntity 和 ScenicConfigResp 中的字段类型
-重构 ScenicRepository 中的配置解析逻辑,使用 ConfigValueUtil 工具类
2025-08-26 14:26:44 +08:00
5871beb84e refactor: 移除 FeignConfig 类
删除了 FeignConfig 类及相关配置,包括日志级别设置、请求拦截器、错误解码器等。这部分配置可能已经不再需要,或者已经被其他配置所替代。
2025-08-26 13:45:38 +08:00
291b3d620f refactor(basic): 重构景区相关接口调用
- 移除 Redis 缓存操作,改为直接调用 ScenicIntegrationService- 新增 convertToScenicEntity 和 convertToScenicConfigEntity 方法进行数据转换
- 优化异常处理,fallback 到数据库查询
2025-08-26 13:45:28 +08:00
32feaa9692 feat(integration): 添加 ZT-Scenic 集成服务模块
- 新增 FeignConfig、IntegrationProperties 等基础配置类
- 实现自定义 FeignErrorDecoder 和 IntegrationException
- 添加 CommonResponse 和 PageResponse 等通用响应模型
- 定义多个 Feign 客户端接口,用于调用 ZT-Scenic 服务
- 实现 DefaultConfigIntegrationService 和 ScenicConfigIntegrationService 服务类
- 添加 ScenicIntegrationExample 示例类,展示如何使用集成服务
2025-08-26 13:36:06 +08:00
135 changed files with 8411 additions and 1001 deletions

View File

@@ -24,6 +24,11 @@ import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
import com.ycwl.basic.pricing.dto.PriceCalculationRequest;
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.service.IPriceCalculationService;
import com.ycwl.basic.profitsharing.biz.ProfitSharingBiz;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.OrderRepository;
@@ -40,6 +45,7 @@ import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
@@ -79,6 +85,8 @@ public class OrderBiz {
@Autowired
@Lazy
private PrinterService printerService;
@Autowired
private IPriceCalculationService iPriceCalculationService;
public PriceObj queryPrice(Long scenicId, int goodsType, Long goodsId) {
PriceObj priceObj = new PriceObj();
@@ -86,9 +94,8 @@ public class OrderBiz {
priceObj.setGoodsId(goodsId);
ScenicEntity scenic = scenicRepository.getScenic(scenicId);
ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId);
priceObj.setScenicAllPrice(scenic.getPrice());
if (scenicConfig != null) {
if (Integer.valueOf(1).equals(scenicConfig.getAllFree())) {
if (Boolean.TRUE.equals(scenicConfig.getAllFree())) {
// 景区全免
priceObj.setFree(true);
priceObj.setPrice(BigDecimal.ZERO);
@@ -120,13 +127,20 @@ public class OrderBiz {
priceObj.setScenicId(video.getScenicId());
break;
case 1: // source
priceObj.setPrice(scenic.getSourceVideoPrice());
priceObj.setSlashPrice(scenic.getSourceVideoPrice());
priceObj.setFaceId(goodsId);
break;
case 2: // source
priceObj.setPrice(scenic.getSourceImagePrice());
priceObj.setSlashPrice(scenic.getSourceImagePrice());
FaceEntity face = faceRepository.getFace(goodsId);
PriceCalculationRequest calculationRequest = new PriceCalculationRequest();
ProductItem productItem = new ProductItem();
productItem.setProductType(goodsType == 1 ? ProductType.RECORDING_SET : ProductType.PHOTO_SET);
productItem.setProductId(scenic.getId().toString());
productItem.setPurchaseCount(1);
productItem.setScenicId(scenic.getId().toString());
calculationRequest.setProducts(Collections.singletonList(productItem));
calculationRequest.setUserId(face.getMemberId());
calculationRequest.setFaceId(face.getId());
PriceCalculationResult priceCalculationResult = iPriceCalculationService.calculatePrice(calculationRequest);
priceObj.setPrice(priceCalculationResult.getFinalAmount());
priceObj.setSlashPrice(priceCalculationResult.getOriginalAmount());
priceObj.setFaceId(goodsId);
break;
}

View File

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

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,91 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.model.mobile.claim.ClaimReq;
import com.ycwl.basic.model.mobile.claim.ClaimResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
import com.ycwl.basic.pricing.service.ICouponService;
import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.util.ScenicConfigManager;
import com.ycwl.basic.utils.ApiResponse;
import lombok.AllArgsConstructor;
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;
@RestController
@RequestMapping("/api/mobile/claim/v1")
@AllArgsConstructor
public class AppClaimController {
private final FaceRepository faceRepository;
private final ScenicRepository scenicRepository;
private final VoucherPrintService voucherPrintService;
private final ICouponService couponService;
@PostMapping("tryClaim")
public ApiResponse<ClaimResp> tryClaim(@RequestBody ClaimReq req) {
FaceEntity face = faceRepository.getFace(req.getFaceId());
if (face == null) {
return ApiResponse.fail("请选择人脸");
}
ClaimResp claimResp = new ClaimResp();
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (Boolean.TRUE.equals(scenicConfig.getBoolean("voucher_enable"))) {
// 可以领券
VoucherPrintResp voucherPrintResp = voucherPrintService.queryPrintedVoucher(face.getId());
if (voucherPrintResp == null) {
// 打印
if (req.getMorphId() != null) {
voucherPrintResp = voucherPrintService.printVoucherTicket(new VoucherPrintReq(face.getId(), req.getMorphId(), face.getScenicId()));
}
}
if (voucherPrintResp != null) {
claimResp.setHasCoupon(false);
claimResp.setHasPrint(true);
claimResp.setPrintCode(voucherPrintResp.getCode());
claimResp.setPrintType(voucherPrintResp.getType());
return ApiResponse.success(claimResp);
}
}
if (Boolean.TRUE.equals(scenicConfig.getBoolean("booking_enable"))) {
VoucherPrintResp voucherPrintResp = voucherPrintService.queryPrintedVoucher(face.getId());
if (voucherPrintResp == null) {
// 打印
if (req.getMorphId() != null) {
voucherPrintResp = voucherPrintService.printBookingTicket(new VoucherPrintReq(face.getId(), req.getMorphId(), face.getScenicId()));
}
}
if (voucherPrintResp != null) {
claimResp.setHasCoupon(false);
claimResp.setHasPrint(true);
claimResp.setPrintCode(voucherPrintResp.getCode());
claimResp.setPrintType(voucherPrintResp.getType());
return ApiResponse.success(claimResp);
}
}
if (req.getType() != null) {
// 第几次进入
Integer couponId = scenicConfig.getInteger("coupon_id_for_type_" + req.getType());
if (couponId != null) {
// 可以领券
CouponClaimRequest request = new CouponClaimRequest(face.getMemberId(), Long.valueOf(couponId));
CouponClaimResult claimResult = couponService.claimCoupon(request);
if (claimResult.isSuccess()) {
// 领到了
claimResp.setHasCoupon(true);
claimResp.setCouponDesc(scenicConfig.getString("coupon_desc_for_type_" + req.getType(), "专属折扣券"));
claimResp.setCouponCountdown(scenicConfig.getString("coupon_countdown_for_type_" + req.getType(), "送你优惠,保存美好!"));
return ApiResponse.success(claimResp);
}
}
}
return ApiResponse.fail("异常");
}
}

View File

@@ -0,0 +1,382 @@
package com.ycwl.basic.controller.mobile;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.pc.order.req.CreateOrderReqVO;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.pc.OrderService;
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 com.ycwl.basic.utils.JacksonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 移动端订单控制器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;
private final OrderService oldOrderService;
private final SourceMapper sourceMapper;
private final VideoMapper videoMapper;
private final VideoTaskRepository videoTaskRepository;
private final TemplateRepository templateRepository;
/**
* 移动端价格计算
* 包含权限验证:验证人脸所属景区与当前用户匹配
* 集成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);
}
request.getProducts().forEach(product -> {
switch (product.getProductType()) {
case VLOG_VIDEO:
AtomicInteger deviceCount = new AtomicInteger();
List<MemberVideoEntity> videoEntities = videoMapper.listRelationByFaceAndTemplate(face.getId(), Long.valueOf(product.getProductId()));
if (videoEntities != null && !videoEntities.isEmpty()) {
TaskEntity task = videoTaskRepository.getTaskById(videoEntities.getFirst().getTaskId());
if (task != null) {
Map<String, Object> paramJson = JacksonUtil.parseObject(task.getTaskParams(), Map.class);
if (paramJson == null) {
deviceCount.set(1);
} else {
List<String> templatePlaceholder = templateRepository.getTemplatePlaceholder(task.getTemplateId());
paramJson.entrySet().stream()
.filter(entry -> StringUtils.isNumeric(entry.getKey()))
.forEach(entry -> {
List<Object> jsonArray = JacksonUtil.parseArray(JacksonUtil.toJSONString(entry.getValue()), Object.class);
if (jsonArray != null && !jsonArray.isEmpty()) {
for (Object ignored : jsonArray) {
if (templatePlaceholder.contains(entry.getKey())) {
deviceCount.getAndIncrement();
templatePlaceholder.remove(entry.getKey());
}
}
}
});
}
product.setQuantity(deviceCount.get());
}
}
break;
case RECORDING_SET:
case PHOTO_SET:
SourceReqQuery sourceReqQuery = new SourceReqQuery();
sourceReqQuery.setMemberId(currentUserId);
sourceReqQuery.setType(product.getProductType() == ProductType.RECORDING_SET ? 1 : 2);
sourceReqQuery.setFaceId(face.getId());
Integer count = sourceMapper.countUser(sourceReqQuery);
product.setQuantity(count);
break;
default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;
}
});
// 转换为标准价格计算请求
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 = oldOrderService.createOrderCompact(currentUserId, request, cachedResult);
return ApiResponse.success(String.valueOf(orderId));
} catch (Exception e) {
return ApiResponse.fail("订单创建失败,请稍后重试");
}
// 创建订单
// 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);
return oldOrderService.getPaymentParams(orderId, currentUserId, request);
//
// 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.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.scenic.ScenicAppVO;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.mobile.scenic.ScenicIndexVO;
import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicConfigResp;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
@@ -16,11 +16,14 @@ import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@@ -49,7 +52,7 @@ public class AppScenicController {
// 分页查询景区列表
@PostMapping("/page")
public ApiResponse<PageInfo<ScenicAppVO>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){
public ApiResponse<PageInfo<ScenicEntity>> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){
String userId = BaseContextHandler.getUserId();
if (ENABLED_USER_IDs.contains(userId)) {
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.constant.BaseContextHandler;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginOldRespVO;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginReq;
import com.ycwl.basic.model.mobile.scenic.account.ScenicLoginRespVO;
@@ -12,6 +13,7 @@ import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.pc.ScenicAccountService;
import com.ycwl.basic.service.pc.ScenicService;
@@ -25,10 +27,10 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
/**
@@ -45,6 +47,8 @@ public class AppScenicAccountController {
private AppScenicService scenicService;
@Autowired
private ScenicService adminScenicService;
@Autowired
private ScenicRepository scenicRepository;
// 登录
@PostMapping("/login")
@@ -73,8 +77,8 @@ public class AppScenicAccountController {
}
@GetMapping("/myScenicList")
public ApiResponse<List<ScenicRespVO>> myScenicList() {
List<ScenicRespVO> list;
public ApiResponse<List<ScenicV2DTO>> myScenicList() {
List<ScenicV2DTO> list = Collections.emptyList();
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
String userId = BaseContextHandler.getUserId();
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
@@ -82,10 +86,12 @@ public class AppScenicAccountController {
return ApiResponse.fail("景区账号未绑定景区");
}
list = account.getScenicId().stream()
.map(id -> scenicService.getDetails(id).getData())
.map(id -> scenicRepository.getScenicBasic(id))
.toList();
} else {
list = adminScenicService.list(new ScenicReqQuery()).getData();
} else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
ScenicReqQuery query = new ScenicReqQuery();
query.setPageSize(1000);
list = scenicRepository.list(query);
}
return ApiResponse.success(list);
}

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;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicAccountEntity;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.AppScenicService;
import com.ycwl.basic.service.mobile.AppStatisticsService;
import com.ycwl.basic.service.pc.ScenicAccountService;
@@ -17,16 +16,20 @@ import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.Collections;
import java.util.List;
import static com.ycwl.basic.constant.JwtRoleConstant.ADMIN;
import static com.ycwl.basic.constant.JwtRoleConstant.MERCHANT;
/**
@@ -50,68 +53,6 @@ public class ScenicController {
@Autowired
private ScenicAccountService accountService;
// 分页查询景区
@PostMapping("/page")
public ApiResponse<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下载小程序二维码
@GetMapping("/{id}/QRCode")
public ApiResponse<String> downloadQrCode(@PathVariable Long id) {
@@ -167,19 +108,19 @@ public class ScenicController {
}
@GetMapping("/myScenicList")
public ApiResponse<List<ScenicRespVO>> myScenicList() {
List<ScenicRespVO> list = Collections.emptyList();
public ApiResponse<List<ScenicV2DTO>> myScenicList() {
List<ScenicV2DTO> list = Collections.emptyList();
if (Strings.CS.equals(BaseContextHandler.getRoleId(), MERCHANT.type)) {
String userId = BaseContextHandler.getUserId();
ScenicAccountEntity account = accountService.getScenicAccountById(Long.valueOf(userId));
if (account == null || account.getScenicId().isEmpty()) {
return ApiResponse.fail("景区账号未绑定景区");
}
list = account.getScenicId().stream().map(id -> {
return appScenicService.getDetails(id).getData();
}).toList();
} else {
list = scenicService.list(new ScenicReqQuery()).getData();
list = account.getScenicId().stream().map(id -> scenicRepository.getScenicBasic(id)).toList();
} else if (Strings.CS.equals(BaseContextHandler.getRoleId(), ADMIN.type)) {
ScenicReqQuery query = new ScenicReqQuery();
query.setPageSize(1000);
list = scenicRepository.list(query);
}
return ApiResponse.success(list);
}

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;
import com.ycwl.basic.enums.BizCodeEnum;
import com.ycwl.basic.order.exception.DuplicatePurchaseException;
import com.ycwl.basic.utils.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException;
@@ -106,4 +107,18 @@ public class CustomExceptionHandle {
public ApiResponse<String> handle(SizeLimitExceededException sizeLimitExceededException) {
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,38 @@
package com.ycwl.basic.integration.scenic.dto.config;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Date;
@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 Date createTime;
@JsonProperty("updateTime")
private Date 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,27 @@
package com.ycwl.basic.integration.scenic.dto.scenic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Date;
@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 Date createTime;
@JsonProperty("updateTime")
private Date 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

@@ -57,4 +57,6 @@ public interface OrderMapper {
OrderEntity getUserOrderItem(Long userId, Long scenicId, int orderType, Long configId, Integer goodsType, Long goodsId);
int updateMemberIdByFaceId(OrderEntity orderEntity);
List<OrderItemEntity> getOrderItems(Long orderId);
}

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

@@ -0,0 +1,12 @@
package com.ycwl.basic.model.mobile.claim;
import lombok.Data;
@Data
public class ClaimReq {
private Long faceId;
// 扫码进入的推客ID
private Long morphId;
// 通知进入的type
private Integer type;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.model.mobile.claim;
import lombok.Data;
@Data
public class ClaimResp {
private Boolean hasPrint;
private String printType;
private String printCode;
private Boolean hasCoupon;
private String couponDesc;
private String couponCountdown;
}

View File

@@ -12,6 +12,5 @@ public class PriceObj {
private Long goodsId;
private Long faceId;
private BigDecimal price = BigDecimal.ZERO;
private BigDecimal scenicAllPrice;
private BigDecimal slashPrice;
}

View File

@@ -7,6 +7,4 @@ public class CreateOrderReqVO {
private Long scenicId;
private Integer goodsType;
private Long goodsId;
private String brokerCode;
private Long couponId;
}

View File

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

View File

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

View File

@@ -77,10 +77,6 @@ public class ScenicAddOrUpdateReq {
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 String account;

View File

@@ -23,51 +23,9 @@ public class ScenicReqQuery extends BaseQueryParameterReq {
*/
// 景区名称
private String name;
/**
* 景区介绍
*/
// 景区介绍
private String introduction;
/**
* 经度
*/
// 经度
private BigDecimal longitude;
/***
* 纬度
*/
// 纬度
private BigDecimal latitude;
/**
* 半径(km)
*/
// 半径(km)
private BigDecimal radius;
/**
* 省份
*/
// 省份
private String province;
/**
* 城市
*/
// 城市
private String city;
/**
* 区
*/
// 区
private String area;
/**
* 详细地址
*/
// 详细地址
private String address;
/**
* 状态 1启用0关闭
*/
// 状态 1启用0关闭
private String status;
private Date startTime;
private Date endTime;
}

View File

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

View File

@@ -74,19 +74,6 @@ public class ScenicRespVO {
*/
// 详细地址
private String address;
/**
* 状态 1启用0关闭
*/
// 状态 1启用0关闭
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
// 景区源素材价格,元
private BigDecimal price;
private BigDecimal sourceVideoPrice;
private BigDecimal sourceImagePrice;
// 镜头数
private Integer lensNum;
private String kfCodeUrl;

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

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

@@ -1,10 +1,12 @@
package com.ycwl.basic.pricing.dto;
import com.ycwl.basic.pricing.enums.ProductType;
import com.ycwl.basic.pricing.enums.VoucherDiscountType;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 券码信息DTO
@@ -81,4 +83,10 @@ public class VoucherInfo {
* 不可用原因
*/
private String unavailableReason;
/**
* 适用商品类型列表
* null表示适用所有商品类型
*/
private List<ProductType> applicableProducts;
}

View File

@@ -1,8 +1,10 @@
package com.ycwl.basic.pricing.dto.req;
import com.ycwl.basic.pricing.enums.ProductType;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class VoucherBatchCreateReq {
@@ -12,4 +14,10 @@ public class VoucherBatchCreateReq {
private Integer discountType;
private BigDecimal discountValue;
private Integer totalCount;
/**
* 适用商品类型列表
* null或空列表表示适用所有商品类型
*/
private List<ProductType> applicableProducts;
}

View File

@@ -1,11 +1,13 @@
package com.ycwl.basic.pricing.dto.req;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 打印小票请求
*/
@Data
@AllArgsConstructor
public class VoucherPrintReq {
private Long faceId;

View File

@@ -1,9 +1,11 @@
package com.ycwl.basic.pricing.dto.resp;
import com.ycwl.basic.pricing.enums.ProductType;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class VoucherBatchResp {
@@ -22,4 +24,10 @@ public class VoucherBatchResp {
private String statusName;
private Date createTime;
private Long createBy;
/**
* 适用商品类型列表
* null表示适用所有商品类型
*/
private List<ProductType> applicableProducts;
}

View File

@@ -14,6 +14,7 @@ public class VoucherPrintResp {
* 流水号
*/
private String code;
private String type;
/**
* 券码

View File

@@ -4,18 +4,27 @@ 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.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.pricing.enums.ProductType;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 券码批次配置实体
*/
@Data
@Slf4j
@TableName("price_voucher_batch_config")
public class PriceVoucherBatchConfig {
private static final ObjectMapper objectMapper = new ObjectMapper();
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@@ -44,6 +53,19 @@ public class PriceVoucherBatchConfig {
*/
private BigDecimal discountValue;
/**
* 适用商品类型列表(JSON字符串,数据库字段)
*/
@TableField("applicable_products")
private String applicableProductsJson;
/**
* 适用商品类型列表(业务字段)
* null表示适用所有商品类型
*/
@TableField(exist = false)
private List<ProductType> applicableProducts;
/**
* 总券码数量
*/
@@ -77,4 +99,73 @@ public class PriceVoucherBatchConfig {
private Integer deleted;
private Date deletedAt;
/**
* 获取适用商品类型列表
*/
public List<ProductType> getApplicableProducts() {
if (this.applicableProducts == null && this.applicableProductsJson != null) {
this.applicableProducts = parseApplicableProductsFromJson(this.applicableProductsJson);
}
return this.applicableProducts;
}
/**
* 设置适用商品类型列表
*/
public void setApplicableProducts(List<ProductType> applicableProducts) {
this.applicableProducts = applicableProducts;
this.applicableProductsJson = convertApplicableProductsToJson(applicableProducts);
}
/**
* 从JSON字符串解析商品类型列表
*/
private List<ProductType> parseApplicableProductsFromJson(String json) {
if (json == null || json.trim().isEmpty()) {
return null;
}
try {
TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
List<String> codeList = objectMapper.readValue(json, typeReference);
if (codeList == null || codeList.isEmpty()) {
return null;
}
return codeList.stream()
.map(code -> {
try {
return ProductType.fromCode(code);
} catch (IllegalArgumentException e) {
log.warn("未知的商品类型代码: {}", code);
return null;
}
})
.filter(type -> type != null)
.collect(java.util.stream.Collectors.toList());
} catch (JsonProcessingException e) {
log.error("解析适用商品类型JSON失败,JSON: {}", json, e);
return null;
}
}
/**
* 将商品类型列表转换为JSON字符串
*/
private String convertApplicableProductsToJson(List<ProductType> products) {
if (products == null || products.isEmpty()) {
return null;
}
try {
List<String> codeList = products.stream()
.map(ProductType::getCode)
.collect(java.util.stream.Collectors.toList());
return objectMapper.writeValueAsString(codeList);
} catch (JsonProcessingException e) {
log.error("转换适用商品类型为JSON失败", e);
return null;
}
}
}

View File

@@ -0,0 +1,94 @@
package com.ycwl.basic.pricing.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.pricing.enums.ProductType;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 商品类型列表类型处理器
* 用于处理券码适用商品类型的JSON序列化和反序列化
*/
@Slf4j
public class ProductTypeListTypeHandler extends BaseTypeHandler<List<ProductType>> {
private final ObjectMapper objectMapper = new ObjectMapper();
private final TypeReference<List<String>> typeReference = new TypeReference<List<String>>() {};
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<ProductType> parameter, JdbcType jdbcType) throws SQLException {
try {
// 将ProductType枚举转换为字符串列表
List<String> codeList = parameter.stream()
.map(ProductType::getCode)
.toList();
String json = objectMapper.writeValueAsString(codeList);
ps.setString(i, json);
log.debug("序列化商品类型列表: {}", json);
} catch (JsonProcessingException e) {
log.error("序列化商品类型列表失败", e);
throw new SQLException("序列化商品类型列表失败", e);
}
}
@Override
public List<ProductType> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json, columnName);
}
@Override
public List<ProductType> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json, "columnIndex:" + columnIndex);
}
@Override
public List<ProductType> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json, "columnIndex:" + columnIndex);
}
private List<ProductType> parseJson(String json, String source) {
if (json == null || json.trim().isEmpty()) {
log.debug("从{}获取的JSON为空,返回null表示适用所有商品类型", source);
return null; // null表示适用所有商品类型
}
try {
List<String> codeList = objectMapper.readValue(json, typeReference);
if (codeList == null || codeList.isEmpty()) {
log.debug("从{}反序列化得到空列表,返回null表示适用所有商品类型", source);
return null;
}
List<ProductType> result = new ArrayList<>();
for (String code : codeList) {
try {
ProductType productType = ProductType.fromCode(code);
result.add(productType);
} catch (IllegalArgumentException e) {
log.warn("从{}反序列化时发现未知商品类型代码: {}", source, code);
// 忽略未知的商品类型代码,继续处理其他类型
}
}
log.debug("从{}反序列化商品类型列表成功,数量: {}", source, result.size());
return result.isEmpty() ? null : result;
} catch (JsonProcessingException e) {
log.error("从{}反序列化商品类型列表失败,JSON: {}", source, json, e);
// 解析失败时返回null,表示适用所有商品类型
return null;
}
}
}

View File

@@ -44,4 +44,11 @@ public interface PriceVoucherBatchConfigMapper extends BaseMapper<PriceVoucherBa
* @return 统计信息
*/
PriceVoucherBatchConfig selectBatchStats(@Param("batchId") Long batchId);
/**
* 根据券码查询对应的券码批次配置
* @param voucherCode 券码
* @return 券码批次配置
*/
PriceVoucherBatchConfig selectByVoucherCode(@Param("voucherCode") String voucherCode);
}

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.CouponUseRequest;
import com.ycwl.basic.pricing.dto.CouponUseResult;
import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.ProductItem;
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
@@ -59,4 +61,12 @@ public interface ICouponService {
* @return 可用优惠券列表
*/
List<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();

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