feat:支付成功后生成车票

This commit is contained in:
amos
2025-11-03 14:35:16 +08:00
parent 59fbc53daa
commit f6503dd76f
8 changed files with 510 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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重试
}
}
}

View File

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

View File

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

View File

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