酒店
This commit is contained in:
1 parent
c339e5df09
commit
a2dc29722b
13 files changed
+747
-19
No files matched your search
+18
@@ -79,6 +79,24 @@ public class SmHotelOrderController {
|
||||
return orderService.rejectLocalByAdmin(id, reason);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店同意取消申请")
|
||||
@Operation(summary = "本地:同意下游取消申请(PENDING_CANCEL)")
|
||||
@PostMapping("/confirmCancel/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel') or @pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Boolean> confirmCancel(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
|
||||
String reason = body == null ? null : String.valueOf(body.getOrDefault("reason", "人工同意取消"));
|
||||
return orderService.confirmCancelLocalByAdmin(id, reason);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店拒绝取消申请")
|
||||
@Operation(summary = "本地:拒绝下游取消申请,恢复预订成功")
|
||||
@PostMapping("/rejectCancel/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel') or @pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Boolean> rejectCancel(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
|
||||
String reason = body == null ? null : String.valueOf(body.getOrDefault("reason", "人工拒绝取消申请"));
|
||||
return orderService.rejectCancelLocalByAdmin(id, reason);
|
||||
}
|
||||
|
||||
@SysLog("后台退款酒店订单")
|
||||
@Operation(summary = "后台退款:已支付订单原路退款并关闭预订")
|
||||
@PostMapping("/refund/{id}")
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.hotel.upstream;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelCancelPolicy;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 酒店上游取消规则解析(按数据来源渠道插拔)。
|
||||
* <p>
|
||||
* 新增第三方:实现本接口并注册为 Spring Bean,将对方原始取消结构归一到
|
||||
* {@link SmHotelCancelPolicy};字典 {@code sm_hotel_data_source} 增加对应 item_value 即可。
|
||||
* 下游取消 / 后台取消只依赖统一模型,不感知各家字段差异。
|
||||
*/
|
||||
public interface HotelUpstreamCancelPolicy {
|
||||
|
||||
/** 渠道标识,与 hotel.sheet_name / 字典值一致,如 shangyou */
|
||||
String channel();
|
||||
|
||||
/**
|
||||
* 从已落库产品解析统一规则(可兼容本渠道历史 rawJson)。
|
||||
*
|
||||
* @return null=无规则信息不拦截
|
||||
*/
|
||||
SmHotelCancelPolicy.Policy resolve(SmHotelProductEntity product);
|
||||
|
||||
/**
|
||||
* 上游房价/产品原始 JSON → 统一 canCancel + cancelRule,供同步落库。
|
||||
*
|
||||
* @return empty=本条无取消信息
|
||||
*/
|
||||
Optional<NormalizedCancel> normalizeFromUpstreamRate(JSONObject rate);
|
||||
|
||||
default String denyReason(SmHotelProductEntity product, LocalDate checkIn, LocalDateTime now) {
|
||||
return SmHotelCancelPolicy.denyReason(resolve(product), checkIn, now);
|
||||
}
|
||||
|
||||
/** 同步落库用的归一结果 */
|
||||
record NormalizedCancel(int canCancel, String cancelRuleJson) {
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.hotel.upstream;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 按酒店数据来源(sheet_name)解析上游取消规则实现。
|
||||
* <ul>
|
||||
* <li>local:无上游规则</li>
|
||||
* <li>空白历史:兼容映射到商游</li>
|
||||
* <li>已注册渠道:返回对应实现</li>
|
||||
* <li>未注册:empty,取消判定仅用已归一的 cancelRule/canCancel</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
public class HotelUpstreamCancelPolicyRegistry {
|
||||
|
||||
private final Map<String, HotelUpstreamCancelPolicy> byChannel = new HashMap<>();
|
||||
|
||||
public HotelUpstreamCancelPolicyRegistry(List<HotelUpstreamCancelPolicy> policies) {
|
||||
if (policies != null) {
|
||||
for (HotelUpstreamCancelPolicy p : policies) {
|
||||
if (p == null || StrUtil.isBlank(p.channel())) {
|
||||
continue;
|
||||
}
|
||||
byChannel.put(SmHotelDataSource.normalize(p.channel()).toLowerCase(), p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveChannelKey(String hotelSheetName) {
|
||||
if (SmHotelDataSource.isLocal(hotelSheetName)) {
|
||||
return SmHotelDataSource.LOCAL;
|
||||
}
|
||||
String ds = SmHotelDataSource.normalize(hotelSheetName);
|
||||
if (ds.isEmpty()) {
|
||||
return SmHotelDataSource.SHANGYOU;
|
||||
}
|
||||
return ds.toLowerCase();
|
||||
}
|
||||
|
||||
public Optional<HotelUpstreamCancelPolicy> find(String hotelSheetName) {
|
||||
if (SmHotelDataSource.isLocal(hotelSheetName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String key = resolveChannelKey(hotelSheetName);
|
||||
return Optional.ofNullable(byChannel.get(key.toLowerCase()));
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -5,7 +5,8 @@ import com.alibaba.fastjson.JSONObject;
|
||||
/**
|
||||
* 酒店上游订单网关(按数据来源渠道插拔)。
|
||||
* <p>
|
||||
* 新增第三方:实现本接口并注册为 Spring Bean,字典 {@code sm_hotel_data_source} 增加对应 item_value 即可。
|
||||
* 新增第三方:实现本接口并注册为 Spring Bean,字典 {@code sm_hotel_data_source} 增加对应 item_value 即可;
|
||||
* 取消规则请同步实现 {@link HotelUpstreamCancelPolicy},对方原始结构归一后再做下游/后台取消判定。
|
||||
*/
|
||||
public interface HotelUpstreamOrderGateway {
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.hotel.upstream;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelCancelPolicy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 商游/飞猪取消规则:对方房价里的 {@code cancelInfo}
|
||||
* (isCanCancel / lastCancelDay / lastCancelTime / cancelDesc)。
|
||||
*/
|
||||
@Component
|
||||
public class ShangyouHotelUpstreamCancelPolicy implements HotelUpstreamCancelPolicy {
|
||||
|
||||
@Override
|
||||
public String channel() {
|
||||
return SmHotelDataSource.SHANGYOU;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmHotelCancelPolicy.Policy resolve(SmHotelProductEntity product) {
|
||||
if (product == null) {
|
||||
return null;
|
||||
}
|
||||
SmHotelCancelPolicy.Policy fromRule = SmHotelCancelPolicy.parseStoredCancelRule(product.getCancelRule());
|
||||
SmHotelCancelPolicy.Policy fromRaw = parseFliggyCancelInfo(product.getRawJson());
|
||||
SmHotelCancelPolicy.Policy merged = SmHotelCancelPolicy.merge(fromRule, fromRaw);
|
||||
if (merged == null) {
|
||||
return SmHotelCancelPolicy.resolveStored(product);
|
||||
}
|
||||
merged.channel = StrUtil.blankToDefault(merged.channel, channel());
|
||||
return SmHotelCancelPolicy.applyCanCancelField(merged, product.getCanCancel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<NormalizedCancel> normalizeFromUpstreamRate(JSONObject rate) {
|
||||
if (rate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JSONObject cancelInfo = rate.getJSONObject("cancelInfo");
|
||||
if (cancelInfo == null || cancelInfo.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Boolean can = cancelInfo.getBoolean("isCanCancel");
|
||||
int canCancel = Boolean.TRUE.equals(can) ? 1 : 0;
|
||||
String rule = SmHotelCancelPolicy.toCancelRuleJson(channel(), can, cancelInfo.getInteger("lastCancelDay"),
|
||||
cancelInfo.getString("lastCancelTime"), cancelInfo.getString("cancelDesc"));
|
||||
return Optional.of(new NormalizedCancel(canCancel, rule));
|
||||
}
|
||||
|
||||
/** 兼容尚未刷新 cancelRule 的旧产品:从 rawJson.cancelInfo 补齐。 */
|
||||
private SmHotelCancelPolicy.Policy parseFliggyCancelInfo(String rawJson) {
|
||||
if (StrUtil.isBlank(rawJson)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
cn.hutool.json.JSONObject rate = JSONUtil.parseObj(rawJson);
|
||||
cn.hutool.json.JSONObject cancelInfo = rate.getJSONObject("cancelInfo");
|
||||
if (cancelInfo == null || cancelInfo.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
SmHotelCancelPolicy.Policy p = SmHotelCancelPolicy.fromUnifiedJson(cancelInfo);
|
||||
if (p != null) {
|
||||
p.channel = channel();
|
||||
}
|
||||
return p;
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 上游取消规则的<strong>统一落库模型</strong>与截止时间判定。
|
||||
* <p>
|
||||
* 各上游渠道原始结构不同,解析由 {@code HotelUpstreamCancelPolicy} 按 sheet_name 插拔;
|
||||
* 同步时尽量归一到 cancelRule JSON,下游取消 / 后台取消只认本模型。
|
||||
* <p>
|
||||
* cancelRule 推荐:
|
||||
* {@code {"channel":"shangyou","isCanCancel":true,"lastCancelDay":1,"lastCancelTime":"18:00","cancelDesc":"..."}}
|
||||
*/
|
||||
public final class SmHotelCancelPolicy {
|
||||
|
||||
private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("H:mm");
|
||||
|
||||
private SmHotelCancelPolicy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null=允许取消;非空=拒绝原因
|
||||
*/
|
||||
public static String denyReason(Policy p, LocalDate checkIn, LocalDateTime now) {
|
||||
if (p == null) {
|
||||
return null;
|
||||
}
|
||||
if (!p.canCancel) {
|
||||
return "按供应商取消规则不可取消"
|
||||
+ (StrUtil.isNotBlank(p.desc) ? ":" + p.desc : "");
|
||||
}
|
||||
if (checkIn == null) {
|
||||
return null;
|
||||
}
|
||||
LocalDateTime at = now == null ? LocalDateTime.now() : now;
|
||||
int days = Math.max(0, p.lastCancelDay);
|
||||
LocalTime time = p.lastCancelTime == null ? LocalTime.of(23, 59, 59) : p.lastCancelTime;
|
||||
LocalDateTime deadline = checkIn.minusDays(days).atTime(time);
|
||||
if (at.isAfter(deadline)) {
|
||||
String when = days + "天 " + time.format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
return "已过最晚取消时间(入住前" + when + ")"
|
||||
+ (StrUtil.isNotBlank(p.desc) ? ";" + p.desc : "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅读已归一的 cancelRule + canCancel(无渠道 rawJson)。
|
||||
* 无渠道 SPI 或本地产品时的兜底。
|
||||
*/
|
||||
public static Policy resolveStored(SmHotelProductEntity product) {
|
||||
if (product == null) {
|
||||
return null;
|
||||
}
|
||||
Policy fromRule = parseStoredCancelRule(product.getCancelRule());
|
||||
if (fromRule == null) {
|
||||
if (product.getCanCancel() == null && StrUtil.isBlank(product.getCancelRule())) {
|
||||
return null;
|
||||
}
|
||||
fromRule = new Policy();
|
||||
fromRule.canCancel = Integer.valueOf(1).equals(product.getCanCancel());
|
||||
fromRule.desc = StrUtil.trim(product.getCancelRule());
|
||||
if (looksLikeJson(fromRule.desc)) {
|
||||
fromRule.desc = null;
|
||||
}
|
||||
}
|
||||
else if (product.getCanCancel() != null) {
|
||||
fromRule.canCancel = Integer.valueOf(1).equals(product.getCanCancel());
|
||||
}
|
||||
return fromRule;
|
||||
}
|
||||
|
||||
/** @deprecated 请按酒店 sheet_name 走渠道 SPI;保留给仅有归一字段的推送场景 */
|
||||
@Deprecated
|
||||
public static Policy resolve(SmHotelProductEntity product) {
|
||||
return resolveStored(product);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 {@link #denyReason(Policy, LocalDate, LocalDateTime)} */
|
||||
@Deprecated
|
||||
public static String denyReason(SmHotelProductEntity product, LocalDate checkIn, LocalDateTime now) {
|
||||
return denyReason(resolveStored(product), checkIn, now);
|
||||
}
|
||||
|
||||
/** 同步时写入统一 cancelRule JSON。 */
|
||||
public static String toCancelRuleJson(String channel, Boolean isCanCancel, Integer lastCancelDay,
|
||||
String lastCancelTime, String cancelDesc) {
|
||||
JSONObject o = new JSONObject();
|
||||
if (StrUtil.isNotBlank(channel)) {
|
||||
o.set("channel", channel.trim().toLowerCase());
|
||||
}
|
||||
o.set("isCanCancel", Boolean.TRUE.equals(isCanCancel));
|
||||
if (lastCancelDay != null) {
|
||||
o.set("lastCancelDay", lastCancelDay);
|
||||
}
|
||||
if (StrUtil.isNotBlank(lastCancelTime)) {
|
||||
o.set("lastCancelTime", lastCancelTime.trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(cancelDesc)) {
|
||||
o.set("cancelDesc", cancelDesc.trim());
|
||||
}
|
||||
return o.toString();
|
||||
}
|
||||
|
||||
/** 兼容旧调用(无 channel)。 */
|
||||
public static String toCancelRuleJson(Boolean isCanCancel, Integer lastCancelDay, String lastCancelTime,
|
||||
String cancelDesc) {
|
||||
return toCancelRuleJson(null, isCanCancel, lastCancelDay, lastCancelTime, cancelDesc);
|
||||
}
|
||||
|
||||
public static Policy parseStoredCancelRule(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim();
|
||||
if (!looksLikeJson(s)) {
|
||||
Policy p = new Policy();
|
||||
p.desc = s;
|
||||
p.canCancel = true;
|
||||
return p;
|
||||
}
|
||||
try {
|
||||
return fromUnifiedJson(JSONUtil.parseObj(s));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Policy p = new Policy();
|
||||
p.desc = s;
|
||||
p.canCancel = true;
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
public static Policy fromUnifiedJson(JSONObject o) {
|
||||
if (o == null || o.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Policy p = new Policy();
|
||||
if (o.containsKey("isCanCancel")) {
|
||||
p.canCancel = Boolean.TRUE.equals(o.getBool("isCanCancel"));
|
||||
}
|
||||
else {
|
||||
p.canCancel = true;
|
||||
}
|
||||
Integer day = o.getInt("lastCancelDay");
|
||||
p.lastCancelDay = day == null ? 0 : Math.max(0, day);
|
||||
p.lastCancelTime = parseHm(o.getStr("lastCancelTime"));
|
||||
p.desc = StrUtil.blankToDefault(o.getStr("cancelDesc"), o.getStr("desc"));
|
||||
p.channel = o.getStr("channel");
|
||||
return p;
|
||||
}
|
||||
|
||||
public static Policy merge(Policy a, Policy b) {
|
||||
if (a == null) {
|
||||
return b;
|
||||
}
|
||||
if (b == null) {
|
||||
return a;
|
||||
}
|
||||
Policy p = new Policy();
|
||||
boolean aTimed = a.lastCancelDay > 0 || a.lastCancelTime != null;
|
||||
boolean bTimed = b.lastCancelDay > 0 || b.lastCancelTime != null;
|
||||
if (aTimed) {
|
||||
p.canCancel = a.canCancel;
|
||||
}
|
||||
else if (bTimed) {
|
||||
p.canCancel = b.canCancel;
|
||||
}
|
||||
else {
|
||||
p.canCancel = a.canCancel;
|
||||
}
|
||||
p.lastCancelDay = a.lastCancelDay > 0 ? a.lastCancelDay : b.lastCancelDay;
|
||||
p.lastCancelTime = a.lastCancelTime != null ? a.lastCancelTime : b.lastCancelTime;
|
||||
p.desc = StrUtil.blankToDefault(a.desc, b.desc);
|
||||
p.channel = StrUtil.blankToDefault(a.channel, b.channel);
|
||||
return p;
|
||||
}
|
||||
|
||||
public static Policy applyCanCancelField(Policy base, Integer canCancel) {
|
||||
if (base == null) {
|
||||
return null;
|
||||
}
|
||||
if (canCancel != null) {
|
||||
base.canCancel = Integer.valueOf(1).equals(canCancel);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
public static LocalTime parseHm(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim();
|
||||
try {
|
||||
if (s.length() >= 8 && s.charAt(2) == ':') {
|
||||
return LocalTime.parse(s.substring(0, 5), HM);
|
||||
}
|
||||
return LocalTime.parse(s, HM);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean looksLikeJson(String s) {
|
||||
return s != null && s.length() > 1 && s.charAt(0) == '{';
|
||||
}
|
||||
|
||||
public static final class Policy {
|
||||
/** 来源渠道,如 shangyou;可空 */
|
||||
public String channel;
|
||||
public boolean canCancel;
|
||||
public int lastCancelDay;
|
||||
public LocalTime lastCancelTime;
|
||||
public String desc;
|
||||
}
|
||||
}
|
||||
+3
@@ -34,4 +34,7 @@ public interface SmHotelHuidingOrderService {
|
||||
|
||||
/** 本地酒店人工拒绝后回写慧订 operationOrder=2 */
|
||||
void notifyLocalOrderRejected(SmHotelOrderEntity order);
|
||||
|
||||
/** 本地同意取消后回写慧订 operationOrder=3(取消确认) */
|
||||
void notifyLocalCancelConfirmed(SmHotelOrderEntity order);
|
||||
}
|
||||
+12
@@ -101,6 +101,18 @@ public interface SmHotelOrderService {
|
||||
*/
|
||||
R<Boolean> rejectLocalByAdmin(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 下游取消入站:本地已确认订单挂起为 PENDING_CANCEL 待人工;其余即时取消。
|
||||
* data.parked=true 表示已挂起;false 表示已即时取消。
|
||||
*/
|
||||
R<Map<String, Object>> cancelOrParkForDownstream(Long id, String reason);
|
||||
|
||||
/** 本地:同意下游取消申请(PENDING_CANCEL → 关单并回写下游取消确认) */
|
||||
R<Boolean> confirmCancelLocalByAdmin(Long id, String reason);
|
||||
|
||||
/** 本地:拒绝下游取消申请(PENDING_CANCEL → 恢复预订成功,并回写下游仍接单) */
|
||||
R<Boolean> rejectCancelLocalByAdmin(Long id, String reason);
|
||||
|
||||
/** Worker:扫描到期补偿任务 */
|
||||
int processDueCompensate(int limit);
|
||||
|
||||
|
||||
+9
@@ -22,4 +22,13 @@ public interface SmHotelShengnuoOrderService {
|
||||
|
||||
/** 本地酒店人工拒绝后 orderChange=已取消(5) */
|
||||
void notifyLocalOrderRejected(SmHotelOrderEntity order);
|
||||
|
||||
/** 本地同意取消后 orderChange=已取消(5) */
|
||||
void notifyLocalCancelConfirmed(SmHotelOrderEntity order);
|
||||
|
||||
/** 本地拒绝取消申请后 orderChange=已接单(2) */
|
||||
void notifyLocalCancelRejected(SmHotelOrderEntity order);
|
||||
|
||||
/** 本地挂起取消申请时 orderChange=取消中(4) */
|
||||
void notifyLocalCancelPending(SmHotelOrderEntity order);
|
||||
}
|
||||
+21
-5
@@ -281,13 +281,20 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
&& !partnerCode.equals(order.getPartnerCode())) {
|
||||
return fail200("订单不属于当前伙伴");
|
||||
}
|
||||
R<Boolean> r = hotelOrderService.cancelByAdmin(order.getId(), "慧订取消");
|
||||
R<Map<String, Object>> r = hotelOrderService.cancelOrParkForDownstream(order.getId(), "慧订取消");
|
||||
if (r == null || r.getCode() != 0) {
|
||||
return fail200(r == null ? "取消失败" : r.getMsg());
|
||||
}
|
||||
boolean parked = r.getData() != null && Boolean.TRUE.equals(r.getData().get("parked"));
|
||||
Map<String, Object> outer = ok200();
|
||||
if (parked) {
|
||||
// 本地已确认:挂起待人工,暂不推取消确认(3)
|
||||
outer.put("msg", "取消申请已受理,待确认");
|
||||
outer.put("data", true);
|
||||
return outer;
|
||||
}
|
||||
SmHotelOrderEntity latest = orderMapper.selectById(order.getId());
|
||||
pushOperationOrder(partnerCode, latest != null ? latest : order, OP_CANCEL_CONFIRM);
|
||||
Map<String, Object> outer = ok200();
|
||||
outer.put("msg", "取消申请已处理");
|
||||
outer.put("data", true);
|
||||
return outer;
|
||||
@@ -439,9 +446,9 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
}
|
||||
}
|
||||
|
||||
/** 上游/人工已明确订成(不含待确认) */
|
||||
/** 上游/人工已明确订成(不含待确认/待取消确认) */
|
||||
private static boolean isBookConfirmed(SmHotelOrderEntity order) {
|
||||
if (order == null || isPendingConfirm(order)) {
|
||||
if (order == null || isPendingConfirm(order) || "PENDING_CANCEL".equals(order.getOrderStatus())) {
|
||||
return false;
|
||||
}
|
||||
if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) {
|
||||
@@ -450,7 +457,8 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
return StrUtil.isNotBlank(order.getPlatformNo())
|
||||
&& !"UPSTREAM_FAIL".equals(order.getOrderStatus())
|
||||
&& !"NEED_MANUAL".equals(order.getOrderStatus())
|
||||
&& !"CANCELLED".equals(order.getOrderStatus());
|
||||
&& !"CANCELLED".equals(order.getOrderStatus())
|
||||
&& !"PENDING_CANCEL".equals(order.getOrderStatus());
|
||||
}
|
||||
|
||||
private static boolean isPendingConfirm(SmHotelOrderEntity order) {
|
||||
@@ -473,6 +481,14 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
pushOperationOrder(order.getPartnerCode(), order, OP_REJECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLocalCancelConfirmed(SmHotelOrderEntity order) {
|
||||
if (order == null || StrUtil.isBlank(order.getPartnerCode())) {
|
||||
return;
|
||||
}
|
||||
pushOperationOrder(order.getPartnerCode(), order, OP_CANCEL_CONFIRM);
|
||||
}
|
||||
|
||||
private String resolvePartnerByNotifyPass(String pass) {
|
||||
if (StrUtil.isBlank(pass)) {
|
||||
return null;
|
||||
|
||||
+45
-9
@@ -17,6 +17,8 @@ import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamPushKind;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.huiding.HuidingDownstreamHotelAdapter;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.shengnuo.ShengnuoDownstreamHotelAdapter;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.shengnuo.ShengnuoIdMappingCache;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamCancelPolicy;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamCancelPolicyRegistry;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO;
|
||||
import com.bsd.pjcloud.supermarket.entity.*;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelClient;
|
||||
@@ -25,6 +27,7 @@ import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelCancelPolicy;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAutoPushService;
|
||||
@@ -115,6 +118,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
|
||||
private final ShengnuoIdMappingCache shengnuoIdMappingCache;
|
||||
|
||||
/** 按上游渠道解析取消规则(商游/后续其它家) */
|
||||
private final HotelUpstreamCancelPolicyRegistry upstreamCancelPolicyRegistry;
|
||||
|
||||
public SmHotelMidServiceImpl(ShangyouHotelClient upstreamClient, ShangyouHotelProperties upstreamProperties,
|
||||
DownstreamHotelClient downstreamClient, HotelDownstreamProperties downstreamProperties,
|
||||
SmHotelPriceConfigService priceConfigService, SmHotelChannelConfigService channelConfigService,
|
||||
@@ -124,12 +130,14 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
@Lazy SmHotelAvailabilityNotifyService availabilityNotifyService, SmHotelMapper hotelMapper,
|
||||
SmHotelRoomMapper roomMapper, SmHotelProductMapper productMapper, SmHotelDailyMapper dailyMapper,
|
||||
StringRedisTemplate stringRedisTemplate, ShengnuoDownstreamHotelAdapter shengnuoAdapter,
|
||||
ShengnuoIdMappingCache shengnuoIdMappingCache) {
|
||||
ShengnuoIdMappingCache shengnuoIdMappingCache,
|
||||
HotelUpstreamCancelPolicyRegistry upstreamCancelPolicyRegistry) {
|
||||
this.upstreamClient = upstreamClient;
|
||||
this.upstreamProperties = upstreamProperties;
|
||||
this.downstreamClient = downstreamClient;
|
||||
this.shengnuoAdapter = shengnuoAdapter;
|
||||
this.shengnuoIdMappingCache = shengnuoIdMappingCache;
|
||||
this.upstreamCancelPolicyRegistry = upstreamCancelPolicyRegistry;
|
||||
this.downstreamProperties = downstreamProperties;
|
||||
this.priceConfigService = priceConfigService;
|
||||
this.channelConfigService = channelConfigService;
|
||||
@@ -697,10 +705,20 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
List<Map<String, Object>> planCode = new ArrayList<>(1);
|
||||
Map<String, Object> plan = new LinkedHashMap<>();
|
||||
plan.put("breakfast", breakfast);
|
||||
plan.put("is_cancel", 1);
|
||||
// 圣诺:is_cancel 1不可取消 2可取消;规则按酒店上游渠道解析后映射
|
||||
SmHotelCancelPolicy.Policy cancelPol = resolveUpstreamCancelPolicy(hotel, p);
|
||||
boolean canCancel = cancelPol != null ? cancelPol.canCancel
|
||||
: Integer.valueOf(1).equals(p.getCanCancel());
|
||||
int advanceDay = cancelPol == null ? 0 : Math.max(0, cancelPol.lastCancelDay);
|
||||
String specific = "18:00";
|
||||
if (cancelPol != null && cancelPol.lastCancelTime != null) {
|
||||
specific = cancelPol.lastCancelTime
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("HH:mm"));
|
||||
}
|
||||
plan.put("is_cancel", canCancel ? 2 : 1);
|
||||
plan.put("sale", 1);
|
||||
plan.put("cancel_advance_day", 0);
|
||||
plan.put("cancel_specific_date", "18:00");
|
||||
plan.put("cancel_advance_day", advanceDay);
|
||||
plan.put("cancel_specific_date", specific);
|
||||
plan.put("cancel_cut_type", 3);
|
||||
plan.put("cancel_amount", 0);
|
||||
planCode.add(plan);
|
||||
@@ -2390,6 +2408,14 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
return daily;
|
||||
}
|
||||
|
||||
/** 按酒店 sheet_name 选上游取消规则;无 SPI 时仅用已归一 cancelRule。 */
|
||||
private SmHotelCancelPolicy.Policy resolveUpstreamCancelPolicy(SmHotelEntity hotel, SmHotelProductEntity product) {
|
||||
String sheet = hotel == null ? null : hotel.getSheetName();
|
||||
return upstreamCancelPolicyRegistry.find(sheet)
|
||||
.map(pol -> pol.resolve(product))
|
||||
.orElseGet(() -> SmHotelCancelPolicy.resolveStored(product));
|
||||
}
|
||||
|
||||
private SmHotelEntity requireLocalHotel(Long hotelId) {
|
||||
// 手工维护房型/产品/日历:本地来源 + 部门数据隔离
|
||||
SmHotelEntity hotel = requireHotelInScope(hotelId);
|
||||
@@ -2567,11 +2593,15 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
int breakfast = resolveBreakfast(rate);
|
||||
int canCancel = 0;
|
||||
String cancelRule = null;
|
||||
JSONObject cancelInfo = rate.getJSONObject("cancelInfo");
|
||||
if (cancelInfo != null) {
|
||||
Boolean can = cancelInfo.getBoolean("isCanCancel");
|
||||
canCancel = Boolean.TRUE.equals(can) ? 1 : 0;
|
||||
cancelRule = cancelInfo.getString("cancelDesc");
|
||||
boolean hasCancel = false;
|
||||
// 商游拉价路径:按渠道 SPI 归一取消规则(后续其它上游在各自拉取处同样调用 registry)
|
||||
Optional<HotelUpstreamCancelPolicy.NormalizedCancel> normalizedCancel = upstreamCancelPolicyRegistry
|
||||
.find(SmHotelDataSource.SHANGYOU)
|
||||
.flatMap(pol -> pol.normalizeFromUpstreamRate(rate));
|
||||
if (normalizedCancel.isPresent()) {
|
||||
hasCancel = true;
|
||||
canCancel = normalizedCancel.get().canCancel();
|
||||
cancelRule = normalizedCancel.get().cancelRuleJson();
|
||||
}
|
||||
|
||||
Map<String, Map<String, Integer>> stockByRoomDate = new HashMap<>();
|
||||
@@ -2649,6 +2679,12 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
if (stock > (product.getStock() == null ? 0 : product.getStock())) {
|
||||
product.setStock(stock);
|
||||
}
|
||||
// 取消规则随房价刷新,避免旧产品仍用过期规则
|
||||
if (hasCancel) {
|
||||
product.setCanCancel(canCancel);
|
||||
product.setCancelRule(cancelRule);
|
||||
product.setRawJson(rate.toJSONString());
|
||||
}
|
||||
}
|
||||
if (roomTypesOut != null) {
|
||||
roomTypesOut.add(roomTypeId);
|
||||
|
||||
+201
@@ -16,12 +16,14 @@ import com.bsd.pjcloud.supermarket.constant.SmHotelStayDates;
|
||||
import com.bsd.pjcloud.supermarket.entity.*;
|
||||
import com.bsd.pjcloud.supermarket.event.HotelUpstreamFailRefundEvent;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamOpenCallbackClient;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamCancelPolicyRegistry;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderGateway;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderGatewayRegistry;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderResult;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouBookStatus;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelCancelPolicy;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAutoPushService;
|
||||
@@ -103,6 +105,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
private final HotelUpstreamOrderGatewayRegistry upstreamGatewayRegistry;
|
||||
|
||||
private final HotelUpstreamCancelPolicyRegistry upstreamCancelPolicyRegistry;
|
||||
|
||||
private final SmHotelOrderUpstreamLogService upstreamLogService;
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
@@ -2054,6 +2058,125 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return R.ok(true, "已拒绝并取消");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Map<String, Object>> cancelOrParkForDownstream(Long id, String reason) {
|
||||
SmHotelOrderEntity order = baseMapper.selectById(id);
|
||||
if (order == null) {
|
||||
return R.failed("订单不存在");
|
||||
}
|
||||
if ("CANCELLED".equals(order.getOrderStatus()) || PAY_REFUNDED.equals(order.getPayStatus())) {
|
||||
Map<String, Object> done = new LinkedHashMap<>(4);
|
||||
done.put("parked", false);
|
||||
done.put("orderStatus", order.getOrderStatus());
|
||||
return R.ok(done, "订单已取消");
|
||||
}
|
||||
if ("PENDING_CANCEL".equals(order.getOrderStatus())) {
|
||||
Map<String, Object> parked = new LinkedHashMap<>(4);
|
||||
parked.put("parked", true);
|
||||
parked.put("orderStatus", "PENDING_CANCEL");
|
||||
return R.ok(parked, "取消申请处理中");
|
||||
}
|
||||
// 按酒店上游渠道规则拦截(各家结构不同,经 SPI 归一后判定)
|
||||
String policyDeny = denyCancelByUpstreamPolicy(order, reason);
|
||||
if (policyDeny != null) {
|
||||
return R.failed(policyDeny);
|
||||
}
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
boolean local = hotel != null && SmHotelDataSource.isLocal(hotel.getSheetName());
|
||||
boolean confirmed = "UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus());
|
||||
// 本地已确认:挂起待人工同意/拒绝取消(圣诺取消中 / 慧订稍后取消确认)
|
||||
if (local && confirmed) {
|
||||
String tip = StrUtil.blankToDefault(reason, "下游申请取消");
|
||||
order.setOrderStatus("PENDING_CANCEL");
|
||||
order.setFailMsg(StrUtil.maxLength(tip + ",待人工确认是否同意取消", 500));
|
||||
order.setLastCompensateMsg(StrUtil.maxLength("下游取消申请待确认", 200));
|
||||
baseMapper.updateById(order);
|
||||
Map<String, Object> parked = new LinkedHashMap<>(4);
|
||||
parked.put("parked", true);
|
||||
parked.put("orderStatus", "PENDING_CANCEL");
|
||||
parked.put("orderId", order.getId());
|
||||
log.info("|酒店订单|下游取消挂起待确认|orderId={}|channelNo={}|", order.getId(), order.getChannelNo());
|
||||
return R.ok(parked, "取消申请已受理,待人工确认");
|
||||
}
|
||||
R<Boolean> r = cancelByAdmin(id, StrUtil.blankToDefault(reason, "下游取消"));
|
||||
if (r == null || r.getCode() != 0) {
|
||||
return R.failed(r == null ? "取消失败" : StrUtil.blankToDefault(r.getMsg(), "取消失败"));
|
||||
}
|
||||
Map<String, Object> done = new LinkedHashMap<>(4);
|
||||
done.put("parked", false);
|
||||
done.put("orderStatus", "CANCELLED");
|
||||
return R.ok(done, "取消成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> confirmCancelLocalByAdmin(Long id, String reason) {
|
||||
SmHotelOrderEntity order = baseMapper.selectById(id);
|
||||
if (order == null) {
|
||||
return R.failed("订单不存在");
|
||||
}
|
||||
String scopeErr = assertOrderHotelInScope(order);
|
||||
if (scopeErr != null) {
|
||||
return R.failed(scopeErr);
|
||||
}
|
||||
if ("CANCELLED".equals(order.getOrderStatus()) || PAY_REFUNDED.equals(order.getPayStatus())) {
|
||||
return R.ok(true, "订单已取消");
|
||||
}
|
||||
if (!"PENDING_CANCEL".equals(order.getOrderStatus())) {
|
||||
return R.failed("仅「待取消确认」订单可同意取消,当前状态=" + order.getOrderStatus());
|
||||
}
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
if (hotel == null || !SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
return R.failed("仅本地数据来源酒店订单可操作取消确认");
|
||||
}
|
||||
String tip = StrUtil.blankToDefault(reason, "人工同意取消");
|
||||
// 先关单成功,再回写下游取消确认,避免下游已确认我方仍失败
|
||||
if (PAY_PAID.equals(order.getPayStatus())) {
|
||||
R<Boolean> refundR = refundByAdmin(id, tip);
|
||||
if (refundR == null || !refundR.isOk()) {
|
||||
return R.failed(refundR == null ? "同意取消失败:退款异常" : refundR.getMsg());
|
||||
}
|
||||
order = baseMapper.selectById(id);
|
||||
notifyDownstreamAfterLocalCancelDecision(order, true);
|
||||
return R.ok(true, "已同意取消并退款");
|
||||
}
|
||||
R<Boolean> cancelR = cancelByAdmin(id, tip);
|
||||
if (cancelR == null || !cancelR.isOk()) {
|
||||
return R.failed(cancelR == null ? "同意取消失败" : cancelR.getMsg());
|
||||
}
|
||||
order = baseMapper.selectById(id);
|
||||
notifyDownstreamAfterLocalCancelDecision(order, true);
|
||||
return R.ok(true, "已同意取消");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> rejectCancelLocalByAdmin(Long id, String reason) {
|
||||
SmHotelOrderEntity order = baseMapper.selectById(id);
|
||||
if (order == null) {
|
||||
return R.failed("订单不存在");
|
||||
}
|
||||
String scopeErr = assertOrderHotelInScope(order);
|
||||
if (scopeErr != null) {
|
||||
return R.failed(scopeErr);
|
||||
}
|
||||
if (!"PENDING_CANCEL".equals(order.getOrderStatus())) {
|
||||
return R.failed("仅「待取消确认」订单可拒绝取消申请,当前状态=" + order.getOrderStatus());
|
||||
}
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
if (hotel == null || !SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
return R.failed("仅本地数据来源酒店订单可操作取消确认");
|
||||
}
|
||||
order.setOrderStatus("UPSTREAM_OK");
|
||||
order.setFailMsg(null);
|
||||
order.setLastCompensateMsg(StrUtil.maxLength(
|
||||
StrUtil.blankToDefault(reason, "人工拒绝下游取消申请,订单继续有效"), 500));
|
||||
baseMapper.updateById(order);
|
||||
notifyDownstreamAfterLocalCancelDecision(order, false);
|
||||
return R.ok(true, "已拒绝取消申请,订单保持预订成功");
|
||||
}
|
||||
|
||||
/** 确认/拒绝时校验订单酒店在当前用户 DataScope 内(无酒店 ID 则拒绝) */
|
||||
private String assertOrderHotelInScope(SmHotelOrderEntity order) {
|
||||
if (order == null || order.getHotelId() == null) {
|
||||
@@ -2102,6 +2225,36 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
}
|
||||
|
||||
/** 按伙伴协议回写下游:同意取消 / 拒绝取消申请 */
|
||||
private void notifyDownstreamAfterLocalCancelDecision(SmHotelOrderEntity order, boolean cancelApproved) {
|
||||
if (order == null || StrUtil.isBlank(order.getPartnerCode())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(order.getPartnerCode());
|
||||
String protocol = props == null ? null : props.getProtocol();
|
||||
if (SmHotelDownstreamProtocol.isHuiding(protocol)) {
|
||||
if (cancelApproved) {
|
||||
huidingOrderService.notifyLocalCancelConfirmed(order);
|
||||
}
|
||||
// 慧订无「拒绝取消」专用码:不推 operationOrder,订单继续有效即可
|
||||
return;
|
||||
}
|
||||
if (SmHotelDownstreamProtocol.isShengnuo(protocol)) {
|
||||
if (cancelApproved) {
|
||||
shengnuoOrderService.notifyLocalCancelConfirmed(order);
|
||||
}
|
||||
else {
|
||||
shengnuoOrderService.notifyLocalCancelRejected(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|本地取消确认|下游通知失败|orderId={}|approved={}|{}", order.getId(), cancelApproved,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对占用日历日 CAS 增加 sold(过夜 [checkIn, checkOut);钟点房占入住当天)。
|
||||
*
|
||||
@@ -2655,6 +2808,10 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|| ShangyouBookStatus.X.equalsIgnoreCase(order.getBookStatus())) {
|
||||
return R.ok(true, "订单已取消");
|
||||
}
|
||||
String policyDeny = denyCancelByUpstreamPolicy(order, reason);
|
||||
if (policyDeny != null) {
|
||||
return R.failed(policyDeny);
|
||||
}
|
||||
if (SmHotelInventoryStatus.isApplied(order.getInventoryStatus())
|
||||
|| (order.getPlatformNo() != null && order.getPlatformNo().startsWith("LOCAL-"))) {
|
||||
restoreLocalInventory(order);
|
||||
@@ -3077,6 +3234,50 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
StrUtil.blankToDefault(reason, "上游下单失败自动退款")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 非本地酒店:按 hotel.sheet_name 对应上游渠道的取消规则判断。
|
||||
* 本地酒店、未履约未支付、系统超时关单等跳过;无规则信息时不拦。
|
||||
*
|
||||
* @return null=允许;非空=拒绝文案
|
||||
*/
|
||||
private String denyCancelByUpstreamPolicy(SmHotelOrderEntity order, String reason) {
|
||||
if (order == null) {
|
||||
return null;
|
||||
}
|
||||
String tip = StrUtil.blankToDefault(reason, "");
|
||||
if (tip.contains("支付超时") || tip.contains("系统自动") || tip.contains("自动关单")) {
|
||||
return null;
|
||||
}
|
||||
if (PAY_UNPAID.equals(order.getPayStatus())
|
||||
|| "CREATED".equals(order.getOrderStatus())
|
||||
|| "PENDING_CONFIRM".equals(order.getOrderStatus())
|
||||
|| StrUtil.isBlank(order.getPlatformNo())) {
|
||||
return null;
|
||||
}
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
if (hotel != null && SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
return null;
|
||||
}
|
||||
Long hotelId = hotel != null ? hotel.getId() : order.getHotelId();
|
||||
SmHotelProductEntity product = null;
|
||||
if (StrUtil.isNotBlank(order.getProductCode())) {
|
||||
product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(hotelId != null, SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getProductCode, order.getProductCode())
|
||||
.last("limit 1"));
|
||||
}
|
||||
String sheet = hotel == null ? null : hotel.getSheetName();
|
||||
SmHotelCancelPolicy.Policy policy = upstreamCancelPolicyRegistry.find(sheet)
|
||||
.map(pol -> pol.resolve(product))
|
||||
.orElseGet(() -> SmHotelCancelPolicy.resolveStored(product));
|
||||
String deny = SmHotelCancelPolicy.denyReason(policy, order.getCheckInDate(), LocalDateTime.now());
|
||||
if (deny != null) {
|
||||
log.info("|酒店订单|取消被上游规则拒绝|orderId={}|channel={}|product={}|msg={}|", order.getId(),
|
||||
upstreamCancelPolicyRegistry.resolveChannelKey(sheet), order.getProductCode(), deny);
|
||||
}
|
||||
return deny;
|
||||
}
|
||||
|
||||
private R<Boolean> cancelUpstreamOrder(SmHotelOrderEntity order, String reason) {
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = hotel == null ? Optional.empty()
|
||||
|
||||
+36
-4
@@ -54,6 +54,8 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ
|
||||
|
||||
private static final int ST_ACCEPTED = 2;
|
||||
|
||||
private static final int ST_CANCELING = 4;
|
||||
|
||||
private static final int ST_CANCELLED = 5;
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
@@ -235,12 +237,17 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ
|
||||
&& !partnerCode.equals(order.getPartnerCode())) {
|
||||
return fail("订单不属于当前伙伴");
|
||||
}
|
||||
R<Boolean> r = hotelOrderService.cancelByAdmin(order.getId(), "圣诺取消");
|
||||
R<Map<String, Object>> r = hotelOrderService.cancelOrParkForDownstream(order.getId(), "圣诺取消");
|
||||
if (r == null || r.getCode() != 0) {
|
||||
// 拒绝取消:回写已接单(2)
|
||||
pushOrderChange(partnerCode, orderId, ST_ACCEPTED);
|
||||
return fail(r == null ? "取消失败" : r.getMsg());
|
||||
}
|
||||
boolean parked = r.getData() != null && Boolean.TRUE.equals(r.getData().get("parked"));
|
||||
if (parked) {
|
||||
// 本地已确认:取消中,待人工同意/拒绝
|
||||
pushOrderChange(partnerCode, orderId, ST_CANCELING);
|
||||
return ok("取消申请已受理,待确认", order.getId());
|
||||
}
|
||||
pushOrderChange(partnerCode, orderId, ST_CANCELLED);
|
||||
return ok("取消成功", order.getId());
|
||||
}
|
||||
@@ -581,7 +588,7 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ
|
||||
}
|
||||
|
||||
private static boolean isBookConfirmed(SmHotelOrderEntity order) {
|
||||
if (order == null || isPendingConfirm(order)) {
|
||||
if (order == null || isPendingConfirm(order) || "PENDING_CANCEL".equals(order.getOrderStatus())) {
|
||||
return false;
|
||||
}
|
||||
if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) {
|
||||
@@ -590,7 +597,8 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ
|
||||
return StrUtil.isNotBlank(order.getPlatformNo())
|
||||
&& !"UPSTREAM_FAIL".equals(order.getOrderStatus())
|
||||
&& !"NEED_MANUAL".equals(order.getOrderStatus())
|
||||
&& !"CANCELLED".equals(order.getOrderStatus());
|
||||
&& !"CANCELLED".equals(order.getOrderStatus())
|
||||
&& !"PENDING_CANCEL".equals(order.getOrderStatus());
|
||||
}
|
||||
|
||||
private static boolean isPendingConfirm(SmHotelOrderEntity order) {
|
||||
@@ -613,6 +621,30 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ
|
||||
pushOrderChange(order.getPartnerCode(), order.getChannelNo(), ST_CANCELLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLocalCancelConfirmed(SmHotelOrderEntity order) {
|
||||
if (order == null || StrUtil.isBlank(order.getPartnerCode()) || StrUtil.isBlank(order.getChannelNo())) {
|
||||
return;
|
||||
}
|
||||
pushOrderChange(order.getPartnerCode(), order.getChannelNo(), ST_CANCELLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLocalCancelRejected(SmHotelOrderEntity order) {
|
||||
if (order == null || StrUtil.isBlank(order.getPartnerCode()) || StrUtil.isBlank(order.getChannelNo())) {
|
||||
return;
|
||||
}
|
||||
pushOrderChange(order.getPartnerCode(), order.getChannelNo(), ST_ACCEPTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLocalCancelPending(SmHotelOrderEntity order) {
|
||||
if (order == null || StrUtil.isBlank(order.getPartnerCode()) || StrUtil.isBlank(order.getChannelNo())) {
|
||||
return;
|
||||
}
|
||||
pushOrderChange(order.getPartnerCode(), order.getChannelNo(), ST_CANCELING);
|
||||
}
|
||||
|
||||
private static Map<String, Object> ok(String msg, Long localId) {
|
||||
Map<String, Object> m = new LinkedHashMap<>(6);
|
||||
m.put("code", 0);
|
||||
|
||||
Reference in new issue
Block a user