酒店
This commit is contained in:
1 parent
a2dc29722b
commit
22a4b940dd
10 files changed
+337
-151
No files matched your search
+3
@@ -97,4 +97,7 @@ public class SmHotelDownstreamChannelConfigDTO {
|
||||
|
||||
@Schema(description = "是否参与资源日更定时推送;false=仅手动/变更推,默认 false")
|
||||
private Boolean resourcePushJoinSchedule;
|
||||
|
||||
@Schema(description = "本地改价/库存/房态后是否自动推本伙伴;大批量补数可临时 false,默认 true(未配则跟进程级)")
|
||||
private Boolean resourceAutoPushEnabled;
|
||||
}
|
||||
+10
@@ -177,4 +177,14 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
|
||||
@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;
|
||||
}
|
||||
+5
-2
@@ -130,11 +130,14 @@ public class HotelDownstreamProperties {
|
||||
|
||||
/**
|
||||
* 本地/回调变更后是否自动推下游(防抖)。
|
||||
* false:只靠手动推与日更。
|
||||
* <ul>
|
||||
* <li>进程级(Nacos {@code hotel-downstream.resource-auto-push-enabled}):总开关,false 时所有伙伴都不自动推</li>
|
||||
* <li>伙伴 YAML {@code resource-auto-push-enabled}:可覆盖本伙伴;大批量补数时对该家临时 false</li>
|
||||
* </ul>
|
||||
*/
|
||||
private boolean resourceAutoPushEnabled = true;
|
||||
|
||||
/** 变更后防抖秒数(同酒店多次变更合并为一次推送) */
|
||||
/** 变更后防抖秒数(同酒店多次变更合并为一次推送);伙伴 YAML 可覆盖 */
|
||||
private int resourceAutoPushDebounceSeconds = 90;
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -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<Boolean> confirmCancel(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> 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<Boolean> rejectCancel(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
|
||||
|
||||
+190
-2
@@ -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;
|
||||
* 上游取消规则的<strong>统一落库模型</strong>与截止时间判定。
|
||||
* <p>
|
||||
* 各上游渠道原始结构不同,解析由 {@code HotelUpstreamCancelPolicy} 按 sheet_name 插拔;
|
||||
* 同步时尽量归一到 cancelRule JSON,下游取消 / 后台取消只认本模型。
|
||||
* 同步时尽量归一到 cancelRule JSON;本地酒店默认「预订后 30 分钟内可取消」。
|
||||
* <p>
|
||||
* 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<String, Object> 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。
|
||||
* <ul>
|
||||
* <li>不可取消:cancelType=13(沿用现网)</li>
|
||||
* <li>可取消:cancelType=2(限时取消),daySelect=入住前几天,timeSelect=最晚整点小时</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void putHuidingNightCancel(java.util.Map<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -102,15 +102,15 @@ public interface SmHotelOrderService {
|
||||
R<Boolean> rejectLocalByAdmin(Long id, String reason);
|
||||
|
||||
/**
|
||||
* 下游取消入站:本地已确认订单挂起为 PENDING_CANCEL 待人工;其余即时取消。
|
||||
* data.parked=true 表示已挂起;false 表示已即时取消。
|
||||
* 下游取消入站:一律挂起 PENDING_CANCEL 待人工(取消规则仅展示核对,不硬拦)。
|
||||
* data.parked=true 表示已挂起。
|
||||
*/
|
||||
R<Map<String, Object>> cancelOrParkForDownstream(Long id, String reason);
|
||||
|
||||
/** 本地:同意下游取消申请(PENDING_CANCEL → 关单并回写下游取消确认) */
|
||||
/** 同意下游取消申请(PENDING_CANCEL → 关单/退款并回写下游取消确认) */
|
||||
R<Boolean> confirmCancelLocalByAdmin(Long id, String reason);
|
||||
|
||||
/** 本地:拒绝下游取消申请(PENDING_CANCEL → 恢复预订成功,并回写下游仍接单) */
|
||||
/** 拒绝下游取消申请(PENDING_CANCEL → 恢复挂起前状态,并回写下游仍接单) */
|
||||
R<Boolean> rejectCancelLocalByAdmin(Long id, String reason);
|
||||
|
||||
/** Worker:扫描到期补偿任务 */
|
||||
|
||||
+3
@@ -386,6 +386,9 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl<SmHotelChannelC
|
||||
if (dto.getResourcePushJoinSchedule() != null) {
|
||||
target.setResourcePushJoinSchedule(dto.getResourcePushJoinSchedule());
|
||||
}
|
||||
if (dto.getResourceAutoPushEnabled() != null) {
|
||||
target.setResourceAutoPushEnabled(dto.getResourceAutoPushEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
private void overlayShangyou(ShangyouHotelProperties target, SmHotelShangyouChannelConfigDTO dto) {
|
||||
|
||||
+38
-9
@@ -80,7 +80,8 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu
|
||||
if (hotelId == null) {
|
||||
return;
|
||||
}
|
||||
if (!isAutoPushEnabled()) {
|
||||
// 进程级总开关;各伙伴还可在 YAML 单独关(见 resolveAutoPushPartners)
|
||||
if (!isGlobalAutoPushEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
@@ -105,7 +106,7 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu
|
||||
if (hotel == null) {
|
||||
return;
|
||||
}
|
||||
List<String> partners = resolvePartners(hotel);
|
||||
List<String> 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<String> partners = resolvePartners(hotel);
|
||||
if (CollUtil.isEmpty(partners)) {
|
||||
List<String> scopePartners = resolveScopePartners(hotel);
|
||||
if (CollUtil.isEmpty(scopePartners)) {
|
||||
// 提到 info:改价后「没推出去」最常见原因是渠道范围未命中
|
||||
log.info("|{}|无命中下游范围,跳过自动推|hotelId={}|deptId={}|", TAG, hotelId, hotel.getDeptId());
|
||||
return;
|
||||
}
|
||||
List<String> 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<String> resolvePartners(SmHotelEntity hotel) {
|
||||
/** 渠道范围命中的启用伙伴(不含自动推开关过滤) */
|
||||
private List<String> resolveScopePartners(SmHotelEntity hotel) {
|
||||
List<String> 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<String> filterAutoPushEnabledPartners(List<String> 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;
|
||||
|
||||
+14
-26
@@ -705,22 +705,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
List<Map<String, Object>> planCode = new ArrayList<>(1);
|
||||
Map<String, Object> 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<String, Object> staticEnvelope = buildHuidingStaticEnvelope(ds, hotel, rooms);
|
||||
downstreamClient.hotelSync(partnerCode, hotelId, supplierHotelId, staticEnvelope);
|
||||
|
||||
List<Map<String, Object>> roomPriceInfoList = buildHuidingRoomPriceInfoList(supplierHotelId, products, dailies);
|
||||
List<Map<String, Object>> roomPriceInfoList = buildHuidingRoomPriceInfoList(hotel, supplierHotelId, products,
|
||||
dailies);
|
||||
Map<String, Object> 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<Map<String, Object>> buildHuidingRoomPriceInfoList(String supplierHotelId,
|
||||
private List<Map<String, Object>> buildHuidingRoomPriceInfoList(SmHotelEntity hotel, String supplierHotelId,
|
||||
List<SmHotelProductEntity> products, List<SmHotelDailyEntity> dailies) {
|
||||
Map<String, SmHotelProductEntity> 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<String> 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.<SmHotelRoomEntity>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) {
|
||||
|
||||
+67
-105
@@ -413,7 +413,15 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
}
|
||||
if (product != null) {
|
||||
o.setProductName(product.getProductName());
|
||||
SmHotelProductEntity prod = product;
|
||||
o.setProductName(prod.getProductName());
|
||||
o.setProductCanCancel(prod.getCanCancel());
|
||||
String sheet = hotel == null ? null : hotel.getSheetName();
|
||||
SmHotelCancelPolicy.Policy pol = upstreamCancelPolicyRegistry.find(sheet)
|
||||
.map(p -> 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<SmHotelOrderMapper, SmH
|
||||
.last("limit 1"));
|
||||
}
|
||||
if (product != null) {
|
||||
SmHotelProductEntity prod = product;
|
||||
SmHotelOrderDetailVo.ProductBrief pb = new SmHotelOrderDetailVo.ProductBrief();
|
||||
pb.setId(product.getId());
|
||||
pb.setProductCode(product.getProductCode());
|
||||
pb.setProductName(product.getProductName());
|
||||
pb.setRoomTypeCode(product.getRoomTypeCode());
|
||||
pb.setPriceCode(product.getPriceCode());
|
||||
pb.setCancelCode(product.getCancelCode());
|
||||
pb.setMealType(product.getMealType());
|
||||
pb.setMealCount(product.getMealCount());
|
||||
pb.setCostPrice(product.getCostPrice());
|
||||
pb.setSellPrice(product.getSellPrice());
|
||||
pb.setStock(product.getStock());
|
||||
pb.setCanCancel(product.getCanCancel());
|
||||
pb.setCancelRule(product.getCancelRule());
|
||||
pb.setProductType(product.getProductType());
|
||||
pb.setApartmentType(product.getApartmentType());
|
||||
pb.setId(prod.getId());
|
||||
pb.setProductCode(prod.getProductCode());
|
||||
pb.setProductName(prod.getProductName());
|
||||
pb.setRoomTypeCode(prod.getRoomTypeCode());
|
||||
pb.setPriceCode(prod.getPriceCode());
|
||||
pb.setCancelCode(prod.getCancelCode());
|
||||
pb.setMealType(prod.getMealType());
|
||||
pb.setMealCount(prod.getMealCount());
|
||||
pb.setCostPrice(prod.getCostPrice());
|
||||
pb.setSellPrice(prod.getSellPrice());
|
||||
pb.setStock(prod.getStock());
|
||||
pb.setCanCancel(prod.getCanCancel());
|
||||
pb.setCancelRule(prod.getCancelRule());
|
||||
pb.setProductType(prod.getProductType());
|
||||
pb.setApartmentType(prod.getApartmentType());
|
||||
vo.setProduct(pb);
|
||||
order.setProductName(product.getProductName());
|
||||
order.setProductName(prod.getProductName());
|
||||
order.setProductCanCancel(prod.getCanCancel());
|
||||
String sheet = hotel == null ? null : hotel.getSheetName();
|
||||
SmHotelCancelPolicy.Policy pol = upstreamCancelPolicyRegistry.find(sheet)
|
||||
.map(p -> 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<SmHotelOrderMapper, SmH
|
||||
return R.ok(true, "已拒绝并取消");
|
||||
}
|
||||
|
||||
/** 挂起取消前状态写入 lastCompensateMsg,拒绝时恢复 */
|
||||
private static final String PENDING_CANCEL_FROM_PREFIX = "PENDING_CANCEL_FROM:";
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Map<String, Object>> cancelOrParkForDownstream(Long id, String reason) {
|
||||
@@ -2077,36 +2100,20 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
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, "取消成功");
|
||||
// 各上游/下游取消规则不一,不硬拦:一律挂起待人工核对产品取消规则后同意/拒绝
|
||||
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<String, Object> 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 ServiceImpl<SmHotelOrderMapper, SmH
|
||||
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())) {
|
||||
@@ -2164,17 +2167,24 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
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");
|
||||
String resume = parsePendingCancelFromStatus(order.getLastCompensateMsg());
|
||||
order.setOrderStatus(resume);
|
||||
order.setFailMsg(null);
|
||||
order.setLastCompensateMsg(StrUtil.maxLength(
|
||||
StrUtil.blankToDefault(reason, "人工拒绝下游取消申请,订单继续有效"), 500));
|
||||
baseMapper.updateById(order);
|
||||
notifyDownstreamAfterLocalCancelDecision(order, false);
|
||||
return R.ok(true, "已拒绝取消申请,订单保持预订成功");
|
||||
return R.ok(true, "已拒绝取消申请,订单恢复为「" + resume + "」");
|
||||
}
|
||||
|
||||
private static String parsePendingCancelFromStatus(String lastCompensateMsg) {
|
||||
if (StrUtil.isNotBlank(lastCompensateMsg) && lastCompensateMsg.startsWith(PENDING_CANCEL_FROM_PREFIX)) {
|
||||
String s = lastCompensateMsg.substring(PENDING_CANCEL_FROM_PREFIX.length()).trim();
|
||||
if (StrUtil.isNotBlank(s) && !"PENDING_CANCEL".equals(s) && !"CANCELLED".equals(s)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return "UPSTREAM_OK";
|
||||
}
|
||||
|
||||
/** 确认/拒绝时校验订单酒店在当前用户 DataScope 内(无酒店 ID 则拒绝) */
|
||||
@@ -2808,10 +2818,6 @@ 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);
|
||||
@@ -3234,50 +3240,6 @@ 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()
|
||||
|
||||
Reference in new issue
Block a user