diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java index c9473470e..e6d2d5589 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java @@ -97,4 +97,7 @@ public class SmHotelDownstreamChannelConfigDTO { @Schema(description = "是否参与资源日更定时推送;false=仅手动/变更推,默认 false") private Boolean resourcePushJoinSchedule; + + @Schema(description = "本地改价/库存/房态后是否自动推本伙伴;大批量补数可临时 false,默认 true(未配则跟进程级)") + private Boolean resourceAutoPushEnabled; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java index 8a73e1ea7..96c9db98e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java @@ -177,4 +177,14 @@ public class SmHotelOrderEntity extends Model { @TableField(exist = false) @Schema(description = "酒店归属部门名称") private String deptName; + + /** 列表/详情:产品可否取消 1/0(非表字段,来自产品) */ + @TableField(exist = false) + @Schema(description = "产品可否取消") + private Integer productCanCancel; + + /** 列表/详情:取消规则可读文案(非表字段,供人工核对) */ + @TableField(exist = false) + @Schema(description = "取消规则说明") + private String cancelRuleHint; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java index dd226db43..f2e6756d4 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java @@ -130,11 +130,14 @@ public class HotelDownstreamProperties { /** * 本地/回调变更后是否自动推下游(防抖)。 - * false:只靠手动推与日更。 + * */ private boolean resourceAutoPushEnabled = true; - /** 变更后防抖秒数(同酒店多次变更合并为一次推送) */ + /** 变更后防抖秒数(同酒店多次变更合并为一次推送);伙伴 YAML 可覆盖 */ private int resourceAutoPushDebounceSeconds = 90; /** diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java index 255990ef1..1dd77055e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java @@ -80,7 +80,7 @@ public class SmHotelOrderController { } @SysLog("本地酒店同意取消申请") - @Operation(summary = "本地:同意下游取消申请(PENDING_CANCEL)") + @Operation(summary = "同意下游取消申请(PENDING_CANCEL,人工核对取消规则)") @PostMapping("/confirmCancel/{id}") @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel') or @pms.hasPermission('supermarket_smHotel_edit')") public R confirmCancel(@PathVariable Long id, @RequestBody(required = false) Map body) { @@ -88,8 +88,8 @@ public class SmHotelOrderController { return orderService.confirmCancelLocalByAdmin(id, reason); } - @SysLog("本地酒店拒绝取消申请") - @Operation(summary = "本地:拒绝下游取消申请,恢复预订成功") + @SysLog("拒绝下游取消申请") + @Operation(summary = "拒绝下游取消申请,恢复挂起前状态") @PostMapping("/rejectCancel/{id}") @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel') or @pms.hasPermission('supermarket_smHotel_edit')") public R rejectCancel(@PathVariable Long id, @RequestBody(required = false) Map body) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelCancelPolicy.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelCancelPolicy.java index 7939fa03e..3f96ca069 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelCancelPolicy.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelCancelPolicy.java @@ -3,6 +3,7 @@ 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.constant.SmHotelDataSource; import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity; import java.time.LocalDate; @@ -14,21 +15,133 @@ import java.time.format.DateTimeFormatter; * 上游取消规则的统一落库模型与截止时间判定。 *

* 各上游渠道原始结构不同,解析由 {@code HotelUpstreamCancelPolicy} 按 sheet_name 插拔; - * 同步时尽量归一到 cancelRule JSON,下游取消 / 后台取消只认本模型。 + * 同步时尽量归一到 cancelRule JSON;本地酒店默认「预订后 30 分钟内可取消」。 *

* cancelRule 推荐: * {@code {"channel":"shangyou","isCanCancel":true,"lastCancelDay":1,"lastCancelTime":"18:00","cancelDesc":"..."}} + * 本地默认: + * {@code {"channel":"local","isCanCancel":true,"cancelWithinMinutes":30,"cancelDesc":"预订后30分钟内可免费取消"}} */ public final class SmHotelCancelPolicy { private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("H:mm"); + /** 本地酒店默认:下单后可取消窗口(分钟) */ + public static final int LOCAL_CANCEL_WITHIN_MINUTES = 30; + + public static final String LOCAL_CANCEL_DESC = "预订后30分钟内可免费取消"; + private SmHotelCancelPolicy() { } + /** 本地酒店默认取消规则(推下游用可取消,不再一律不可取消) */ + public static Policy localDefault() { + Policy p = new Policy(); + p.channel = SmHotelDataSource.LOCAL; + p.canCancel = true; + p.cancelWithinMinutes = LOCAL_CANCEL_WITHIN_MINUTES; + p.desc = LOCAL_CANCEL_DESC; + // 仅支持「入住前天数」的下游:映射为当日可取消,文案以 cancelWithinMinutes 为准 + p.lastCancelDay = 0; + p.lastCancelTime = LocalTime.of(23, 59); + return p; + } + + /** 本地产品未填取消规则时写入默认值 */ + public static void ensureLocalProductDefaults(SmHotelProductEntity product) { + if (product == null) { + return; + } + if (product.getCanCancel() != null || StrUtil.isNotBlank(product.getCancelRule())) { + return; + } + product.setCanCancel(1); + product.setCancelRule(toLocalDefaultCancelRuleJson()); + } + + public static String toLocalDefaultCancelRuleJson() { + JSONObject o = new JSONObject(); + o.set("channel", SmHotelDataSource.LOCAL); + o.set("isCanCancel", true); + o.set("cancelWithinMinutes", LOCAL_CANCEL_WITHIN_MINUTES); + o.set("lastCancelDay", 0); + o.set("lastCancelTime", "23:59"); + o.set("cancelDesc", LOCAL_CANCEL_DESC); + return o.toString(); + } + + /** + * 按酒店来源解析规则:本地无配置时用 30 分钟默认;上游走已落库/渠道 SPI。 + */ + public static Policy resolveForHotel(String hotelSheetName, SmHotelProductEntity product) { + Policy stored = resolveStored(product); + if (!SmHotelDataSource.isLocal(hotelSheetName)) { + return stored; + } + // 本地明确不可取消 + if (product != null && Integer.valueOf(0).equals(product.getCanCancel())) { + if (stored == null) { + Policy p = new Policy(); + p.channel = SmHotelDataSource.LOCAL; + p.canCancel = false; + p.desc = "不可取消"; + return p; + } + return stored; + } + if (stored == null) { + return localDefault(); + } + if (stored.canCancel && stored.cancelWithinMinutes <= 0) { + stored.cancelWithinMinutes = LOCAL_CANCEL_WITHIN_MINUTES; + if (StrUtil.isBlank(stored.desc)) { + stored.desc = LOCAL_CANCEL_DESC; + } + if (StrUtil.isBlank(stored.channel)) { + stored.channel = SmHotelDataSource.LOCAL; + } + if (stored.lastCancelTime == null && stored.lastCancelDay == 0) { + stored.lastCancelTime = LocalTime.of(23, 59); + } + } + return stored; + } + + /** + * 人工核对用文案:可否取消 + 最晚时点 + 说明。无规则返回空串。 + */ + public static String formatDisplay(Policy p) { + if (p == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(p.canCancel ? "可取消" : "不可取消"); + if (p.canCancel && p.cancelWithinMinutes > 0) { + sb.append(";预订后").append(p.cancelWithinMinutes).append("分钟内"); + } + else if (p.canCancel && (p.lastCancelDay > 0 || p.lastCancelTime != null)) { + String hm = p.lastCancelTime == null ? "23:59" + : p.lastCancelTime.format(DateTimeFormatter.ofPattern("HH:mm")); + sb.append(";最晚:入住前").append(Math.max(0, p.lastCancelDay)).append("天 ").append(hm); + } + if (StrUtil.isNotBlank(p.desc)) { + sb.append(";").append(p.desc.trim()); + } + if (StrUtil.isNotBlank(p.channel)) { + sb.append("(").append(p.channel).append(")"); + } + return sb.toString(); + } + + public static String formatDisplay(SmHotelProductEntity product) { + return formatDisplay(resolveStored(product)); + } + /** * @return null=允许取消;非空=拒绝原因 + * @deprecated 各上游/下游规则不一,取消改为人工确认;保留供推送映射等只读场景 */ + @Deprecated public static String denyReason(Policy p, LocalDate checkIn, LocalDateTime now) { if (p == null) { return null; @@ -93,6 +206,11 @@ public final class SmHotelCancelPolicy { /** 同步时写入统一 cancelRule JSON。 */ public static String toCancelRuleJson(String channel, Boolean isCanCancel, Integer lastCancelDay, String lastCancelTime, String cancelDesc) { + return toCancelRuleJson(channel, isCanCancel, lastCancelDay, lastCancelTime, null, cancelDesc); + } + + public static String toCancelRuleJson(String channel, Boolean isCanCancel, Integer lastCancelDay, + String lastCancelTime, Integer cancelWithinMinutes, String cancelDesc) { JSONObject o = new JSONObject(); if (StrUtil.isNotBlank(channel)) { o.set("channel", channel.trim().toLowerCase()); @@ -104,6 +222,9 @@ public final class SmHotelCancelPolicy { if (StrUtil.isNotBlank(lastCancelTime)) { o.set("lastCancelTime", lastCancelTime.trim()); } + if (cancelWithinMinutes != null && cancelWithinMinutes > 0) { + o.set("cancelWithinMinutes", cancelWithinMinutes); + } if (StrUtil.isNotBlank(cancelDesc)) { o.set("cancelDesc", cancelDesc.trim()); } @@ -152,6 +273,8 @@ public final class SmHotelCancelPolicy { Integer day = o.getInt("lastCancelDay"); p.lastCancelDay = day == null ? 0 : Math.max(0, day); p.lastCancelTime = parseHm(o.getStr("lastCancelTime")); + Integer within = o.getInt("cancelWithinMinutes"); + p.cancelWithinMinutes = within == null ? 0 : Math.max(0, within); p.desc = StrUtil.blankToDefault(o.getStr("cancelDesc"), o.getStr("desc")); p.channel = o.getStr("channel"); return p; @@ -214,11 +337,76 @@ public final class SmHotelCancelPolicy { } public static final class Policy { - /** 来源渠道,如 shangyou;可空 */ + /** 来源渠道,如 shangyou / local;可空 */ public String channel; public boolean canCancel; public int lastCancelDay; public LocalTime lastCancelTime; + /** 本地:预订后 N 分钟内可取消;0=未使用该维度 */ + public int cancelWithinMinutes; public String desc; } + + /** + * 环启产品同步:canCancel + cancelRule(可取消时必填文案)。 + */ + public static void putHuanqiProductCancel(java.util.Map body, Policy p) { + if (body == null) { + return; + } + boolean ok = p != null && p.canCancel; + body.put("canCancel", ok ? 1 : 0); + if (ok) { + String rule = formatDisplay(p); + body.put("cancelRule", StrUtil.blankToDefault(rule, LOCAL_CANCEL_DESC)); + } + } + + /** + * 慧订每日价态:cancelType / daySelect / timeSelect。 + *

    + *
  • 不可取消:cancelType=13(沿用现网)
  • + *
  • 可取消:cancelType=2(限时取消),daySelect=入住前几天,timeSelect=最晚整点小时
  • + *
+ */ + public static void putHuidingNightCancel(java.util.Map night, Policy p) { + if (night == null) { + return; + } + if (p == null || !p.canCancel) { + night.put("cancelType", 13); + night.put("daySelect", 0); + night.put("timeSelect", 18); + night.put("cutValue", 0); + return; + } + night.put("cancelType", 2); + night.put("daySelect", Math.max(0, p.lastCancelDay)); + int hour = 18; + if (p.lastCancelTime != null) { + hour = p.lastCancelTime.getHour(); + } + night.put("timeSelect", Math.min(23, Math.max(0, hour))); + night.put("cutValue", 0); + } + + /** + * 圣诺计划:is_cancel 1不可/2可;cancel_advance_day + cancel_specific_date(HH:mm)。 + */ + public static void putShengnuoPlanCancel(java.util.Map plan, Policy p) { + if (plan == null) { + return; + } + boolean ok = p != null && p.canCancel; + int advance = p == null ? 0 : Math.max(0, p.lastCancelDay); + String specific = "18:00"; + if (p != null && p.lastCancelTime != null) { + specific = p.lastCancelTime.format(DateTimeFormatter.ofPattern("HH:mm")); + } + plan.put("is_cancel", ok ? 2 : 1); + plan.put("cancel_advance_day", advance); + plan.put("cancel_specific_date", specific); + plan.put("cancel_cut_type", 3); + plan.put("cancel_amount", 0); + } } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java index 5d5b73697..81baa1be0 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java @@ -102,15 +102,15 @@ public interface SmHotelOrderService { R rejectLocalByAdmin(Long id, String reason); /** - * 下游取消入站:本地已确认订单挂起为 PENDING_CANCEL 待人工;其余即时取消。 - * data.parked=true 表示已挂起;false 表示已即时取消。 + * 下游取消入站:一律挂起 PENDING_CANCEL 待人工(取消规则仅展示核对,不硬拦)。 + * data.parked=true 表示已挂起。 */ R> cancelOrParkForDownstream(Long id, String reason); - /** 本地:同意下游取消申请(PENDING_CANCEL → 关单并回写下游取消确认) */ + /** 同意下游取消申请(PENDING_CANCEL → 关单/退款并回写下游取消确认) */ R confirmCancelLocalByAdmin(Long id, String reason); - /** 本地:拒绝下游取消申请(PENDING_CANCEL → 恢复预订成功,并回写下游仍接单) */ + /** 拒绝下游取消申请(PENDING_CANCEL → 恢复挂起前状态,并回写下游仍接单) */ R rejectCancelLocalByAdmin(Long id, String reason); /** Worker:扫描到期补偿任务 */ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java index 1a968430b..4473d5a1a 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java @@ -386,6 +386,9 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl partners = resolvePartners(hotel); + List partners = resolveScopePartners(hotel); if (CollUtil.isEmpty(partners)) { return; } @@ -156,10 +157,11 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu @Override public String autoPushScheduleHint() { - if (!isAutoPushEnabled()) { - return "自动推下游已关闭,请到酒店列表手动「推下游」"; + if (!isGlobalAutoPushEnabled()) { + return "进程级自动推已关闭(hotel-downstream.resource-auto-push-enabled),请手动「推下游」"; } - return "约" + resolveDebounceSeconds() + "秒后自动推已授权下游(圣诺/环启等),无需再点手动推"; + return "约" + resolveDebounceSeconds() + "秒后自动推「已开 resource-auto-push」的下游;" + + "大批量补数可在该伙伴 YAML 临时设 resource-auto-push-enabled: false"; } private void scheduleDebounced(Long hotelId) { @@ -191,12 +193,18 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu if (hotel == null) { return; } - List partners = resolvePartners(hotel); - if (CollUtil.isEmpty(partners)) { + List scopePartners = resolveScopePartners(hotel); + if (CollUtil.isEmpty(scopePartners)) { // 提到 info:改价后「没推出去」最常见原因是渠道范围未命中 log.info("|{}|无命中下游范围,跳过自动推|hotelId={}|deptId={}|", TAG, hotelId, hotel.getDeptId()); return; } + List partners = filterAutoPushEnabledPartners(scopePartners); + if (CollUtil.isEmpty(partners)) { + log.info("|{}|命中范围但伙伴均关闭 resource-auto-push,跳过|hotelId={}|scoped={}|", TAG, hotelId, + scopePartners); + return; + } for (String partner : partners) { long t0 = System.currentTimeMillis(); try { @@ -249,7 +257,8 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu } } - private List resolvePartners(SmHotelEntity hotel) { + /** 渠道范围命中的启用伙伴(不含自动推开关过滤) */ + private List resolveScopePartners(SmHotelEntity hotel) { List enabled = channelConfigService.listEnabledDownstreamPartners(); if (CollUtil.isEmpty(enabled)) { return List.of(); @@ -265,7 +274,27 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu return matched.stream().filter(enabled::contains).distinct().toList(); } - private boolean isAutoPushEnabled() { + /** 再按各伙伴 YAML/运行时的 resource-auto-push-enabled 过滤 */ + private List filterAutoPushEnabledPartners(List partners) { + if (CollUtil.isEmpty(partners)) { + return List.of(); + } + return partners.stream().filter(this::isPartnerAutoPushEnabled).distinct().toList(); + } + + private boolean isPartnerAutoPushEnabled(String partnerCode) { + try { + HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(partnerCode); + return props != null && props.isResourceAutoPushEnabled(); + } + catch (Exception e) { + log.warn("|{}|读伙伴自动推开关失败,按关闭处理|partner={}|err={}|", TAG, partnerCode, e.getMessage()); + return false; + } + } + + /** 进程级总开关(Nacos hotel-downstream);false 时 schedule 直接不排 */ + private boolean isGlobalAutoPushEnabled() { HotelDownstreamProperties def = channelConfigService.downstreamYamlDefaultsSnapshot(); if (def == null) { def = yamlDefaults; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java index 92e8041f4..46523a04e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java @@ -705,22 +705,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { List> planCode = new ArrayList<>(1); Map plan = new LinkedHashMap<>(); plan.put("breakfast", breakfast); - // 圣诺: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); + SmHotelCancelPolicy.putShengnuoPlanCancel(plan, cancelPol); plan.put("sale", 1); - 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); body.put("planCode", planCode); try { @@ -1066,7 +1054,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { Map staticEnvelope = buildHuidingStaticEnvelope(ds, hotel, rooms); downstreamClient.hotelSync(partnerCode, hotelId, supplierHotelId, staticEnvelope); - List> roomPriceInfoList = buildHuidingRoomPriceInfoList(supplierHotelId, products, dailies); + List> roomPriceInfoList = buildHuidingRoomPriceInfoList(hotel, supplierHotelId, products, + dailies); Map dateEnvelope = buildHuidingDatePushEnvelope(ds, hotel, dailyFrom, dailyTo, roomPriceInfoList); downstreamClient.dailySync(partnerCode, hotelId, supplierHotelId, dateEnvelope); @@ -1154,7 +1143,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { /** * 按产品聚合日历;慧订 roomTypeCode 全局唯一;ratePlanCode 按早餐建议值。 */ - private List> buildHuidingRoomPriceInfoList(String supplierHotelId, + private List> buildHuidingRoomPriceInfoList(SmHotelEntity hotel, String supplierHotelId, List products, List dailies) { Map productByCode = new LinkedHashMap<>(); for (SmHotelProductEntity p : products) { @@ -1203,10 +1192,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { night.put("price", price); night.put("roomCount", remain); night.put("status", status); - night.put("cancelType", 13); - night.put("daySelect", 0); - night.put("timeSelect", 18); - night.put("cutValue", 0); + // 慧订:按酒店来源取消规则推送(本地默认 30 分钟可取消) + SmHotelCancelPolicy.putHuidingNightCancel(night, resolveUpstreamCancelPolicy(hotel, p)); planRateList.add(night); } if (planRateList.isEmpty()) { @@ -1568,8 +1555,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { } /* - * 产品:不看取消规则(本地无可靠取消数据,环启 canCancel=0 时也不必填)。 - * 同房型+同早餐只能推 1 条:在有库存的候选里取价格最低的。 + * 产品:上游(飞猪等)有取消规则则推 canCancel+cancelRule,让下游先自拦; + * 本地无规则时 canCancel=0。同房型+同早餐只推 1 条(有库存里最低价)。 */ List productErrors = new ArrayList<>(); int productOk = 0; @@ -1607,8 +1594,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { body.put("breakfastNumber", breakfast); body.put("apartmentType", p.getApartmentType() == null ? 1 : p.getApartmentType()); body.put("productType", p.getProductType() == null ? 2 : p.getProductType()); - // 不传取消规则,固定不可取消,避免环启要求 cancelRule - body.put("canCancel", 0); + SmHotelCancelPolicy.putHuanqiProductCancel(body, resolveUpstreamCancelPolicy(hotel, p)); BigDecimal price = p.getSellPrice() != null ? p.getSellPrice() : p.getCostPrice(); body.put("homePrice", price == null || price.compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ONE : price); body.put("homeNumber", roomQtyMap.getOrDefault(roomCode, 1)); @@ -2105,6 +2091,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { product.setGroupCode(hotel.getGroupCode()); product.setProductCode(product.getProductCode().trim()); product.setRoomTypeCode(product.getRoomTypeCode().trim()); + // 本地默认:预订后 30 分钟内可取消,推下游不再一律不可取消 + SmHotelCancelPolicy.ensureLocalProductDefaults(product); SmHotelRoomEntity room = roomMapper.selectOne(Wrappers.lambdaQuery() .eq(SmHotelRoomEntity::getHotelId, hotelId) .eq(SmHotelRoomEntity::getRoomTypeCode, product.getRoomTypeCode()) @@ -2408,12 +2396,12 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { return daily; } - /** 按酒店 sheet_name 选上游取消规则;无 SPI 时仅用已归一 cancelRule。 */ + /** 按酒店 sheet_name 选取消规则;本地无配置时默认预订后 30 分钟可取消。 */ 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)); + .orElseGet(() -> SmHotelCancelPolicy.resolveForHotel(sheet, product)); } private SmHotelEntity requireLocalHotel(Long hotelId) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java index bbb65fe38..8bbafdc84 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java @@ -413,7 +413,15 @@ public class SmHotelOrderServiceImpl extends ServiceImpl p.resolve(prod)) + .orElseGet(() -> SmHotelCancelPolicy.resolveForHotel(sheet, prod)); + String hint = SmHotelCancelPolicy.formatDisplay(pol); + o.setCancelRuleHint(StrUtil.isBlank(hint) ? null : hint); } String roomTypeCode = StrUtil.blankToDefault(o.getRoomTypeCode(), product == null ? null : product.getRoomTypeCode()); @@ -650,24 +658,36 @@ public class SmHotelOrderServiceImpl extends ServiceImpl p.resolve(prod)) + .orElseGet(() -> SmHotelCancelPolicy.resolveForHotel(sheet, prod)); + String hint = SmHotelCancelPolicy.formatDisplay(pol); + order.setCancelRuleHint(StrUtil.isBlank(hint) ? null : hint); + // 详情展示用可读文案覆盖原始 JSON,便于人工核对 + if (StrUtil.isNotBlank(hint)) { + pb.setCancelRule(hint); + } } String roomTypeCode = StrUtil.blankToDefault(order.getRoomTypeCode(), @@ -2058,6 +2078,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl> cancelOrParkForDownstream(Long id, String reason) { @@ -2077,36 +2100,20 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 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 r = cancelByAdmin(id, StrUtil.blankToDefault(reason, "下游取消")); - if (r == null || r.getCode() != 0) { - return R.failed(r == null ? "取消失败" : StrUtil.blankToDefault(r.getMsg(), "取消失败")); - } - Map done = new LinkedHashMap<>(4); - done.put("parked", false); - done.put("orderStatus", "CANCELLED"); - return R.ok(done, "取消成功"); + // 各上游/下游取消规则不一,不硬拦:一律挂起待人工核对产品取消规则后同意/拒绝 + String prevStatus = StrUtil.blankToDefault(order.getOrderStatus(), "UPSTREAM_OK"); + String tip = StrUtil.blankToDefault(reason, "下游申请取消"); + order.setOrderStatus("PENDING_CANCEL"); + order.setFailMsg(StrUtil.maxLength(tip + ",待人工确认是否同意取消", 500)); + order.setLastCompensateMsg(StrUtil.maxLength(PENDING_CANCEL_FROM_PREFIX + prevStatus, 200)); + baseMapper.updateById(order); + Map parked = new LinkedHashMap<>(4); + parked.put("parked", true); + parked.put("orderStatus", "PENDING_CANCEL"); + parked.put("orderId", order.getId()); + log.info("|酒店订单|下游取消挂起待确认|orderId={}|fromStatus={}|channelNo={}|", order.getId(), prevStatus, + order.getChannelNo()); + return R.ok(parked, "取消申请已受理,待人工确认"); } @Override @@ -2126,10 +2133,6 @@ public class SmHotelOrderServiceImpl extends ServiceImpllambdaQuery() - .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 cancelUpstreamOrder(SmHotelOrderEntity order, String reason) { SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId()); Optional gatewayOpt = hotel == null ? Optional.empty()