From 7a8210aeb43da869c63793f2d9af43409f4be14a Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Tue, 13 Oct 2026 16:42:01 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=92=E5=BA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/feign/RemoteSmHotelOrderService.java | 7 + .../entity/SmHotelOrderEntity.java | 5 + .../supermarket/vo/SmHotelOrderDetailVo.java | 119 ++++ .../pjcloud-supermarket-biz/pom.xml | 6 + .../config/ShangyouHotelProperties.java | 6 + .../controller/SmHotelAppController.java | 40 ++ .../controller/SmHotelOrderController.java | 20 +- .../SmHotelOrderInnerController.java | 7 + .../job/SmHotelShangyouSpringSchedule.java | 20 + .../service/SmHotelOrderService.java | 41 +- .../service/impl/SmHotelOrderServiceImpl.java | 631 ++++++++++++++++-- .../impl/appplus/ThirdAppPlusServiceImpl.java | 8 + 12 files changed, 857 insertions(+), 53 deletions(-) create mode 100644 pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelOrderDetailVo.java diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelOrderService.java index 7eca164d6..3e7c823d9 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelOrderService.java @@ -28,4 +28,11 @@ public interface RemoteSmHotelOrderService { @NoToken @PostMapping("/inner/sm-hotel-order/pay-success") R paySuccess(@RequestParam("orderId") Long orderId); + + /** + * 第三方退款成功后置:标记已退款并关闭预订(幂等)。 + */ + @NoToken + @PostMapping("/inner/sm-hotel-order/refund-success") + R refundSuccess(@RequestParam("orderId") Long orderId); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java index 415bbad7f..23e6228c4 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java @@ -114,6 +114,11 @@ public class SmHotelOrderEntity extends Model { @Schema(description = "酒店名称") private String hotelName; + /** 详情展示:产品名称(非表字段) */ + @TableField(exist = false) + @Schema(description = "产品名称") + private String productName; + /** 列表展示:下单来源 APP/ADMIN/DOWNSTREAM(非表字段,由 channelNo 推断) */ @TableField(exist = false) @Schema(description = "下单来源") diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelOrderDetailVo.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelOrderDetailVo.java new file mode 100644 index 000000000..88397f104 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelOrderDetailVo.java @@ -0,0 +1,119 @@ +package com.bsd.pjcloud.supermarket.vo; + +import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 后台酒店订单详情:订单 + 酒店基础信息 + 产品 + 入住日历 + 入住人。 + */ +@Data +@Schema(description = "酒店订单详情(后台)") +public class SmHotelOrderDetailVo { + + @Schema(description = "订单主体") + private SmHotelOrderEntity order; + + @Schema(description = "酒店基础信息") + private HotelBrief hotel; + + @Schema(description = "下单产品") + private ProductBrief product; + + @Schema(description = "房型名称") + private String roomTypeName; + + @Schema(description = "入住区间日历明细(含当日价库存)") + private List dailyList; + + @Schema(description = "入住人列表(解析自 guestJson)") + private Object guests; + + @Schema(description = "下单请求快照(解析自 reqJson)") + private Object reqSnapshot; + + @Schema(description = "上游响应快照(解析自 upstreamRespJson)") + private Object upstreamResp; + + @Data + @Schema(description = "酒店摘要") + public static class HotelBrief { + + private Long id; + + private String hotelCode; + + private String hotelName; + + private String groupCode; + + /** local / shangyou */ + private String dataSource; + + private String upstreamStatus; + + private String coverImage; + + private String address; + + private String district; + + private String bizArea; + + private Integer starLevel; + + private BigDecimal score; + + private String phone; + + private String intro; + + private String facilityTags; + + private BigDecimal longitude; + + private BigDecimal latitude; + + private Long deptId; + } + + @Data + @Schema(description = "产品摘要") + public static class ProductBrief { + + private Long id; + + private String productCode; + + private String productName; + + private String roomTypeCode; + + private String priceCode; + + private String cancelCode; + + private Integer mealType; + + private Integer mealCount; + + private BigDecimal costPrice; + + private BigDecimal sellPrice; + + private Integer stock; + + private Integer canCancel; + + private String cancelRule; + + private Integer productType; + + private Integer apartmentType; + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/pom.xml b/pjcloud-supermarket/pjcloud-supermarket-biz/pom.xml index 8cdfd5cc0..9768b3504 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/pom.xml +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/pom.xml @@ -91,6 +91,12 @@ pjcloud-wallet-api 5.7.0 + + + com.bsd + pjcloud-thirdparty-api + 5.7.0 + com.bsd diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java index 2a252c818..04f6d4eac 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java @@ -69,6 +69,12 @@ public class ShangyouHotelProperties { /** HTTP 超时毫秒 */ private int timeoutMs = 60000; + /** + * App 待支付订单超时分钟数(按 createTime 推算,不落库)。 + * 超时自动取消;本地酒店会释放创单时预占的日历库存。默认 10 分钟。 + */ + private int appPayTimeoutMinutes = 10; + /** token 提前失效秒数;默认 120 → 缓存约 5 小时 58 分(21600-120),与文档建议一致 */ private int tokenSkewSeconds = 120; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java index 51ae3c6bb..fd1588096 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java @@ -25,6 +25,7 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -190,9 +191,33 @@ public class SmHotelAppController { if ("OFFLINE".equalsIgnoreCase(r.getData().getHotel().getUpstreamStatus())) { return R.failed("酒店已下架"); } + // App 侧:没有未来日历的产品无法预订,不展示 + hideProductsWithoutCalendar(r.getData()); return r; } + /** + * 过滤无未来日历的产品。无日历则无法计价/占库存,列表展示无意义。 + */ + private void hideProductsWithoutCalendar(SmHotelDetailVO vo) { + if (vo == null || vo.getHotel() == null || vo.getHotel().getId() == null + || vo.getProducts() == null || vo.getProducts().isEmpty()) { + return; + } + List rows = Db.lambdaQuery(SmHotelDailyEntity.class) + .eq(SmHotelDailyEntity::getHotelId, vo.getHotel().getId()) + .ge(SmHotelDailyEntity::getStayDate, LocalDate.now()) + .select(SmHotelDailyEntity::getProductCode) + .list(); + Set withDaily = rows.stream() + .map(SmHotelDailyEntity::getProductCode) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toCollection(LinkedHashSet::new)); + vo.setProducts(vo.getProducts().stream() + .filter(p -> p != null && withDaily.contains(p.getProductCode())) + .collect(Collectors.toList())); + } + @Operation(summary = "酒店日历分页") @GetMapping("/detail/{id}/daily") public R> daily(@PathVariable Long id, @@ -218,6 +243,8 @@ public class SmHotelAppController { Map req = body == null ? new HashMap<>() : new HashMap<>(body); // 渠道单号绑定用户 ID:APP{userId}{ts},列表/详情/取消均按此前缀认主(与用户名无关) req.put("channelNo", "APP" + user.getId() + System.currentTimeMillis()); + // 供待支付幂等复用:同用户同房同日期未付单直接返回,避免连点重复创单 + req.put("appUserId", user.getId()); if (StrUtil.isBlank(str(req.get("contactName")))) { req.put("contactName", StrUtil.blankToDefault(user.getName(), user.getUsername())); } @@ -287,9 +314,12 @@ public class SmHotelAppController { row.put("sellAmount", o.getSellAmount()); row.put("orderStatus", o.getOrderStatus()); row.put("bookStatus", o.getBookStatus()); + row.put("payStatus", o.getPayStatus()); row.put("contactName", o.getContactName()); row.put("contactPhone", o.getContactPhone()); row.put("createTime", o.getCreateTime()); + row.put("payExpireTime", orderService.calcAppPayExpireTime(o)); + row.put("remainSeconds", orderService.calcAppPayRemainSeconds(o)); return row; }).toList()); return R.ok(out); @@ -309,6 +339,12 @@ public class SmHotelAppController { if (!orderService.isOwnedByAppUser(order, user)) { return R.failed("无权查看该订单"); } + // 详情懒清理超时未付 + orderService.cancelIfAppPayExpired(order); + order = orderService.getDetail(id); + if (order == null) { + return R.failed("订单不存在"); + } Map row = new LinkedHashMap<>(); row.put("id", order.getId()); row.put("channelNo", order.getChannelNo()); @@ -323,10 +359,13 @@ public class SmHotelAppController { row.put("sellAmount", order.getSellAmount()); row.put("orderStatus", order.getOrderStatus()); row.put("bookStatus", order.getBookStatus()); + row.put("payStatus", order.getPayStatus()); row.put("contactName", order.getContactName()); row.put("contactPhone", order.getContactPhone()); row.put("createTime", order.getCreateTime()); row.put("failMsg", order.getFailMsg()); + row.put("payExpireTime", orderService.calcAppPayExpireTime(order)); + row.put("remainSeconds", orderService.calcAppPayRemainSeconds(order)); if (order.getHotelId() != null) { SmHotelEntity hotel = hotelService.getById(order.getHotelId()); if (hotel != null) { @@ -334,6 +373,7 @@ public class SmHotelAppController { row.put("coverImage", hotel.getCoverImage()); row.put("address", hotel.getAddress()); row.put("phone", hotel.getPhone()); + row.put("hotelDeptId", hotel.getDeptId()); } if (StrUtil.isNotBlank(order.getProductCode())) { SmHotelProductEntity p = Db.lambdaQuery(SmHotelProductEntity.class) diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java index 0324129d9..d7ea715ce 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java @@ -3,6 +3,7 @@ package com.bsd.pjcloud.supermarket.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity; import com.bsd.pjcloud.supermarket.service.SmHotelOrderService; +import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.log.annotation.SysLog; import io.swagger.v3.oas.annotations.Operation; @@ -33,11 +34,15 @@ public class SmHotelOrderController { return R.ok(orderService.pageOrders(page, query)); } - @Operation(summary = "订单详情") + @Operation(summary = "订单详情(含酒店/产品/日历)") @GetMapping("/{id}") @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view')") - public R detail(@PathVariable Long id) { - return R.ok(orderService.getDetail(id)); + public R detail(@PathVariable Long id) { + SmHotelOrderDetailVo vo = orderService.getAdminDetail(id); + if (vo == null || vo.getOrder() == null) { + return R.failed("订单不存在"); + } + return R.ok(vo); } @SysLog("后台取消酒店订单") @@ -49,6 +54,15 @@ public class SmHotelOrderController { return orderService.cancelByAdmin(id, reason); } + @SysLog("后台退款酒店订单") + @Operation(summary = "后台退款:已支付订单原路退款并关闭预订") + @PostMapping("/refund/{id}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel')") + public R refund(@PathVariable Long id, @RequestBody(required = false) Map body) { + String reason = body == null ? null : String.valueOf(body.getOrDefault("reason", "后台退款")); + return orderService.refundByAdmin(id, reason); + } + @SysLog("后台试下单(调上游)") @Operation(summary = "后台试下单:校验可定后调上游 order/add") @PostMapping("/create") diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderInnerController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderInnerController.java index 2af7f6657..d40aa407b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderInnerController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderInnerController.java @@ -37,4 +37,11 @@ public class SmHotelOrderInnerController { public R paySuccess(@RequestParam("orderId") Long orderId) { return R.ok(orderService.onAppPaySuccess(orderId)); } + + @Inner + @Operation(summary = "退款成功后置") + @PostMapping("/refund-success") + public R refundSuccess(@RequestParam("orderId") Long orderId) { + return R.ok(orderService.onAppRefundSuccess(orderId)); + } } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java index c294c387c..9881f0c30 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java @@ -1,6 +1,8 @@ package com.bsd.pjcloud.supermarket.job; +import com.bsd.pjcloud.common.data.tenant.TenantBroker; import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties; +import com.bsd.pjcloud.supermarket.service.SmHotelOrderService; import com.bsd.pjcloud.supermarket.service.SmHotelSyncService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,6 +24,8 @@ public class SmHotelShangyouSpringSchedule { private final SmHotelSyncService syncService; + private final SmHotelOrderService hotelOrderService; + @Scheduled(cron = "${shangyou-hotel.hotel-list-daily-cron:0 0 2 * * ?}") public void scheduledListSync() { if (!properties.isSpringScheduleEnabled()) { @@ -72,4 +76,20 @@ public class SmHotelShangyouSpringSchedule { log.error("|酒店日更|Spring 本地滚加异常", e); } } + + /** + * App 待支付超时清理:默认每分钟扫一次,取消过期单并释放本地预占库存。 + * 不依赖 spring-schedule-enabled(支付超时与日更无关)。 + * 租户与订单补偿 Worker 一致,走 callback-tenant-id。 + */ + @Scheduled(fixedDelayString = "${shangyou-hotel.app-pay-expire-scan-ms:60000}") + public void scheduledCancelExpiredAppUnpaid() { + try { + Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId(); + TenantBroker.runAs(tenantId, tid -> hotelOrderService.cancelExpiredAppUnpaidOrders(100)); + } + catch (Exception e) { + log.error("|酒店待支付超时|定时清理异常", e); + } + } } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java index b07e57666..3669d01f7 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java @@ -4,9 +4,11 @@ import cn.hutool.json.JSONObject; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.common.security.service.PjcloudUser; import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity; +import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo; import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo; import com.bsd.pjcloud.common.core.util.R; +import java.time.LocalDateTime; import java.util.Map; public interface SmHotelOrderService { @@ -21,6 +23,9 @@ public interface SmHotelOrderService { SmHotelOrderEntity getDetail(Long id); + /** PC 后台详情:订单 + 酒店基础信息 + 产品 + 入住日历 + 入住人 */ + SmHotelOrderDetailVo getAdminDetail(Long id); + /** 下游回传下单 → 可定校验 → 上游创建 → 必要时挂补偿 */ R createFromDownstream(String appKey, Map body); @@ -31,18 +36,48 @@ public interface SmHotelOrderService { R createByAdmin(Map body); /** - * App 待支付创单:只落业务订单,不扣本地库存、不调上游;支付成功后再履约。 + * App 待支付创单:落业务订单;本地酒店预占日历库存,上游酒店此时不调上游。 + * 支付成功后再确认本地单 / 调上游;超时未付自动取消并释放预占。 */ R createAppPendingOrder(Map body); - /** 供 thirdparty 拉起支付前校验 */ + /** 供 thirdparty 拉起支付前校验(含支付超时自动取消) */ SmHotelOrderPayInfoVo getPayInfo(Long orderId); /** - * App 支付成功履约。本地酒店只确认库存;上游酒店再调上游下单。 + * App 支付成功履约。本地酒店确认已预占库存;上游酒店再调上游下单。 */ Boolean onAppPaySuccess(Long orderId); + /** + * 第三方退款成功后置(幂等):关闭预订并标记已退款。 + */ + Boolean onAppRefundSuccess(Long orderId); + + /** + * PC 后台对已支付订单发起原路退款,并关闭预订。 + */ + R refundByAdmin(Long id, String reason); + + /** + * 扫描并取消已超时的 App 待支付订单(释放本地预占库存)。 + * + * @return 本次取消条数 + */ + int cancelExpiredAppUnpaidOrders(int limit); + + /** + * 若仍为待支付且已超时则取消;返回是否已取消。 + * 用于列表/详情懒清理。 + */ + boolean cancelIfAppPayExpired(SmHotelOrderEntity order); + + /** App 待支付剩余秒数;非待支付返回 null */ + Long calcAppPayRemainSeconds(SmHotelOrderEntity order); + + /** App 待支付截止时间;非待支付返回 null */ + LocalDateTime calcAppPayExpireTime(SmHotelOrderEntity order); + /** 下游回传取消 → 调上游取消 */ R cancelFromDownstream(String appKey, Map body); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java index b10faafe5..af7e82ba4 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java @@ -16,11 +16,15 @@ import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouOrderCallResult; import com.bsd.pjcloud.supermarket.mapper.*; import com.bsd.pjcloud.supermarket.service.SmHotelOrderService; import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService; +import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo; import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.tenant.TenantBroker; import com.bsd.pjcloud.common.data.tenant.TenantContextHolder; import com.bsd.pjcloud.common.security.service.PjcloudUser; +import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdAppPlusPayService; +import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderBizTypeEnum; +import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdRemoterRefundDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -28,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -63,6 +68,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl pageOrders(Page page, SmHotelOrderEntity query) { Page result = baseMapper.selectPage(page, Wrappers.lambdaQuery() @@ -112,6 +124,12 @@ public class SmHotelOrderServiceImpl extends ServiceImpllambdaQuery() + .eq(SmHotelEntity::getHotelCode, order.getHotelCode()) + .last("limit 1")); + } + if (hotel != null) { + SmHotelOrderDetailVo.HotelBrief hb = new SmHotelOrderDetailVo.HotelBrief(); + hb.setId(hotel.getId()); + hb.setHotelCode(hotel.getHotelCode()); + hb.setHotelName(hotel.getHotelName()); + hb.setGroupCode(hotel.getGroupCode()); + hb.setDataSource(SmHotelDataSource.isLocal(hotel.getSheetName()) ? "local" : "shangyou"); + hb.setUpstreamStatus(hotel.getUpstreamStatus()); + hb.setCoverImage(hotel.getCoverImage()); + hb.setAddress(hotel.getAddress()); + hb.setDistrict(hotel.getDistrict()); + hb.setBizArea(hotel.getBizArea()); + hb.setStarLevel(hotel.getStarLevel()); + hb.setScore(hotel.getScore()); + hb.setPhone(hotel.getPhone()); + hb.setIntro(hotel.getIntro()); + hb.setFacilityTags(hotel.getFacilityTags()); + hb.setLongitude(hotel.getLongitude()); + hb.setLatitude(hotel.getLatitude()); + hb.setDeptId(hotel.getDeptId()); + vo.setHotel(hb); + if (StrUtil.isBlank(order.getHotelName())) { + order.setHotelName(hotel.getHotelName()); + } + } + + SmHotelProductEntity product = null; + Long hotelId = hotel != null ? hotel.getId() : order.getHotelId(); + if (StrUtil.isNotBlank(order.getProductCode())) { + product = productMapper.selectOne(Wrappers.lambdaQuery() + .eq(hotelId != null, SmHotelProductEntity::getHotelId, hotelId) + .eq(SmHotelProductEntity::getProductCode, order.getProductCode()) + .last("limit 1")); + } + if (product != null) { + SmHotelOrderDetailVo.ProductBrief pb = new SmHotelOrderDetailVo.ProductBrief(); + pb.setId(product.getId()); + pb.setProductCode(product.getProductCode()); + pb.setProductName(product.getProductName()); + pb.setRoomTypeCode(product.getRoomTypeCode()); + pb.setPriceCode(product.getPriceCode()); + pb.setCancelCode(product.getCancelCode()); + pb.setMealType(product.getMealType()); + pb.setMealCount(product.getMealCount()); + pb.setCostPrice(product.getCostPrice()); + pb.setSellPrice(product.getSellPrice()); + pb.setStock(product.getStock()); + pb.setCanCancel(product.getCanCancel()); + pb.setCancelRule(product.getCancelRule()); + pb.setProductType(product.getProductType()); + pb.setApartmentType(product.getApartmentType()); + vo.setProduct(pb); + order.setProductName(product.getProductName()); + } + + String roomTypeCode = StrUtil.blankToDefault(order.getRoomTypeCode(), + product != null ? product.getRoomTypeCode() : null); + if (hotelId != null && StrUtil.isNotBlank(roomTypeCode)) { + SmHotelRoomEntity room = roomMapper.selectOne(Wrappers.lambdaQuery() + .eq(SmHotelRoomEntity::getHotelId, hotelId) + .eq(SmHotelRoomEntity::getRoomTypeCode, roomTypeCode) + .last("limit 1")); + if (room != null) { + vo.setRoomTypeName(room.getRoomTypeName()); + } + } + + if (hotelId != null && StrUtil.isNotBlank(order.getProductCode()) && order.getCheckInDate() != null + && order.getCheckOutDate() != null) { + List dailies = dailyMapper.selectList(Wrappers.lambdaQuery() + .eq(SmHotelDailyEntity::getHotelId, hotelId) + .eq(SmHotelDailyEntity::getProductCode, order.getProductCode()) + .ge(SmHotelDailyEntity::getStayDate, order.getCheckInDate()) + .lt(SmHotelDailyEntity::getStayDate, order.getCheckOutDate()) + .orderByAsc(SmHotelDailyEntity::getStayDate)); + for (SmHotelDailyEntity d : dailies) { + int stock = d.getStock() == null ? 0 : d.getStock(); + int sold = d.getSold() == null ? 0 : d.getSold(); + d.setRemainStock(Math.max(0, stock - sold)); + } + vo.setDailyList(dailies); + } + else { + vo.setDailyList(List.of()); + } + return vo; + } + + /** 详情补产品名称,便于后台抽屉展示。 */ + private void fillProductName(SmHotelOrderEntity order) { + if (order == null || StrUtil.isBlank(order.getProductCode())) { + return; + } + SmHotelProductEntity product = productMapper.selectOne(Wrappers.lambdaQuery() + .eq(order.getHotelId() != null, SmHotelProductEntity::getHotelId, order.getHotelId()) + .eq(SmHotelProductEntity::getProductCode, order.getProductCode()) + .last("limit 1")); + if (product != null) { + order.setProductName(product.getProductName()); + } + } + + private Object parseJsonLenient(String raw) { + if (StrUtil.isBlank(raw)) { + return null; + } + try { + return JSONUtil.parse(raw); + } + catch (Exception e) { + return raw; + } } @Override @@ -358,6 +517,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 30) { + return R.failed("连住最多30晚,请缩短离店日期"); + } String contactName = str(body.get("contactName")); String contactPhone = str(body.get("contactPhone")); if (StrUtil.hasBlank(contactName, contactPhone)) { @@ -467,7 +629,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 30) { + return R.failed("连住最多30晚,请缩短离店日期"); + } String contactName = str(body.get("contactName")); String contactPhone = str(body.get("contactPhone")); if (StrUtil.hasBlank(contactName, contactPhone)) { return R.failed("联系人姓名和手机号不能为空"); } + if (!contactPhone.matches("^1\\d{10}$")) { + return R.failed("手机号格式不正确"); + } String channelNo = str(body.get("channelNo")); if (StrUtil.isBlank(channelNo)) { @@ -535,18 +708,51 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 10) { + return R.failed("单次预订间数不能超过10间"); + } + + // 同用户 + 酒店 + 房型 + 入离店 + 间数:复用未过期待支付单,避免支付失败连点刷出多单 + Long appUserId = null; + Object appUserIdObj = body.get("appUserId"); + if (appUserIdObj != null && StrUtil.isNotBlank(String.valueOf(appUserIdObj))) { + try { + appUserId = Long.valueOf(String.valueOf(appUserIdObj)); + } + catch (Exception ignored) { + appUserId = null; + } + } + if (appUserId == null) { + appUserId = parseAppUserId(channelNo); + } + if (appUserId != null) { + SmHotelOrderEntity pending = findReusableAppUnpaidOrder(appUserId, hotel.getId(), productCode, checkIn, + checkOut, count); + if (pending != null) { + cancelIfAppPayExpired(pending); + pending = baseMapper.selectById(pending.getId()); + if (pending != null && isAppUnpaidPending(pending)) { + // 刷新联系人信息(仍同一待支付单) + pending.setContactName(contactName); + pending.setContactPhone(contactPhone); + baseMapper.updateById(pending); + return R.ok(buildAppPendingOut(pending, hotel, product), "已复用待支付订单"); + } + } + } SmHotelOrderEntity order = new SmHotelOrderEntity(); order.setDownstreamAppId(null); @@ -587,7 +793,13 @@ public class SmHotelOrderServiceImpl extends ServiceImpllambdaQuery() + .likeRight(SmHotelOrderEntity::getChannelNo, "APP" + appUserId) + .eq(SmHotelOrderEntity::getHotelId, hotelId) + .eq(SmHotelOrderEntity::getProductCode, productCode) + .eq(SmHotelOrderEntity::getCheckInDate, checkIn) + .eq(SmHotelOrderEntity::getCheckOutDate, checkOut) + .eq(SmHotelOrderEntity::getCount, count) + .eq(SmHotelOrderEntity::getPayStatus, PAY_UNPAID) + .eq(SmHotelOrderEntity::getOrderStatus, "CREATED") + .orderByDesc(SmHotelOrderEntity::getCreateTime) + .last("limit 1")); + } + + private JSONObject buildAppPendingOut(SmHotelOrderEntity order, SmHotelEntity hotel, SmHotelProductEntity product) { + JSONObject out = buildOut(order); + if (hotel != null) { + out.set("hotelName", hotel.getHotelName()); + out.set("hotelDeptId", hotel.getDeptId()); + } + if (product != null) { + out.set("productName", product.getProductName()); + } + out.set("sellAmount", order.getSellAmount()); + out.set("amountFen", toAmountFen(order.getSellAmount())); + out.set("payExpireTime", calcAppPayExpireTime(order)); + out.set("remainSeconds", calcAppPayRemainSeconds(order)); + return out; + } + @Override public SmHotelOrderPayInfoVo getPayInfo(Long orderId) { SmHotelOrderPayInfoVo vo = new SmHotelOrderPayInfoVo(); @@ -634,6 +883,13 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 0; + && vo.getAmountFen() > 0 + && remain != null + && remain > 0; vo.setPayable(payable); return vo; } + @Override + public int cancelExpiredAppUnpaidOrders(int limit) { + int batch = limit < 1 ? 50 : Math.min(limit, 200); + int minutes = appPayTimeoutMinutes(); + LocalDateTime deadline = LocalDateTime.now().minusMinutes(minutes); + List list = baseMapper.selectList(Wrappers.lambdaQuery() + .eq(SmHotelOrderEntity::getPayStatus, PAY_UNPAID) + .eq(SmHotelOrderEntity::getOrderStatus, "CREATED") + .likeRight(SmHotelOrderEntity::getChannelNo, "APP") + .lt(SmHotelOrderEntity::getCreateTime, deadline) + .orderByAsc(SmHotelOrderEntity::getCreateTime) + .last("limit " + batch)); + int n = 0; + for (SmHotelOrderEntity order : list) { + R r = cancelByAdmin(order.getId(), "支付超时自动取消"); + if (r != null && r.isOk()) { + n++; + } + } + if (n > 0) { + log.info("|酒店待支付超时|自动取消|count={}|timeoutMinutes={}|", n, minutes); + } + return n; + } + + @Override + public boolean cancelIfAppPayExpired(SmHotelOrderEntity order) { + if (order == null || order.getId() == null) { + return false; + } + if (!isAppUnpaidPending(order)) { + return false; + } + Long remain = calcAppPayRemainSeconds(order); + if (remain == null || remain > 0) { + return false; + } + R r = cancelByAdmin(order.getId(), "支付超时自动取消"); + if (r != null && r.isOk()) { + order.setOrderStatus("CANCELLED"); + order.setFailMsg("支付超时自动取消"); + return true; + } + return false; + } + + @Override + public Long calcAppPayRemainSeconds(SmHotelOrderEntity order) { + if (!isAppUnpaidPending(order) || order.getCreateTime() == null) { + return null; + } + LocalDateTime expire = order.getCreateTime().plusMinutes(appPayTimeoutMinutes()); + long sec = Duration.between(LocalDateTime.now(), expire).getSeconds(); + return Math.max(0L, sec); + } + + @Override + public LocalDateTime calcAppPayExpireTime(SmHotelOrderEntity order) { + if (!isAppUnpaidPending(order) || order.getCreateTime() == null) { + return null; + } + return order.getCreateTime().plusMinutes(appPayTimeoutMinutes()); + } + + private boolean isAppUnpaidPending(SmHotelOrderEntity order) { + return order != null + && PAY_UNPAID.equals(order.getPayStatus()) + && "CREATED".equals(order.getOrderStatus()); + } + + private int appPayTimeoutMinutes() { + int minutes = shangyouProperties == null ? 10 : shangyouProperties.getAppPayTimeoutMinutes(); + return minutes > 0 ? minutes : 10; + } + /** * 支付成功履约:本地酒店扣日历库存确认;上游酒店再调上游下单。幂等可重入。 */ @@ -785,28 +1119,139 @@ public class SmHotelOrderServiceImpl extends ServiceImpl missing = new ArrayList<>(); + List closed = new ArrayList<>(); + List shortStock = new ArrayList<>(); for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1)) { SmHotelDailyEntity row = dailyMapper.selectOne(Wrappers.lambdaQuery() .eq(SmHotelDailyEntity::getProductId, productId) .eq(SmHotelDailyEntity::getStayDate, d) .last("limit 1")); if (row == null) { - return "本地库存不足:缺少日期 " + d + " 的日历,请先在后台维护本地房价房量"; + missing.add(d); + continue; } if (!Integer.valueOf(1).equals(row.getDailyStatus())) { - return "本地库存不足:日期 " + d + " 已关房"; + closed.add(d); + continue; } int stock = row.getStock() == null ? 0 : row.getStock(); int sold = row.getSold() == null ? 0 : row.getSold(); if (stock - sold < need) { - return "本地库存不足:日期 " + d + " 可售 " + Math.max(0, stock - sold) + ",需要 " + need; + shortStock.add(d + " 可售" + Math.max(0, stock - sold)); } } - return null; + if (missing.isEmpty() && closed.isEmpty() && shortStock.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder("所选入住区间不可订"); + if (!missing.isEmpty()) { + sb.append(":缺少日历 ").append(formatDateList(missing)); + } + if (!closed.isEmpty()) { + sb.append(missing.isEmpty() ? ":" : ";").append("已关房 ").append(formatDateList(closed)); + } + if (!shortStock.isEmpty()) { + sb.append(missing.isEmpty() && closed.isEmpty() ? ":" : ";") + .append("库存不足 ").append(String.join("、", shortStock)); + } + String hint = buildAvailableDatesHint(productId, need); + if (StrUtil.isNotBlank(hint)) { + sb.append("。").append(hint); + } + else { + sb.append("。请更换入住日期或联系商家维护房价房量"); + } + return sb.toString(); + } + + /** 汇总近期可住日期,便于用户改期。 */ + private String buildAvailableDatesHint(Long productId, int need) { + if (productId == null) { + return ""; + } + int rooms = need < 1 ? 1 : need; + LocalDate from = LocalDate.now(); + LocalDate to = from.plusDays(60); + List rows = dailyMapper.selectList(Wrappers.lambdaQuery() + .eq(SmHotelDailyEntity::getProductId, productId) + .ge(SmHotelDailyEntity::getStayDate, from) + .lt(SmHotelDailyEntity::getStayDate, to) + .eq(SmHotelDailyEntity::getDailyStatus, 1) + .orderByAsc(SmHotelDailyEntity::getStayDate)); + List ok = new ArrayList<>(); + for (SmHotelDailyEntity row : rows) { + if (row.getStayDate() == null) { + continue; + } + int stock = row.getStock() == null ? 0 : row.getStock(); + int sold = row.getSold() == null ? 0 : row.getSold(); + if (stock - sold >= rooms) { + ok.add(row.getStayDate()); + } + } + if (ok.isEmpty()) { + return "该房型近60天暂无可住日期"; + } + return "近期可住:" + formatDateRanges(ok); + } + + private static String formatDateList(List dates) { + if (dates == null || dates.isEmpty()) { + return ""; + } + int limit = Math.min(dates.size(), 8); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < limit; i++) { + if (i > 0) { + sb.append("、"); + } + sb.append(dates.get(i)); + } + if (dates.size() > limit) { + sb.append("等").append(dates.size()).append("天"); + } + return sb.toString(); + } + + /** 将连续日期压成区间文案,如 2026-03-01~03、2026-03-05 */ + private static String formatDateRanges(List dates) { + if (dates == null || dates.isEmpty()) { + return ""; + } + List parts = new ArrayList<>(); + LocalDate start = dates.get(0); + LocalDate prev = start; + for (int i = 1; i < dates.size(); i++) { + LocalDate cur = dates.get(i); + if (prev.plusDays(1).equals(cur)) { + prev = cur; + continue; + } + parts.add(formatRangePart(start, prev)); + if (parts.size() >= 6) { + parts.add("…"); + return String.join("、", parts); + } + start = cur; + prev = cur; + } + parts.add(formatRangePart(start, prev)); + return String.join("、", parts); + } + + private static String formatRangePart(LocalDate start, LocalDate end) { + if (start.equals(end)) { + return start.toString(); + } + if (start.getYear() == end.getYear() && start.getMonth() == end.getMonth()) { + return start + "~" + end.getDayOfMonth(); + } + return start + "~" + end; } /** - * 本地酒店下单:按入住夜 CAS 增加 sold,成功则本地确认,不调上游。 + * 本地酒店确认:若待支付阶段已预占库存则不再二次扣减,只落确认状态。 */ private R finishLocalOrder(SmHotelOrderEntity order, SmHotelProductEntity product) { if (product == null || product.getId() == null) { @@ -817,19 +1262,22 @@ public class SmHotelOrderServiceImpl extends ServiceImpllambdaQuery() - .eq(SmHotelDailyEntity::getProductId, productId) - .eq(SmHotelDailyEntity::getStayDate, d) - .last("limit 1")); - if (row == null) { - return "本地库存不足:缺少日期 " + d + " 的日历"; - } - if (!Integer.valueOf(1).equals(row.getDailyStatus())) { - return "本地库存不足:日期 " + d + " 已关房"; - } int updated = dailyMapper.increaseSoldIfEnough(productId, d, rooms); if (updated == 0) { - int stock = row.getStock() == null ? 0 : row.getStock(); - int sold = row.getSold() == null ? 0 : row.getSold(); - return "本地库存不足:日期 " + d + " 可售 " + Math.max(0, stock - sold) + ",需要 " + rooms; + SmHotelDailyEntity row = dailyMapper.selectOne(Wrappers.lambdaQuery() + .eq(SmHotelDailyEntity::getProductId, productId) + .eq(SmHotelDailyEntity::getStayDate, d) + .last("limit 1")); + int stock = row == null || row.getStock() == null ? 0 : row.getStock(); + int sold = row == null || row.getSold() == null ? 0 : row.getSold(); + String hint = buildAvailableDatesHint(productId, rooms); + return "所选入住区间不可订:日期 " + d + " 可售 " + Math.max(0, stock - sold) + ",需要 " + rooms + + (StrUtil.isNotBlank(hint) ? "。" + hint : ""); } } return null; @@ -1339,9 +1788,12 @@ public class SmHotelOrderServiceImpl extends ServiceImpl refundByAdmin(Long id, String reason) { + SmHotelOrderEntity order = baseMapper.selectById(id); + if (order == null) { + return R.failed("订单不存在"); + } + if (PAY_REFUNDED.equals(order.getPayStatus())) { + return R.ok(true, "订单已退款"); + } + if (!PAY_PAID.equals(order.getPayStatus())) { + return R.failed("仅已支付订单可退款"); + } + if (order.getSellAmount() == null || order.getSellAmount().compareTo(BigDecimal.ZERO) <= 0) { + return R.failed("订单金额异常,无法退款"); + } + int refundFen = order.getSellAmount().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + .intValue(); + if (refundFen <= 0) { + return R.failed("退款金额无效"); + } + ThirdRemoterRefundDto refundDto = new ThirdRemoterRefundDto(); + refundDto.setOrderId(order.getId()); + // 整单退款用业务订单 ID 作售后幂等号,避免重复发起渠道退款 + refundDto.setAfterSaleId(order.getId()); + refundDto.setBizType(ThirdPayOrderBizTypeEnum.HOTEL_ORDER_PAY); + refundDto.setPrice(refundFen); + R refundRes; + try { + refundRes = remoterThirdAppPlusPayService.remoterRefund(refundDto); + } + catch (Exception e) { + log.error("|酒店订单退款|调用第三方退款异常|orderId={}|", id, e); + return R.failed("退款失败: " + e.getMessage()); + } + if (refundRes == null || !refundRes.isOk()) { + return R.failed(refundRes == null ? "退款失败" : StrUtil.blankToDefault(refundRes.getMsg(), "退款失败")); + } + // 渠道受理后关闭预订并落已退款(回调后置也会幂等执行) + Boolean closed = onAppRefundSuccess(id, StrUtil.blankToDefault(reason, "后台退款")); + if (!Boolean.TRUE.equals(closed)) { + return R.failed("支付已退款,但关闭预订失败,请人工核对"); + } + return R.ok(true, "退款已受理"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean onAppRefundSuccess(Long orderId) { + return onAppRefundSuccess(orderId, "退款成功关闭预订"); + } + + /** + * 退款成功后置:关闭预订、回补库存、标记已退款(可重复调用)。 + */ + private Boolean onAppRefundSuccess(Long orderId, String reason) { + SmHotelOrderEntity order = baseMapper.selectById(orderId); + if (order == null) { + return false; + } + if (PAY_REFUNDED.equals(order.getPayStatus()) && "CANCELLED".equals(order.getOrderStatus())) { + return true; + } + // 先尽量关闭预订;已取消则跳过 + if (!"CANCELLED".equals(order.getOrderStatus()) + && !ShangyouBookStatus.X.equalsIgnoreCase(order.getBookStatus())) { + R cancelR = cancelByAdmin(orderId, reason); + if (cancelR == null || !cancelR.isOk()) { + log.warn("|酒店订单退款|关闭预订失败|orderId={}|msg={}|", orderId, + cancelR == null ? "null" : cancelR.getMsg()); + return false; + } + order = baseMapper.selectById(orderId); + if (order == null) { + return false; + } + } + order.setPayStatus(PAY_REFUNDED); + order.setOrderStatus("CANCELLED"); + order.setCompensateStatus(COMP_DONE); + order.setNextCompensateTime(null); + if (StrUtil.isNotBlank(reason)) { + order.setFailMsg(StrUtil.sub(reason, 0, 200)); + } + baseMapper.updateById(order); + return true; + } + private JSONObject buildOut(SmHotelOrderEntity order) { JSONObject out = new JSONObject(); out.set("channelNo", order.getChannelNo()); diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAppPlusServiceImpl.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAppPlusServiceImpl.java index c0eed75b5..9a518fa37 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAppPlusServiceImpl.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAppPlusServiceImpl.java @@ -1752,6 +1752,14 @@ public class ThirdAppPlusServiceImpl implements ThirdAppPlusService { log.error("|APP支付-购买商品(有订单)|刷新退款聚合状态异常", e); return false; } + } else if (ObjectUtil.equals(thirdPayOrder.getBizType(), ThirdPayOrderBizTypeEnum.HOTEL_ORDER_PAY.getType())) { + try { + R result = remoteSmHotelOrderService.refundSuccess(Long.valueOf(thirdPayOrder.getBizId())); + return result != null && result.isOk() && Boolean.TRUE.equals(result.getData()); + } catch (Exception e) { + log.error("|酒店订单支付|处理退款成功回调异常", e); + return false; + } } else { return true; }