feat:支付成功后生成车票
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package pers.amos.mall.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 支付回调DTO
|
||||
*/
|
||||
@Data
|
||||
public class PaymentCallbackDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 订单号
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 支付单号
|
||||
*/
|
||||
private String paymentNo;
|
||||
|
||||
/**
|
||||
* 第三方支付流水号
|
||||
*/
|
||||
private String transactionId;
|
||||
|
||||
/**
|
||||
* 支付金额
|
||||
*/
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 支付状态:PAID-成功,FAILED-失败
|
||||
*/
|
||||
private String paymentStatus;
|
||||
|
||||
/**
|
||||
* 支付时间(格式:yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
private String paymentTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package pers.amos.mall.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 支付成功消息
|
||||
*/
|
||||
@Data
|
||||
public class PaymentSuccessMessage implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 订单号
|
||||
*/
|
||||
private String orderNo;
|
||||
|
||||
/**
|
||||
* 支付单号
|
||||
*/
|
||||
private String paymentNo;
|
||||
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private String userNo;
|
||||
|
||||
/**
|
||||
* 线路编码
|
||||
*/
|
||||
private String routeCode;
|
||||
|
||||
/**
|
||||
* 订单类型:FIXED-固定班次,ROLLING-滚动发车
|
||||
*/
|
||||
private String orderType;
|
||||
|
||||
/**
|
||||
* 固定班次编号(固定班次时使用)
|
||||
*/
|
||||
private String scheduleCode;
|
||||
|
||||
/**
|
||||
* 滚动班次编号(滚动发车时使用)
|
||||
*/
|
||||
private String rollingScheduleCode;
|
||||
|
||||
/**
|
||||
* 票种类型
|
||||
*/
|
||||
private String ticketType;
|
||||
|
||||
/**
|
||||
* 购票数量
|
||||
*/
|
||||
private Integer quantity;
|
||||
|
||||
/**
|
||||
* 支付金额
|
||||
*/
|
||||
private BigDecimal paymentAmount;
|
||||
|
||||
/**
|
||||
* 支付时间(格式:yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
private String paymentTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package pers.amos.mall.common.constant;
|
||||
|
||||
/**
|
||||
* MQ常量
|
||||
*/
|
||||
public class MQConstant {
|
||||
|
||||
/**
|
||||
* 支付成功Topic
|
||||
*/
|
||||
public static final String PAYMENT_SUCCESS_TOPIC = "PAYMENT_SUCCESS_TOPIC";
|
||||
|
||||
/**
|
||||
* 支付成功Tag
|
||||
*/
|
||||
public static final String PAYMENT_SUCCESS_TAG = "PAYMENT_SUCCESS";
|
||||
|
||||
/**
|
||||
* 订单超时Topic
|
||||
*/
|
||||
public static final String ORDER_TIMEOUT_TOPIC = "ORDER_TIMEOUT_TOPIC";
|
||||
|
||||
/**
|
||||
* 订单超时Tag
|
||||
*/
|
||||
public static final String ORDER_TIMEOUT_TAG = "ORDER_TIMEOUT";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package pers.amos.mall.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 支付状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PaymentStatusEnum {
|
||||
|
||||
/**
|
||||
* 待支付
|
||||
*/
|
||||
UNPAID("UNPAID", "待支付"),
|
||||
|
||||
/**
|
||||
* 支付中
|
||||
*/
|
||||
PAYING("PAYING", "支付中"),
|
||||
|
||||
/**
|
||||
* 支付成功
|
||||
*/
|
||||
PAID("PAID", "支付成功"),
|
||||
|
||||
/**
|
||||
* 支付失败
|
||||
*/
|
||||
FAILED("FAILED", "支付失败"),
|
||||
|
||||
/**
|
||||
* 已退款
|
||||
*/
|
||||
REFUNDED("REFUNDED", "已退款");
|
||||
|
||||
private final String code;
|
||||
private final String desc;
|
||||
|
||||
public static PaymentStatusEnum fromCode(String code) {
|
||||
for (PaymentStatusEnum status : values()) {
|
||||
if (status.getCode().equals(code)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知的支付状态: " + code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package pers.amos.mall.perform.listener;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import pers.amos.mall.api.dto.PaymentSuccessMessage;
|
||||
import pers.amos.mall.api.dto.TicketGenerateDTO;
|
||||
import pers.amos.mall.api.service.TicketService;
|
||||
import pers.amos.mall.common.constant.MQConstant;
|
||||
import pers.amos.mall.common.response.Result;
|
||||
|
||||
/**
|
||||
* 支付成功消息监听器
|
||||
* 监听到支付成功消息后生成车票
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = MQConstant.PAYMENT_SUCCESS_TOPIC,
|
||||
selectorExpression = MQConstant.PAYMENT_SUCCESS_TAG,
|
||||
consumerGroup = "PERFORM_PAYMENT_SUCCESS_GROUP"
|
||||
)
|
||||
public class PaymentSuccessListener implements RocketMQListener<PaymentSuccessMessage> {
|
||||
|
||||
@Autowired
|
||||
private TicketService ticketService;
|
||||
|
||||
@Override
|
||||
public void onMessage(PaymentSuccessMessage message) {
|
||||
log.info("收到支付成功消息, orderNo={}, message={}",
|
||||
message.getOrderNo(), JSONUtil.toJsonStr(message));
|
||||
|
||||
try {
|
||||
// 构建车票生成DTO
|
||||
TicketGenerateDTO generateDTO = new TicketGenerateDTO();
|
||||
generateDTO.setOrderNo(message.getOrderNo());
|
||||
generateDTO.setUserNo(message.getUserNo());
|
||||
generateDTO.setRouteCode(message.getRouteCode());
|
||||
generateDTO.setOrderType(message.getOrderType());
|
||||
generateDTO.setScheduleCode(message.getScheduleCode());
|
||||
generateDTO.setRollingScheduleCode(message.getRollingScheduleCode());
|
||||
generateDTO.setTicketType(message.getTicketType());
|
||||
generateDTO.setQuantity(message.getQuantity());
|
||||
|
||||
// 生成车票
|
||||
Result<?> result = ticketService.generateTickets(generateDTO);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
log.info("车票生成成功, orderNo={}, quantity={}",
|
||||
message.getOrderNo(), message.getQuantity());
|
||||
} else {
|
||||
log.error("车票生成失败, orderNo={}, errorMsg={}",
|
||||
message.getOrderNo(), result.getErrorDesc());
|
||||
// TODO: 这里可以考虑重试机制或补偿逻辑
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理支付成功消息失败, orderNo={}", message.getOrderNo(), e);
|
||||
// TODO: 这里可以考虑将失败消息发送到死信队列
|
||||
throw e; // 抛出异常让RocketMQ重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package pers.amos.mall.trade.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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 pers.amos.mall.api.dto.PaymentCallbackDTO;
|
||||
import pers.amos.mall.common.response.Result;
|
||||
import pers.amos.mall.trade.service.PaymentService;
|
||||
|
||||
/**
|
||||
* 支付控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/payment")
|
||||
public class PaymentController {
|
||||
|
||||
@Autowired
|
||||
private PaymentService paymentService;
|
||||
|
||||
/**
|
||||
* 支付回调接口
|
||||
*
|
||||
* @param callbackDTO 支付回调数据
|
||||
* @return 处理结果
|
||||
*/
|
||||
@PostMapping("/callback")
|
||||
public Result<Boolean> paymentCallback(@RequestBody PaymentCallbackDTO callbackDTO) {
|
||||
log.info("接收支付回调, paymentNo={}, orderNo={}",
|
||||
callbackDTO.getPaymentNo(), callbackDTO.getOrderNo());
|
||||
|
||||
return paymentService.handlePaymentCallback(callbackDTO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package pers.amos.mall.trade.service;
|
||||
|
||||
import pers.amos.mall.api.dto.PaymentCallbackDTO;
|
||||
import pers.amos.mall.common.response.Result;
|
||||
|
||||
/**
|
||||
* 支付服务接口
|
||||
*/
|
||||
public interface PaymentService {
|
||||
|
||||
/**
|
||||
* 处理支付回调
|
||||
*
|
||||
* @param callbackDTO 支付回调DTO
|
||||
* @return 处理结果
|
||||
*/
|
||||
Result<Boolean> handlePaymentCallback(PaymentCallbackDTO callbackDTO);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package pers.amos.mall.trade.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import pers.amos.mall.api.dto.InventoryOperationDTO;
|
||||
import pers.amos.mall.api.dto.PaymentCallbackDTO;
|
||||
import pers.amos.mall.api.dto.PaymentSuccessMessage;
|
||||
import pers.amos.mall.api.service.RouteService;
|
||||
import pers.amos.mall.common.constant.MQConstant;
|
||||
import pers.amos.mall.common.enums.OrderStatusEnum;
|
||||
import pers.amos.mall.common.enums.PaymentStatusEnum;
|
||||
import pers.amos.mall.common.response.Result;
|
||||
import pers.amos.mall.trade.dal.dataobject.Order;
|
||||
import pers.amos.mall.trade.dal.dataobject.Payment;
|
||||
import pers.amos.mall.trade.dal.repository.OrderRepository;
|
||||
import pers.amos.mall.trade.dal.repository.PaymentRepository;
|
||||
import pers.amos.mall.trade.service.PaymentService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 支付服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PaymentServiceImpl implements PaymentService {
|
||||
|
||||
@Autowired
|
||||
private PaymentRepository paymentRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private RouteService routeService;
|
||||
|
||||
@Autowired
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Boolean> handlePaymentCallback(PaymentCallbackDTO callbackDTO) {
|
||||
log.info("收到支付回调, paymentNo={}, orderNo={}, status={}",
|
||||
callbackDTO.getPaymentNo(), callbackDTO.getOrderNo(), callbackDTO.getPaymentStatus());
|
||||
|
||||
try {
|
||||
// 1. 查询支付单
|
||||
Payment payment = paymentRepository.getOne(
|
||||
Wrappers.lambdaQuery(Payment.class)
|
||||
.eq(Payment::getPaymentNo, callbackDTO.getPaymentNo())
|
||||
);
|
||||
|
||||
if (payment == null) {
|
||||
log.error("支付单不存在, paymentNo={}", callbackDTO.getPaymentNo());
|
||||
return Result.fail("PAYMENT_NOT_FOUND", "支付单不存在");
|
||||
}
|
||||
|
||||
// 防止重复回调
|
||||
if (!PaymentStatusEnum.UNPAID.getCode().equals(payment.getPaymentStatus())
|
||||
&& !PaymentStatusEnum.PAYING.getCode().equals(payment.getPaymentStatus())) {
|
||||
log.warn("支付单已处理, paymentNo={}, currentStatus={}",
|
||||
callbackDTO.getPaymentNo(), payment.getPaymentStatus());
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
// 2. 查询订单
|
||||
Order order = orderRepository.getOne(
|
||||
Wrappers.lambdaQuery(Order.class)
|
||||
.eq(Order::getOrderNo, callbackDTO.getOrderNo())
|
||||
);
|
||||
|
||||
if (order == null) {
|
||||
log.error("订单不存在, orderNo={}", callbackDTO.getOrderNo());
|
||||
return Result.fail("ORDER_NOT_FOUND", "订单不存在");
|
||||
}
|
||||
|
||||
// 3. 更新支付单状态
|
||||
payment.setPaymentStatus(callbackDTO.getPaymentStatus());
|
||||
payment.setTransactionNo(callbackDTO.getTransactionId());
|
||||
payment.setPaymentTime(LocalDateTime.parse(callbackDTO.getPaymentTime(), FORMATTER));
|
||||
payment.setUpdateTime(LocalDateTime.now());
|
||||
paymentRepository.updateById(payment);
|
||||
|
||||
// 4. 根据支付结果处理
|
||||
if (PaymentStatusEnum.PAID.getCode().equals(callbackDTO.getPaymentStatus())) {
|
||||
// 支付成功
|
||||
handlePaymentSuccess(order, payment, callbackDTO);
|
||||
} else if (PaymentStatusEnum.FAILED.getCode().equals(callbackDTO.getPaymentStatus())) {
|
||||
// 支付失败
|
||||
handlePaymentFailed(order);
|
||||
}
|
||||
|
||||
log.info("支付回调处理成功, paymentNo={}, status={}",
|
||||
callbackDTO.getPaymentNo(), callbackDTO.getPaymentStatus());
|
||||
|
||||
return Result.success(true);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理支付回调失败, paymentNo={}", callbackDTO.getPaymentNo(), e);
|
||||
return Result.fail("PAYMENT_CALLBACK_ERROR", "处理支付回调失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付成功
|
||||
*/
|
||||
private void handlePaymentSuccess(Order order, Payment payment, PaymentCallbackDTO callbackDTO) {
|
||||
// 1. 更新订单状态为已支付
|
||||
order.setOrderStatus(OrderStatusEnum.PAID.getCode());
|
||||
order.setPayTime(LocalDateTime.parse(callbackDTO.getPaymentTime(), FORMATTER));
|
||||
order.setUpdateTime(LocalDateTime.now());
|
||||
orderRepository.updateById(order);
|
||||
|
||||
// 2. 扣减库存(将锁定的库存转为已售)
|
||||
InventoryOperationDTO inventoryDTO = new InventoryOperationDTO();
|
||||
inventoryDTO.setOrderNo(order.getOrderNo());
|
||||
inventoryDTO.setOrderType(order.getOrderType());
|
||||
inventoryDTO.setScheduleCode(order.getScheduleCode());
|
||||
inventoryDTO.setRollingScheduleCode(order.getRollingScheduleCode());
|
||||
inventoryDTO.setQuantity(order.getQuantity());
|
||||
|
||||
Result<Boolean> deductResult = routeService.deductInventory(inventoryDTO);
|
||||
if (!deductResult.isSuccess()) {
|
||||
log.error("扣减库存失败, orderNo={}", order.getOrderNo());
|
||||
throw new RuntimeException("扣减库存失败:" + deductResult.getErrorDesc());
|
||||
}
|
||||
|
||||
// 3. 发送支付成功消息到MQ
|
||||
sendPaymentSuccessMessage(order, payment, callbackDTO);
|
||||
|
||||
log.info("支付成功处理完成, orderNo={}, paymentNo={}", order.getOrderNo(), payment.getPaymentNo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付失败
|
||||
*/
|
||||
private void handlePaymentFailed(Order order) {
|
||||
// 1. 更新订单状态为已取消
|
||||
order.setOrderStatus(OrderStatusEnum.CANCELLED.getCode());
|
||||
order.setUpdateTime(LocalDateTime.now());
|
||||
orderRepository.updateById(order);
|
||||
|
||||
// 2. 释放库存
|
||||
InventoryOperationDTO inventoryDTO = new InventoryOperationDTO();
|
||||
inventoryDTO.setOrderNo(order.getOrderNo());
|
||||
inventoryDTO.setOrderType(order.getOrderType());
|
||||
inventoryDTO.setScheduleCode(order.getScheduleCode());
|
||||
inventoryDTO.setRollingScheduleCode(order.getRollingScheduleCode());
|
||||
inventoryDTO.setQuantity(order.getQuantity());
|
||||
|
||||
Result<Boolean> releaseResult = routeService.releaseInventory(inventoryDTO);
|
||||
if (!releaseResult.isSuccess()) {
|
||||
log.error("释放库存失败, orderNo={}", order.getOrderNo());
|
||||
}
|
||||
|
||||
log.info("支付失败处理完成, orderNo={}", order.getOrderNo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送支付成功消息
|
||||
*/
|
||||
private void sendPaymentSuccessMessage(Order order, Payment payment, PaymentCallbackDTO callbackDTO) {
|
||||
PaymentSuccessMessage message = new PaymentSuccessMessage();
|
||||
message.setOrderNo(order.getOrderNo());
|
||||
message.setPaymentNo(payment.getPaymentNo());
|
||||
message.setUserNo(order.getUserNo());
|
||||
message.setRouteCode(order.getRouteCode());
|
||||
message.setOrderType(order.getOrderType());
|
||||
message.setScheduleCode(order.getScheduleCode());
|
||||
message.setRollingScheduleCode(order.getRollingScheduleCode());
|
||||
message.setTicketType(order.getTicketType());
|
||||
message.setQuantity(order.getQuantity());
|
||||
message.setPaymentAmount(payment.getPaymentAmount());
|
||||
message.setPaymentTime(callbackDTO.getPaymentTime());
|
||||
|
||||
String destination = MQConstant.PAYMENT_SUCCESS_TOPIC + ":" + MQConstant.PAYMENT_SUCCESS_TAG;
|
||||
rocketMQTemplate.syncSend(destination, message);
|
||||
|
||||
log.info("发送支付成功消息, orderNo={}, message={}", order.getOrderNo(), JSONUtil.toJsonStr(message));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user