小如酒店
This commit is contained in:
1 parent
0a320a2c5a
commit
87a2b76250
15 files changed
+759
-162
No files matched your search
+5
@@ -85,5 +85,10 @@ public interface ErrorCodeConstants {
|
||||
/** 供应方业务拒绝(可订校验/下单等),msg 透传给 C 端 */
|
||||
ErrorCode HOTEL_ZAX_SUPPLIER_BIZ_FAIL = new ErrorCode(1_008_020_006, "{}");
|
||||
ErrorCode HOTEL_ZAX_OFFLINE = new ErrorCode(1_008_020_007, "酒店已下架");
|
||||
/** 用户超半小时取消:需人工审核,不可重复提交 */
|
||||
ErrorCode HOTEL_ZAX_ORDER_CANCEL_APPLY_EXISTS = new ErrorCode(1_008_020_008, "已提交取消申请,请等待人工审核");
|
||||
ErrorCode HOTEL_ZAX_ORDER_CANCEL_NOT_ALLOWED = new ErrorCode(1_008_020_009, "当前订单状态不可取消");
|
||||
/** 异常退款:未支付或已退过 */
|
||||
ErrorCode HOTEL_ZAX_ORDER_ABNORMAL_REFUND_FAIL = new ErrorCode(1_008_020_010, "异常退款失败:{}");
|
||||
|
||||
}
|
||||
+10
@@ -209,6 +209,16 @@ public class HotelZaxController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/order/abnormal-refund")
|
||||
@Operation(summary = "异常退款(已支付且未退时强制原路退款,不依赖可取消状态)")
|
||||
@Parameter(name = "id", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax-order:query')")
|
||||
public CommonResult<Boolean> abnormalRefund(@RequestParam("id") Long id,
|
||||
@RequestParam(value = "reason", required = false) String reason) {
|
||||
hotelZaxAdminService.abnormalRefund(id, reason);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/sync")
|
||||
@Operation(summary = "从资安心开放平台同步到本地(默认全量,适配授权范围变更)")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:sync')")
|
||||
|
||||
+3
@@ -59,5 +59,8 @@ public class HotelZaxOrderRespVO {
|
||||
|
||||
private String failMsg;
|
||||
|
||||
@Schema(description = "本地是否已发起退款")
|
||||
private Boolean localRefunded;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
+45
-29
@@ -53,7 +53,13 @@ public class AppHotelController {
|
||||
if (hotelZaxLocalService.hasLocalData()) {
|
||||
return success(hotelZaxLocalService.pageList(body));
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/list", body)));
|
||||
try {
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/list", body)));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// 供应方未授权等:C 端只展示空列表,不抛配置文案
|
||||
return success(emptyHotelPage(body));
|
||||
}
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
@@ -63,8 +69,42 @@ public class AppHotelController {
|
||||
if (hotelZaxLocalService.hasLocalData()) {
|
||||
return success(hotelZaxLocalService.filterMeta());
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/filter-meta",
|
||||
body == null ? new HashMap<>() : body)));
|
||||
try {
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/filter-meta",
|
||||
body == null ? new HashMap<>() : body)));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Map<String, Object> empty = new HashMap<>(4);
|
||||
empty.put("areas", new java.util.ArrayList<>());
|
||||
empty.put("districts", new java.util.ArrayList<>());
|
||||
empty.put("bizAreas", new java.util.ArrayList<>());
|
||||
empty.put("starLevels", new java.util.ArrayList<>());
|
||||
return success(empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptyHotelPage(Map<String, Object> body) {
|
||||
long current = 1L;
|
||||
long size = 10L;
|
||||
if (body != null) {
|
||||
try {
|
||||
if (body.get("current") != null) {
|
||||
current = Long.parseLong(String.valueOf(body.get("current")));
|
||||
}
|
||||
if (body.get("size") != null) {
|
||||
size = Long.parseLong(String.valueOf(body.get("size")));
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>(8);
|
||||
data.put("records", new java.util.ArrayList<>());
|
||||
data.put("total", 0);
|
||||
data.put("current", current);
|
||||
data.put("size", size);
|
||||
return data;
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
@@ -119,7 +159,7 @@ public class AppHotelController {
|
||||
}
|
||||
|
||||
@PostMapping("/order/cancel")
|
||||
@Operation(summary = "取消订单(已支付则退款,并通知资安心)")
|
||||
@Operation(summary = "取消订单:支付后半小时内直接取消退款;超时则提交取消申请,待后台人工二次确认")
|
||||
public CommonResult<Object> orderCancel(@RequestBody Map<String, Object> body) {
|
||||
String channelNo = body == null ? null : str(body.get("channelNo"));
|
||||
hotelZaxTradeService.cancelByChannelNo(getLoginUserId(), channelNo, "用户取消");
|
||||
@@ -160,31 +200,7 @@ public class AppHotelController {
|
||||
local = hotelZaxTradeService.getByChannelNo(uid, channelNo);
|
||||
}
|
||||
if (local != null) {
|
||||
boolean paid = Boolean.TRUE.equals(local.get("payStatus"));
|
||||
boolean booked = local.get("orderNo") != null && StrUtil.isNotBlank(String.valueOf(local.get("orderNo")));
|
||||
if (paid && booked) {
|
||||
try {
|
||||
Map<String, Object> q = new HashMap<>(2);
|
||||
q.put("channelNo", local.get("channelNo"));
|
||||
Object remote = dataOf(zaxHotelOpenClient.post("order/query", q));
|
||||
if (remote instanceof Map) {
|
||||
Map<?, ?> r = (Map<?, ?>) remote;
|
||||
if (r.get("orderStatus") != null) {
|
||||
local.put("orderStatus", r.get("orderStatus"));
|
||||
local.put("status", r.get("orderStatus"));
|
||||
}
|
||||
if (r.get("bookStatus") != null) {
|
||||
local.put("bookStatus", r.get("bookStatus"));
|
||||
}
|
||||
if (r.get("platformNo") != null) {
|
||||
local.put("platformNo", r.get("platformNo"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// 本地单仍可展示
|
||||
}
|
||||
}
|
||||
// getByChannelNo / getByPayOrderId 内已按需拉取供应方并落库,这里直接返回最新本地视图
|
||||
return success(local);
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("order/query", body == null ? new HashMap<>() : body)));
|
||||
|
||||
+11
-4
@@ -63,20 +63,27 @@ public class HotelZaxOpenCallbackController {
|
||||
|
||||
@PermitAll
|
||||
@PostMapping("/hotel/resource/notify")
|
||||
@Operation(summary = "资源变更回调(验签后异步拉 detail)")
|
||||
@Operation(summary = "资源变更回调(验签后异步拉 detail;DAILY_FULL 则全量拉取)")
|
||||
public CommonResult<Map<String, Object>> resourceNotify(HttpServletRequest request,
|
||||
@RequestBody String rawBody) {
|
||||
callbackAuth.assertValid(request, rawBody);
|
||||
JSONObject body = JSONUtil.parseObj(StrUtil.blankToDefault(rawBody, "{}"));
|
||||
String hotelCode = StrUtil.trim(body.getStr("hotelCode"));
|
||||
String changeType = StrUtil.blankToDefault(body.getStr("changeType"), "HOTEL_UPSERT");
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
boolean dailyFull = "DAILY_FULL".equalsIgnoreCase(changeType) || "FULL_SYNC".equalsIgnoreCase(changeType)
|
||||
|| "*".equals(hotelCode) || "_ALL_".equalsIgnoreCase(hotelCode);
|
||||
if (!dailyFull && StrUtil.isBlank(hotelCode)) {
|
||||
throw new IllegalArgumentException("hotelCode 不能为空");
|
||||
}
|
||||
Long tenantId = openProperties.getCallbackTenantId() == null ? 1L : openProperties.getCallbackTenantId();
|
||||
// 先快速 ACK,再异步同步,避免供应方回调超时
|
||||
ASYNC.execute(() -> TenantUtils.execute(tenantId, () -> {
|
||||
try {
|
||||
if (dailyFull) {
|
||||
Map<String, Object> r = hotelZaxSyncService.syncOnce(true);
|
||||
log.info("|hotel-zax-callback|日更全量拉取完成|{}", r);
|
||||
return;
|
||||
}
|
||||
if ("HOTEL_DELETE".equalsIgnoreCase(changeType) || "HOTEL_OFFLINE".equalsIgnoreCase(changeType)
|
||||
|| "OFFLINE".equalsIgnoreCase(changeType) || "DELETE".equalsIgnoreCase(changeType)) {
|
||||
hotelZaxSyncService.markOffline(hotelCode);
|
||||
@@ -90,8 +97,8 @@ public class HotelZaxOpenCallbackController {
|
||||
}
|
||||
}));
|
||||
Map<String, Object> ack = new LinkedHashMap<>(4);
|
||||
ack.put("hotelCode", hotelCode);
|
||||
ack.put("changeType", changeType);
|
||||
ack.put("hotelCode", dailyFull ? "*" : hotelCode);
|
||||
ack.put("changeType", dailyFull ? "DAILY_FULL" : changeType);
|
||||
ack.put("accepted", true);
|
||||
return success(ack);
|
||||
}
|
||||
|
||||
+70
-28
@@ -21,12 +21,18 @@ import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX
|
||||
|
||||
/**
|
||||
* 调用资安心酒店下游开放 API(服务端代签)。
|
||||
* 网络/5xx 即时重试;业务拒绝(code!=0)不重试。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ZaxHotelOpenClient {
|
||||
|
||||
private static final int INLINE_ATTEMPTS = 3;
|
||||
|
||||
/** 第 1/2/3 次发起前等待:0 → 5s → 15s */
|
||||
private static final long[] INLINE_BACKOFF_MS = { 0L, 5_000L, 15_000L };
|
||||
|
||||
private final ZaxHotelOpenProperties properties;
|
||||
|
||||
public JSONObject post(String apiPath, Map<String, Object> body) {
|
||||
@@ -37,38 +43,74 @@ public class ZaxHotelOpenClient {
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店预订服务配置不完整");
|
||||
}
|
||||
String bodyJson = body == null || body.isEmpty() ? "{}" : JSONUtil.toJsonStr(body);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = sign(properties.getAppKey(), properties.getAppSecret(), timestamp, nonce, bodyJson);
|
||||
String url = trimSlash(properties.getBaseUrl()) + "/" + StrUtil.removePrefix(apiPath, "/");
|
||||
try (HttpResponse resp = HttpRequest.post(url)
|
||||
.header("appKey", properties.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(bodyJson)
|
||||
.setConnectionTimeout(properties.getConnectTimeoutMs())
|
||||
.setReadTimeout(properties.getReadTimeoutMs())
|
||||
.execute()) {
|
||||
String raw = resp.body();
|
||||
if (!resp.isOk()) {
|
||||
log.warn("|zax-hotel|HTTP失败|url={}|status={}|body={}|", url, resp.getStatus(),
|
||||
StrUtil.maxLength(raw, 300));
|
||||
// 业务可读异常,避免被 GlobalExceptionHandler 吞成「系统异常」
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店供应方接口异常,请稍后重试");
|
||||
ServiceException lastBiz = null;
|
||||
Exception lastIo = null;
|
||||
for (int i = 0; i < INLINE_ATTEMPTS; i++) {
|
||||
if (i > 0) {
|
||||
long sleep = INLINE_BACKOFF_MS[Math.min(i, INLINE_BACKOFF_MS.length - 1)];
|
||||
try {
|
||||
Thread.sleep(sleep);
|
||||
}
|
||||
catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店供应方接口异常,请稍后重试");
|
||||
}
|
||||
log.info("|zax-hotel|即时重试|attempt={}|url={}|", i + 1, url);
|
||||
}
|
||||
JSONObject json = JSONUtil.parseObj(raw);
|
||||
// 资安心 R:code=0 成功
|
||||
Integer code = json.getInt("code");
|
||||
if (code != null && code != 0) {
|
||||
String msg = StrUtil.blankToDefault(json.getStr("msg"), "酒店供应方业务失败");
|
||||
log.warn("|zax-hotel|业务失败|url={}|code={}|msg={}|", url, code, StrUtil.maxLength(msg, 200));
|
||||
// 透传供应方文案(库存不足/不可订等),供小程序直接弹出
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, msg);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = sign(properties.getAppKey(), properties.getAppSecret(), timestamp, nonce, bodyJson);
|
||||
try (HttpResponse resp = HttpRequest.post(url)
|
||||
.header("appKey", properties.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(bodyJson)
|
||||
.setConnectionTimeout(properties.getConnectTimeoutMs())
|
||||
.setReadTimeout(properties.getReadTimeoutMs())
|
||||
.execute()) {
|
||||
String raw = resp.body();
|
||||
if (!resp.isOk()) {
|
||||
log.warn("|zax-hotel|HTTP失败|url={}|status={}|attempt={}|body={}|", url, resp.getStatus(),
|
||||
i + 1, StrUtil.maxLength(raw, 300));
|
||||
// 4xx 业务/鉴权问题不重试;5xx/超时类继续重试
|
||||
if (resp.getStatus() >= 400 && resp.getStatus() < 500 && resp.getStatus() != 408
|
||||
&& resp.getStatus() != 429) {
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店供应方接口异常,请稍后重试");
|
||||
}
|
||||
lastIo = new IllegalStateException("HTTP " + resp.getStatus());
|
||||
continue;
|
||||
}
|
||||
JSONObject json = JSONUtil.parseObj(raw);
|
||||
Integer code = json.getInt("code");
|
||||
if (code != null && code != 0) {
|
||||
String msg = StrUtil.blankToDefault(json.getStr("msg"), "酒店供应方业务失败");
|
||||
log.warn("|zax-hotel|业务失败|url={}|code={}|msg={}|", url, code, StrUtil.maxLength(msg, 200));
|
||||
// 业务拒绝不重试
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, msg);
|
||||
}
|
||||
if (i > 0) {
|
||||
log.info("|zax-hotel|即时重试成功|attempt={}|url={}|", i + 1, url);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
catch (ServiceException e) {
|
||||
lastBiz = e;
|
||||
// 业务异常直接抛
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
lastIo = e;
|
||||
log.warn("|zax-hotel|调用异常|url={}|attempt={}|{}", url, i + 1, e.getMessage());
|
||||
}
|
||||
return json;
|
||||
}
|
||||
if (lastBiz != null) {
|
||||
throw lastBiz;
|
||||
}
|
||||
log.warn("|zax-hotel|即时重试耗尽|url={}|last={}|", url, lastIo == null ? null : lastIo.getMessage());
|
||||
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店供应方接口异常,请稍后重试");
|
||||
}
|
||||
|
||||
/** 是否供应方业务拒绝(可订校验失败等),便于调用方决定是否刷新本地日历 */
|
||||
|
||||
+10
@@ -48,4 +48,14 @@ public class ZaxHotelOpenProperties {
|
||||
* 同步详情时顺带拉取日历的天数(含今天)。0=不同步日历;最大 90(开放平台窗口上限)。
|
||||
*/
|
||||
private int dailySyncDays = 90;
|
||||
|
||||
/**
|
||||
* 是否启用每日定时从资安心全量拉取(兜底:对方日更推送丢失时仍能对齐)。
|
||||
*/
|
||||
private boolean dailyPullEnabled = true;
|
||||
|
||||
/**
|
||||
* 每日全量拉取 Cron,默认 03:20(建议晚于资安心开放应用日更推送 02:10)。
|
||||
*/
|
||||
private String dailyPullCron = "0 20 3 * * ?";
|
||||
}
|
||||
+8
-3
@@ -15,6 +15,8 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* 每天从资安心拉取酒店主数据、房型、产品与价态日历到本地。
|
||||
* <p>
|
||||
* 与资安心「开放应用日更推送 DAILY_FULL」互补:推送触发即时全量;本任务作兜底防推送丢失。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -34,10 +36,13 @@ public class HotelZaxDailySyncJob implements JobHandler {
|
||||
return formatResult(full, r);
|
||||
}
|
||||
|
||||
/** 每天 03:20 全量同步(适配授权范围变更;增量水位拉不到新授权酒店) */
|
||||
@Scheduled(cron = "0 20 3 * * ?")
|
||||
/**
|
||||
* 每天定时全量同步(适配授权范围变更;增量水位拉不到新授权酒店)。
|
||||
* Cron 可通过 xrhome.hotel.zax.daily-pull-cron 配置。
|
||||
*/
|
||||
@Scheduled(cron = "${xrhome.hotel.zax.daily-pull-cron:0 20 3 * * ?}")
|
||||
public void scheduleDaily() {
|
||||
if (!openProperties.isEnabled()) {
|
||||
if (openProperties == null || !openProperties.isEnabled() || !openProperties.isDailyPullEnabled()) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = openProperties.getCallbackTenantId() == null ? 1L : openProperties.getCallbackTenantId();
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package cn.iocoder.yudao.module.product.job.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenProperties;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxTradeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 酒店订单状态补偿:主动拉资安心,覆盖对方维护导致回调丢失;关单未退则补退款。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HotelZaxOrderStatusSyncJob implements JobHandler {
|
||||
|
||||
@Resource
|
||||
private HotelZaxTradeService hotelZaxTradeService;
|
||||
|
||||
@Resource
|
||||
private ZaxHotelOpenProperties openProperties;
|
||||
|
||||
@Override
|
||||
@TenantJob
|
||||
public String execute(String param) {
|
||||
int n = hotelZaxTradeService.syncPendingOrders(50);
|
||||
return String.format("酒店订单状态同步 %s 单", n);
|
||||
}
|
||||
|
||||
/** 默认每 2 分钟扫一轮 */
|
||||
@Scheduled(cron = "40 */2 * * * ?")
|
||||
public void schedule() {
|
||||
if (openProperties == null || !openProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = openProperties.getCallbackTenantId() == null ? 1L : openProperties.getCallbackTenantId();
|
||||
TenantUtils.execute(tenantId, () -> {
|
||||
try {
|
||||
int n = hotelZaxTradeService.syncPendingOrders(40);
|
||||
if (n > 0) {
|
||||
log.info("|hotel-status|定时同步|count={}|", n);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-status|定时同步失败|{}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+45
-57
@@ -94,43 +94,8 @@ public class DeviceServiceImpl implements DeviceService {
|
||||
// 传入部门
|
||||
device.setDeptId(getLoginUserDeptId());
|
||||
deviceMapper.insert(device);
|
||||
DispenseRequestBo request = new DispenseRequestBo();
|
||||
request.setDeviceId(createReqVO.getDeviceId());
|
||||
request.setMessage(createReqVO.getProductUrl() + "," + createReqVO.getFileUrl());
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", token);
|
||||
|
||||
// 创建请求实体
|
||||
HttpEntity<DispenseRequestBo> requestEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
// 创建RestTemplate
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
try {
|
||||
// 发送请求并解析响应
|
||||
ResponseEntity<DispenseResponseBo> response = restTemplate.exchange(
|
||||
api_url,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
DispenseResponseBo.class
|
||||
);
|
||||
|
||||
DispenseResponseBo body = response.getBody();
|
||||
if (body != null) {
|
||||
System.out.println("响应结果: " + body);
|
||||
if (body.getCode() == 0) {
|
||||
System.out.println("上传成功!详情: " + body.getData());
|
||||
} else {
|
||||
System.out.println("操作失败!原因: " + body.getMsg());
|
||||
log.error("设备--{}:[图片上传失败]:原因: {}", createReqVO.getDeviceId(), body.getMsg());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("图片上传请求异常: " + e.getMessage());
|
||||
log.error("设备--{}:[图片上传异常]:原因: {}", createReqVO.getDeviceId(), e.getMessage());
|
||||
}
|
||||
// 推送设备屏广告:二维码 + 广告图/视频地址
|
||||
pushDeviceScreenAd(createReqVO.getDeviceId(), createReqVO.getProductUrl(), createReqVO.getFileUrl(), "create");
|
||||
// 返回
|
||||
return device.getId();
|
||||
}
|
||||
@@ -143,44 +108,67 @@ public class DeviceServiceImpl implements DeviceService {
|
||||
checkDeviceDataPermission(device.getId());
|
||||
// 更新
|
||||
DeviceDO updateObj = BeanUtils.toBean(updateReqVO, DeviceDO.class);
|
||||
// 推送设备屏广告:二维码 + 广告图/视频地址
|
||||
pushDeviceScreenAd(updateReqVO.getDeviceId(), updateReqVO.getProductDeviceUrl(), updateReqVO.getFileUrl(), "update");
|
||||
deviceMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用第三方接口,把设备屏广告(二维码 + 文件地址)推到售货机
|
||||
*
|
||||
* @param deviceId 设备 IMEI
|
||||
* @param qrUrl 商品二维码地址
|
||||
* @param fileUrl 广告图/视频地址
|
||||
* @param scene create / update
|
||||
*/
|
||||
private void pushDeviceScreenAd(String deviceId, String qrUrl, String fileUrl, String scene) {
|
||||
DispenseRequestBo request = new DispenseRequestBo();
|
||||
request.setDeviceId(updateReqVO.getDeviceId());
|
||||
request.setMessage(updateReqVO.getProductDeviceUrl() + "," + updateReqVO.getFileUrl());
|
||||
// 设置请求头
|
||||
request.setDeviceId(deviceId);
|
||||
request.setMessage((qrUrl == null ? "" : qrUrl) + "," + (fileUrl == null ? "" : fileUrl));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", token);
|
||||
|
||||
// 创建请求实体
|
||||
HttpEntity<DispenseRequestBo> requestEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
// 创建RestTemplate
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
log.info("[设备屏广告推送-请求] scene={}, url={}, deviceId={}, qrUrl={}, fileUrl={}, message={}, Authorization={}",
|
||||
scene, api_url, deviceId, qrUrl, fileUrl, request.getMessage(), maskToken(token));
|
||||
|
||||
try {
|
||||
// 发送请求并解析响应
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<DispenseResponseBo> response = restTemplate.exchange(
|
||||
api_url,
|
||||
HttpMethod.POST,
|
||||
requestEntity,
|
||||
DispenseResponseBo.class
|
||||
);
|
||||
|
||||
DispenseResponseBo body = response.getBody();
|
||||
if (body != null) {
|
||||
System.out.println("响应结果: " + body);
|
||||
if (body.getCode() == 0) {
|
||||
System.out.println("上传成功!详情: " + body.getData());
|
||||
} else {
|
||||
System.out.println("操作失败!原因: " + body.getMsg());
|
||||
log.error("设备--{}:[图片上传失败]:原因: {}", updateReqVO.getDeviceId(), body.getMsg());
|
||||
}
|
||||
log.info("[设备屏广告推送-响应] scene={}, deviceId={}, httpStatus={}, body={}",
|
||||
scene, deviceId, response.getStatusCodeValue(), body);
|
||||
if (body == null) {
|
||||
log.warn("[设备屏广告推送-响应为空] scene={}, deviceId={}", scene, deviceId);
|
||||
return;
|
||||
}
|
||||
if (body.getCode() == 0) {
|
||||
log.info("[设备屏广告推送-成功] scene={}, deviceId={}, data={}", scene, deviceId, body.getData());
|
||||
} else {
|
||||
log.error("[设备屏广告推送-失败] scene={}, deviceId={}, code={}, msg={}, data={}",
|
||||
scene, deviceId, body.getCode(), body.getMsg(), body.getData());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("图片上传请求异常: " + e.getMessage());
|
||||
log.error("设备--{}:[图片上传异常]:原因: {}", updateReqVO.getDeviceId(), e.getMessage());
|
||||
log.error("[设备屏广告推送-异常] scene={}, deviceId={}, url={}, message={}, err={}",
|
||||
scene, deviceId, api_url, request.getMessage(), e.getMessage(), e);
|
||||
}
|
||||
deviceMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
private String maskToken(String raw) {
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (raw.length() <= 8) {
|
||||
return "****";
|
||||
}
|
||||
return raw.substring(0, 4) + "****" + raw.substring(raw.length() - 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
@@ -53,4 +53,7 @@ public interface HotelZaxAdminService {
|
||||
|
||||
/** 后台取消:已支付原路退款,并通知资安心取消预订 */
|
||||
void cancelOrder(Long id, String reason);
|
||||
|
||||
/** 异常退款:已支付且未退时强制原路退款,不依赖可取消状态 */
|
||||
void abnormalRefund(Long id, String reason);
|
||||
}
|
||||
+65
-4
@@ -598,8 +598,8 @@ public class HotelZaxAdminServiceImpl implements HotelZaxAdminService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台上下架:下架写成 LOCAL_OFFLINE,避免与供应方 OFFLINE、授权外 OUT_OF_SCOPE 混淆;
|
||||
* 同步不会把 LOCAL_OFFLINE 冲回 ACTIVE。
|
||||
* 后台上下架:下架写成 LOCAL_OFFLINE,避免与供应方 OFFLINE、授权外 OUT_OF_SCOPE 混淆。
|
||||
* 下次「同步资安心」若供应方仍为上架,会按供应方状态恢复为 ACTIVE。
|
||||
*/
|
||||
private static String normalizeStatus(String status) {
|
||||
String s = StrUtil.trim(status);
|
||||
@@ -622,7 +622,30 @@ public class HotelZaxAdminServiceImpl implements HotelZaxAdminService {
|
||||
.eqIfPresent(HotelZaxOrderDO::getContactPhone, req.getContactPhone())
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, req.getUserId())
|
||||
.orderByDesc(HotelZaxOrderDO::getId));
|
||||
return BeanUtils.toBean(page, HotelZaxOrderRespVO.class);
|
||||
// 当前页待确认单:主动拉供应方,避免人工确认后列表仍显示旧状态
|
||||
if (page.getList() != null) {
|
||||
for (HotelZaxOrderDO row : page.getList()) {
|
||||
if (row == null || StrUtil.isBlank(row.getChannelNo())) {
|
||||
continue;
|
||||
}
|
||||
String st = StrUtil.trim(row.getOrderStatus());
|
||||
if ("PENDING_CONFIRM".equalsIgnoreCase(st) || "NEED_MANUAL".equalsIgnoreCase(st)
|
||||
|| "PAID".equalsIgnoreCase(st) || "PENDING".equalsIgnoreCase(st)) {
|
||||
hotelZaxTradeService.syncStatusByChannelNo(row.getChannelNo());
|
||||
}
|
||||
}
|
||||
page = orderMapper.selectPage(req, new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.likeIfPresent(HotelZaxOrderDO::getHotelName, req.getHotelName())
|
||||
.eqIfPresent(HotelZaxOrderDO::getHotelCode, req.getHotelCode())
|
||||
.eqIfPresent(HotelZaxOrderDO::getChannelNo, req.getChannelNo())
|
||||
.eqIfPresent(HotelZaxOrderDO::getOrderNo, req.getOrderNo())
|
||||
.eqIfPresent(HotelZaxOrderDO::getOrderStatus, req.getOrderStatus())
|
||||
.eqIfPresent(HotelZaxOrderDO::getPayStatus, req.getPayStatus())
|
||||
.eqIfPresent(HotelZaxOrderDO::getContactPhone, req.getContactPhone())
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, req.getUserId())
|
||||
.orderByDesc(HotelZaxOrderDO::getId));
|
||||
}
|
||||
return toOrderPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -634,7 +657,11 @@ public class HotelZaxAdminServiceImpl implements HotelZaxAdminService {
|
||||
if (order == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_NOT_EXISTS);
|
||||
}
|
||||
return BeanUtils.toBean(order, HotelZaxOrderRespVO.class);
|
||||
if (StrUtil.isNotBlank(order.getChannelNo())) {
|
||||
hotelZaxTradeService.syncStatusByChannelNo(order.getChannelNo());
|
||||
order = orderMapper.selectById(id);
|
||||
}
|
||||
return toOrderVo(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -642,6 +669,40 @@ public class HotelZaxAdminServiceImpl implements HotelZaxAdminService {
|
||||
hotelZaxTradeService.cancelOrder(id, StrUtil.blankToDefault(reason, "后台取消"), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abnormalRefund(Long id, String reason) {
|
||||
hotelZaxTradeService.abnormalRefund(id, StrUtil.blankToDefault(reason, "异常退款"));
|
||||
}
|
||||
|
||||
private PageResult<HotelZaxOrderRespVO> toOrderPage(PageResult<HotelZaxOrderDO> page) {
|
||||
if (page == null) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
List<HotelZaxOrderRespVO> list = page.getList() == null ? Collections.emptyList()
|
||||
: page.getList().stream().map(this::toOrderVo).collect(Collectors.toList());
|
||||
return new PageResult<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
private HotelZaxOrderRespVO toOrderVo(HotelZaxOrderDO order) {
|
||||
HotelZaxOrderRespVO vo = BeanUtils.toBean(order, HotelZaxOrderRespVO.class);
|
||||
if (vo != null) {
|
||||
vo.setLocalRefunded(isLocalRefunded(order));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static boolean isLocalRefunded(HotelZaxOrderDO order) {
|
||||
if (order == null || StrUtil.isBlank(order.getPayloadJson())) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return Boolean.TRUE.equals(JSONUtil.parseObj(order.getPayloadJson()).getBool("localRefunded"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeCsv(String v) {
|
||||
if (StrUtil.isBlank(v)) {
|
||||
return null;
|
||||
|
||||
+25
-27
@@ -85,12 +85,16 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
int offlineOutOfScope = 0;
|
||||
String nextSince = updatedSince;
|
||||
boolean hasMore = true;
|
||||
boolean pullOk = false;
|
||||
// 必须整页拉完(hasMore=false)才允许按授权范围清库,避免中途失败/触顶把大量酒店误标未授权
|
||||
boolean listComplete = false;
|
||||
Set<String> syncedCodes = new HashSet<>();
|
||||
while (hasMore && pageNo <= 40) {
|
||||
// 单页 50,上限覆盖约 2.5 万家;触顶仍不 listComplete
|
||||
final int maxPages = 500;
|
||||
while (hasMore && pageNo <= maxPages) {
|
||||
Map<String, Object> req = new HashMap<>();
|
||||
req.put("current", pageNo);
|
||||
req.put("size", 50);
|
||||
// 授权接口本身只返回上架;此参数保留兼容
|
||||
req.put("includeOffline", true);
|
||||
if (StrUtil.isNotBlank(updatedSince)) {
|
||||
req.put("updatedSince", updatedSince);
|
||||
@@ -98,12 +102,13 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
JSONObject resp = openClient.post("hotel/sync", req);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
log.warn("|hotel-zax-sync|列表页无data|page={}|中止,跳过授权外清理", pageNo);
|
||||
break;
|
||||
}
|
||||
pullOk = true;
|
||||
JSONArray records = data.getJSONArray("records");
|
||||
if (records == null || records.isEmpty()) {
|
||||
hasMore = false;
|
||||
listComplete = true;
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
@@ -143,11 +148,17 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
nextSince = ns;
|
||||
}
|
||||
hasMore = Boolean.TRUE.equals(data.getBool("hasMore"));
|
||||
if (!hasMore) {
|
||||
listComplete = true;
|
||||
}
|
||||
pageNo++;
|
||||
}
|
||||
// 全量且拉取成功后:本地有、当前授权已不含的酒店标记下架(避免改范围后仍显示旧数据)
|
||||
// pullOk=false 时不清理,防止接口异常把本地全下架
|
||||
if (full && pullOk) {
|
||||
if (hasMore && pageNo > maxPages) {
|
||||
log.warn("|hotel-zax-sync|列表分页触顶|pages={}|synced={}|跳过授权外清理,请加大分页或排查",
|
||||
maxPages, syncedCodes.size());
|
||||
}
|
||||
// 仅全量且列表完整拉完后,才把「本次授权结果集没有」的本地酒店标为 OUT_OF_SCOPE
|
||||
if (full && listComplete) {
|
||||
offlineOutOfScope = markOutOfScopeOffline(syncedCodes);
|
||||
if (StrUtil.isBlank(nextSince)) {
|
||||
nextSince = LocalDateTime.now().format(DT);
|
||||
@@ -157,7 +168,7 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
cursor.setLastSyncAt(LocalDateTime.now());
|
||||
cursor.setLastMsg((full ? "full," : "incr,") + "upsert=" + upsert + ",detail=" + detailOk
|
||||
+ ",dailyOk=" + dailyOk + ",dailyRows=" + dailyRows + ",dailyFail=" + dailyFail
|
||||
+ ",offlineOut=" + offlineOutOfScope);
|
||||
+ ",offlineOut=" + offlineOutOfScope + ",listComplete=" + listComplete);
|
||||
cursorMapper.updateById(cursor);
|
||||
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
@@ -271,7 +282,7 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
if (minSell != null) {
|
||||
entity.setMinSellPrice(minSell);
|
||||
}
|
||||
entity.setStatus(resolveSyncStatus(exist == null ? null : exist.getStatus(), row.getStr("status")));
|
||||
entity.setStatus(resolveSyncStatus(row.getStr("status")));
|
||||
entity.setSupplierUpdateTime(parseDt(row.getStr("updateTime")));
|
||||
entity.setSyncedAt(LocalDateTime.now());
|
||||
if (exist == null) {
|
||||
@@ -283,29 +294,16 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步状态合并:
|
||||
* 同步状态(以供应方为准):
|
||||
* - 供应方 OFFLINE → OFFLINE
|
||||
* - 本地 LOCAL_OFFLINE(后台手动下架)不被 ACTIVE 冲掉
|
||||
* - OUT_OF_SCOPE / 历史误标的 OFFLINE:供应方 ACTIVE 时可恢复上架(重新授权场景)
|
||||
* - 其余(ACTIVE/空/未知)→ ACTIVE,覆盖本地 LOCAL_OFFLINE / OUT_OF_SCOPE / 历史 OFFLINE
|
||||
*/
|
||||
private static String resolveSyncStatus(String localStatus, String incomingRaw) {
|
||||
String local = StrUtil.trim(localStatus);
|
||||
private static String resolveSyncStatus(String incomingRaw) {
|
||||
String incoming = StrUtil.trim(incomingRaw);
|
||||
if (isOfflineStatus(incoming)) {
|
||||
return "OFFLINE";
|
||||
}
|
||||
if (isLocalOffline(local)) {
|
||||
return "LOCAL_OFFLINE";
|
||||
}
|
||||
boolean incomingActive = StrUtil.isBlank(incoming) || "ACTIVE".equalsIgnoreCase(incoming);
|
||||
if (incomingActive) {
|
||||
// 重新进入授权范围:OUT_OF_SCOPE 恢复;历史撤权曾写成 OFFLINE 的也可恢复
|
||||
if (isOutOfScope(local) || isOfflineStatus(local)) {
|
||||
return "ACTIVE";
|
||||
}
|
||||
return StrUtil.blankToDefault(local, "ACTIVE");
|
||||
}
|
||||
return StrUtil.blankToDefault(local, "ACTIVE");
|
||||
return "ACTIVE";
|
||||
}
|
||||
|
||||
private static boolean isOfflineStatus(String status) {
|
||||
@@ -359,8 +357,8 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
if (StrUtil.isNotBlank(data.getStr("hotelName"))) {
|
||||
exist.setHotelName(data.getStr("hotelName"));
|
||||
}
|
||||
// 详情同步同样遵守:仅供应方 OFFLINE 可改状态,不把本地下架冲成上架
|
||||
exist.setStatus(resolveSyncStatus(exist.getStatus(), data.getStr("status")));
|
||||
// 详情同步同样以供应方货架为准
|
||||
exist.setStatus(resolveSyncStatus(data.getStr("status")));
|
||||
exist.setSyncedAt(LocalDateTime.now());
|
||||
hotelZaxMapper.updateById(exist);
|
||||
|
||||
|
||||
+19
@@ -49,6 +49,12 @@ public interface HotelZaxTradeService {
|
||||
*/
|
||||
void cancelOrder(Long orderId, String reason, boolean notifySupplier);
|
||||
|
||||
/**
|
||||
* 异常退款:已支付且本地未退款时强制原路退款,不依赖可取消状态,不通知供应方。
|
||||
* 用于状态卡死、取消入口消失、或已取消但钱未退回等运维补救。
|
||||
*/
|
||||
void abnormalRefund(Long orderId, String reason);
|
||||
|
||||
/** 用户按渠道单号取消(校验归属) */
|
||||
void cancelByChannelNo(Long userId, String channelNo, String reason);
|
||||
|
||||
@@ -56,4 +62,17 @@ public interface HotelZaxTradeService {
|
||||
* 资安心订单状态回调:更新本地状态;若已取消且已支付则退款给用户。
|
||||
*/
|
||||
Map<String, Object> applySupplierStatusNotify(Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 主动向供应方查询并落库(待确认等中间态;回调丢失时的补偿)。
|
||||
*/
|
||||
void syncStatusByChannelNo(String channelNo);
|
||||
|
||||
/**
|
||||
* 定时补偿:扫描中间态/已取消未退订单,主动拉供应方状态并补退款。
|
||||
* 对方维护导致回调丢失时的兜底。
|
||||
*
|
||||
* @return 本轮处理条数
|
||||
*/
|
||||
int syncPendingOrders(int limit);
|
||||
}
|
||||
+387
-10
@@ -23,6 +23,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -44,7 +46,10 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.add
|
||||
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_EXISTS;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_HAS_REFUNDING;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_ABNORMAL_REFUND_FAIL;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_CANCEL_APPLY_EXISTS;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_CANCEL_FAIL;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_CANCEL_NOT_ALLOWED;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_NOT_EXISTS;
|
||||
|
||||
@Slf4j
|
||||
@@ -57,6 +62,12 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
|
||||
private static final Duration PAY_EXPIRE = Duration.ofMinutes(10);
|
||||
|
||||
/** 用户自助取消窗口:支付(或下单)后半小时内可直接取消;超时需人工二次确认 */
|
||||
private static final Duration USER_CANCEL_FREE_WINDOW = Duration.ofMinutes(30);
|
||||
|
||||
/** 用户超时取消申请中(待后台确认) */
|
||||
private static final String STATUS_CANCEL_APPLY = "CANCEL_APPLY";
|
||||
|
||||
/** 支付商户单号前缀,避免与 trade_order.id 撞号 */
|
||||
private static final String PAY_MERCHANT_PREFIX = "HZ";
|
||||
|
||||
@@ -275,6 +286,7 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
if (order != null && order.getId() != null) {
|
||||
order = orderMapper.selectById(order.getId());
|
||||
}
|
||||
order = refreshStatusFromSupplier(order);
|
||||
return toView(order);
|
||||
}
|
||||
|
||||
@@ -287,6 +299,7 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
.eq(HotelZaxOrderDO::getChannelNo, channelNo.trim())
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, userId)
|
||||
.last("limit 1"));
|
||||
order = refreshStatusFromSupplier(order);
|
||||
return toView(order);
|
||||
}
|
||||
|
||||
@@ -310,6 +323,7 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
for (HotelZaxOrderDO row : page.getList()) {
|
||||
syncPaidFromPayOrder(row);
|
||||
HotelZaxOrderDO latest = row.getId() == null ? row : orderMapper.selectById(row.getId());
|
||||
latest = refreshStatusFromSupplier(latest == null ? row : latest);
|
||||
list.add(toView(latest == null ? row : latest));
|
||||
}
|
||||
}
|
||||
@@ -356,6 +370,38 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
doCancel(order, StrUtil.blankToDefault(reason, "后台取消"), notifySupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void abnormalRefund(Long orderId, String reason) {
|
||||
if (orderId == null) {
|
||||
throw invalidParamException("订单 id 不能为空");
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_NOT_EXISTS);
|
||||
}
|
||||
if (!Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
throw exception(HOTEL_ZAX_ORDER_ABNORMAL_REFUND_FAIL, "订单未支付,无需退款");
|
||||
}
|
||||
if (order.getPayOrderId() == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_ABNORMAL_REFUND_FAIL, "缺少支付单,无法原路退款");
|
||||
}
|
||||
if (isLocalRefunded(order)) {
|
||||
throw exception(HOTEL_ZAX_ORDER_ABNORMAL_REFUND_FAIL, "本地已标记退款,请勿重复操作");
|
||||
}
|
||||
String msg = StrUtil.blankToDefault(reason, "异常退款");
|
||||
refundIfPaid(order, msg);
|
||||
// 关本地单;不通知供应方,避免状态卡死时被对方拦截
|
||||
String st = StrUtil.trim(order.getOrderStatus());
|
||||
if (!"CANCELLED".equalsIgnoreCase(st) && !"CANCELED".equalsIgnoreCase(st)
|
||||
&& !"COMPLETED".equalsIgnoreCase(st)) {
|
||||
order.setOrderStatus("CANCELLED");
|
||||
}
|
||||
order.setFailMsg(StrUtil.maxLength(msg, 200));
|
||||
orderMapper.updateById(order);
|
||||
log.info("|hotel-refund|异常退款完成|orderId={}|channelNo={}|", order.getId(), order.getChannelNo());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelByChannelNo(Long userId, String channelNo, String reason) {
|
||||
@@ -369,7 +415,85 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
if (order == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_NOT_EXISTS);
|
||||
}
|
||||
doCancel(order, StrUtil.blankToDefault(reason, "用户取消"), true);
|
||||
if (isTerminalUnbookable(order.getOrderStatus())) {
|
||||
throw exception(HOTEL_ZAX_ORDER_CANCEL_NOT_ALLOWED);
|
||||
}
|
||||
if (STATUS_CANCEL_APPLY.equalsIgnoreCase(StrUtil.trim(order.getOrderStatus()))) {
|
||||
throw exception(HOTEL_ZAX_ORDER_CANCEL_APPLY_EXISTS);
|
||||
}
|
||||
// 未支付:只关本地,不调供应方
|
||||
if (!Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
doCancel(order, StrUtil.blankToDefault(reason, "用户取消"), false);
|
||||
return;
|
||||
}
|
||||
// 半小时内:直接取消并通知供应方即时关单
|
||||
if (withinUserCancelFreeWindow(order)) {
|
||||
doCancel(order, StrUtil.blankToDefault(reason, "用户取消"), true);
|
||||
return;
|
||||
}
|
||||
// 超半小时:必须先调供应方挂起「待取消确认」,再落本地申请态,等对方人工确认后回调更新
|
||||
try {
|
||||
Map<String, Object> req = new HashMap<>(4);
|
||||
req.put("channelNo", order.getChannelNo());
|
||||
req.put("applyOnly", true);
|
||||
req.put("reason", StrUtil.blankToDefault(reason, "用户申请取消(超半小时)"));
|
||||
openClient.post("order/cancel", req);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-cancel|提交供应方取消申请失败|channelNo={}|{}", order.getChannelNo(), e.getMessage());
|
||||
throw exception(HOTEL_ZAX_ORDER_CANCEL_FAIL, StrUtil.maxLength(
|
||||
StrUtil.blankToDefault(e.getMessage(), "提交取消申请失败"), 80));
|
||||
}
|
||||
order.setOrderStatus(STATUS_CANCEL_APPLY);
|
||||
order.setFailMsg(StrUtil.maxLength(
|
||||
StrUtil.blankToDefault(reason, "用户申请取消(超半小时,待酒店确认)"), 200));
|
||||
orderMapper.updateById(order);
|
||||
log.info("|hotel-cancel|用户超时取消申请已提交供应方|channelNo={}|userId={}|payTime={}|createTime={}|",
|
||||
order.getChannelNo(), userId, order.getPayTime(), order.getCreateTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncStatusByChannelNo(String channelNo) {
|
||||
if (StrUtil.isBlank(channelNo)) {
|
||||
return;
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectOne(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getChannelNo, channelNo.trim())
|
||||
.last("limit 1"));
|
||||
refreshStatusFromSupplier(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int syncPendingOrders(int limit) {
|
||||
int size = Math.max(1, Math.min(limit, 100));
|
||||
// 中间态:主动拉供应方;已取消已付:补偿退款(refresh 内处理)
|
||||
List<HotelZaxOrderDO> rows = orderMapper.selectList(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getPayStatus, true)
|
||||
.and(w -> w.in(HotelZaxOrderDO::getOrderStatus,
|
||||
"PENDING_CONFIRM", "NEED_MANUAL", "PAID", "PENDING", "CREATED", STATUS_CANCEL_APPLY,
|
||||
"CANCELLED", "CANCELED", "BOOK_FAIL", "UPSTREAM_FAIL")
|
||||
.or()
|
||||
.isNull(HotelZaxOrderDO::getOrderStatus))
|
||||
.orderByDesc(HotelZaxOrderDO::getId)
|
||||
.last("limit " + size));
|
||||
int n = 0;
|
||||
for (HotelZaxOrderDO row : rows) {
|
||||
if (row == null || StrUtil.isBlank(row.getChannelNo())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// 已关单且已退:跳过,避免无意义打供应方
|
||||
if (isTerminalUnbookable(row.getOrderStatus()) && isLocalRefunded(row)) {
|
||||
continue;
|
||||
}
|
||||
refreshStatusFromSupplier(row);
|
||||
n++;
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-status|定时同步失败|channelNo={}|{}", row.getChannelNo(), e.getMessage());
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -392,11 +516,24 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
row.setPlatformNo(trim(src.get("platformNo")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("orderStatus")))) {
|
||||
row.setOrderStatus(trim(src.get("orderStatus")));
|
||||
String remoteSt = trim(src.get("orderStatus"));
|
||||
// 供应方待取消确认 → 本地取消审核中
|
||||
if ("PENDING_CANCEL".equalsIgnoreCase(remoteSt)) {
|
||||
row.setOrderStatus(STATUS_CANCEL_APPLY);
|
||||
}
|
||||
else {
|
||||
row.setOrderStatus(remoteSt);
|
||||
}
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("bookStatus")))) {
|
||||
row.setBookStatus(trim(src.get("bookStatus")));
|
||||
}
|
||||
// 供应方取消:orderStatus=CANCELLED 或 bookStatus=X 都视为关单
|
||||
String book = StrUtil.trim(row.getBookStatus());
|
||||
if ("X".equalsIgnoreCase(book) || "CANCEL".equalsIgnoreCase(book)
|
||||
|| "CANCELLED".equalsIgnoreCase(book)) {
|
||||
row.setOrderStatus("CANCELLED");
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("hotelCode")))) {
|
||||
row.setHotelCode(trim(src.get("hotelCode")));
|
||||
}
|
||||
@@ -415,24 +552,85 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
row.setFailMsg(trim(src.get("failMsg")));
|
||||
}
|
||||
row.setLastNotifyAt(LocalDateTime.now());
|
||||
boolean closed = isTerminalUnbookable(row.getOrderStatus());
|
||||
boolean needRefund = closed && Boolean.TRUE.equals(row.getPayStatus()) && !isLocalRefunded(row);
|
||||
String changeType = StrUtil.trim(trim(src.get("changeType")));
|
||||
String refundReason = friendlyRefundReason(trim(src.get("failMsg")), changeType);
|
||||
// 拒单/同意取消等:明确 changeType 时也强制走退款
|
||||
if ("ORDER_REJECTED".equalsIgnoreCase(changeType) || "CANCEL_APPROVED".equalsIgnoreCase(changeType)
|
||||
|| "REJECT".equalsIgnoreCase(changeType)) {
|
||||
if (Boolean.TRUE.equals(row.getPayStatus()) && !isLocalRefunded(row)) {
|
||||
needRefund = true;
|
||||
row.setOrderStatus("CANCELLED");
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
// 展示给用户的失败原因也用友好文案
|
||||
if (StrUtil.isNotBlank(row.getFailMsg())) {
|
||||
row.setFailMsg(StrUtil.maxLength(friendlyRefundReason(row.getFailMsg(), changeType), 200));
|
||||
}
|
||||
if (exist == null) {
|
||||
orderMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
orderMapper.updateById(row);
|
||||
}
|
||||
if (isTerminalUnbookable(row.getOrderStatus()) && Boolean.TRUE.equals(row.getPayStatus())) {
|
||||
refundIfPaid(row, StrUtil.blankToDefault(trim(src.get("failMsg")), "供应方取消退款"));
|
||||
row.setOrderStatus("CANCELLED");
|
||||
orderMapper.updateById(row);
|
||||
if (needRefund) {
|
||||
// 先落取消态,再退款,避免退款异常把关单状态一起回滚
|
||||
scheduleRefundAfterClose(row.getId(), channelNo, refundReason);
|
||||
}
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("channelNo", channelNo);
|
||||
out.put("orderStatus", row.getOrderStatus());
|
||||
out.put("id", row.getId());
|
||||
out.put("refundScheduled", needRefund);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关单后退款:事务提交后执行,幂等(已退则跳过)。
|
||||
* 覆盖回调与主动查询两条路径,避免「状态已取消但钱没退回」。
|
||||
*/
|
||||
private void scheduleRefundAfterClose(Long orderId, String channelNo, String reason) {
|
||||
if (orderId == null) {
|
||||
return;
|
||||
}
|
||||
final String refundReason = friendlyRefundReason(reason, null);
|
||||
Runnable refundTask = () -> {
|
||||
try {
|
||||
HotelZaxOrderDO latest = orderMapper.selectById(orderId);
|
||||
if (latest == null) {
|
||||
return;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(latest.getPayStatus()) || isLocalRefunded(latest)) {
|
||||
return;
|
||||
}
|
||||
refundIfPaid(latest, refundReason);
|
||||
if (!"CANCELLED".equalsIgnoreCase(StrUtil.trim(latest.getOrderStatus()))
|
||||
&& !"CANCELED".equalsIgnoreCase(StrUtil.trim(latest.getOrderStatus()))) {
|
||||
latest.setOrderStatus("CANCELLED");
|
||||
}
|
||||
orderMapper.updateById(latest);
|
||||
log.info("|hotel-refund|供应方关单已退款|channelNo={}|orderId={}|", channelNo, orderId);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("|hotel-refund|供应方关单退款失败|channelNo={}|orderId={}|{}", channelNo, orderId,
|
||||
e.getMessage(), e);
|
||||
}
|
||||
};
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
refundTask.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
refundTask.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void doCancel(HotelZaxOrderDO order, String reason, boolean notifySupplier) {
|
||||
boolean alreadyClosed = isTerminalUnbookable(order.getOrderStatus());
|
||||
if (alreadyClosed && (!Boolean.TRUE.equals(order.getPayStatus()) || isLocalRefunded(order))) {
|
||||
@@ -440,9 +638,19 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
}
|
||||
if (!alreadyClosed && notifySupplier && shouldNotifySupplierCancel(order)) {
|
||||
try {
|
||||
Map<String, Object> req = new HashMap<String, Object>();
|
||||
Map<String, Object> req = new HashMap<>(4);
|
||||
req.put("channelNo", order.getChannelNo());
|
||||
openClient.post("order/cancel", req);
|
||||
JSONObject resp = openClient.post("order/cancel", req);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
String remoteSt = data == null ? null : data.getStr("orderStatus");
|
||||
// 供应方已挂起待确认:本地改为取消审核中,等对方人工确认后再回调关单/退款
|
||||
if ("PENDING_CANCEL".equalsIgnoreCase(remoteSt)) {
|
||||
order.setOrderStatus(STATUS_CANCEL_APPLY);
|
||||
order.setFailMsg(StrUtil.maxLength(
|
||||
StrUtil.blankToDefault(reason, "取消申请已提交,待酒店确认"), 200));
|
||||
orderMapper.updateById(order);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-cancel|通知资安心取消失败|channelNo={}|{}", order.getChannelNo(), e.getMessage());
|
||||
@@ -534,11 +742,15 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> openBody = rebuildOpenBody(order);
|
||||
// 支付成功后才调供应方下单,显式带已支付,便于对方拒单时按已付单回调退款
|
||||
openBody.put("payStatus", "PAID");
|
||||
try {
|
||||
JSONObject createResp = openClient.post("order/create", openBody);
|
||||
JSONObject data = createResp == null ? null : createResp.getJSONObject("data");
|
||||
if (data != null) {
|
||||
order.setOrderNo(data.getStr("orderNo"));
|
||||
String supplierOrderNo = firstNonBlank(data.getStr("orderNo"),
|
||||
data.getStr("innerOrderId"), data.getStr("platformNo"));
|
||||
order.setOrderNo(supplierOrderNo);
|
||||
order.setPlatformNo(data.getStr("platformNo"));
|
||||
if (StrUtil.isNotBlank(data.getStr("orderStatus"))) {
|
||||
order.setOrderStatus(data.getStr("orderStatus"));
|
||||
@@ -695,9 +907,33 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
m.put("payExpireTime", order.getCreateTime().plus(PAY_EXPIRE));
|
||||
m.put("expireTime", order.getCreateTime().plus(PAY_EXPIRE));
|
||||
}
|
||||
boolean cancelApply = STATUS_CANCEL_APPLY.equalsIgnoreCase(StrUtil.trim(order.getOrderStatus()));
|
||||
boolean cancelDirect = !isTerminalUnbookable(order.getOrderStatus()) && !cancelApply
|
||||
&& Boolean.TRUE.equals(order.getPayStatus()) && withinUserCancelFreeWindow(order);
|
||||
boolean cancelNeedManual = !isTerminalUnbookable(order.getOrderStatus()) && !cancelApply
|
||||
&& Boolean.TRUE.equals(order.getPayStatus()) && !withinUserCancelFreeWindow(order);
|
||||
m.put("cancelApplyPending", cancelApply);
|
||||
m.put("canCancelDirect", cancelDirect);
|
||||
m.put("canCancelApply", cancelNeedManual);
|
||||
LocalDateTime cancelBase = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
|
||||
if (cancelBase != null) {
|
||||
m.put("cancelDeadline", cancelBase.plus(USER_CANCEL_FREE_WINDOW));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/** 支付成功后半小时内(未支付则按下单时间)可自助取消 */
|
||||
private static boolean withinUserCancelFreeWindow(HotelZaxOrderDO order) {
|
||||
if (order == null) {
|
||||
return false;
|
||||
}
|
||||
LocalDateTime base = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
|
||||
if (base == null) {
|
||||
return false;
|
||||
}
|
||||
return !LocalDateTime.now().isAfter(base.plus(USER_CANCEL_FREE_WINDOW));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Map<String, Object>> parseGuests(Object raw) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
@@ -807,6 +1043,115 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 待确认/预定中:主动查供应方并落库,避免回调丢失后小程序一直停在「待酒店确认中」。
|
||||
* 若已关单且已付未退,同步发起退款(拒单回调丢失时的补偿)。
|
||||
*/
|
||||
private HotelZaxOrderDO refreshStatusFromSupplier(HotelZaxOrderDO order) {
|
||||
if (order == null || StrUtil.isBlank(order.getChannelNo())) {
|
||||
return order;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
return order;
|
||||
}
|
||||
// 已关单但未退:直接补偿退款(例如回调丢了、只靠查询刷成了 CANCELLED)
|
||||
if (isTerminalUnbookable(order.getOrderStatus()) && !isLocalRefunded(order)) {
|
||||
scheduleRefundAfterClose(order.getId(), order.getChannelNo(),
|
||||
StrUtil.blankToDefault(order.getFailMsg(), "供应方拒单/取消退款"));
|
||||
return order;
|
||||
}
|
||||
String st = StrUtil.trim(order.getOrderStatus());
|
||||
boolean need = "PENDING_CONFIRM".equalsIgnoreCase(st) || "NEED_MANUAL".equalsIgnoreCase(st)
|
||||
|| "PAID".equalsIgnoreCase(st) || "PENDING".equalsIgnoreCase(st)
|
||||
|| "CREATED".equalsIgnoreCase(st) || STATUS_CANCEL_APPLY.equalsIgnoreCase(st);
|
||||
if (!need) {
|
||||
return order;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> q = new HashMap<>(2);
|
||||
q.put("channelNo", order.getChannelNo());
|
||||
JSONObject resp = openClient.post("order/query", q);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
return order;
|
||||
}
|
||||
boolean changed = false;
|
||||
String remoteStatus = firstNonBlank(data.getStr("orderStatus"), data.getStr("status"));
|
||||
if ("PENDING_CANCEL".equalsIgnoreCase(remoteStatus)) {
|
||||
remoteStatus = STATUS_CANCEL_APPLY;
|
||||
}
|
||||
if (StrUtil.isNotBlank(remoteStatus)
|
||||
&& !remoteStatus.equalsIgnoreCase(StrUtil.trim(order.getOrderStatus()))) {
|
||||
// 本地已是取消申请中时,不要被供应方仍显示的「已订成」冲掉(等对方人工处理回调)
|
||||
if (STATUS_CANCEL_APPLY.equalsIgnoreCase(st)
|
||||
&& !"CANCELLED".equalsIgnoreCase(remoteStatus)
|
||||
&& !STATUS_CANCEL_APPLY.equalsIgnoreCase(remoteStatus)) {
|
||||
// 若供应方已恢复为 UPSTREAM_OK,说明拒绝取消,允许覆盖
|
||||
if ("UPSTREAM_OK".equalsIgnoreCase(remoteStatus) || "CREATED".equalsIgnoreCase(remoteStatus)
|
||||
|| "CHECKED_IN".equalsIgnoreCase(remoteStatus)) {
|
||||
order.setOrderStatus(remoteStatus);
|
||||
order.setFailMsg(null);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
order.setOrderStatus(remoteStatus);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
String remoteBook = data.getStr("bookStatus");
|
||||
if (StrUtil.isNotBlank(remoteBook) && !remoteBook.equalsIgnoreCase(StrUtil.trim(order.getBookStatus()))) {
|
||||
order.setBookStatus(remoteBook);
|
||||
changed = true;
|
||||
}
|
||||
String remoteOrderNo = firstNonBlank(data.getStr("orderNo"), data.getStr("innerOrderId"));
|
||||
if (StrUtil.isNotBlank(remoteOrderNo) && StrUtil.isBlank(order.getOrderNo())) {
|
||||
order.setOrderNo(remoteOrderNo);
|
||||
changed = true;
|
||||
}
|
||||
String remotePlatform = data.getStr("platformNo");
|
||||
if (StrUtil.isNotBlank(remotePlatform) && !remotePlatform.equals(StrUtil.trim(order.getPlatformNo()))) {
|
||||
order.setPlatformNo(remotePlatform);
|
||||
changed = true;
|
||||
}
|
||||
if ("X".equalsIgnoreCase(StrUtil.trim(order.getBookStatus()))) {
|
||||
order.setOrderStatus("CANCELLED");
|
||||
changed = true;
|
||||
}
|
||||
String remoteFail = data.getStr("failMsg");
|
||||
if (StrUtil.isNotBlank(remoteFail) && StrUtil.isBlank(order.getFailMsg())) {
|
||||
order.setFailMsg(StrUtil.maxLength(remoteFail, 200));
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
orderMapper.updateById(order);
|
||||
log.info("|hotel-status|已从供应方刷新|channelNo={}|status={}|", order.getChannelNo(),
|
||||
order.getOrderStatus());
|
||||
}
|
||||
// 查询发现已关单:同步退款(不依赖回调是否到达)
|
||||
if (isTerminalUnbookable(order.getOrderStatus()) && !isLocalRefunded(order)) {
|
||||
scheduleRefundAfterClose(order.getId(), order.getChannelNo(),
|
||||
StrUtil.blankToDefault(order.getFailMsg(), "供应方拒单/取消退款"));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-status|刷新供应方状态失败|channelNo={}|{}", order.getChannelNo(), e.getMessage());
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String... vals) {
|
||||
if (vals == null) {
|
||||
return null;
|
||||
}
|
||||
for (String v : vals) {
|
||||
if (StrUtil.isNotBlank(v)) {
|
||||
return v.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolvePayMerchantOrderId(HotelZaxOrderDO order) {
|
||||
if (order.getPayOrderId() != null) {
|
||||
try {
|
||||
@@ -860,6 +1205,35 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
|| "UPSTREAM_FAIL".equalsIgnoreCase(s);
|
||||
}
|
||||
|
||||
/** 退款/关单原因对用户展示友好化(避免「人工拒绝接单」等内部话术) */
|
||||
private static String friendlyRefundReason(String raw, String changeType) {
|
||||
String msg = StrUtil.trim(raw);
|
||||
if ("ORDER_REJECTED".equalsIgnoreCase(StrUtil.trim(changeType))
|
||||
|| "REJECT".equalsIgnoreCase(StrUtil.trim(changeType))) {
|
||||
if (StrUtil.isBlank(msg) || looksInternalRejectWording(msg)) {
|
||||
return "酒店暂无法确认预订,已为您取消并全额退款";
|
||||
}
|
||||
}
|
||||
if (looksInternalRejectWording(msg)) {
|
||||
return "酒店暂无法确认预订,已为您取消并全额退款";
|
||||
}
|
||||
if (StrUtil.isBlank(msg) || "供应方取消/拒单退款".equals(msg) || "供应方拒单/取消退款".equals(msg)) {
|
||||
if ("CANCEL_APPROVED".equalsIgnoreCase(StrUtil.trim(changeType))) {
|
||||
return "预订已取消,退款将原路退回";
|
||||
}
|
||||
return "预订未成功,已为您全额退款";
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static boolean looksInternalRejectWording(String msg) {
|
||||
if (StrUtil.isBlank(msg)) {
|
||||
return false;
|
||||
}
|
||||
return msg.contains("人工拒绝") || msg.contains("拒绝接单") || "后台取消".equals(msg)
|
||||
|| "后台取消退款".equals(msg);
|
||||
}
|
||||
|
||||
/** 顾客端状态文案,不直接展示内部英文码 */
|
||||
private static String customerStatusName(String orderStatus, Boolean payStatus) {
|
||||
String s = StrUtil.trim(orderStatus);
|
||||
@@ -870,7 +1244,10 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
return "已支付,预定中";
|
||||
}
|
||||
if ("PENDING_CONFIRM".equals(s) || "NEED_MANUAL".equals(s)) {
|
||||
return "预定处理中";
|
||||
return "待酒店确认中";
|
||||
}
|
||||
if (STATUS_CANCEL_APPLY.equalsIgnoreCase(s)) {
|
||||
return "取消审核中";
|
||||
}
|
||||
if ("CREATED".equals(s) || "UPSTREAM_OK".equals(s)) {
|
||||
return "预定成功";
|
||||
|
||||
Reference in new issue
Block a user