记得上下班打卡 | git大法好,push需谨慎

Commit 6ef594fd authored by 姜秀龙's avatar 姜秀龙

fix: 收钱吧退款卡单纳入自动退并修复库存双回

自动退候选增加退款中超8小时的订单;refund 支持 status=61 重试时不改状态/不回滚;库存仅在回调20回一次。
Co-authored-by: 's avatarCursor <cursoragent@cursor.com>
parent f83b5de2
......@@ -38,8 +38,8 @@ public interface IGoblinSqbService {
ResponseDto<Boolean> syncCouponStatus(String userId, String orderId);
/**
* 演出结束自动退款(定时任务调用)
* SQL 已联表过滤:收钱吧扩展单、主单已支付(2)、演出已结束、扩展未核销;循环内仅调 refund。
* 自动退款(定时任务调用)
* SQL 已联表过滤:收钱吧扩展单、未核销,且(主单已支付(2)+演出已结束)或(退款中(61)+超 8 小时);循环内调 refund。
*
* @param performancesId 演出ID,传 null 或空则不按演出缩小(全库符合条件候选,慎用)
* @return 处理结果摘要(成功/失败笔数)
......
......@@ -17,7 +17,8 @@ import java.util.List;
public interface GoblinSqbOrderMapper extends BaseMapper<GoblinSqbOrder> {
/**
* 联表过滤:主单已支付(2)、演出已结束、扩展未核销;可选按演出 id 缩小范围
* 联表过滤候选:未核销 + 有演出关联,且满足其一:
* 主单已支付(2)+演出已结束,或主单退款中(61)+进入退款中已超 8 小时;可选按演出 id 缩小范围
*/
List<GoblinSqbOrder> selectAutoRefundCandidates(@Param("performancesId") String performancesId);
}
......@@ -2,17 +2,24 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liquidnet.service.goblin.mapper.GoblinSqbOrderMapper">
<!-- 收钱吧扩展单 + 主单已支付 + 演出已结束 + 未核销(定时自动退候选) -->
<!-- 定时自动退候选:未核销 + 有演出关联,且满足其一:
1) 主单已支付(2) + 演出已结束
2) 主单退款中(61) + 进入退款中已超 8 小时(卡单重试,不要求演出结束) -->
<select id="selectAutoRefundCandidates" resultType="com.liquidnet.service.goblin.entity.GoblinSqbOrder">
SELECT so.*
FROM goblin_sqb_order so
INNER JOIN goblin_store_order o ON o.order_id = so.order_id
INNER JOIN kylin_performances kp ON kp.performances_id = so.performances_id
WHERE (so.coupon_used_status = 0 OR so.coupon_used_status IS NULL)
AND o.status = 2
AND kp.time_end &lt; NOW()
AND so.performances_id IS NOT NULL
AND so.performances_id != ''
AND (
(o.status = 2 AND kp.time_end &lt; NOW())
OR (
o.status = 61
AND o.updated_at &lt;= DATE_SUB(NOW(), INTERVAL 8 HOUR)
)
)
<if test="performancesId != null and performancesId != ''">
AND so.performances_id = #{performancesId}
</if>
......
......@@ -70,7 +70,7 @@ public class GoblinTaskHandler {
}
/**
* 演出结束自动退款
* 演出结束自动退款 / 退款中超8小时卡单重试
* xxl-job 配置:JobHandler = sev-goblin:sqbAutoRefund;参数为演出ID、或 performancesId=xxx;空则走 goblin 全库候选(慎用)
*/
@XxlJob(value = "sev-goblin:sqbAutoRefund")
......
......@@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
* - JobHandler:sev-goblin:sqbAutoRefund(接口走 rsc/** 免登录,与 NFT job 一致)
* - CRON:按演出结束时间触发(如演出结束后 5 分钟:0 5 * * * ?,具体由运维根据演出时间动态配置)
* - 执行器:liquidnet-service-executor
* - 任务参数:演出ID 或 performancesId=xxx;留空则处理全库「演出已结束 + 主单已支付 + 扩展未核销」候选(慎用)
* - 任务参数:演出ID 或 performancesId=xxx;留空则处理全库候选(演出结束已支付 / 退款中超8小时,慎用)
* - 路由策略:第一个
*/
@Slf4j
......@@ -32,7 +32,7 @@ public class GoblinSqbJobController {
private IGoblinSqbService goblinSqbService;
@PostMapping("/autoRefund")
@ApiOperation("演出结束自动退款")
@ApiOperation("收钱吧自动退款(演出结束已支付 / 退款中超8小时卡单重试)")
public ResponseDto<String> autoRefund(@RequestParam(value = "performancesId", required = false) String performancesId) {
log.info("[收钱吧自动退款] 收到任务,performancesId={}", performancesId);
return goblinSqbService.autoRefundByPerformance(performancesId);
......
......@@ -134,7 +134,8 @@ public class GoblinSqbServiceImpl implements IGoblinSqbService {
if (Integer.valueOf(GoblinStatusConst.Status.ORDER_STATUS_6.getValue()).equals(storeSt)) {
return ResponseDto.failure("订单已退款,不可重复退款");
}
if (!Integer.valueOf(GoblinStatusConst.Status.ORDER_STATUS_2.getValue()).equals(storeSt)) {
boolean isRetry = Integer.valueOf(GoblinStatusConst.Status.ORDER_STATUS_61.getValue()).equals(storeSt);
if (!isRetry && !Integer.valueOf(GoblinStatusConst.Status.ORDER_STATUS_2.getValue()).equals(storeSt)) {
return ResponseDto.failure("订单状态不可退款");
}
List<String> orderSkuVoIds = storeOrderVo.getOrderSkuVoIds();
......@@ -160,11 +161,13 @@ public class GoblinSqbServiceImpl implements IGoblinSqbService {
return ResponseDto.failure("券码已核销,不可退款");
}
// 更新状态为退款中
String now = LocalDateTime.now().format(DTF);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 4);
// 首次申请:置退款中;卡单重试(已是61):不改主单状态,避免刷新 updated_at
if (!isRetry) {
String now = LocalDateTime.now().format(DTF);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 4);
}
List<GoblinOrderSkuVo> orderSkuVos = new ArrayList<>();
for (String skuVoId : storeOrderVo.getOrderSkuVoIds()) {
......@@ -183,32 +186,39 @@ public class GoblinSqbServiceImpl implements IGoblinSqbService {
reason,
buildRequestId(IDGenerator.nextSnowId())
);
log.info("[收钱吧-退款] 收钱吧退款响应: {}, orderId: {}.", JsonUtils.toJson(refundData), orderId);
log.info("[收钱吧-退款] 收钱吧退款响应: {}, orderId: {}, isRetry={}.", JsonUtils.toJson(refundData), orderId, isRetry);
if (refundData == null) {
// 退款失败,回滚状态
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
if (!isRetry) {
// 首次申请失败:回滚为已支付
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
} else {
log.warn("[收钱吧-退款] 卡单重试失败,保持退款中,orderId={}", orderId);
}
return ResponseDto.failure("退款失败,请稍后重试");
}
// 退款成功
// 调收钱吧成功:写退款号;库存只在退款回调 targetState=20 时回(避免申请成功+回调各回一次)
orderVo.setRefundSn(refundData.getTicketsSn());
orderVo.setRefundSignature(refundData.getTicketSignature());
orderVo.setRefundReason(reason);
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
goblinRedisUtils.incrSkuStock(null, orderVo.getSkuId(), orderVo.getQuantity());
// syncOrderStatus(orderId, 3);
log.info("[收钱吧-退款] 调用收钱吧退款成功,orderId={}", orderId);
log.info("[收钱吧-退款] 调用收钱吧退款成功,orderId={}, isRetry={}", orderId, isRetry);
return ResponseDto.success(Boolean.TRUE);
} catch (Exception e) {
log.error("[收钱吧退款] 退款异常,orderId={},回滚主单为已支付", orderId, e);
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
if (!isRetry) {
log.error("[收钱吧退款] 退款异常,orderId={},回滚主单为已支付", orderId, e);
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
} else {
log.error("[收钱吧退款] 卡单重试异常,保持退款中,orderId={}", orderId, e);
}
return ResponseDto.failure("退款失败:" + e.getMessage());
}
}
......@@ -272,14 +282,14 @@ public class GoblinSqbServiceImpl implements IGoblinSqbService {
pid = null;
}
if (pid == null) {
log.warn("[收钱吧自动退款] performancesId 为空,将处理库内所有「演出已结束 + 主单已支付 + 扩展未核销」候选单");
log.warn("[收钱吧自动退款] performancesId 为空,将处理库内所有符合条件候选单(演出结束已支付 / 退款中超8小时)");
} else {
log.info("[收钱吧自动退款] 开始 performancesId={}", pid);
}
List<GoblinSqbOrder> orders = goblinSqbOrderMapper.selectAutoRefundCandidates(pid);
if (CollectionUtils.isEmpty(orders)) {
String scope = pid != null ? ("演出 " + pid) : "全库(演出已结束)";
String scope = pid != null ? ("演出 " + pid) : "全库";
String summary = scope + " 无待自动退款的收钱吧扩展单";
log.info("[收钱吧自动退款] {}", summary);
return ResponseDto.success(summary);
......@@ -311,7 +321,7 @@ public class GoblinSqbServiceImpl implements IGoblinSqbService {
}
}
String scopeLabel = pid != null ? ("演出 " + pid) : "全库(演出已结束)";
String scopeLabel = pid != null ? ("演出 " + pid) : "全库";
String summary = String.format("%s 自动退款完成:成功 %d 笔,失败 %d 笔", scopeLabel, successCount, failCount);
log.info("[收钱吧自动退款] {}", summary);
return ResponseDto.success(summary);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment