You've already forked FrameTour-BE
feat(order): 添加支付相关接口和功能
- 新增获取支付参数接口和处理支付回调接口 - 实现支付参数获取和支付回调处理的逻辑 - 添加支付相关数据传输对象(DTO) - 修改订单服务接口和实现类,增加支付相关方法
This commit is contained in:
@@ -13,10 +13,14 @@ import com.ycwl.basic.order.service.IOrderService;
|
||||
import com.ycwl.basic.order.dto.OrderV2DetailResponse;
|
||||
import com.ycwl.basic.order.dto.OrderV2ListResponse;
|
||||
import com.ycwl.basic.order.dto.OrderV2PageRequest;
|
||||
import com.ycwl.basic.order.dto.PaymentParamsRequest;
|
||||
import com.ycwl.basic.order.dto.PaymentParamsResponse;
|
||||
import com.ycwl.basic.order.dto.PaymentCallbackResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
@@ -142,14 +146,14 @@ public class AppOrderV2Controller {
|
||||
|
||||
if (cachedResult == null) {
|
||||
log.warn("移动端下单:价格缓存已过期或不存在, userId={}, scenicId={}", currentUserId, scenicId);
|
||||
return ApiResponse.fail("价格信息已过期,请重新查询价格后再下单");
|
||||
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("价格信息不匹配,请重新查询价格后再下单");
|
||||
return ApiResponse.fail("请重新下单!");
|
||||
}
|
||||
|
||||
// 验证原价是否匹配(可选)
|
||||
@@ -239,4 +243,62 @@ public class AppOrderV2Controller {
|
||||
return ApiResponse.fail("查询失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 支付相关接口 ======
|
||||
|
||||
/**
|
||||
* 获取订单支付参数
|
||||
* 用于小程序调起支付
|
||||
*/
|
||||
@PostMapping("/{orderId}/payment-params")
|
||||
public ApiResponse<PaymentParamsResponse> getPaymentParams(
|
||||
@PathVariable("orderId") Long orderId,
|
||||
@RequestBody PaymentParamsRequest request) {
|
||||
|
||||
String currentUserIdStr = BaseContextHandler.getUserId();
|
||||
if (currentUserIdStr == null) {
|
||||
log.warn("用户未登录");
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(currentUserIdStr);
|
||||
|
||||
log.info("获取支付参数: userId={}, orderId={}", currentUserId, orderId);
|
||||
|
||||
try {
|
||||
PaymentParamsResponse response = orderService.getPaymentParams(orderId, currentUserId, request);
|
||||
return ApiResponse.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取支付参数失败: userId={}, orderId={}", currentUserId, orderId, e);
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付回调处理接口
|
||||
* 供第三方支付平台回调使用
|
||||
*/
|
||||
@PostMapping("/payment/callback/{scenicId}")
|
||||
public String handlePaymentCallback(
|
||||
@PathVariable("scenicId") Long scenicId,
|
||||
HttpServletRequest request) {
|
||||
|
||||
log.info("接收支付回调: scenicId={}", scenicId);
|
||||
|
||||
try {
|
||||
PaymentCallbackResponse response = orderService.handlePaymentCallback(scenicId, request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
log.info("支付回调处理成功: scenicId={}, orderId={}, statusChangeType={}",
|
||||
scenicId, response.getOrderId(), response.getStatusChangeType());
|
||||
return "SUCCESS"; // 返回给第三方支付平台的成功标识
|
||||
} else {
|
||||
log.error("支付回调处理失败: scenicId={}, message={}", scenicId, response.getMessage());
|
||||
return "FAIL"; // 返回给第三方支付平台的失败标识
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("支付回调异常: scenicId={}", scenicId, e);
|
||||
return "FAIL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.ycwl.basic.order.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 获取支付参数请求DTO
|
||||
* 所有参数都是可选的,系统会自动生成商品名称和描述
|
||||
*/
|
||||
@Data
|
||||
public class PaymentParamsRequest {
|
||||
|
||||
// 预留字段,目前所有信息都由系统自动生成
|
||||
// 可以在未来版本中扩展支持自定义参数
|
||||
|
||||
public PaymentParamsRequest() {
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -6,6 +6,11 @@ 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;
|
||||
|
||||
/**
|
||||
* 订单服务接口
|
||||
@@ -116,4 +121,25 @@ public interface IOrderService {
|
||||
* @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);
|
||||
}
|
@@ -4,6 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ycwl.basic.dto.MobileOrderRequest;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.mapper.MemberMapper;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.model.pc.member.resp.MemberRespVO;
|
||||
import com.ycwl.basic.order.dto.*;
|
||||
import com.ycwl.basic.order.entity.OrderDiscountV2;
|
||||
import com.ycwl.basic.order.entity.OrderItemV2;
|
||||
@@ -20,11 +24,19 @@ import com.ycwl.basic.pricing.dto.DiscountDetail;
|
||||
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
||||
import com.ycwl.basic.pricing.dto.ProductItem;
|
||||
import com.ycwl.basic.pricing.dto.VoucherInfo;
|
||||
import com.ycwl.basic.pay.adapter.IPayAdapter;
|
||||
import com.ycwl.basic.pay.entity.CreateOrderRequest;
|
||||
import com.ycwl.basic.pay.entity.CreateOrderResponse;
|
||||
import com.ycwl.basic.pay.entity.PayResponse;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import com.ycwl.basic.service.pc.ScenicService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
@@ -44,12 +56,15 @@ public class OrderServiceImpl implements IOrderService {
|
||||
private final OrderDiscountMapper orderDiscountMapper;
|
||||
private final OrderRefundMapper orderRefundMapper;
|
||||
private final OrderEventManager orderEventManager;
|
||||
private final ScenicService scenicService;
|
||||
private final FaceRepository faceRepository;
|
||||
private final MemberMapper memberMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createOrder(MobileOrderRequest request, Long userId, Long scenicId, PriceCalculationResult priceResult) {
|
||||
Date now = new Date();
|
||||
|
||||
MemberRespVO member = memberMapper.getById(userId);
|
||||
// 1. 生成订单号
|
||||
String orderNo = generateOrderNo();
|
||||
|
||||
@@ -57,7 +72,7 @@ public class OrderServiceImpl implements IOrderService {
|
||||
OrderV2 order = new OrderV2();
|
||||
order.setOrderNo(orderNo);
|
||||
order.setMemberId(userId);
|
||||
order.setOpenId(request.getFaceId().toString()); // 临时使用faceId,实际应传入openId
|
||||
order.setOpenId(member.getOpenId()); // 临时使用faceId,实际应传入openId
|
||||
order.setFaceId(request.getFaceId());
|
||||
order.setScenicId(scenicId);
|
||||
|
||||
@@ -527,4 +542,195 @@ public class OrderServiceImpl implements IOrderService {
|
||||
int random = (int) (Math.random() * 900) + 100;
|
||||
return "REFUND" + timestamp + random;
|
||||
}
|
||||
|
||||
// ====== 支付相关方法实现 ======
|
||||
|
||||
@Override
|
||||
public PaymentParamsResponse getPaymentParams(Long orderId, Long userId, PaymentParamsRequest request) {
|
||||
log.info("获取支付参数: orderId={}, userId={}", orderId, userId);
|
||||
|
||||
// 1. 查询订单
|
||||
OrderV2 order = orderV2Mapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
throw new RuntimeException("订单不存在");
|
||||
}
|
||||
|
||||
// 2. 验证订单权限
|
||||
if (!userId.equals(order.getMemberId())) {
|
||||
throw new RuntimeException("无权获取该订单的支付参数");
|
||||
}
|
||||
|
||||
// 3. 验证订单状态
|
||||
if (order.getPaymentStatus() != PaymentStatus.UNPAID) {
|
||||
throw new RuntimeException("订单状态不允许支付");
|
||||
}
|
||||
|
||||
// 4. 获取用户openId(从订单中获取)
|
||||
String openId = order.getOpenId();
|
||||
if (openId == null || openId.trim().isEmpty()) {
|
||||
throw new RuntimeException("用户openId不存在,无法发起支付");
|
||||
}
|
||||
|
||||
// 5. 检查是否为免费订单
|
||||
if (order.getFinalAmount().compareTo(BigDecimal.ZERO) == 0) {
|
||||
log.info("免费订单,无需支付: orderId={}", orderId);
|
||||
return PaymentParamsResponse.free(orderId, order.getOrderNo());
|
||||
}
|
||||
|
||||
// 6. 获取景区支付适配器
|
||||
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(order.getScenicId());
|
||||
|
||||
try {
|
||||
// 7. 生成商品名称和描述
|
||||
String goodsName = generateGoodsName(orderId);
|
||||
String description = goodsName; // 直接使用商品名称作为描述
|
||||
|
||||
// 8. 创建支付订单请求
|
||||
CreateOrderRequest payRequest = new CreateOrderRequest()
|
||||
.setOrderNo(order.getOrderNo())
|
||||
.setPriceInCents(order.getFinalAmount().multiply(BigDecimal.valueOf(100)).intValue())
|
||||
.setDescription(description)
|
||||
.setGoodsName(goodsName)
|
||||
.setUserIdentify(openId)
|
||||
.setNotifyUrl("https://zhentuai.com/api/mobile/order/v2/payment/callback/" + order.getScenicId());
|
||||
|
||||
// 9. 调用支付适配器创建订单
|
||||
CreateOrderResponse payResponse = payAdapter.createOrder(payRequest);
|
||||
|
||||
// 10. 获取支付参数
|
||||
java.util.Map<String, Object> paymentParams = payAdapter.getPaymentParams(payResponse);
|
||||
|
||||
// 11. 构建响应
|
||||
PaymentParamsResponse response = PaymentParamsResponse.success(
|
||||
orderId,
|
||||
order.getOrderNo(),
|
||||
order.getFinalAmount(),
|
||||
!payResponse.isSkipPay(),
|
||||
paymentParams
|
||||
);
|
||||
response.setDescription(description);
|
||||
response.setGoodsName(goodsName);
|
||||
|
||||
log.info("支付参数生成成功: orderId={}, needPay={}, amount={}",
|
||||
orderId, !payResponse.isSkipPay(), order.getFinalAmount());
|
||||
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取支付参数失败: orderId={}, error={}", orderId, e.getMessage(), e);
|
||||
throw new RuntimeException("获取支付参数失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentCallbackResponse handlePaymentCallback(Long scenicId, HttpServletRequest request) {
|
||||
log.info("处理支付回调: scenicId={}", scenicId);
|
||||
|
||||
try {
|
||||
// 1. 获取支付适配器
|
||||
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
|
||||
|
||||
// 2. 解析回调数据
|
||||
PayResponse callbackResponse = payAdapter.handleCallback(request);
|
||||
log.info("解析支付回调数据: {}", callbackResponse);
|
||||
|
||||
if (callbackResponse == null || callbackResponse.getOrderNo() == null) {
|
||||
return PaymentCallbackResponse.failure("回调数据解析失败");
|
||||
}
|
||||
|
||||
// 3. 查询订单
|
||||
String orderNo = callbackResponse.getOrderNo();
|
||||
OrderV2 order = getByOrderNo(orderNo);
|
||||
if (order == null) {
|
||||
return PaymentCallbackResponse.failure("订单不存在", null, orderNo);
|
||||
}
|
||||
|
||||
// 4. 处理不同的支付状态变更
|
||||
String statusChangeType = null;
|
||||
if (callbackResponse.isPay()) {
|
||||
// 支付成功
|
||||
statusChangeType = "PAID";
|
||||
updatePaymentStatus(order.getId(), PaymentStatus.PAID.name());
|
||||
|
||||
// 触发支付成功事件
|
||||
orderEventManager.publishPaymentStatusChangeEvent(
|
||||
new PaymentStatusChangeEvent(order.getId(), PaymentStatus.PENDING, PaymentStatus.PAID)
|
||||
);
|
||||
|
||||
} else if (callbackResponse.isCancel()) {
|
||||
// 支付取消
|
||||
statusChangeType = "CANCELLED";
|
||||
updatePaymentStatus(order.getId(), PaymentStatus.CANCELLED.name());
|
||||
|
||||
// 触发支付取消事件
|
||||
orderEventManager.publishPaymentStatusChangeEvent(
|
||||
new PaymentStatusChangeEvent(order.getId(), PaymentStatus.PENDING, PaymentStatus.CANCELLED)
|
||||
);
|
||||
|
||||
} else if (callbackResponse.isRefund()) {
|
||||
// 退款
|
||||
statusChangeType = "REFUNDED";
|
||||
updateRefundStatus(order.getId(), RefundStatus.COMPLETED.name());
|
||||
|
||||
// 触发退款事件
|
||||
orderEventManager.publishRefundStatusChangeEvent(
|
||||
new RefundStatusChangeEvent(order.getId(), RefundStatus.PENDING, RefundStatus.COMPLETED)
|
||||
);
|
||||
}
|
||||
|
||||
log.info("支付回调处理成功: orderId={}, orderNo={}, statusChangeType={}",
|
||||
order.getId(), orderNo, statusChangeType);
|
||||
|
||||
return PaymentCallbackResponse.success(order.getId(), orderNo, statusChangeType);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理支付回调失败: scenicId={}, error={}", scenicId, e.getMessage(), e);
|
||||
return PaymentCallbackResponse.failure("回调处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成商品名称
|
||||
* 根据订单的商品类型生成用于支付页面展示的商品名称
|
||||
* 如果是一种商品类型,显示具体类型名称;如果是多种,显示"多项景区商品"
|
||||
*/
|
||||
private String generateGoodsName(Long orderId) {
|
||||
// 查询订单商品明细
|
||||
QueryWrapper<OrderItemV2> itemQuery = new QueryWrapper<>();
|
||||
itemQuery.eq("order_id", orderId);
|
||||
java.util.List<OrderItemV2> orderItems = orderItemMapper.selectList(itemQuery);
|
||||
|
||||
if (orderItems.isEmpty()) {
|
||||
return "景区商品";
|
||||
}
|
||||
|
||||
// 获取所有不同的商品类型
|
||||
java.util.Set<String> productTypes = new java.util.HashSet<>();
|
||||
for (OrderItemV2 item : orderItems) {
|
||||
productTypes.add(item.getProductType());
|
||||
}
|
||||
|
||||
// 如果只有一种商品类型,返回具体类型名称
|
||||
if (productTypes.size() == 1) {
|
||||
String productType = productTypes.iterator().next();
|
||||
return getProductTypeName(productType);
|
||||
} else {
|
||||
// 如果有多种商品类型,返回通用名称
|
||||
return "多项景区商品";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品类型中文名称
|
||||
*/
|
||||
private String getProductTypeName(String productType) {
|
||||
return switch (productType) {
|
||||
case "VLOG_VIDEO" -> "Vlog视频";
|
||||
case "RECORDING_SET" -> "录像集";
|
||||
case "PHOTO_SET" -> "照相集";
|
||||
case "PHOTO_PRINT" -> "照片打印";
|
||||
case "MACHINE_PRINT" -> "一体机打印";
|
||||
default -> "景区商品";
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user